diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt
index 40b08699d3c..d1936c792b0 100644
--- a/buildSrc/src/main/java/Dependencies.kt
+++ b/buildSrc/src/main/java/Dependencies.kt
@@ -46,6 +46,7 @@ object Deps {
const val support_fragment = "com.android.support:support-fragment:${Versions.support_libraries}"
const val support_constraintlayout = "com.android.support.constraint:constraint-layout:${Versions.constraint_layout}"
const val support_compat = "com.android.support:support-compat:${Versions.support_libraries}"
+ const val support_palette = "com.android.support:palette-v7:${Versions.support_libraries}"
const val arch_workmanager = "android.arch.work:work-runtime:${Versions.workmanager}"
diff --git a/components/browser/icons/README.md b/components/browser/icons/README.md
new file mode 100644
index 00000000000..9437e8ae53d
--- /dev/null
+++ b/components/browser/icons/README.md
@@ -0,0 +1,23 @@
+# [Android Components](../../../README.md) > Browser > Icons
+
+Favicons support for Android browser apps.
+
+## Usage
+
+### Setting up the dependency
+
+Use gradle to download the library from JCenter:
+
+```Groovy
+implementation "org.mozilla.components:browser-icons:{latest-version}"
+```
+### Quick Start
+
+(TBD)
+
+## License
+
+ 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/
+
diff --git a/components/browser/icons/build.gradle b/components/browser/icons/build.gradle
new file mode 100644
index 00000000000..ea6c08dcf56
--- /dev/null
+++ b/components/browser/icons/build.gradle
@@ -0,0 +1,61 @@
+/*
+ * 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/.
+ */
+
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion Config.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion Config.minSdkVersion
+ targetSdkVersion Config.targetSdkVersion
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ lintOptions {
+ // Lint reports (falsely) a bunch of unused resources for this project. Those resources
+ // are references from Kotlin code and it looks like Android lint can't see those references
+ // yet. Let's disable this check and retry once a newer SDK is available.
+ disable 'UnusedResources'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':support-utils')
+
+ implementation Deps.support_annotations
+ implementation Deps.kotlin_stdlib
+
+ testImplementation project(':support-test')
+
+ testImplementation Deps.testing_junit
+ testImplementation Deps.testing_robolectric
+ testImplementation Deps.testing_mockito
+}
+
+archivesBaseName = "icons"
+
+apply from: '../../../publish.gradle'
+ext.configurePublish(
+ 'org.mozilla.components',
+ 'icons',
+ 'Favicons support for Android browser apps.')
diff --git a/components/browser/icons/proguard-rules.pro b/components/browser/icons/proguard-rules.pro
new file mode 100644
index 00000000000..f1b424510da
--- /dev/null
+++ b/components/browser/icons/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/components/browser/icons/src/main/AndroidManifest.xml b/components/browser/icons/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..0721810803e
--- /dev/null
+++ b/components/browser/icons/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconCallback.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconCallback.java
new file mode 100644
index 00000000000..5b7af71340f
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconCallback.java
@@ -0,0 +1,25 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons;
+
+/**
+ * Interface for a callback that will be executed once an icon has been loaded successfully.
+ */
+public interface IconCallback {
+ void onIconResponse(IconResponse response);
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconDescriptor.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconDescriptor.java
new file mode 100644
index 00000000000..6c4997cdd17
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconDescriptor.java
@@ -0,0 +1,125 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons;
+
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+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, TYPE_BUNDLED_TILE })
+ @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;
+ @VisibleForTesting
+ static final int TYPE_BUNDLED_TILE = 15;
+
+ 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(@NonNull 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(@NonNull 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(@NonNull 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(@NonNull String url) {
+ return new IconDescriptor(TYPE_LOOKUP, url, 0, null);
+ }
+
+ /**
+ * Create a bundled tile icon at the given URL. MIME type or size is not known until we load
+ * the icons, but we know these icons are high fidelity. (Although the icons are png's at time
+ * of writing, they could be changed to webp or VectorDrawable in future.)
+ */
+ public static IconDescriptor createBundledTileIcon(@NonNull String url) {
+ return new IconDescriptor(TYPE_BUNDLED_TILE, url, 0, null);
+ }
+
+
+ private IconDescriptor(@IconType int type, @NonNull 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;
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconDescriptorComparator.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconDescriptorComparator.java
new file mode 100644
index 00000000000..582d0437f71
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconDescriptorComparator.java
@@ -0,0 +1,79 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.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 {
+ @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 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;
+ }
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequest.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequest.java
new file mode 100644
index 00000000000..f77a8f7a1e7
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequest.java
@@ -0,0 +1,227 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+
+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 */ boolean isPrivate;
+ /* package-private */ TreeSet icons;
+ /* package-private */ boolean skipNetwork;
+ /* package-private */ boolean backgroundThread;
+ /* package-private */ boolean skipDisk;
+ /* package-private */ boolean skipMemory;
+ /* package-private */ int targetSize;
+ /* package-private */ int minimumSizePxAfterScaling;
+ /* package-private */ boolean prepareOnly;
+ /* package-private */ float textSize;
+ 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.isPrivate = false;
+ this.skipMemory = false;
+ this.skipDisk = false;
+ this.skipNetwork = false;
+ this.targetSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
+ this.minimumSizePxAfterScaling = 0;
+ this.prepareOnly = false;
+
+ // textSize is only used in IconGenerator.java for creating a icon with specific text size.
+ this.textSize = 0;
+ }
+
+ /**
+ * 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 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;
+ }
+
+ /**
+ * Is this request initiated from a tab in private browsing mode?
+ */
+ public boolean isPrivateMode() {
+ return isPrivate;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Gets the minimum size the icon can be before we substitute a generated icon.
+ *
+ * N.B. the minimum size is compared to the icon *after* scaling: consider using
+ * {@link mozilla.components.browser.icons.processing.ResizingProcessor#MAX_SCALE_FACTOR}
+ * when setting this value.
+ */
+ public int getMinimumSizePxAfterScaling() {
+ return minimumSizePxAfterScaling;
+ }
+
+ /**
+ * 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 getIconIterator() {
+ return icons.iterator();
+ }
+
+ /**
+ * Get the required text size of the icon created by
+ * {@link mozilla.components.browser.icons.loader.IconGenerator}.
+ */
+ public float getTextSize() {
+ return textSize;
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequestBuilder.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequestBuilder.java
new file mode 100644
index 00000000000..659d9cde85a
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequestBuilder.java
@@ -0,0 +1,225 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.support.annotation.CheckResult;
+import android.text.TextUtils;
+import mozilla.components.browser.icons.processing.ResizingProcessor;
+
+import java.util.TreeSet;
+
+/**
+ * Builder for creating a request to load an icon.
+ */
+public class IconRequestBuilder {
+ private final IconRequest internal;
+
+ /* package-private */ IconRequestBuilder(Context context) {
+ internal = new IconRequest(context);
+ }
+
+ /* package-private */ IconRequestBuilder(IconRequest request) {
+ internal = request;
+ }
+
+ /**
+ * Set the URL of the page for which the icon should be loaded.
+ */
+ @CheckResult
+ public IconRequestBuilder pageUrl(String pageUrl) {
+ internal.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) {
+ internal.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) {
+ internal.icons.add(descriptor);
+ return this;
+ }
+
+ /**
+ * Set the private mode to avoid saving the result to the disk.
+ */
+ @CheckResult
+ public IconRequestBuilder setPrivateMode(boolean isPrivate) {
+ internal.isPrivate = isPrivate;
+ return this;
+ }
+
+ /**
+ * Skip the network and do not load an icon from a network connection.
+ */
+ @CheckResult
+ public IconRequestBuilder skipNetwork() {
+ internal.skipNetwork = true;
+ return this;
+ }
+
+ /**
+ * If shouldSkipNetwork is true then do not load icon from a network connection.
+ */
+ @CheckResult
+ public IconRequestBuilder skipNetworkIf(boolean shouldSkipNetwork) {
+ internal.skipNetwork = shouldSkipNetwork;
+ return this;
+ }
+
+ /**
+ * Skip the disk cache and do not load an icon from disk.
+ */
+ @CheckResult
+ public IconRequestBuilder skipDisk() {
+ internal.skipDisk = true;
+ return this;
+ }
+
+ /**
+ * Skip the memory cache and do not return a previously loaded icon.
+ */
+ @CheckResult
+ public IconRequestBuilder skipMemory() {
+ internal.skipMemory = true;
+ return this;
+ }
+
+ /**
+ * If shouldSkipMemory is true then skip the memory cache and do not return
+ * a previously loaded icon.
+ */
+ @CheckResult
+ public IconRequestBuilder skipMemoryIf(boolean shouldSkipMemory) {
+ internal.skipMemory = shouldSkipMemory;
+ 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() {
+ ActivityManager am = (ActivityManager) internal.getContext().getSystemService(Context.ACTIVITY_SERVICE);
+ internal.targetSize = am.getLauncherLargeIconSize();
+ return this;
+ }
+
+ /**
+ * The icon will be scaled to the given size.
+ */
+ public IconRequestBuilder targetSize(final int targetSize) {
+ internal.targetSize = targetSize;
+ return this;
+ }
+
+ /**
+ * The icon will be used in Activity Stream: a minimum size for the icon will be set.
+ */
+ public IconRequestBuilder forActivityStream() {
+ // This value was set anecdotally: 16px icons scaled up both look blurry and
+ // don't fill the space well. 32px icons look good enough.
+ internal.minimumSizePxAfterScaling = 32 * ResizingProcessor.MAX_SCALE_FACTOR;
+ 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() {
+ internal.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() {
+ internal.prepareOnly = true;
+ return this;
+ }
+
+ /**
+ * The text size will be resized to the given size, and this field is only used by
+ * {@link mozilla.components.browser.icons.loader.IconGenerator} for creating a new icon.
+ */
+ public IconRequestBuilder textSize(final float textSize) {
+ internal.textSize = textSize;
+ return this;
+ }
+
+ /**
+ * Return the request built with this builder.
+ */
+ @CheckResult
+ public IconRequest build() {
+ if (TextUtils.isEmpty(internal.pageUrl)) {
+ throw new IllegalStateException("Page URL is required");
+ }
+
+ IconRequest request = new IconRequest(internal.getContext());
+ request.pageUrl = internal.pageUrl;
+ request.privileged = internal.privileged;
+ request.isPrivate = internal.isPrivate;
+ request.icons = new TreeSet<>(internal.icons);
+ request.skipNetwork = internal.skipNetwork;
+ request.backgroundThread = internal.backgroundThread;
+ request.skipDisk = internal.skipDisk;
+ request.skipMemory = internal.skipMemory;
+ request.targetSize = internal.targetSize;
+ request.minimumSizePxAfterScaling = internal.minimumSizePxAfterScaling;
+ request.prepareOnly = internal.prepareOnly;
+ request.textSize = internal.textSize;
+ 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
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequestExecutor.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequestExecutor.java
new file mode 100644
index 00000000000..5e80642bd72
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequestExecutor.java
@@ -0,0 +1,152 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons;
+
+import android.support.annotation.NonNull;
+import mozilla.components.browser.icons.loader.*;
+import mozilla.components.browser.icons.preparation.*;
+import mozilla.components.browser.icons.processing.*;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * 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 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(),
+
+ // Suggested sites have icons bundled in the app - we should use them until the user has
+ // visited a specified page (after which the standard icon lookup will generally provide
+ // an update icon.
+ //new SuggestedSitePreparer(),
+
+ // 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 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(),
+
+ // Try to load from the suggested site tile builder
+ //new SuggestedSiteLoader(),
+
+ // 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 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(),
+
+ // Substitute a generated icon if the final result is not large enough.
+ // Expected to be called after ResizingProcessor.
+ new MinimumSizeProcessor()
+ );
+
+ 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(),
+ factory);
+ }
+
+ /**
+ * Submit the request for execution.
+ */
+ /* package-private */ static Future submit(IconRequest request) {
+ return EXECUTOR.submit(
+ new IconTask(request, PREPARERS, LOADERS, PROCESSORS, GENERATOR)
+ );
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconResponse.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconResponse.java
new file mode 100644
index 00000000000..e8097277c98
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconResponse.java
@@ -0,0 +1,179 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.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;
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconTask.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconTask.java
new file mode 100644
index 00000000000..5c3bc1b8c38
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconTask.java
@@ -0,0 +1,236 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons;
+
+import android.graphics.Bitmap;
+import android.support.annotation.NonNull;
+import mozilla.components.browser.icons.loader.IconLoader;
+import mozilla.components.browser.icons.preparation.Preparer;
+import mozilla.components.browser.icons.processing.Processor;
+import mozilla.components.support.base.log.logger.Logger;
+import mozilla.components.support.utils.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 {
+ private static final String LOGTAG = "Gecko/IconTask";
+ private static final boolean DEBUG = false;
+
+ private final List preparers;
+ private final List loaders;
+ private final List processors;
+ private final IconLoader generator;
+ private final IconRequest request;
+
+ /* package-private */ IconTask(
+ @NonNull IconRequest request,
+ @NonNull List preparers,
+ @NonNull List loaders,
+ @NonNull List 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) {
+ Logger.Companion.debug("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 (DEBUG) {
+ // TODO: Confirm that this new logic should be.
+ //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 {
+ Logger.Companion.error("Icon task crashed", t);
+ }
+ }
+
+ private boolean shouldLog() {
+ // Do not log anything if debugging is disabled and never log anything in a non-nightly build.
+ // TODO: Confirm that this new logic should be.
+ //return DEBUG && AppConstants.NIGHTLY_BUILD;
+ return DEBUG;
+ }
+
+ private void logPreparer(IconRequest request, Preparer preparer) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Logger.Companion.debug(String.format(" PREPARE %s" + " (%s)",
+ preparer.getClass().getSimpleName(),
+ request.getIconCount()), null);
+ }
+
+ private void logLoader(IconRequest request, IconLoader loader, IconResponse response) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Logger.Companion.debug(String.format(" LOAD [%s] %s : %s",
+ response != null ? "X" : " ",
+ loader.getClass().getSimpleName(),
+ request.getBestIcon().getUrl()), null);
+ }
+
+ private void logProcessor(Processor processor) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Logger.Companion.debug(" PROCESS " + processor.getClass().getSimpleName(), null);
+ }
+
+ private void logResponse(IconResponse response) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ final Bitmap bitmap = response.getBitmap();
+
+ Logger.Companion.debug(String.format("=> ICON: %sx%s", bitmap.getWidth(), bitmap.getHeight()), null);
+ }
+
+ private void logRequest(IconRequest request) {
+ if (!shouldLog()) {
+ return;
+ }
+
+ Logger.Companion.debug(String.format("REQUEST (%s) %s",
+ request.getIconCount(),
+ request.getPageUrl()), null);
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/Icons.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/Icons.java
new file mode 100644
index 00000000000..8507c956ae2
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/Icons.java
@@ -0,0 +1,47 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.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);
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconsHelper.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconsHelper.java
new file mode 100644
index 00000000000..72852354e7d
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconsHelper.java
@@ -0,0 +1,180 @@
+/*
+ * 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/.
+ */
+
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import mozilla.components.support.ktx.kotlin.StringKt;
+
+import java.io.ByteArrayOutputStream;
+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 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 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 (StringKt.isAboutPage(pageURL)) {
+ return pageURL;
+ }
+
+ if (!StringKt.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);
+ }
+
+ /**
+ * Create an icon callback that encodes the icon as base64 image URI and returns it via the
+ * EventCallback to JavaScript.
+ */
+ /*public static IconCallback createBase64EventCallback(final EventCallback callback) {
+ return new IconCallback() {
+ @Override
+ public void onIconResponse(IconResponse response) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+ try {
+ response.getBitmap().compress(
+ Bitmap.CompressFormat.PNG,
+ 100, // PNG which is lossless will ignore the quality setting
+ stream);
+
+ // TODO: Find generic alternative to this?
+ callback.sendSuccess(
+ "data:image/x-icon;base64,"
+ + Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP));
+ } finally {
+ IOUtils.safeStreamClose(stream);
+ }
+ }
+ };
+ }*/
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/FaviconDecoder.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/FaviconDecoder.java
new file mode 100644
index 00000000000..abd40165525
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/FaviconDecoder.java
@@ -0,0 +1,204 @@
+/*
+ * 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/.
+ */
+
+/* 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 mozilla.components.browser.icons.decoders;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.Base64;
+import android.util.Log;
+import mozilla.components.support.base.log.logger.Logger;
+import mozilla.components.support.utils.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";
+
+ 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, null);
+ 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) {
+ Logger.Companion.warn("Can't decode null data: URI.", null);
+ 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) {
+ Logger.Companion.warn("No ',' in data: URI; malformed?", null);
+ return null;
+ }
+
+ try {
+ String base64 = uri.substring(offset);
+ byte[] raw = Base64.decode(base64, Base64.DEFAULT);
+ return decodeFavicon(context, raw);
+ } catch (Exception e) {
+ Logger.Companion.warn("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 {
+ 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.");
+ }
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/ICODecoder.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/ICODecoder.java
new file mode 100644
index 00000000000..8a0fe3ed328
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/ICODecoder.java
@@ -0,0 +1,397 @@
+/*
+ * 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/.
+ */
+
+/* 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 mozilla.components.browser.icons.decoders;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.support.annotation.VisibleForTesting;
+import android.util.SparseArray;
+import mozilla.components.browser.icons.R;
+import mozilla.components.support.utils.BitmapUtils;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Utility class for determining the region of a provided array which contains the largest bitmap,
+ * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning
+ * unwanted entries from ICO files, if desired.
+ *
+ * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format.
+ * A mixture of image types may not exist.
+ *
+ * The format consists of a header specifying the number, n, of images, followed by the Icon Directory.
+ *
+ * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for
+ * the corresponding image, the dimensions, colour information, payload size, and location in the file.
+ *
+ * All numerical fields follow a little-endian byte ordering.
+ *
+ * Header format:
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Image count (n) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * The type field is expected to always be 1. CUR format images should not be used for Favicons.
+ *
+ *
+ * Icon Directory Entry format:
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Image width | Image height | Palette size | Reserved (0) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Colour plane count | Bits per pixel |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Size of image data, in bytes |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Start of image data, as an offset from start of file |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Image dimensions of zero are to be interpreted as image dimensions of 256.
+ *
+ * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero
+ * if the payload is a PNG or no palette is in use.
+ *
+ * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be
+ * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field.
+ * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.)
+ *
+ *
+ * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps.
+ *
+ * This class is not thread safe.
+ */
+public class ICODecoder implements Iterable {
+ // 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;
+
+ 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 preferenceArray = new SparseArray();
+
+ 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, null);
+ }
+
+ // 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, 0, decodeTarget.length, null);
+ }
+
+ /**
+ * 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 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
+ public IconDirectoryEntry[] getIconDirectory() {
+ return iconDirectory;
+ }
+
+ @VisibleForTesting
+ 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 {
+ 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;
+ }
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/IconDirectoryEntry.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/IconDirectoryEntry.java
new file mode 100644
index 00000000000..ac7ee1182dd
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/IconDirectoryEntry.java
@@ -0,0 +1,214 @@
+/*
+ * 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/.
+ */
+
+/* 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 mozilla.components.browser.icons.decoders;
+
+import android.support.annotation.VisibleForTesting;
+
+/**
+ * Representation of an ICO file ICONDIRENTRY structure.
+ */
+public class IconDirectoryEntry implements Comparable {
+
+ 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;
+
+ 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
+ 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 +
+ '}';
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/LoadFaviconResult.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/LoadFaviconResult.java
new file mode 100644
index 00000000000..6ed75761c94
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoders/LoadFaviconResult.java
@@ -0,0 +1,139 @@
+/*
+ * 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/.
+ */
+
+/* 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 mozilla.components.browser.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 bitmapsDecoded;
+
+ public Iterator 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 iconMap = new SparseArray<>();
+ final List 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 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);
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/ContentProviderLoader.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/ContentProviderLoader.java
new file mode 100644
index 00000000000..87676c8976f
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/ContentProviderLoader.java
@@ -0,0 +1,103 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.loader;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.text.TextUtils;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.decoders.FaviconDecoder;
+import mozilla.components.browser.icons.decoders.LoadFaviconResult;
+
+/**
+ * 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 {
+ private static final String TOUCHICON = "touchicon";
+ private static final String FAVICON = "favicon";
+
+ @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[] {
+ TOUCHICON,
+ 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, TOUCHICON, targetSize);
+ if (icon != null) {
+ return IconResponse.create(icon);
+ }
+
+ icon = decodeFromCursor(request.getContext(), cursor, 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);
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DataUriLoader.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DataUriLoader.java
new file mode 100644
index 00000000000..3b233fe9c90
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DataUriLoader.java
@@ -0,0 +1,42 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.loader;
+
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.decoders.FaviconDecoder;
+import mozilla.components.browser.icons.decoders.LoadFaviconResult;
+
+/**
+ * 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()));
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconDownloader.kt b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconDownloader.kt
new file mode 100644
index 00000000000..34ac648b924
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconDownloader.kt
@@ -0,0 +1,234 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.loader
+
+import android.content.Context
+import android.support.annotation.VisibleForTesting
+import android.util.Log
+import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.IconResponse
+import mozilla.components.browser.icons.decoders.FaviconDecoder
+import mozilla.components.browser.icons.decoders.LoadFaviconResult
+import mozilla.components.browser.icons.storage.FailureCache
+import mozilla.components.browser.icons.util.ProxySelector
+import mozilla.components.support.ktx.kotlin.*
+
+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.
+ */
+class IconDownloader : IconLoader {
+
+ override fun load(request: IconRequest): IconResponse? {
+ if (request.shouldSkipNetwork()) {
+ return null
+ }
+
+ val iconUrl = request.bestIcon.url
+
+ if (!iconUrl.isHttpOrHttps()) {
+ return null
+ }
+
+ try {
+ val result = downloadAndDecodeImage(request.context, iconUrl) ?: return null
+
+ val bitmap = result.getBestBitmap(request.targetSize) ?: return null
+
+ return IconResponse.createFromNetwork(bitmap, iconUrl)
+ } catch (e: Exception) {
+ Log.e(LOGTAG, "Error reading favicon", e)
+ } catch (e: OutOfMemoryError) {
+ 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.
+ */
+ fun downloadAndDecodeImage(context: Context, targetFaviconURL: String): LoadFaviconResult? {
+ // Try the URL we were given.
+ val connection = tryDownload(targetFaviconURL) ?: return null
+
+ // Decode the image from the fetched response.
+ return connection.inputStream.use {
+ decodeImageFromResponse(context, it, connection.getHeaderFieldInt("Content-Length", -1))
+ }.also {
+ connection.disconnect()
+ }
+
+// // Decode the image from the fetched response.
+// try {
+// stream = connection.inputStream
+// return decodeImageFromResponse(context, stream, connection.getHeaderFieldInt("Content-Length", -1))
+// } catch (e: IOException) {
+// Log.d(LOGTAG, "IOException while reading and decoding ixon", e)
+// return null
+// } finally {
+// // Close the stream and free related resources.
+// // TODO: Use try-with-resources in Kotlin to close this stream.
+// //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 fun tryDownload(faviconURI: String): HttpURLConnection? {
+ val visitedLinkSet = HashSet()
+ visitedLinkSet.add(faviconURI)
+ return tryDownloadRecurse(faviconURI, visitedLinkSet)
+ }
+
+ /**
+ * Try to download from the favicon URL and recursively follow redirects.
+ */
+ private fun tryDownloadRecurse(faviconURI: String, visited: HashSet): HttpURLConnection? {
+ if (visited.size == MAX_REDIRECTS_TO_FOLLOW) {
+ return null
+ }
+
+ var connection: HttpURLConnection? = null
+
+ try {
+ connection = connectTo(faviconURI)
+
+ // Was the response a failure?
+ val status = connection.responseCode
+
+ // Handle HTTP status codes requesting a redirect.
+ if (status in 300..399) {
+ val newURI = connection.getHeaderField("Location")
+
+ // Handle mad web servers.
+ try {
+ if (newURI == null || newURI == 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
+ }
+ } catch (e: IOException) {
+ connection?.disconnect()
+ return null
+ } catch (e: URISyntaxException) {
+ connection?.disconnect()
+ return null
+ }
+
+ return connection
+ }
+
+ @Throws(URISyntaxException::class, IOException::class)
+ fun connectTo(uri: String): HttpURLConnection {
+ val connection = ProxySelector.openConnectionWithProxy(
+ URI(uri)
+ ) as HttpURLConnection
+
+ // TODO: Find a way to pass the User-Agent to the IconDownloader
+ //connection.setRequestProperty("User-Agent", GeckoApplication.getDefaultUAString())
+ connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Android 5.0; Mobile; rv: 63.0) Gecko/63.0 Firefox/63.0")
+
+ // 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.instanceFollowRedirects = false
+
+ 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.
+ */
+ @Throws(IOException::class)
+ private fun decodeImageFromResponse(
+ context: Context,
+ stream: InputStream?,
+ contentLength: Int
+ ): LoadFaviconResult? {
+ // This may not be provided, but if it is, it's useful.
+ val 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...
+ contentLength + 1
+ } else {
+ // No declared size, so guess and reallocate later if it turns out to be too small.
+ DEFAULT_FAVICON_BUFFER_SIZE_BYTES
+ }
+
+ stream?.use {
+ // Read the InputStream into a byte[].
+ val data = it.readBytes(bufferSize)
+
+ // Having downloaded the image, decode it.
+ return FaviconDecoder.decodeFavicon(context, data, 0, data.size)
+ }
+
+ return null
+ }
+
+ companion object {
+ private const val LOGTAG = "Gecko/Downloader"
+
+ /**
+ * The maximum number of http redirects (3xx) until we give up.
+ */
+ private const val 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 const val DEFAULT_FAVICON_BUFFER_SIZE_BYTES = 25000
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconGenerator.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconGenerator.java
new file mode 100644
index 00000000000..c83801b0edd
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconGenerator.java
@@ -0,0 +1,169 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.loader;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.*;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.R;
+import mozilla.components.support.ktx.kotlin.StringKt;
+
+/**
+ * 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 = {
+ 0xFF9A4C00,
+ 0xFFAB008D,
+ 0xFF4C009C,
+ 0xFF002E9C,
+ 0xFF009EC2,
+ 0xFF009D02,
+ 0xFF51AB00,
+ 0xFF36385A,
+ };
+
+ @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(), request.getTargetSize(), request.getTextSize());
+ }
+
+ public static IconResponse generate(Context context, String pageURL) {
+ return generate(context, pageURL, 0, 0);
+ }
+
+ /**
+ * Generate default favicon for the given page URL.
+ */
+ public static IconResponse generate(final Context context, final String pageURL,
+ int widthAndHeight, float textSize) {
+ final Resources resources = context.getResources();
+ if (widthAndHeight == 0) {
+ 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);
+
+ if (textSize == 0) {
+ // The text size is calculated dynamically based on the target icon size (1/8th). For an icon
+ // size of 112dp we'd use a text size of 14dp (112 / 8).
+ textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ widthAndHeight / 8,
+ resources.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 characterS
+ snippet = StringKt.stripCommonSubdomains(snippet);
+
+ return snippet;
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconLoader.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconLoader.java
new file mode 100644
index 00000000000..d78a1defbc6
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconLoader.java
@@ -0,0 +1,28 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.loader;
+
+import android.support.annotation.Nullable;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.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);
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/MemoryLoader.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/MemoryLoader.java
new file mode 100644
index 00000000000..00c171bfa3e
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/MemoryLoader.java
@@ -0,0 +1,38 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.loader;
+
+
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.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);
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/AddDefaultIconUrl.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/AddDefaultIconUrl.java
new file mode 100644
index 00000000000..77ce4e47eb9
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/AddDefaultIconUrl.java
@@ -0,0 +1,44 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.preparation;
+
+import android.text.TextUtils;
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconsHelper;
+import mozilla.components.support.ktx.kotlin.StringKt;
+
+/**
+ * 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 (!StringKt.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();
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterKnownFailureUrls.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterKnownFailureUrls.java
new file mode 100644
index 00000000000..9a19909c39b
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterKnownFailureUrls.java
@@ -0,0 +1,36 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.preparation;
+
+
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.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 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();
+ }
+ }
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterMimeTypes.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterMimeTypes.java
new file mode 100644
index 00000000000..746c37b2813
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterMimeTypes.java
@@ -0,0 +1,44 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.preparation;
+
+import android.text.TextUtils;
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.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 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();
+ }
+ }
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterPrivilegedUrls.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterPrivilegedUrls.java
new file mode 100644
index 00000000000..8c99b19deed
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/FilterPrivilegedUrls.java
@@ -0,0 +1,36 @@
+/*
+ * 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 mozilla.components.browser.icons.preparation;
+
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.support.ktx.kotlin.StringKt;
+
+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 iterator = request.getIconIterator();
+
+ while (iterator.hasNext()) {
+ IconDescriptor descriptor = iterator.next();
+
+ if (!StringKt.isHttpOrHttps(descriptor.getUrl())) {
+ iterator.remove();
+ }
+ }
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/LookupIconUrl.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/LookupIconUrl.java
new file mode 100644
index 00000000000..7836aa25642
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/LookupIconUrl.java
@@ -0,0 +1,44 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.preparation;
+
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.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;
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/Preparer.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/Preparer.java
new file mode 100644
index 00000000000..bb2704f3b50
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparation/Preparer.java
@@ -0,0 +1,26 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.preparation;
+
+
+import mozilla.components.browser.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);
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/ColorProcessor.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/ColorProcessor.java
new file mode 100644
index 00000000000..931e73e1301
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/ColorProcessor.java
@@ -0,0 +1,108 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.processing;
+
+import android.graphics.Bitmap;
+import android.support.annotation.ColorInt;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.support.utils.BitmapUtils;
+
+/**
+ * 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 int DEFAULT_COLOR = 0xFFB1B1B3; // 0 == No color, here we use photon color
+
+ @Override
+ public void process(IconRequest request, IconResponse response) {
+ if (response.hasColor()) {
+ return;
+ }
+
+ final Bitmap bitmap = response.getBitmap();
+
+ final @ColorInt Integer edgeColor = getEdgeColor(bitmap);
+ if (edgeColor != null) {
+ response.updateColor(edgeColor);
+ return;
+ }
+
+ final @ColorInt int dominantColor = BitmapUtils.getDominantColor(response.getBitmap(), DEFAULT_COLOR);
+ response.updateColor(dominantColor & 0x7FFFFFFF);
+ }
+
+ /**
+ * If a bitmap has a consistent edge colour (i.e. if all the border pixels have the same colour),
+ * return that colour.
+ * @param bitmap The Bitmap in question.
+ * @return The edge colour. null if there is no consistent edge color.
+ */
+ @ColorInt
+ private Integer getEdgeColor(final Bitmap bitmap) {
+ final int width = bitmap.getWidth();
+ final int height = bitmap.getHeight();
+
+ // Only allocate an array once, with the max width we need once, to minimise the number
+ // of allocations.
+ @ColorInt int[] edge = new int[Math.max(width, height)];
+
+ // Top:
+ bitmap.getPixels(edge, 0, width, 0, 0, width, 1);
+ final @ColorInt Integer edgeColor = getEdgeColorFromSingleDimension(edge, width);
+ if (edgeColor == null) {
+ return null;
+ }
+
+ // Bottom:
+ bitmap.getPixels(edge, 0, width, 0, height - 1, width, 1);
+ if (!edgeColor.equals(getEdgeColorFromSingleDimension(edge, width))) {
+ return null;
+ }
+
+ // Left:
+ bitmap.getPixels(edge, 0, 1, 0, 0, 1, height);
+ if (!edgeColor.equals(getEdgeColorFromSingleDimension(edge, height))) {
+ return null;
+ }
+
+ // Right:
+ bitmap.getPixels(edge, 0, 1, width - 1, 0, 1, height);
+ if (!edgeColor.equals(getEdgeColorFromSingleDimension(edge, height))) {
+ return null;
+ }
+
+ return edgeColor;
+ }
+
+ /**
+ * Obtain the colour for a given edge if all colors are the same.
+ *
+ * @param edge An array containing the color values of the pixels constituting the edge of a bitmap.
+ * @param length The length of the array to be traversed. Must be smaller than, or equal to
+ * the total length of the array.
+ * @return The colour contained within the array, or null if colours vary.
+ */
+ @ColorInt
+ private Integer getEdgeColorFromSingleDimension(@ColorInt int[] edge, int length) {
+ @ColorInt int color = edge[0];
+
+ for (int i = 1; i < length; ++i) {
+ if (edge[i] != color) {
+ return null;
+ }
+ }
+
+ return color;
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/MemoryProcessor.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/MemoryProcessor.java
new file mode 100644
index 00000000000..1db409a85df
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/MemoryProcessor.java
@@ -0,0 +1,44 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.processing;
+
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.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);
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/MinimumSizeProcessor.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/MinimumSizeProcessor.java
new file mode 100644
index 00000000000..9c2fb8103e5
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/MinimumSizeProcessor.java
@@ -0,0 +1,43 @@
+/*
+ * 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/.
+ */
+
+/* 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 mozilla.components.browser.icons.processing;
+
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.loader.IconGenerator;
+
+/**
+ * Substitutes a generated image for the given icon if it doesn't not meet the minimum size requirement.
+ *
+ * Ideally, we would discard images while we're loading them: if we found an icon that was too small, we
+ * would have the opportunity to search other places before generating an icon and we wouldn't duplicate
+ * the generate icon call. However, this turned out to be non-trivial: a single icon will appear as two
+ * different sizes when given to the loader, the original size the first time it's loaded and the size after
+ * {@link ResizingProcessor} the second time, when it's loaded from the cache. It turned out to be much simpler
+ * to enforce the requirement that...
+ *
+ * This processor is expected to be called after {@link ResizingProcessor}.
+ */
+public class MinimumSizeProcessor implements Processor {
+
+ @Override
+ public void process(final IconRequest request, final IconResponse response) {
+ // We expect that this bitmap has already been scaled by ResizingProcessor.
+ if (response.getBitmap().getWidth() >= request.getMinimumSizePxAfterScaling()) {
+ return;
+ }
+
+ // This is fragile: ideally, we can return the generated response but instead we're mutating the argument.
+ final IconResponse generatedResponse = IconGenerator.generate(request.getContext(), request.getPageUrl());
+ response.updateBitmap(generatedResponse.getBitmap());
+ response.updateColor(generatedResponse.getColor());
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/Processor.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/Processor.java
new file mode 100644
index 00000000000..47ca1dbee64
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/Processor.java
@@ -0,0 +1,28 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.processing;
+
+
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.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);
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/ResizingProcessor.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/ResizingProcessor.java
new file mode 100644
index 00000000000..34a14a1a01d
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/processing/ResizingProcessor.java
@@ -0,0 +1,81 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.processing;
+
+import android.graphics.Bitmap;
+import android.support.annotation.VisibleForTesting;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+
+/**
+ * Processor implementation for resizing the loaded icon based on the target size.
+ */
+public class ResizingProcessor implements Processor {
+ // This is the largest factor we'll scale up an image by: the goal is an image
+ // that both fills the top site space well but does not have extreme resizing
+ // artifacts. This number was chosen anecdotally by comparing variously-sized
+ // favicons across devices to see which factor(s) looked the best. bug 1398970
+ // is filed to take a more comprehensive approach to favicons.
+ public static final int MAX_SCALE_FACTOR = 3;
+
+ @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 it (to a limit)!
+ // 'largestSize' now reflects the maximum size we can upscale to.
+ final int largestSize = size * MAX_SCALE_FACTOR;
+
+ 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;
+ }
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/storage/FailureCache.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/storage/FailureCache.java
new file mode 100644
index 00000000000..54c7c808276
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/storage/FailureCache.java
@@ -0,0 +1,77 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.storage;
+
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+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 cache;
+
+ private FailureCache() {
+ cache = new LruCache<>(MAX_ENTRIES);
+ }
+
+ /**
+ * Remember this icon URL after loading from it (over the network) has failed.
+ */
+ public void rememberFailure(@NonNull String iconUrl) {
+ cache.put(iconUrl, SystemClock.elapsedRealtime());
+ }
+
+ /**
+ * Has loading from this URL failed previously and recently?
+ */
+ public boolean isKnownFailure(@NonNull 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();
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/storage/MemoryStorage.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/storage/MemoryStorage.java
new file mode 100644
index 00000000000..172057beccc
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/storage/MemoryStorage.java
@@ -0,0 +1,116 @@
+/*
+ * 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/.
+ */
+
+/* -*- 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 mozilla.components.browser.icons.storage;
+
+import android.graphics.Bitmap;
+import android.support.annotation.Nullable;
+import android.util.LruCache;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.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 iconCache; // Guarded by 'this'
+ private final LruCache mappingCache; // Guarded by 'this'
+
+ private MemoryStorage() {
+ iconCache = new LruCache(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();
+ }
+}
diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/util/ProxySelector.java b/components/browser/icons/src/main/java/mozilla/components/browser/icons/util/ProxySelector.java
new file mode 100644
index 00000000000..bf46cf311d9
--- /dev/null
+++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/util/ProxySelector.java
@@ -0,0 +1,145 @@
+/*
+ * 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/.
+ */
+
+// This code is based on AOSP /libcore/luni/src/main/java/java/net/ProxySelectorImpl.java
+
+package mozilla.components.browser.icons.util;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+
+public class ProxySelector {
+ public static URLConnection openConnectionWithProxy(URI uri) throws IOException {
+ java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
+ Proxy proxy = Proxy.NO_PROXY;
+ if (ps != null) {
+ List proxies = ps.select(uri);
+ if (proxies != null && !proxies.isEmpty()) {
+ proxy = proxies.get(0);
+ }
+ }
+
+ return uri.toURL().openConnection(proxy);
+ }
+
+ public ProxySelector() {
+ }
+
+ public Proxy select(String scheme, String host) {
+ int port = -1;
+ Proxy proxy = null;
+ String nonProxyHostsKey = null;
+ boolean httpProxyOkay = true;
+ if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ nonProxyHostsKey = "http.nonProxyHosts";
+ proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
+ proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("ftp".equalsIgnoreCase(scheme)) {
+ port = 80; // not 21 as you might guess
+ nonProxyHostsKey = "ftp.nonProxyHosts";
+ proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("socket".equalsIgnoreCase(scheme)) {
+ httpProxyOkay = false;
+ } else {
+ return Proxy.NO_PROXY;
+ }
+
+ if (nonProxyHostsKey != null
+ && isNonProxyHost(host, System.getProperty(nonProxyHostsKey))) {
+ return Proxy.NO_PROXY;
+ }
+
+ if (proxy != null) {
+ return proxy;
+ }
+
+ if (httpProxyOkay) {
+ proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
+ if (proxy != null) {
+ return proxy;
+ }
+ }
+
+ proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
+ if (proxy != null) {
+ return proxy;
+ }
+
+ return Proxy.NO_PROXY;
+ }
+
+ /**
+ * Returns the proxy identified by the {@code hostKey} system property, or
+ * null.
+ */
+ @Nullable
+ private Proxy lookupProxy(String hostKey, String portKey, Proxy.Type type, int defaultPort) {
+ final String host = System.getProperty(hostKey);
+ if (TextUtils.isEmpty(host)) {
+ return null;
+ }
+
+ final int port = getSystemPropertyInt(portKey, defaultPort);
+ if (port == -1) {
+ // Port can be -1. See bug 1270529.
+ return null;
+ }
+
+ return new Proxy(type, InetSocketAddress.createUnresolved(host, port));
+ }
+
+ private int getSystemPropertyInt(String key, int defaultValue) {
+ String string = System.getProperty(key);
+ if (string != null) {
+ try {
+ return Integer.parseInt(string);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns true if the {@code nonProxyHosts} system property pattern exists
+ * and matches {@code host}.
+ */
+ private boolean isNonProxyHost(String host, String nonProxyHosts) {
+ if (host == null || nonProxyHosts == null) {
+ return false;
+ }
+
+ // construct pattern
+ StringBuilder patternBuilder = new StringBuilder();
+ for (int i = 0; i < nonProxyHosts.length(); i++) {
+ char c = nonProxyHosts.charAt(i);
+ switch (c) {
+ case '.':
+ patternBuilder.append("\\.");
+ break;
+ case '*':
+ patternBuilder.append(".*");
+ break;
+ default:
+ patternBuilder.append(c);
+ }
+ }
+ // check whether the host is the nonProxyHosts.
+ String pattern = patternBuilder.toString();
+ return host.matches(pattern);
+ }
+}
+
diff --git a/components/browser/icons/src/main/res/values/dimens.xml b/components/browser/icons/src/main/res/values/dimens.xml
new file mode 100644
index 00000000000..d42c5f2fd5e
--- /dev/null
+++ b/components/browser/icons/src/main/res/values/dimens.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ 112dp
+ 2dp
+
+ 112dp
+
+ 24dp
+
+
\ No newline at end of file
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconDescriptor.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconDescriptor.java
new file mode 100644
index 00000000000..d64ec3bf335
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconDescriptor.java
@@ -0,0 +1,69 @@
+/*
+ * 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 mozilla.components.browser.icons;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconDescriptor {
+ private static final String ICON_URL = "https://www.mozilla.org/favicon.ico";
+ private static final String MIME_TYPE = "image/png";
+ private static final int ICON_SIZE = 64;
+
+ @Test
+ public void testGenericIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createGenericIcon(ICON_URL);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertNull(descriptor.getMimeType());
+ Assert.assertEquals(0, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_GENERIC, descriptor.getType());
+ }
+
+ @Test
+ public void testFaviconIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createFavicon(ICON_URL, ICON_SIZE, MIME_TYPE);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertEquals(MIME_TYPE, descriptor.getMimeType());
+ Assert.assertEquals(ICON_SIZE, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_FAVICON, descriptor.getType());
+ }
+
+ @Test
+ public void testTouchIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createTouchicon(ICON_URL, ICON_SIZE, MIME_TYPE);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertEquals(MIME_TYPE, descriptor.getMimeType());
+ Assert.assertEquals(ICON_SIZE, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_TOUCHICON, descriptor.getType());
+ }
+
+ @Test
+ public void testLookupIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createLookupIcon(ICON_URL);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertNull(descriptor.getMimeType());
+ Assert.assertEquals(0, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_LOOKUP, descriptor.getType());
+ }
+
+ @Test
+ public void testBundledTileIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createBundledTileIcon(ICON_URL);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertNull(descriptor.getMimeType());
+ Assert.assertEquals(0, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_BUNDLED_TILE, descriptor.getType());
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconDescriptorComparator.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconDescriptorComparator.java
new file mode 100644
index 00000000000..0b0ff7f4cd3
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconDescriptorComparator.java
@@ -0,0 +1,154 @@
+/*
+ * 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 mozilla.components.browser.icons;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.TreeSet;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconDescriptorComparator {
+ private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico";
+ private static final String TEST_ICON_URL_3 = "http://www.example.com/favicon.ico";
+
+ private static final String TEST_MIME_TYPE = "image/png";
+ private static final int TEST_SIZE = 32;
+
+ @Test
+ public void testIconsWithTheSameUrlAreTreatedAsEqual() {
+ final IconDescriptor descriptor1 = IconDescriptor.createGenericIcon(TEST_ICON_URL_1);
+ final IconDescriptor descriptor2 = IconDescriptor.createGenericIcon(TEST_ICON_URL_1);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(0, comparator.compare(descriptor1, descriptor2));
+ Assert.assertEquals(0, comparator.compare(descriptor2, descriptor1));
+ }
+
+ @Test
+ public void testTouchIconsAreRankedHigherThanFavicons() {
+ final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE);
+ final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(faviconDescriptor, touchIconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, faviconDescriptor));
+ }
+
+ @Test
+ public void testFaviconsAndTouchIconsAreRankedHigherThanGenericIcons() {
+ final IconDescriptor genericDescriptor = IconDescriptor.createGenericIcon(TEST_ICON_URL_1);
+
+ final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE);
+ final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_3, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(genericDescriptor, faviconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(faviconDescriptor, genericDescriptor));
+
+ Assert.assertEquals(1, comparator.compare(genericDescriptor, touchIconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, genericDescriptor));
+ }
+
+ @Test
+ public void testLookupIconsAreRankedHigherThanGenericIcons() {
+ final IconDescriptor genericDescriptor = IconDescriptor.createGenericIcon(TEST_ICON_URL_1);
+ final IconDescriptor lookupDescriptor = IconDescriptor.createLookupIcon(TEST_ICON_URL_2);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(genericDescriptor, lookupDescriptor));
+ Assert.assertEquals(-1, comparator.compare(lookupDescriptor, genericDescriptor));
+ }
+
+ @Test
+ public void testFaviconsAndTouchIconsAreRankedHigherThanLookupIcons() {
+ final IconDescriptor lookupDescriptor = IconDescriptor.createLookupIcon(TEST_ICON_URL_1);
+
+ final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE);
+ final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_3, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(lookupDescriptor, faviconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(faviconDescriptor, lookupDescriptor));
+
+ Assert.assertEquals(1, comparator.compare(lookupDescriptor, touchIconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, lookupDescriptor));
+ }
+
+ @Test
+ public void testLargestIconOfSameTypeIsSelected() {
+ final IconDescriptor smallDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, 16, TEST_MIME_TYPE);
+ final IconDescriptor largeDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(smallDescriptor, largeDescriptor));
+ Assert.assertEquals(-1, comparator.compare(largeDescriptor, smallDescriptor));
+ }
+
+ @Test
+ public void testContainerTypesArePreferred() {
+ final IconDescriptor containerDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, "image/x-icon");
+ final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, "image/png");
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(faviconDescriptor, containerDescriptor));
+ Assert.assertEquals(-1, comparator.compare(containerDescriptor, faviconDescriptor));
+ }
+
+ @Test
+ public void testWithNoDifferences() {
+ final IconDescriptor descriptor1 = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE);
+ final IconDescriptor descriptor2 = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertNotEquals(0, comparator.compare(descriptor1, descriptor2));
+ Assert.assertNotEquals(0, comparator.compare(descriptor2, descriptor1));
+ }
+
+ @Test
+ public void testWithSameObject() {
+ final IconDescriptor descriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+ Assert.assertEquals(0, comparator.compare(descriptor, descriptor));
+ }
+
+ /**
+ * This test reconstructs the scenario from bug 1331808. A comparator implementation that does
+ * not return a consistent order can break the implementation of remove() of the TreeSet class.
+ */
+ @Test
+ public void testBug1331808() {
+ TreeSet set = new TreeSet<>(new IconDescriptorComparator());
+
+ set.add(IconDescriptor.createFavicon("http://example.org/new-logo32.jpg", 0, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/new-logo57.jpg", 0, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/new-logo76.jpg", 76, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/new-logo120.jpg", 120, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/new-logo152.jpg", 114, ""));
+ set.add(IconDescriptor.createFavicon("http://example.org/02.png", 32, ""));
+ set.add(IconDescriptor.createFavicon("http://example.org/01.png", 192, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/03.png", 0, ""));
+
+ for (int i = 8; i > 0; i--) {
+ Assert.assertEquals("items in set before deleting: " + i, i, set.size());
+ Assert.assertTrue("item removed successfully: " + i, set.remove(set.first()));
+ Assert.assertEquals("items in set after deleting: " + i, i - 1, set.size());
+ }
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconRequest.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconRequest.java
new file mode 100644
index 00000000000..677809a1e18
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconRequest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 mozilla.components.browser.icons;
+
+import junit.framework.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.TreeSet;
+
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconRequest {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico";
+
+ @Test
+ public void testIconHandling() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ Assert.assertEquals(0, request.getIconCount());
+ Assert.assertFalse(request.hasIconDescriptors());
+
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1))
+ .deferBuild();
+
+ Assert.assertEquals(1, request.getIconCount());
+ Assert.assertTrue(request.hasIconDescriptors());
+
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2))
+ .deferBuild();
+
+ Assert.assertEquals(2, request.getIconCount());
+ Assert.assertTrue(request.hasIconDescriptors());
+
+ Assert.assertEquals(TEST_ICON_URL_2, request.getBestIcon().getUrl());
+
+ request.moveToNextIcon();
+
+ Assert.assertEquals(1, request.getIconCount());
+ Assert.assertTrue(request.hasIconDescriptors());
+
+ Assert.assertEquals(TEST_ICON_URL_1, request.getBestIcon().getUrl());
+
+ request.moveToNextIcon();
+
+ Assert.assertEquals(0, request.getIconCount());
+ Assert.assertFalse(request.hasIconDescriptors());
+ }
+
+ /**
+ * If removing an icon from the internal set failed then we want to throw an exception.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testMoveToNextIconThrowsException() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ @SuppressWarnings("unchecked")
+ //noinspection unchecked - Creating a mock of a generic type
+ final TreeSet icons = (TreeSet) mock(TreeSet.class);
+ request.icons = icons;
+
+ //noinspection SuspiciousMethodCalls
+ doReturn(false).when(request.icons).remove(anyObject());
+
+ request.moveToNextIcon();
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconRequestBuilder.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconRequestBuilder.java
new file mode 100644
index 00000000000..5e636164eb7
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconRequestBuilder.java
@@ -0,0 +1,262 @@
+/*
+ * 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 mozilla.components.browser.icons;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Iterator;
+
+import static org.junit.Assert.fail;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconRequestBuilder {
+ private static final String TEST_PAGE_URL_1 = "http://www.mozilla.org";
+ private static final String TEST_PAGE_URL_2 = "http://www.example.org";
+ private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico";
+
+ @Test
+ public void testPrivileged() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.isPrivileged());
+
+ request.modify()
+ .privileged(true)
+ .deferBuild();
+
+ Assert.assertTrue(request.isPrivileged());
+ }
+
+ @Test
+ public void testPageUrl() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertEquals(TEST_PAGE_URL_1, request.getPageUrl());
+
+ request.modify()
+ .pageUrl(TEST_PAGE_URL_2)
+ .deferBuild();
+
+ Assert.assertEquals(TEST_PAGE_URL_2, request.getPageUrl());
+ }
+
+ @Test
+ public void testIcons() {
+ // Initially a request is empty.
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ // Adding one icon URL.
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1))
+ .deferBuild();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ // Adding the same icon URL again is ignored.
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1))
+ .deferBuild();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ // Adding another new icon URL.
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2))
+ .deferBuild();
+
+ Assert.assertEquals(2, request.getIconCount());
+ }
+
+ @Test
+ public void testPrivateMode() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.isPrivateMode());
+
+ request.modify()
+ .setPrivateMode(true)
+ .deferBuild();
+
+ Assert.assertTrue(request.isPrivateMode());
+
+ request.modify()
+ .setPrivateMode(false)
+ .deferBuild();
+
+ Assert.assertFalse(request.isPrivateMode());
+ }
+
+ @Test
+ public void testSkipNetwork() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldSkipNetwork());
+
+ request.modify()
+ .skipNetwork()
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldSkipNetwork());
+ }
+
+ @Test
+ public void testSkipNetworkIf() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldSkipNetwork());
+
+ request.modify()
+ .skipNetworkIf(true)
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldSkipNetwork());
+
+ request.modify()
+ .skipNetworkIf(false)
+ .deferBuild();
+
+ Assert.assertFalse(request.shouldSkipNetwork());
+ }
+
+ @Test
+ public void testSkipDisk() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldSkipDisk());
+
+ request.modify()
+ .skipDisk()
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldSkipDisk());
+ }
+
+ @Test
+ public void testSkipMemory() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldSkipMemory());
+
+ request.modify()
+ .skipMemory()
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldSkipMemory());
+ }
+
+ @Test
+ public void testSkipMemoryIf() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldSkipMemory());
+
+ request.modify()
+ .skipMemoryIf(true)
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldSkipMemory());
+
+ request.modify()
+ .skipMemoryIf(false)
+ .deferBuild();
+
+ Assert.assertFalse(request.shouldSkipMemory());
+ }
+
+ @Test
+ public void testExecutionOnBackgroundThread() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldRunOnBackgroundThread());
+
+ request.modify()
+ .executeCallbackOnBackgroundThread()
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldRunOnBackgroundThread());
+ }
+
+ @Test
+ public void testForLauncherIcon() {
+ // This code will call into GeckoAppShell to determine the launcher icon size for this configuration
+ //TODO: Make sure this is correct.
+ //GeckoAppShell.setApplicationContext(RuntimeEnvironment.application);
+
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertEquals(112, request.getTargetSize());
+
+ request.modify()
+ .forLauncherIcon()
+ .deferBuild();
+
+ Assert.assertEquals(48, request.getTargetSize());
+ }
+
+ @Test
+ public void testConcurrentAccess() {
+ IconRequestBuilder builder = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2));
+
+ // Call build() twice on a builder and verify that the two objects are not the same
+ IconRequest request = builder.build();
+ IconRequest compare = builder.build();
+ Assert.assertNotSame(request, compare);
+ Assert.assertNotSame(request.icons, compare.icons);
+
+ // After building call methods on the builder and verify that the previously build object is not changed
+ int iconCount = request.getIconCount();
+ builder.icon(IconDescriptor.createGenericIcon(TEST_PAGE_URL_2))
+ .deferBuild();
+ int iconCountAfterBuild = request.getIconCount();
+ Assert.assertEquals(iconCount, iconCountAfterBuild);
+
+ // Iterate the TreeSet and call methods on the builder
+ try {
+ final Iterator iterator = request.icons.iterator();
+ while (iterator.hasNext()) {
+ iterator.next();
+ builder.icon(IconDescriptor.createGenericIcon(TEST_PAGE_URL_2))
+ .deferBuild();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconResponse.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconResponse.java
new file mode 100644
index 00000000000..fb61a122f09
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconResponse.java
@@ -0,0 +1,150 @@
+/*
+ * 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 mozilla.components.browser.icons;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import static org.mockito.Mockito.mock;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconResponse {
+ private static final String ICON_URL = "http://www.mozilla.org/favicon.ico";
+
+ @Test
+ public void testDefaultResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.create(bitmap);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertFalse(response.hasUrl());
+ Assert.assertNull(response.getUrl());
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ Assert.assertFalse(response.isGenerated());
+ Assert.assertFalse(response.isFromNetwork());
+ Assert.assertFalse(response.isFromDisk());
+ Assert.assertFalse(response.isFromMemory());
+ }
+
+ @Test
+ public void testNetworkResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.createFromNetwork(bitmap, ICON_URL);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertTrue(response.hasUrl());
+ Assert.assertEquals(ICON_URL, response.getUrl());
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ Assert.assertFalse(response.isGenerated());
+ Assert.assertTrue(response.isFromNetwork());
+ Assert.assertFalse(response.isFromDisk());
+ Assert.assertFalse(response.isFromMemory());
+ }
+
+ @Test
+ public void testGeneratedResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.createGenerated(bitmap, Color.CYAN);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertFalse(response.hasUrl());
+ Assert.assertNull(response.getUrl());
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.CYAN, response.getColor());
+
+ Assert.assertTrue(response.isGenerated());
+ Assert.assertFalse(response.isFromNetwork());
+ Assert.assertFalse(response.isFromDisk());
+ Assert.assertFalse(response.isFromMemory());
+ }
+
+ @Test
+ public void testMemoryResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.createFromMemory(bitmap, ICON_URL, Color.CYAN);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertTrue(response.hasUrl());
+ Assert.assertEquals(ICON_URL, response.getUrl());
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.CYAN, response.getColor());
+
+ Assert.assertFalse(response.isGenerated());
+ Assert.assertFalse(response.isFromNetwork());
+ Assert.assertFalse(response.isFromDisk());
+ Assert.assertTrue(response.isFromMemory());
+ }
+
+ @Test
+ public void testDiskResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.createFromDisk(bitmap, ICON_URL);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertTrue(response.hasUrl());
+ Assert.assertEquals(ICON_URL, response.getUrl());
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ Assert.assertFalse(response.isGenerated());
+ Assert.assertFalse(response.isFromNetwork());
+ Assert.assertTrue(response.isFromDisk());
+ Assert.assertFalse(response.isFromMemory());
+ }
+
+ @Test
+ public void testUpdatingColor() {
+ final IconResponse response = IconResponse.create(mock(Bitmap.class));
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ response.updateColor(Color.YELLOW);
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.YELLOW, response.getColor());
+
+ response.updateColor(Color.MAGENTA);
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.MAGENTA, response.getColor());
+ }
+
+ @Test
+ public void testUpdatingBitmap() {
+ final Bitmap originalBitmap = mock(Bitmap.class);
+ final Bitmap updatedBitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.create(originalBitmap);
+
+ Assert.assertEquals(originalBitmap, response.getBitmap());
+ Assert.assertNotEquals(updatedBitmap, response.getBitmap());
+
+ response.updateBitmap(updatedBitmap);
+
+ Assert.assertNotEquals(originalBitmap, response.getBitmap());
+ Assert.assertEquals(updatedBitmap, response.getBitmap());
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconTask.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconTask.java
new file mode 100644
index 00000000000..bc5febad684
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconTask.java
@@ -0,0 +1,573 @@
+/*
+ * 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 mozilla.components.browser.icons;
+
+import android.graphics.Bitmap;
+import mozilla.components.browser.icons.loader.IconLoader;
+import mozilla.components.browser.icons.preparation.Preparer;
+import mozilla.components.browser.icons.processing.Processor;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconTask {
+ @Test
+ public void testGeneratorIsInvokedIfAllLoadersFail() {
+ final List loaders = Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createFailingLoader());
+
+ final Bitmap bitmap = mock(Bitmap.class);
+ final IconLoader generator = createSuccessfulLoader(bitmap);
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ loaders,
+ Collections.emptyList(),
+ generator);
+
+ final IconResponse response = task.call();
+
+ // Verify all loaders have been tried
+ for (IconLoader loader : loaders) {
+ verify(loader).load(request);
+ }
+
+ // Verify generator was called
+ verify(generator).load(request);
+
+ // Verify response contains generated bitmap
+ Assert.assertEquals(bitmap, response.getBitmap());
+ }
+
+ @Test
+ public void testGeneratorIsNotCalledIfOneLoaderWasSuccessful() {
+ final List loaders = Collections.singletonList(
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final IconLoader generator = createSuccessfulLoader(mock(Bitmap.class));
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ loaders,
+ Collections.emptyList(),
+ generator);
+
+ final IconResponse response = task.call();
+
+ // Verify all loaders have been tried
+ for (IconLoader loader : loaders) {
+ verify(loader).load(request);
+ }
+
+ // Verify generator was NOT called
+ verify(generator, never()).load(request);
+
+ Assert.assertNotNull(response);
+ }
+
+ @Test
+ public void testNoLoaderIsInvokedForRequestWithoutUrls() {
+ final List loaders = Collections.singletonList(
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final Bitmap bitmap = mock(Bitmap.class);
+ final IconLoader generator = createSuccessfulLoader(bitmap);
+
+ final IconRequest request = createIconRequestWithoutUrls();
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ loaders,
+ Collections.emptyList(),
+ generator);
+
+ final IconResponse response = task.call();
+
+ // Verify NO loaders have been called
+ for (IconLoader loader : loaders) {
+ verify(loader, never()).load(request);
+ }
+
+ // Verify generator was called
+ verify(generator).load(request);
+
+ // Verify response contains generated bitmap
+ Assert.assertEquals(bitmap, response.getBitmap());
+ }
+
+ @Test
+ public void testAllPreparersAreCalledBeforeLoading() {
+ final List preparers = Arrays.asList(
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class));
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ createListWithSuccessfulLoader(),
+ Collections.emptyList(),
+ createGenerator());
+
+ task.call();
+
+ // Verify all preparers have been called
+ for (Preparer preparer : preparers) {
+ verify(preparer).prepare(request);
+ }
+ }
+
+ @Test
+ public void testSubsequentLoadersAreNotCalledAfterSuccessfulLoad() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final List loaders = Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createSuccessfulLoader(bitmap),
+ createSuccessfulLoader(mock(Bitmap.class)),
+ createFailingLoader(),
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ loaders,
+ Collections.emptyList(),
+ createGenerator());
+
+ final IconResponse response = task.call();
+
+ // First loaders are called
+ verify(loaders.get(0)).load(request);
+ verify(loaders.get(1)).load(request);
+ verify(loaders.get(2)).load(request);
+
+ // Loaders after successful load are not called
+ verify(loaders.get(3), never()).load(request);
+ verify(loaders.get(4), never()).load(request);
+ verify(loaders.get(5), never()).load(request);
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(bitmap, response.getBitmap());
+ }
+
+ @Test
+ public void testNoProcessorIsCalledForUnsuccessfulLoads() {
+ final IconRequest request = createIconRequest();
+
+ final List loaders = createListWithFailingLoaders();
+
+ final List processors = Arrays.asList(
+ createProcessor(),
+ createProcessor(),
+ createProcessor());
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ loaders,
+ processors,
+ createFailingLoader());
+
+ task.call();
+
+ // Verify all loaders have been tried
+ for (IconLoader loader : loaders) {
+ verify(loader).load(request);
+ }
+
+ // Verify no processor was called
+ for (Processor processor : processors) {
+ verify(processor, never()).process(any(IconRequest.class), any(IconResponse.class));
+ }
+ }
+
+ @Test
+ public void testAllProcessorsAreCalledAfterSuccessfulLoad() {
+ final IconRequest request = createIconRequest();
+
+ final List processors = Arrays.asList(
+ createProcessor(),
+ createProcessor(),
+ createProcessor());
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ createListWithSuccessfulLoader(),
+ processors,
+ createGenerator());
+
+ IconResponse response = task.call();
+
+ Assert.assertNotNull(response);
+
+ // Verify that all processors have been called
+ for (Processor processor : processors) {
+ verify(processor).process(request, response);
+ }
+ }
+
+ @Test
+ public void testCallbackIsExecutedForSuccessfulLoads() {
+ final IconCallback callback = mock(IconCallback.class);
+
+ final IconRequest request = createIconRequest();
+ request.setCallback(callback);
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ createListWithSuccessfulLoader(),
+ Collections.emptyList(),
+ createGenerator());
+
+ final IconResponse response = task.call();
+
+ verify(callback).onIconResponse(response);
+ }
+
+ @Test
+ public void testCallbackIsNotExecutedIfLoadingFailed() {
+ final IconCallback callback = mock(IconCallback.class);
+
+ final IconRequest request = createIconRequest();
+ request.setCallback(callback);
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ createListWithFailingLoaders(),
+ Collections.emptyList(),
+ createFailingLoader());
+
+ task.call();
+
+ verify(callback, never()).onIconResponse(any(IconResponse.class));
+ }
+
+ @Test
+ public void testCallbackIsExecutedWithGeneratorResult() {
+ final IconCallback callback = mock(IconCallback.class);
+
+ final IconRequest request = createIconRequest();
+ request.setCallback(callback);
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.emptyList(),
+ createListWithFailingLoaders(),
+ Collections.emptyList(),
+ createGenerator());
+
+ final IconResponse response = task.call();
+
+ verify(callback).onIconResponse(response);
+ }
+
+ @Test
+ public void testTaskCancellationWhileLoading() {
+ // We simulate the cancellation by injecting a loader that interrupts the thread.
+ final IconLoader cancellingLoader = spy(new IconLoader() {
+ @Override
+ public IconResponse load(IconRequest request) {
+ Thread.currentThread().interrupt();
+ return null;
+ }
+ });
+
+ final List preparers = createListOfPreparers();
+ final List processors = createListOfProcessors();
+
+ final List loaders = Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ cancellingLoader,
+ createFailingLoader(),
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ loaders,
+ processors,
+ createGenerator());
+
+ final IconResponse response = task.call();
+ Assert.assertNull(response);
+
+ // Verify that all preparers are called
+ for (Preparer preparer : preparers) {
+ verify(preparer).prepare(request);
+ }
+
+ // Verify that first loaders are called
+ verify(loaders.get(0)).load(request);
+ verify(loaders.get(1)).load(request);
+
+ // Verify that our loader that interrupts the thread is called
+ verify(loaders.get(2)).load(request);
+
+ // Verify that all other loaders are not called
+ verify(loaders.get(3), never()).load(request);
+ verify(loaders.get(4), never()).load(request);
+
+ // Verify that no processors are called
+ for (Processor processor : processors) {
+ verify(processor, never()).process(eq(request), any(IconResponse.class));
+ }
+ }
+
+ @Test
+ public void testTaskCancellationWhileProcessing() {
+ final Processor cancellingProcessor = spy(new Processor() {
+ @Override
+ public void process(IconRequest request, IconResponse response) {
+ Thread.currentThread().interrupt();
+ }
+ });
+
+ final List preparers = createListOfPreparers();
+
+ final List loaders = Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final List processors = Arrays.asList(
+ createProcessor(),
+ createProcessor(),
+ cancellingProcessor,
+ createProcessor(),
+ createProcessor());
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ loaders,
+ processors,
+ createGenerator());
+
+ final IconResponse response = task.call();
+ Assert.assertNull(response);
+
+ // Verify that all preparers are called
+ for (Preparer preparer : preparers) {
+ verify(preparer).prepare(request);
+ }
+
+ // Verify that all loaders are called
+ for (IconLoader loader : loaders) {
+ verify(loader).load(request);
+ }
+
+ // Verify that first processors are called
+ verify(processors.get(0)).process(eq(request), any(IconResponse.class));
+ verify(processors.get(1)).process(eq(request), any(IconResponse.class));
+
+ // Verify that cancelling processor is called
+ verify(processors.get(2)).process(eq(request), any(IconResponse.class));
+
+ // Verify that subsequent processors are not called
+ verify(processors.get(3), never()).process(eq(request), any(IconResponse.class));
+ verify(processors.get(4), never()).process(eq(request), any(IconResponse.class));
+ }
+
+ @Test
+ public void testTaskCancellationWhilePerparing() {
+ final Preparer failingPreparer = spy(new Preparer() {
+ @Override
+ public void prepare(IconRequest request) {
+ Thread.currentThread().interrupt();
+ }
+ });
+
+ final List preparers = Arrays.asList(
+ mock(Preparer.class),
+ mock(Preparer.class),
+ failingPreparer,
+ mock(Preparer.class),
+ mock(Preparer.class));
+
+ final List loaders = createListWithSuccessfulLoader();
+ final List processors = createListOfProcessors();
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ loaders,
+ processors,
+ createGenerator());
+
+ final IconResponse response = task.call();
+ Assert.assertNull(response);
+
+ // Verify that first preparers are called
+ verify(preparers.get(0)).prepare(request);
+ verify(preparers.get(1)).prepare(request);
+
+ // Verify that cancelling preparer is called
+ verify(preparers.get(2)).prepare(request);
+
+ // Verify that subsequent preparers are not called
+ verify(preparers.get(3), never()).prepare(request);
+ verify(preparers.get(4), never()).prepare(request);
+
+ // Verify that no loaders are called
+ for (IconLoader loader : loaders) {
+ verify(loader, never()).load(request);
+ }
+
+ // Verify that no processors are called
+ for (Processor processor : processors) {
+ verify(processor, never()).process(eq(request), any(IconResponse.class));
+ }
+ }
+
+ @Test
+ public void testNoLoadersOrProcessorsAreExecutedForPrepareOnlyTasks() {
+ final List preparers = createListOfPreparers();
+ final List loaders = createListWithSuccessfulLoader();
+ final List processors = createListOfProcessors();
+ final IconLoader generator = createGenerator();
+
+ final IconRequest request = createIconRequest()
+ .modify()
+ .prepareOnly()
+ .build();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ loaders,
+ processors,
+ generator);
+
+ IconResponse response = task.call();
+
+ Assert.assertNull(response);
+
+ // Verify that all preparers are called
+ for (Preparer preparer : preparers) {
+ verify(preparer).prepare(request);
+ }
+
+ // Verify that no loaders are called
+ for (IconLoader loader : loaders) {
+ verify(loader, never()).load(request);
+ }
+
+ // Verify that no processors are called
+ for (Processor processor : processors) {
+ verify(processor, never()).process(eq(request), any(IconResponse.class));
+ }
+ }
+
+ public List createListWithSuccessfulLoader() {
+ return Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createSuccessfulLoader(mock(Bitmap.class)),
+ createFailingLoader());
+ }
+
+ public List createListWithFailingLoaders() {
+ return Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createFailingLoader(),
+ createFailingLoader(),
+ createFailingLoader());
+ }
+
+ public List createListOfPreparers() {
+ return Arrays.asList(
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class));
+ }
+
+ public IconLoader createFailingLoader() {
+ final IconLoader loader = mock(IconLoader.class);
+ doReturn(null).when(loader).load(any(IconRequest.class));
+ return loader;
+ }
+
+ public IconLoader createSuccessfulLoader(Bitmap bitmap) {
+ IconResponse response = IconResponse.create(bitmap);
+
+ final IconLoader loader = mock(IconLoader.class);
+ doReturn(response).when(loader).load(any(IconRequest.class));
+ return loader;
+ }
+
+ public List createListOfProcessors() {
+ return Arrays.asList(
+ mock(Processor.class),
+ mock(Processor.class),
+ mock(Processor.class),
+ mock(Processor.class),
+ mock(Processor.class));
+ }
+
+ public IconRequest createIconRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon("http://www.mozilla.org/favicon.ico"))
+ .build();
+ }
+
+ public IconRequest createIconRequestWithoutUrls() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .build();
+ }
+
+ public IconLoader createGenerator() {
+ return createSuccessfulLoader(mock(Bitmap.class));
+ }
+
+ public Processor createProcessor() {
+ return mock(Processor.class);
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconsHelper.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconsHelper.java
new file mode 100644
index 00000000000..cf8fcc69974
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/TestIconsHelper.java
@@ -0,0 +1,136 @@
+/*
+ * 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 mozilla.components.browser.icons;
+
+import android.annotation.SuppressLint;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconsHelper {
+ @SuppressLint("AuthLeak") // Lint and Android Studio try to prevent developers from writing code
+ // with credentials in the URL (user:password@host). But in this case
+ // we explicitly want to do that, so we suppress the warnings.
+ @Test
+ public void testGuessDefaultFaviconURL() {
+ // Empty values
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL(null));
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL(""));
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL(" "));
+
+ // Special about: URLs.
+
+ Assert.assertEquals(
+ "about:home",
+ IconsHelper.guessDefaultFaviconURL("about:home"));
+
+ Assert.assertEquals(
+ "about:firefox",
+ IconsHelper.guessDefaultFaviconURL("about:firefox"));
+
+ Assert.assertEquals(
+ "about:addons",
+ IconsHelper.guessDefaultFaviconURL("about:addons"));
+
+ // Non http(s) URLS
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("content://some.random.provider/icons"));
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("ftp://ftp.public.mozilla.org/this/is/made/up"));
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("file:///"));
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("file:///system/path"));
+
+ // Various http(s) URLs
+
+ Assert.assertEquals("http://www.mozilla.org/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("http://www.mozilla.org/"));
+
+ Assert.assertEquals("https://www.mozilla.org/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("https://www.mozilla.org/en-US/firefox/products/"));
+
+ Assert.assertEquals("https://example.org/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("https://example.org"));
+
+ Assert.assertEquals("http://user:password@example.org:9991/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("http://user:password@example.org:9991/status/760492829949001728"));
+
+ Assert.assertEquals("https://localhost:8888/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("https://localhost:8888/path/folder/file?some=query¶ms=none"));
+
+ Assert.assertEquals("http://192.168.0.1/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("http://192.168.0.1/local/action.cgi"));
+
+ Assert.assertEquals("https://medium.com/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("https://medium.com/firefox-mobile-engineering/firefox-for-android-hack-week-recap-f1ab12f5cc44#.rpmzz15ia"));
+
+ // Some broken, partial URLs
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("http:"));
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("http://"));
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("https:/"));
+ }
+
+ @Test
+ public void testIsContainerType() {
+ // Empty values
+ Assert.assertFalse(IconsHelper.isContainerType(null));
+ Assert.assertFalse(IconsHelper.isContainerType(""));
+ Assert.assertFalse(IconsHelper.isContainerType(" "));
+
+ // Values that don't make any sense.
+ Assert.assertFalse(IconsHelper.isContainerType("Hello World"));
+ Assert.assertFalse(IconsHelper.isContainerType("no/no/no"));
+ Assert.assertFalse(IconsHelper.isContainerType("42"));
+
+ // Actual image MIME types that are not container types
+ Assert.assertFalse(IconsHelper.isContainerType("image/png"));
+ Assert.assertFalse(IconsHelper.isContainerType("application/bmp"));
+ Assert.assertFalse(IconsHelper.isContainerType("image/gif"));
+ Assert.assertFalse(IconsHelper.isContainerType("image/x-windows-bitmap"));
+ Assert.assertFalse(IconsHelper.isContainerType("image/jpeg"));
+ Assert.assertFalse(IconsHelper.isContainerType("application/x-png"));
+
+ // MIME types of image container
+ Assert.assertTrue(IconsHelper.isContainerType("image/vnd.microsoft.icon"));
+ Assert.assertTrue(IconsHelper.isContainerType("image/ico"));
+ Assert.assertTrue(IconsHelper.isContainerType("image/icon"));
+ Assert.assertTrue(IconsHelper.isContainerType("image/x-icon"));
+ Assert.assertTrue(IconsHelper.isContainerType("text/ico"));
+ Assert.assertTrue(IconsHelper.isContainerType("application/ico"));
+ }
+
+ @Test
+ public void testCanDecodeType() {
+ // Empty values
+ Assert.assertFalse(IconsHelper.canDecodeType(null));
+ Assert.assertFalse(IconsHelper.canDecodeType(""));
+ Assert.assertFalse(IconsHelper.canDecodeType(" "));
+
+ // Some things we can't decode (or that just aren't images)
+ Assert.assertFalse(IconsHelper.canDecodeType("image/svg+xml"));
+ Assert.assertFalse(IconsHelper.canDecodeType("video/avi"));
+ Assert.assertFalse(IconsHelper.canDecodeType("text/plain"));
+ Assert.assertFalse(IconsHelper.canDecodeType("image/x-quicktime"));
+ Assert.assertFalse(IconsHelper.canDecodeType("image/tiff"));
+ Assert.assertFalse(IconsHelper.canDecodeType("application/zip"));
+
+ // Some image MIME types we definitely can decode
+ Assert.assertTrue(IconsHelper.canDecodeType("image/bmp"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/x-icon"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/png"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/jpg"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/jpeg"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/ico"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/icon"));
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestContentProviderLoader.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestContentProviderLoader.java
new file mode 100644
index 00000000000..4fdba59ab14
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestContentProviderLoader.java
@@ -0,0 +1,34 @@
+/*
+ * 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 mozilla.components.browser.icons.loader;
+
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.Icons;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestContentProviderLoader {
+ @Test
+ public void testNothingIsLoadedForHttpUrls() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .build();
+
+ IconLoader loader = new ContentProviderLoader();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNull(response);
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestDataUriLoader.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestDataUriLoader.java
new file mode 100644
index 00000000000..7d4c8a98d55
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestDataUriLoader.java
@@ -0,0 +1,49 @@
+/*
+ * 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 mozilla.components.browser.icons.loader;
+
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.Icons;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestDataUriLoader {
+ @Test
+ public void testNothingIsLoadedForHttpUrls() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .build();
+
+ IconLoader loader = new DataUriLoader();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNull(response);
+ }
+
+ @Test
+ public void testIconIsLoadedFromDataUri() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC"))
+ .build();
+
+ IconLoader loader = new DataUriLoader();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNotNull(response);
+ Assert.assertNotNull(response.getBitmap());
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestIconDownloader.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestIconDownloader.java
new file mode 100644
index 00000000000..1f868dcb111
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestIconDownloader.java
@@ -0,0 +1,139 @@
+/*
+ * 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 mozilla.components.browser.icons.loader;
+
+import android.content.Context;
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.Icons;
+import mozilla.components.browser.icons.storage.FailureCache;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.*;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconDownloader {
+ /**
+ * Scenario: A request with a non HTTP URL (data:image/*) is executed.
+ *
+ * Verify that:
+ * * No download is performed.
+ */
+ @Test
+ public void testDownloaderDoesNothingForNonHttpUrls() throws Exception {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC"))
+ .build();
+
+ final IconDownloader downloader = spy(new IconDownloader());
+ IconResponse response = downloader.load(request);
+
+ Assert.assertNull(response);
+
+ verify(downloader, never()).downloadAndDecodeImage(any(Context.class), anyString());
+ verify(downloader, never()).connectTo(anyString());
+ }
+
+ /**
+ * Scenario: Request contains an URL and server returns 301 with location header (always the same URL).
+ *
+ * Verify that:
+ * * Download code stops and does not loop forever.
+ */
+ @Test
+ public void testRedirectsAreFollowedButNotInCircles() throws Exception {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createFavicon(
+ "https://www.mozilla.org/media/img/favicon.52506929be4c.ico",
+ 32,
+ "image/x-icon"))
+ .build();
+
+ HttpURLConnection mockedConnection = mock(HttpURLConnection.class);
+ doReturn(301).when(mockedConnection).getResponseCode();
+ doReturn("http://example.org/favicon.ico").when(mockedConnection).getHeaderField("Location");
+
+ final IconDownloader downloader = spy(new IconDownloader());
+ doReturn(mockedConnection).when(downloader).connectTo(anyString());
+ IconResponse response = downloader.load(request);
+
+ Assert.assertNull(response);
+
+ verify(downloader).connectTo("https://www.mozilla.org/media/img/favicon.52506929be4c.ico");
+ verify(downloader).connectTo("http://example.org/favicon.ico");
+ }
+
+ /**
+ * Scenario: Request contains an URL and server returns HTTP 404.
+ *
+ * Verify that:
+ * * URL is added to failure cache.
+ */
+ @Test
+ public void testUrlIsAddedToFailureCacheIfServerReturnsClientError() throws Exception {
+ final String faviconUrl = "https://www.mozilla.org/404.ico";
+
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createFavicon(faviconUrl, 32, "image/x-icon"))
+ .build();
+
+ HttpURLConnection mockedConnection = mock(HttpURLConnection.class);
+ doReturn(404).when(mockedConnection).getResponseCode();
+
+ Assert.assertFalse(FailureCache.get().isKnownFailure(faviconUrl));
+
+ final IconDownloader downloader = spy(new IconDownloader());
+ doReturn(mockedConnection).when(downloader).connectTo(anyString());
+ IconResponse response = downloader.load(request);
+
+ Assert.assertNull(response);
+
+ Assert.assertTrue(FailureCache.get().isKnownFailure(faviconUrl));
+ }
+
+ /**
+ * Scenario: Connected to successfully to server but reading the response code throws an exception.
+ *
+ * Verify that:
+ * * disconnect() is called on HttpUrlConnection
+ */
+ @Test
+ public void testConnectionIsClosedWhenReadingResponseCodeThrows() throws Exception {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createFavicon(
+ "https://www.mozilla.org/media/img/favicon.52506929be4c.ico",
+ 32,
+ "image/x-icon"))
+ .build();
+
+ HttpURLConnection mockedConnection = mock(HttpURLConnection.class);
+ doThrow(new IOException()).when(mockedConnection).getResponseCode();
+
+ final IconDownloader downloader = spy(new IconDownloader());
+ doReturn(mockedConnection).when(downloader).connectTo(anyString());
+ IconResponse response = downloader.load(request);
+
+ Assert.assertNull(response);
+
+ verify(mockedConnection).disconnect();
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestIconGenerator.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestIconGenerator.java
new file mode 100644
index 00000000000..68ec6334024
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestIconGenerator.java
@@ -0,0 +1,125 @@
+/*
+ * 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 mozilla.components.browser.icons.loader;
+
+import android.graphics.Bitmap;
+import mozilla.components.browser.icons.*;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestIconGenerator {
+ @Test
+ public void testNoIconIsGeneratorIfThereAreIconUrlsToLoadFrom() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon.52506929be4c.ico"))
+ .build();
+
+ IconLoader loader = new IconGenerator();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNull(response);
+ }
+
+ @Test
+ public void testIconIsGeneratedForLastUrl() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .build();
+
+ IconLoader loader = new IconGenerator();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNotNull(response);
+ Assert.assertNotNull(response.getBitmap());
+ }
+
+ @Test
+ public void testRepresentativeCharacter() {
+ Assert.assertEquals("M", IconGenerator.getRepresentativeCharacter("https://mozilla.org"));
+ Assert.assertEquals("W", IconGenerator.getRepresentativeCharacter("http://wikipedia.org"));
+ Assert.assertEquals("P", IconGenerator.getRepresentativeCharacter("http://plus.google.com"));
+ Assert.assertEquals("E", IconGenerator.getRepresentativeCharacter("https://en.m.wikipedia.org/wiki/Main_Page"));
+
+ // Stripping common prefixes
+ Assert.assertEquals("T", IconGenerator.getRepresentativeCharacter("http://www.theverge.com"));
+ Assert.assertEquals("F", IconGenerator.getRepresentativeCharacter("https://m.facebook.com"));
+ Assert.assertEquals("T", IconGenerator.getRepresentativeCharacter("https://mobile.twitter.com"));
+
+ // Special urls
+ Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter("file:///"));
+ Assert.assertEquals("S", IconGenerator.getRepresentativeCharacter("file:///system/"));
+ Assert.assertEquals("P", IconGenerator.getRepresentativeCharacter("ftp://people.mozilla.org/test"));
+
+ // No values
+ Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter(""));
+ Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter(null));
+
+ // Rubbish
+ Assert.assertEquals("Z", IconGenerator.getRepresentativeCharacter("zZz"));
+ Assert.assertEquals("Ö", IconGenerator.getRepresentativeCharacter("ölkfdpou3rkjaslfdköasdfo8"));
+ Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter("_*+*'##"));
+ Assert.assertEquals("ツ", IconGenerator.getRepresentativeCharacter("¯\\_(ツ)_/¯"));
+ Assert.assertEquals("ಠ", IconGenerator.getRepresentativeCharacter("ಠ_ಠ Look of Disapproval"));
+
+ // Non-ASCII
+ Assert.assertEquals("Ä", IconGenerator.getRepresentativeCharacter("http://www.ätzend.de"));
+ Assert.assertEquals("名", IconGenerator.getRepresentativeCharacter("http://名がドメイン.com"));
+ Assert.assertEquals("C", IconGenerator.getRepresentativeCharacter("http://√.com"));
+ Assert.assertEquals("ß", IconGenerator.getRepresentativeCharacter("http://ß.de"));
+ Assert.assertEquals("Ԛ", IconGenerator.getRepresentativeCharacter("http://ԛәлп.com/")); // cyrillic
+
+ // Punycode
+ Assert.assertEquals("X", IconGenerator.getRepresentativeCharacter("http://xn--tzend-fra.de")); // ätzend.de
+ Assert.assertEquals("X", IconGenerator.getRepresentativeCharacter("http://xn--V8jxj3d1dzdz08w.com")); // 名がドメイン.com
+
+ // Numbers
+ Assert.assertEquals("1", IconGenerator.getRepresentativeCharacter("https://www.1and1.com/"));
+
+ // IP
+ Assert.assertEquals("1", IconGenerator.getRepresentativeCharacter("https://192.168.0.1"));
+ }
+
+ @Test
+ public void testPickColor() {
+ final int color = IconGenerator.pickColor("http://m.facebook.com");
+
+ // Color does not change
+ for (int i = 0; i < 100; i++) {
+ Assert.assertEquals(color, IconGenerator.pickColor("http://m.facebook.com"));
+ }
+
+ // Color is stable for "similar" hosts.
+ Assert.assertEquals(color, IconGenerator.pickColor("https://m.facebook.com"));
+ Assert.assertEquals(color, IconGenerator.pickColor("http://facebook.com"));
+ Assert.assertEquals(color, IconGenerator.pickColor("http://www.facebook.com"));
+ Assert.assertEquals(color, IconGenerator.pickColor("http://www.facebook.com/foo/bar/foobar?mobile=1"));
+ }
+
+ @Test
+ public void testGeneratingFavicon() {
+ final IconResponse response = IconGenerator.generate(RuntimeEnvironment.application, "http://m.facebook.com");
+ final Bitmap bitmap = response.getBitmap();
+
+ Assert.assertNotNull(bitmap);
+
+ final int size = RuntimeEnvironment.application.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
+ Assert.assertEquals(size, bitmap.getWidth());
+ Assert.assertEquals(size, bitmap.getHeight());
+
+ Assert.assertEquals(Bitmap.Config.ARGB_8888, bitmap.getConfig());
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestMemoryLoader.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestMemoryLoader.java
new file mode 100644
index 00000000000..088f272bb8e
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/TestMemoryLoader.java
@@ -0,0 +1,79 @@
+/*
+ * 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 mozilla.components.browser.icons.loader;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import junit.framework.Assert;
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.Icons;
+import mozilla.components.browser.icons.storage.MemoryStorage;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import static org.mockito.Mockito.mock;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestMemoryLoader {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
+
+ @Before
+ public void setUp() {
+ // Make sure to start with an empty memory cache.
+ MemoryStorage.get().evictAll();
+ }
+
+ @Test
+ public void testStoringAndLoadingFromMemory() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ final IconLoader loader = new MemoryLoader();
+
+ Assert.assertNull(loader.load(request));
+
+ final Bitmap bitmap = mock(Bitmap.class);
+ final IconResponse response = IconResponse.create(bitmap);
+ response.updateColor(Color.MAGENTA);
+
+ MemoryStorage.get().putIcon(TEST_ICON_URL, response);
+
+ final IconResponse loadedResponse = loader.load(request);
+
+ Assert.assertNotNull(loadedResponse);
+ Assert.assertEquals(bitmap, loadedResponse.getBitmap());
+ Assert.assertEquals(Color.MAGENTA, loadedResponse.getColor());
+ }
+
+ @Test
+ public void testNothingIsLoadedIfMemoryShouldBeSkipped() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .skipMemory()
+ .build();
+
+ final IconLoader loader = new MemoryLoader();
+
+ Assert.assertNull(loader.load(request));
+
+ final Bitmap bitmap = mock(Bitmap.class);
+ final IconResponse response = IconResponse.create(bitmap);
+
+ MemoryStorage.get().putIcon(TEST_ICON_URL, response);
+
+ Assert.assertNull(loader.load(request));
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestAddDefaultIconUrl.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestAddDefaultIconUrl.java
new file mode 100644
index 00000000000..9d071a79f91
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestAddDefaultIconUrl.java
@@ -0,0 +1,82 @@
+/*
+ * 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 mozilla.components.browser.icons.preparation;
+
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.Icons;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Iterator;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestAddDefaultIconUrl {
+ @Test
+ public void testAddingDefaultUrl() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createTouchicon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png",
+ 180,
+ "image/png"))
+ .icon(IconDescriptor.createFavicon(
+ "https://www.mozilla.org/media/img/favicon.52506929be4c.ico",
+ 32,
+ "image/x-icon"))
+ .icon(IconDescriptor.createFavicon(
+ "jar:jar:wtf.png",
+ 16,
+ "image/png"))
+ .build();
+
+
+ Assert.assertEquals(3, request.getIconCount());
+ Assert.assertFalse(containsUrl(request, "http://www.mozilla.org/favicon.ico"));
+
+ Preparer preparer = new AddDefaultIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(4, request.getIconCount());
+ Assert.assertTrue(containsUrl(request, "http://www.mozilla.org/favicon.ico"));
+ }
+
+ @Test
+ public void testDefaultUrlIsNotAddedIfItAlreadyExists() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createFavicon(
+ "http://www.mozilla.org/favicon.ico",
+ 32,
+ "image/x-icon"))
+ .build();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ Preparer preparer = new AddDefaultIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+ }
+
+ private boolean containsUrl(IconRequest request, String url) {
+ final Iterator iterator = request.getIconIterator();
+
+ while (iterator.hasNext()) {
+ IconDescriptor descriptor = iterator.next();
+
+ if (descriptor.getUrl().equals(url)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterKnownFailureUrls.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterKnownFailureUrls.java
new file mode 100644
index 00000000000..0e91cdcdac5
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterKnownFailureUrls.java
@@ -0,0 +1,62 @@
+/*
+ * 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 mozilla.components.browser.icons.preparation;
+
+import junit.framework.Assert;
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.Icons;
+import mozilla.components.browser.icons.storage.FailureCache;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestFilterKnownFailureUrls {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
+
+ @Before
+ public void setUp() {
+ // Make sure we always start with an empty cache.
+ FailureCache.get().evictAll();
+ }
+
+ @Test
+ public void testFilterDoesNothingByDefault() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ final Preparer preparer = new FilterKnownFailureUrls();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+ }
+
+ @Test
+ public void testFilterKnownFailureUrls() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ FailureCache.get().rememberFailure(TEST_ICON_URL);
+
+ final Preparer preparer = new FilterKnownFailureUrls();
+ preparer.prepare(request);
+
+ Assert.assertEquals(0, request.getIconCount());
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterMimeTypes.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterMimeTypes.java
new file mode 100644
index 00000000000..44266661ea2
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterMimeTypes.java
@@ -0,0 +1,70 @@
+/*
+ * 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 mozilla.components.browser.icons.preparation;
+
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.Icons;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestFilterMimeTypes {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "https://mozilla.org/favicon.ico";
+
+ @Test
+ public void testUrlsWithoutMimeTypesAreNotFiltered() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ final Preparer preparer = new FilterMimeTypes();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+ }
+
+ @Test
+ public void testUnknownMimeTypesAreFiltered() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createFavicon(TEST_ICON_URL, 256, "image/zaphod"))
+ .icon(IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, "audio/mpeg"))
+ .build();
+
+ Assert.assertEquals(2, request.getIconCount());
+
+ final Preparer preparer = new FilterMimeTypes();
+ preparer.prepare(request);
+
+ Assert.assertEquals(0, request.getIconCount());
+ }
+
+ @Test
+ public void testKnownMimeTypesAreNotFiltered() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createFavicon(TEST_ICON_URL, 256, "image/x-icon"))
+ .icon(IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, "image/png"))
+ .build();
+
+ Assert.assertEquals(2, request.getIconCount());
+
+ final Preparer preparer = new FilterMimeTypes();
+ preparer.prepare(request);
+
+ Assert.assertEquals(2, request.getIconCount());
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterPrivilegedUrls.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterPrivilegedUrls.java
new file mode 100644
index 00000000000..bc4ef05fa71
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestFilterPrivilegedUrls.java
@@ -0,0 +1,92 @@
+/*
+ * 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 mozilla.components.browser.icons.preparation;
+
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.Icons;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Iterator;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestFilterPrivilegedUrls {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+
+ private static final String TEST_ICON_HTTP_URL = "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png";
+ private static final String TEST_ICON_HTTP_URL_2 = "https://www.mozilla.org/media/img/favicon.52506929be4c.ico";
+ private static final String TEST_ICON_JAR_URL = "jar:jar:wtf.png";
+
+ @Test
+ public void testFiltering() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL_2))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_JAR_URL))
+ .build();
+
+ Assert.assertEquals(3, request.getIconCount());
+
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL));
+
+ Preparer preparer = new FilterPrivilegedUrls();
+ preparer.prepare(request);
+
+ Assert.assertEquals(2, request.getIconCount());
+
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2));
+ Assert.assertFalse(containsUrl(request, TEST_ICON_JAR_URL));
+ }
+
+ @Test
+ public void testNothingIsFilteredForPrivilegedRequests() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL_2))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_JAR_URL))
+ .privileged(true)
+ .build();
+
+ Assert.assertEquals(3, request.getIconCount());
+
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL));
+
+ Preparer preparer = new FilterPrivilegedUrls();
+ preparer.prepare(request);
+
+ Assert.assertEquals(3, request.getIconCount());
+
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL));
+ }
+
+ private boolean containsUrl(IconRequest request, String url) {
+ final Iterator iterator = request.getIconIterator();
+
+ while (iterator.hasNext()) {
+ IconDescriptor descriptor = iterator.next();
+
+ if (descriptor.getUrl().equals(url)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestLookupIconUrl.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestLookupIconUrl.java
new file mode 100644
index 00000000000..dce5e0d3fcc
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparation/TestLookupIconUrl.java
@@ -0,0 +1,65 @@
+/*
+ * 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 mozilla.components.browser.icons.preparation;
+
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.Icons;
+import mozilla.components.browser.icons.storage.MemoryStorage;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestLookupIconUrl {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+
+ private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "http://example.org/favicon.ico";
+ private static final String TEST_ICON_URL_3 = "http://example.com/favicon.ico";
+ private static final String TEST_ICON_URL_4 = "http://example.net/favicon.ico";
+
+
+ @Before
+ public void setUp() {
+ MemoryStorage.get().evictAll();
+ }
+
+ @Test
+ public void testNoIconUrlIsAddedByDefault() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ Preparer preparer = new LookupIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(0, request.getIconCount());
+ }
+
+ @Test
+ public void testIconUrlIsAddedFromMemory() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ MemoryStorage.get().putMapping(request, TEST_ICON_URL_1);
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ Preparer preparer = new LookupIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ Assert.assertEquals(TEST_ICON_URL_1, request.getBestIcon().getUrl());
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestColorProcessor.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestColorProcessor.java
new file mode 100644
index 00000000000..3473073a9c6
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestColorProcessor.java
@@ -0,0 +1,59 @@
+/*
+ * 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 mozilla.components.browser.icons.processing;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import mozilla.components.browser.icons.IconResponse;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.*;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestColorProcessor {
+ @Test
+ public void testExtractingColor() {
+ final IconResponse response = IconResponse.create(createRedBitmapMock());
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ final Processor processor = new ColorProcessor();
+ processor.process(null, response);
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.RED, response.getColor());
+ }
+
+ private Bitmap createRedBitmapMock() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ doReturn(1).when(bitmap).getWidth();
+ doReturn(1).when(bitmap).getHeight();
+
+ doAnswer(new Answer() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ Object[] args = invocation.getArguments();
+ int[] pixels = (int[]) args[0];
+ for (int i = 0; i < pixels.length; i++) {
+ pixels[i] = Color.RED;
+ }
+ return null;
+ }
+ }).when(bitmap).getPixels(any(int[].class), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt());
+
+ return bitmap;
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestMemoryProcessor.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestMemoryProcessor.java
new file mode 100644
index 00000000000..a2fb5de8bfd
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestMemoryProcessor.java
@@ -0,0 +1,136 @@
+/*
+ * 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 mozilla.components.browser.icons.processing;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.Icons;
+import mozilla.components.browser.icons.storage.MemoryStorage;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import static org.mockito.Mockito.mock;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestMemoryProcessor {
+ private static final String PAGE_URL = "https://www.mozilla.org";
+ private static final String ICON_URL = "https://www.mozilla.org/favicon.ico";
+ private static final String DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC";
+
+ @Before
+ public void setUp() {
+ MemoryStorage.get().evictAll();
+ }
+
+ @Test
+ public void testResponsesAreStoredInMemory() {
+ final IconRequest request = createTestRequest();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createTestResponse());
+
+ Assert.assertNotNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNotNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredIfMemoryShouldBeSkipped() {
+ final IconRequest request = createTestRequest()
+ .modify()
+ .skipMemory()
+ .build();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createTestResponse());
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredForRequestsWithoutUrl() {
+ final IconRequest request = createTestRequestWithoutIconUrl();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createTestResponse());
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredForGeneratedResponses() {
+ final IconRequest request = createTestRequest();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createGeneratedTestResponse());
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredForDataUris() {
+ final IconRequest request = createDataUriTestRequest();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(DATA_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createTestResponse());
+
+ Assert.assertNull(MemoryStorage.get().getIcon(DATA_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ private IconRequest createTestRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(ICON_URL))
+ .build();
+ }
+
+ private IconRequest createTestRequestWithoutIconUrl() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .build();
+ }
+
+ private IconRequest createDataUriTestRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(DATA_URL))
+ .build();
+ }
+
+ private IconResponse createTestResponse() {
+ return IconResponse.create(mock(Bitmap.class));
+ }
+
+ private IconResponse createGeneratedTestResponse() {
+ return IconResponse.createGenerated(mock(Bitmap.class), Color.GREEN);
+ }
+}
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestMinimumSizeProcessor.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestMinimumSizeProcessor.java
new file mode 100644
index 00000000000..3e4cfd2ed03
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestMinimumSizeProcessor.java
@@ -0,0 +1,89 @@
+/*
+ * 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 mozilla.components.browser.icons.processing;
+
+import android.graphics.Bitmap;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.*;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestMinimumSizeProcessor {
+
+ private MinimumSizeProcessor processor;
+
+ @Before
+ public void setUp() {
+ processor = new MinimumSizeProcessor();
+ }
+
+ @Test
+ public void testProcessMinimumSizeZeroDoesNotReplaceSmallBitmap() throws Exception {
+ final IconResponse responseMock = getMockResponse(1);
+ processor.process(getMockRequest(0), responseMock);
+
+ verify(responseMock, never()).updateBitmap(any(Bitmap.class));
+ verify(responseMock, never()).updateColor(anyInt());
+ }
+
+ @Test
+ public void testProcessMinimumSizeZeroDoesNotReplaceLargeBitmap() throws Exception {
+ final IconResponse responseMock = getMockResponse(1000);
+ processor.process(getMockRequest(0), responseMock);
+
+ verify(responseMock, never()).updateBitmap(any(Bitmap.class));
+ verify(responseMock, never()).updateColor(anyInt());
+ }
+
+ @Test
+ public void testProcessMinimumSizeFiftyReplacesSmallerBitmap() throws Exception {
+ final IconResponse responseMock = getMockResponse(25);
+ processor.process(getMockRequest(50), responseMock);
+
+ verify(responseMock, atLeastOnce()).updateBitmap(any(Bitmap.class));
+ verify(responseMock, atLeastOnce()).updateColor(anyInt());
+ }
+
+ @Test
+ public void testProcessMinimumSizeFiftyDoesNotReplaceLargerBitmap() throws Exception {
+ final IconResponse responseMock = getMockResponse(1000);
+ processor.process(getMockRequest(50), responseMock);
+
+ verify(responseMock, never()).updateBitmap(any(Bitmap.class));
+ verify(responseMock, never()).updateColor(anyInt());
+ }
+
+ private IconRequest getMockRequest(final int minimumSizePx) {
+ final IconRequest requestMock = mock(IconRequest.class);
+
+ // Under testing.
+ when(requestMock.getMinimumSizePxAfterScaling()).thenReturn(minimumSizePx);
+
+ // Happened to be called.
+ when(requestMock.getPageUrl()).thenReturn("https://mozilla.org");
+ when(requestMock.getContext()).thenReturn(RuntimeEnvironment.application);
+ return requestMock;
+ }
+
+ private IconResponse getMockResponse(final int bitmapWidth) {
+ final Bitmap bitmapMock = mock(Bitmap.class);
+ when(bitmapMock.getWidth()).thenReturn(bitmapWidth);
+ when(bitmapMock.getHeight()).thenReturn(bitmapWidth); // not strictly necessary with the current impl.
+
+ final IconResponse responseMock = mock(IconResponse.class);
+ when(responseMock.getBitmap()).thenReturn(bitmapMock);
+ return responseMock;
+ }
+}
\ No newline at end of file
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestResizingProcessor.java b/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestResizingProcessor.java
new file mode 100644
index 00000000000..3bae1e3c04e
--- /dev/null
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/processing/TestResizingProcessor.java
@@ -0,0 +1,110 @@
+/*
+ * 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 mozilla.components.browser.icons.processing;
+
+import android.graphics.Bitmap;
+import mozilla.components.browser.icons.IconDescriptor;
+import mozilla.components.browser.icons.IconRequest;
+import mozilla.components.browser.icons.IconResponse;
+import mozilla.components.browser.icons.Icons;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.*;
+
+@RunWith(RobolectricTestRunner.class)
+public class TestResizingProcessor {
+ private static final String PAGE_URL = "https://www.mozilla.org";
+ private static final String ICON_URL = "https://www.mozilla.org/favicon.ico";
+
+ @Test
+ public void testBitmapIsNotResizedIfItAlreadyHasTheTargetSize() {
+ final IconRequest request = createTestRequest();
+
+ final Bitmap bitmap = createBitmapMock(request.getTargetSize());
+ final IconResponse response = spy(IconResponse.create(bitmap));
+
+ final ResizingProcessor processor = spy(new ResizingProcessor());
+ processor.process(request, response);
+
+ verify(processor, never()).resize(any(Bitmap.class), anyInt());
+ verify(bitmap, never()).recycle();
+ verify(response, never()).updateBitmap(any(Bitmap.class));
+ }
+
+ @Test
+ public void testLargerBitmapsAreResized() {
+ final IconRequest request = createTestRequest();
+
+ final Bitmap bitmap = createBitmapMock(request.getTargetSize() * 2);
+ final IconResponse response = spy(IconResponse.create(bitmap));
+
+ final ResizingProcessor processor = spy(new ResizingProcessor());
+ final Bitmap resizedBitmap = mock(Bitmap.class);
+ doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt());
+ processor.process(request, response);
+
+ verify(processor).resize(bitmap, request.getTargetSize());
+ verify(bitmap).recycle();
+ verify(response).updateBitmap(resizedBitmap);
+ }
+
+ @Test
+ public void testBitmapIsUpscaledToTargetSize() {
+ final IconRequest request = createTestRequest();
+
+ final Bitmap bitmap = createBitmapMock(request.getTargetSize() / 2 + 1);
+ final IconResponse response = spy(IconResponse.create(bitmap));
+
+ final ResizingProcessor processor = spy(new ResizingProcessor());
+ final Bitmap resizedBitmap = mock(Bitmap.class);
+ doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt());
+ processor.process(request, response);
+
+ verify(processor).resize(bitmap, request.getTargetSize());
+ verify(bitmap).recycle();
+ verify(response).updateBitmap(resizedBitmap);
+ }
+
+ @Test
+ public void testBitmapIsNotScaledMoreThanMaxScaleFactor() {
+ final IconRequest request = createTestRequest();
+
+ final int initialSize = 5;
+ final Bitmap bitmap = createBitmapMock(initialSize);
+ final IconResponse response = spy(IconResponse.create(bitmap));
+
+ final ResizingProcessor processor = spy(new ResizingProcessor());
+ final Bitmap resizedBitmap = mock(Bitmap.class);
+ doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt());
+ processor.process(request, response);
+
+ verify(processor).resize(bitmap, initialSize * ResizingProcessor.MAX_SCALE_FACTOR);
+ verify(bitmap).recycle();
+ verify(response).updateBitmap(resizedBitmap);
+ }
+
+ private IconRequest createTestRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(ICON_URL))
+ .build();
+ }
+
+ private Bitmap createBitmapMock(int size) {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ doReturn(size).when(bitmap).getWidth();
+ doReturn(size).when(bitmap).getHeight();
+
+ return bitmap;
+ }
+}
diff --git a/components/browser/icons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/components/browser/icons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000000..cf1c399ea81
--- /dev/null
+++ b/components/browser/icons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,2 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
diff --git a/components/support/ktx/build.gradle b/components/support/ktx/build.gradle
index 453eb02afa7..e26d982ba90 100644
--- a/components/support/ktx/build.gradle
+++ b/components/support/ktx/build.gradle
@@ -24,6 +24,7 @@ android {
}
dependencies {
+ implementation project(':support-base')
implementation Deps.kotlin_stdlib
implementation Deps.support_compat
diff --git a/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Build.kt b/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Build.kt
new file mode 100644
index 00000000000..29fe665f60e
--- /dev/null
+++ b/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Build.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 mozilla.components.support.ktx.android.os
+
+import android.os.Build.SUPPORTED_ABIS
+import android.system.Os
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Originally taken from [HardwareUtils#isX86System][0].
+ *
+ * [0]: https://dxr.mozilla.org/mozilla-central/source/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
+ */
+object Build {
+ fun isX86System(): Boolean {
+ if ("x86" == SUPPORTED_ABIS[0]) {
+ return true
+ }
+ // On some devices we have to look into the kernel release string.
+ try {
+ return Os.uname().release.contains("-x86_")
+ } catch (e: Exception) {
+ Logger.warn("Cannot get uname", e)
+ }
+ return false
+ }
+}
diff --git a/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt b/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
index cbf2b2b96bc..8cf84c217a9 100644
--- a/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
+++ b/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
@@ -36,3 +36,29 @@ fun String.isPhone(): Boolean = contains("tel:", true)
fun String.isEmail(): Boolean = contains("mailto:", true)
fun String.isGeoLocation(): Boolean = contains("geo:", true)
+
+fun String.stripCommonSubdomains(): String? {
+ // In contrast to desktop, we also strip mobile subdomains,
+ // since its unlikely users are intentionally typing them
+ var start = 0
+
+ if (startsWith("www.")) {
+ start = 4
+ } else if (startsWith("mobile.")) {
+ start = 7
+ } else if (startsWith("m.")) {
+ start = 2
+ }
+
+ return substring(start)
+}
+
+fun String.isHttpOrHttps(): Boolean =
+ if (TextUtils.isEmpty(this)) {
+ false
+ } else {
+ startsWith("http://") || startsWith("https://")
+ }
+
+fun String.isAboutPage(): Boolean =
+ startsWith("about:")
diff --git a/components/support/utils/build.gradle b/components/support/utils/build.gradle
index c047d1743b3..568ae4ce305 100644
--- a/components/support/utils/build.gradle
+++ b/components/support/utils/build.gradle
@@ -26,11 +26,13 @@ android {
dependencies {
implementation project(':support-base')
+ implementation project(':support-ktx')
implementation Deps.kotlin_stdlib
implementation Deps.support_annotations
implementation Deps.support_compat
+ implementation Deps.support_palette
// We expose the app-compat as API so that consumers get access to the Lifecycle classes automatically
api Deps.support_appcompat
diff --git a/components/support/utils/src/main/java/mozilla/components/support/utils/BitmapUtils.kt b/components/support/utils/src/main/java/mozilla/components/support/utils/BitmapUtils.kt
new file mode 100644
index 00000000000..48bde6c267c
--- /dev/null
+++ b/components/support/utils/src/main/java/mozilla/components/support/utils/BitmapUtils.kt
@@ -0,0 +1,157 @@
+/*
+ * 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 mozilla.components.support.utils
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Color
+import android.support.annotation.ColorInt
+import android.support.v7.graphics.Palette
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.os.Build
+
+object BitmapUtils {
+
+ @JvmStatic
+ fun decodeByteArray(
+ bytes: ByteArray,
+ offset: Int = 0,
+ length: Int = bytes.size,
+ options: BitmapFactory.Options? = null
+ ): Bitmap? {
+ if (bytes.isEmpty()) {
+ throw IllegalArgumentException(
+ "bytes.length " + bytes.size
+ + " must be a positive number"
+ )
+ }
+
+ var bitmap: Bitmap? = null
+ try {
+ bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options)
+ } catch (e: OutOfMemoryError) {
+ Logger.error(
+ ("decodeByteArray(bytes.length=" + bytes.size
+ + ", options= " + options + ") OOM!"), e
+ )
+ return null
+ }
+
+ if (bitmap == null) {
+ Logger.warn("decodeByteArray() returning null because BitmapFactory returned null")
+ return null
+ }
+
+ if (bitmap.width <= 0 || bitmap.height <= 0) {
+ Logger.warn(
+ ("decodeByteArray() returning null because BitmapFactory returned "
+ + "a bitmap with dimensions " + bitmap.width
+ + "x" + bitmap.height)
+ )
+ return null
+ }
+
+ return bitmap
+ }
+
+ @ColorInt
+ @JvmStatic
+ fun getDominantColor(source: Bitmap, @ColorInt defaultColor: Int): Int {
+ return if (Build.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.
+ getDominantColorCustomImplementation(source, true, defaultColor)
+ } else {
+ try {
+ val palette = Palette.from(source).generate()
+ palette.getVibrantColor(defaultColor)
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ // 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).
+ Logger.warn("Palette generation failed with ArrayIndexOutOfBoundsException", e)
+
+ defaultColor
+ }
+ }
+ }
+
+ @ColorInt
+ @JvmStatic
+ fun getDominantColorCustomImplementation(
+ source: Bitmap?,
+ applyThreshold: Boolean = true,
+ @ColorInt defaultColor: Int = Color.WHITE
+ ): Int {
+ if (source == null) {
+ return defaultColor
+ }
+
+ // Keep track of how many times a hue in a given bin appears in the image.
+ // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
+ val colorBins = IntArray(36)
+
+ // The bin with the most colors. Initialize to -1 to prevent accidentally
+ // thinking the first bin holds the dominant color.
+ var maxBin = -1
+
+ // Keep track of sum hue/saturation/value per hue bin, which we'll use to
+ // compute an average to for the dominant color.
+ val sumHue = FloatArray(36)
+ val sumSat = FloatArray(36)
+ val sumVal = FloatArray(36)
+ val hsv = FloatArray(3)
+
+ val height = source.height
+ val width = source.width
+ val pixels = IntArray(width * height)
+ source.getPixels(pixels, 0, width, 0, 0, width, height)
+ for (row in 0 until height) {
+ for (col in 0 until width) {
+ val c = pixels[col + row * width]
+ // Ignore pixels with a certain transparency.
+ if (Color.alpha(c) < 128)
+ continue
+
+ Color.colorToHSV(c, hsv)
+
+ // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
+ if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
+ continue
+
+ // We compute the dominant color by putting colors in bins based on their hue.
+ val bin = Math.floor((hsv[0] / 10.0f).toDouble()).toInt()
+
+ // Update the sum hue/saturation/value for this bin.
+ sumHue[bin] = sumHue[bin] + hsv[0]
+ sumSat[bin] = sumSat[bin] + hsv[1]
+ sumVal[bin] = sumVal[bin] + hsv[2]
+
+ // Increment the number of colors in this bin.
+ colorBins[bin]++
+
+ // Keep track of the bin that holds the most colors.
+ if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
+ maxBin = bin
+ }
+ }
+
+ // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
+ if (maxBin < 0) {
+ return defaultColor
+ }
+
+ // Return a color with the average hue/saturation/value of the bin with the most colors.
+ hsv[0] = sumHue[maxBin] / colorBins[maxBin]
+ hsv[1] = sumSat[maxBin] / colorBins[maxBin]
+ hsv[2] = sumVal[maxBin] / colorBins[maxBin]
+ return Color.HSVToColor(hsv)
+ }
+}
\ No newline at end of file
diff --git a/components/support/utils/src/main/java/mozilla/components/support/utils/ThreadUtils.kt b/components/support/utils/src/main/java/mozilla/components/support/utils/ThreadUtils.kt
index d9921d90365..084ed770924 100644
--- a/components/support/utils/src/main/java/mozilla/components/support/utils/ThreadUtils.kt
+++ b/components/support/utils/src/main/java/mozilla/components/support/utils/ThreadUtils.kt
@@ -15,15 +15,18 @@ object ThreadUtils {
private val handler = Handler(Looper.getMainLooper())
private val uiThread = Looper.getMainLooper().thread
+ @JvmStatic
fun postToBackgroundThread(runnable: Runnable) {
backgroundExecutorService.submit(runnable)
}
- fun postToMainThread(runnable: Runnable) {
+ @JvmStatic
+ fun postToUiThread(runnable: Runnable) {
handler.post(runnable)
}
- fun postToMainThreadDelayed(runnable: Runnable, delayMillis: Long) {
+ @JvmStatic
+ fun postToUiThreadDelayed(runnable: Runnable, delayMillis: Long) {
handler.postDelayed(runnable, delayMillis)
}
diff --git a/settings.gradle b/settings.gradle
index 25f9793d837..9b406f827f2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -53,6 +53,9 @@ project(':browser-engine-system').projectDir = new File(rootDir, 'components/bro
include ':browser-errorpages'
project(':browser-errorpages').projectDir = new File(rootDir, 'components/browser/errorpages')
+include ':browser-icons'
+project(':browser-icons').projectDir = new File(rootDir, 'components/browser/icons')
+
include ':browser-menu'
project(':browser-menu').projectDir = new File(rootDir, 'components/browser/menu')