Bug 1286203 - Introduce proxy for partner bookmarks and load icons. r=ahunt, r=grisha, a=gchang
authorSebastian Kaspari <s.kaspari@gmail.com>
Thu, 14 Jul 2016 11:58:14 +0200
changeset 340090 e68714c0861aa70d091066e9dc358dce651d4a62
parent 340089 15a01def6b14b99b52b489eb1c67bacf1e73bd27
child 340091 dd907ccf026d3b5d7fa4f40b0413f1c870a05bb3
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersahunt, grisha, gchang
bugs1286203, 1286794
milestone49.0a2
Bug 1286203 - Introduce proxy for partner bookmarks and load icons. r=ahunt, r=grisha, a=gchang This patch does multiple things: * Introduce a proxy content provider for the partner bookmarks provider: This allows us to hide the id transformation in the proxy and will later be used to filter "deleted" bookmarks from the actual content provider (bug 1286794). * Modifies LoadFaviconTask to support loading icons from a content provider. * Introduces a new flag to not download icons from guessed default favicon URLs. MozReview-Commit-ID: C59ahPcZosn
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderClient.java
mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java
mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
mobile/android/base/java/org/mozilla/gecko/favicons/decoders/LoadFaviconResult.java
mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java
mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
mobile/android/base/moz.build
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -273,16 +273,20 @@
                   android:theme="@style/Gecko.Preferences"
                   android:configChanges="orientation|screenSize|locale|layoutDirection"
                   android:excludeFromRecents="true"/>
 
         <provider android:name="org.mozilla.gecko.db.BrowserProvider"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
                   android:exported="false"/>
 
+        <provider android:name="org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy"
+                  android:authorities="@ANDROID_PACKAGE_NAME@.partnerbookmarks"
+                  android:exported="false"/>
+
         <!-- Share overlay activity
 
              Setting launchMode="singleTop" ensures onNewIntent is called when the Activity is
              reused. Ideally we create a new instance but Android L breaks this (bug 1137928). -->
         <activity android:name="org.mozilla.gecko.overlays.ui.ShareDialog"
                   android:label="@string/overlay_share_label"
                   android:theme="@style/OverlayActivity"
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderClient.java
+++ /dev/null
@@ -1,71 +0,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 org.mozilla.gecko.distribution;
-
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.net.Uri;
-
-import org.mozilla.gecko.db.BrowserContract;
-
-/**
- * Client for reading Android's PartnerBookmarksProvider.
- *
- * Note: This client is only invoked for distributions. Without a distribution the content provider
- *       will not be read and no bookmarks will be added to the UI.
- */
-public class PartnerBookmarksProviderClient {
-    /**
-     * The contract between the partner bookmarks provider and applications. Contains the definition
-     * for the supported URIs and columns.
-     */
-    private static class PartnerContract {
-        public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
-
-        public static final int TYPE_BOOKMARK = 1;
-        public static final int TYPE_FOLDER = 2;
-
-        public static final int PARENT_ROOT_ID = 0;
-
-        public static final String ID = "_id";
-        public static final String TYPE = "type";
-        public static final String URL = "url";
-        public static final String TITLE = "title";
-        public static final String FAVICON = "favicon";
-        public static final String TOUCHICON = "touchicon";
-        public static final String PARENT = "parent";
-    }
-
-    public static Cursor getBookmarksInFolder(ContentResolver contentResolver, int folderId) {
-        // Use root folder id or transform negative id into actual (positive) folder id.
-        final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
-                ? PartnerContract.PARENT_ROOT_ID
-                : BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
-
-        return contentResolver.query(
-                PartnerContract.CONTENT_URI,
-                new String[] {
-                        // Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
-                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
-                        PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
-                        PartnerContract.URL +  " as " + BrowserContract.Bookmarks.URL,
-                        // Transform parent ids to negative ids as well
-                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
-                        // Convert types (we use 0-1 and the partner provider 1-2)
-                        "(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
-                        // Use the ID of the entry as GUID
-                        PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
-                },
-                PartnerContract.PARENT + " = ?"
-                        // Only select entries with valid type
-                        + " AND (" + BrowserContract.Bookmarks.TYPE + " = 1 OR " + BrowserContract.Bookmarks.TYPE + " = 2)"
-                        // Only select entries with non empty title
-                        + " AND " + BrowserContract.Bookmarks.TITLE + " <> ''",
-                new String[] { String.valueOf(actualFolderId) },
-                // Same order we use in our content provider (without position)
-                BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
-    }
-}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java
@@ -0,0 +1,192 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.distribution;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.db.BrowserContract;
+
+/**
+ * A proxy for the partner bookmarks provider. Bookmark and folder ids of the partner bookmarks providers
+ * will be transformed so that they do not overlap with the ids from the local database.
+ *
+ * Bookmarks in folder:
+ *   content://{PACKAGE_ID}.partnerbookmarks/bookmarks/{folderId}
+ * Icon of bookmark:
+ *   content://{PACKAGE_ID}.partnerbookmarks/icons/{bookmarkId}
+ */
+public class PartnerBookmarksProviderProxy extends ContentProvider {
+    /**
+     * The contract between the partner bookmarks provider and applications. Contains the definition
+     * for the supported URIs and columns.
+     */
+    public static class PartnerContract {
+        public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
+
+        public static final int TYPE_BOOKMARK = 1;
+        public static final int TYPE_FOLDER = 2;
+
+        public static final int PARENT_ROOT_ID = 0;
+
+        public static final String ID = "_id";
+        public static final String TYPE = "type";
+        public static final String URL = "url";
+        public static final String TITLE = "title";
+        public static final String FAVICON = "favicon";
+        public static final String TOUCHICON = "touchicon";
+        public static final String PARENT = "parent";
+    }
+
+    private static final String AUTHORITY_PREFIX = ".partnerbookmarks";
+
+    private static final int URI_MATCH_BOOKMARKS = 1000;
+    private static final int URI_MATCH_ICON = 1001;
+
+    private static String getAuthority(Context context) {
+        return context.getPackageName() + AUTHORITY_PREFIX;
+    }
+
+    public static Uri getUriForBookmarks(Context context, long folderId) {
+        return new Uri.Builder()
+                .scheme("content")
+                .authority(getAuthority(context))
+                .appendPath("bookmarks")
+                .appendPath(String.valueOf(folderId))
+                .build();
+    }
+
+    public static Uri getUriForIcon(Context context, long bookmarkId) {
+        return new Uri.Builder()
+                .scheme("content")
+                .authority(getAuthority(context))
+                .appendPath("icons")
+                .appendPath(String.valueOf(bookmarkId))
+                .build();
+    }
+
+    private final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    @Override
+    public boolean onCreate() {
+        String authority = getAuthority(assertAndGetContext());
+
+        uriMatcher.addURI(authority, "bookmarks/*", URI_MATCH_BOOKMARKS);
+        uriMatcher.addURI(authority, "icons/*", URI_MATCH_ICON);
+
+        return true;
+    }
+
+    @Override
+    public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+        final int match = uriMatcher.match(uri);
+
+        final ContentResolver contentResolver = assertAndGetContext().getContentResolver();
+
+        switch (match) {
+            case URI_MATCH_BOOKMARKS:
+                final long bookmarkId = ContentUris.parseId(uri);
+                if (bookmarkId == -1) {
+                    throw new IllegalArgumentException("Bookmark id is not a number");
+                }
+                return getBookmarksInFolder(contentResolver, bookmarkId);
+
+            case URI_MATCH_ICON:
+                return getIcon(contentResolver, ContentUris.parseId(uri));
+
+            default:
+                throw new UnsupportedOperationException("Unknown URI " + uri.toString());
+        }
+    };
+
+    private Cursor getBookmarksInFolder(ContentResolver contentResolver, long folderId) {
+        // Use root folder id or transform negative id into actual (positive) folder id.
+        final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
+                ? PartnerContract.PARENT_ROOT_ID
+                : BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
+
+        return contentResolver.query(
+                PartnerContract.CONTENT_URI,
+                new String[] {
+                        // Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
+                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
+                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Combined.BOOKMARK_ID,
+                        PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
+                        PartnerContract.URL +  " as " + BrowserContract.Bookmarks.URL,
+                        // Transform parent ids to negative ids as well
+                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
+                        // Convert types (we use 0-1 and the partner provider 1-2)
+                        "(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
+                        // Use the ID of the entry as GUID
+                        PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
+                },
+                PartnerContract.PARENT + " = ?"
+                        // Only select entries with valid type
+                        + " AND (" + BrowserContract.Bookmarks.TYPE + " = ? OR " + BrowserContract.Bookmarks.TYPE + " = ?)"
+                        // Only select entries with non empty title
+                        + " AND " + BrowserContract.Bookmarks.TITLE + " <> ''",
+                new String[] {
+                        String.valueOf(actualFolderId),
+                        String.valueOf(PartnerContract.TYPE_BOOKMARK),
+                        String.valueOf(PartnerContract.TYPE_FOLDER)
+                },
+                // Same order we use in our content provider (without position)
+                BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
+    }
+
+    private Cursor getIcon(ContentResolver contentResolver, long bookmarkId) {
+        final long actualId = BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - bookmarkId;
+
+        return contentResolver.query(
+                PartnerContract.CONTENT_URI,
+                new String[] {
+                    PartnerContract.TOUCHICON,
+                    PartnerContract.FAVICON
+                },
+                PartnerContract.ID + " = ?",
+                new String[] {
+                    String.valueOf(actualId)
+                },
+                null);
+    }
+
+    private Context assertAndGetContext() {
+        final Context context = super.getContext();
+
+        if (context == null) {
+            throw new AssertionError("Context is null");
+        }
+
+        return context;
+    }
+
+    @Override
+    public String getType(@NonNull Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(@NonNull Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
@@ -1,22 +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/. */
 
 package org.mozilla.gecko.favicons;
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.ProxySelector;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.IOException;
@@ -52,16 +56,22 @@ public class LoadFaviconTask {
     /**
      * Bypass all caches - this is used to directly retrieve the requested icon. Without this flag,
      * favicons will first be pushed into the memory cache (and possibly permanent cache if using FLAG_PERSIST),
      * where they will be downscaled to the maximum cache size, before being retrieved from the cache (resulting
      * in a possibly smaller icon size).
      */
     public static final int FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS = 2;
 
+    /**
+     * If downloading from the favicon URL failed then do NOT try to guess the default URL and
+     * download from the default URL.
+     */
+    public static final int FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL = 4;
+
     private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
     // The default size of the buffer to use for downloading Favicons in the event no size is given
     // by the server.
     public static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000;
 
     private static final AtomicInteger nextFaviconLoadId = new AtomicInteger(0);
     private final Context context;
     private final int id;
@@ -193,16 +203,83 @@ public class LoadFaviconTask {
                 // Just about anything could happen here.
                 Log.w(LOGTAG, "Error fetching favicon from JAR.", e);
                 return null;
             }
         }
         return null;
     }
 
+    /**
+     * Fetch icon from a content provider following the partner bookmarks provider contract.
+     */
+    private Bitmap fetchContentProviderFavicon(String uri, int targetWidthAndHeight) {
+        if (TextUtils.isEmpty(uri)) {
+            return null;
+        }
+
+        if (!uri.startsWith("content://")) {
+            return null;
+        }
+
+        Cursor cursor = context.getContentResolver().query(
+                Uri.parse(uri),
+                new String[] {
+                        PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON,
+                        PartnerBookmarksProviderProxy.PartnerContract.FAVICON,
+                },
+                null,
+                null,
+                null
+        );
+
+        if (cursor == null) {
+            return null;
+        }
+
+        try {
+            if (!cursor.moveToFirst()) {
+                return null;
+            }
+
+            Bitmap icon = decodeFromCursor(cursor, PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON, targetWidthAndHeight);
+            if (icon != null) {
+                return icon;
+            }
+
+            icon = decodeFromCursor(cursor, PartnerBookmarksProviderProxy.PartnerContract.FAVICON, targetWidthAndHeight);
+            if (icon != null) {
+                return icon;
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return null;
+    }
+
+    private Bitmap decodeFromCursor(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(data, 0, data.length);
+        if (result == null) {
+            return null;
+        }
+
+        return result.getBestBitmap(targetWidthAndHeight);
+    }
+
     // Runs in background thread.
     // Does not attempt to fetch from JARs.
     private LoadFaviconResult downloadFavicon(URI targetFaviconURI) {
         if (targetFaviconURI == null) {
             return null;
         }
 
         // Only get favicons for HTTP/HTTPS.
@@ -418,16 +495,24 @@ public class LoadFaviconTask {
         // Let's see if it's in a JAR.
         image = fetchJARFavicon(faviconURL);
         if (imageIsValid(image)) {
             // We don't want to put this into the DB.
             Favicons.putFaviconInMemCache(faviconURL, image);
             return image;
         }
 
+        // Download from a content provider
+        image = fetchContentProviderFavicon(faviconURL, targetWidthAndHeight);
+        if (imageIsValid(image)) {
+            // We don't want to put this into the DB.
+            Favicons.putFaviconInMemCache(faviconURL, image);
+            return image;
+        }
+
         try {
             loadedBitmaps = downloadFavicon(new URI(faviconURL));
         } catch (URISyntaxException e) {
             Log.e(LOGTAG, "The provided favicon URL is not valid");
             return null;
         } catch (Exception e) {
             Log.e(LOGTAG, "Couldn't download favicon.", e);
         }
@@ -435,40 +520,22 @@ public class LoadFaviconTask {
         if (loadedBitmaps != null) {
             if ((flags & FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS) == 0) {
                 // Fetching bytes to store can fail. saveFaviconToDb will
                 // do the right thing, but we still choose to cache the
                 // downloaded icon in memory.
                 saveFaviconToDb(db, loadedBitmaps.getBytesForDatabaseStorage());
                 return pushToCacheAndGetResult(loadedBitmaps);
             } else {
-                final Map<Integer, Bitmap> iconMap = new HashMap<>();
-                final List<Integer> sizes = new ArrayList<>();
-
-                while (loadedBitmaps.getBitmaps().hasNext()) {
-                    final Bitmap b = loadedBitmaps.getBitmaps().next();
+                return loadedBitmaps.getBestBitmap(targetWidthAndHeight);
+            }
+        }
 
-                    // 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 = Favicons.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);
-            }
+        if ((FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL & flags) == FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL) {
+            return null;
         }
 
         if (isUsingDefaultURL) {
             Favicons.putFaviconInFailedCache(faviconURL);
             return null;
         }
 
         if (isCancelled()) {
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/decoders/LoadFaviconResult.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/decoders/LoadFaviconResult.java
@@ -1,19 +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/. */
 
 package org.mozilla.gecko.favicons.decoders;
 
 import android.graphics.Bitmap;
 import android.util.Log;
+import android.util.SparseArray;
+
+import org.mozilla.gecko.favicons.Favicons;
 
 import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 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.)
@@ -68,9 +75,34 @@ public class LoadFaviconResult {
         } catch (OutOfMemoryError e) {
             Log.w(LOGTAG, "Out of memory re-compressing favicon.");
         }
 
         Log.w(LOGTAG, "Favicon re-compression failed.");
         return null;
     }
 
+    public Bitmap getBestBitmap(int targetWidthAndHeight) {
+        final SparseArray<Bitmap> iconMap = new SparseArray<>();
+        final List<Integer> sizes = new ArrayList<>();
+
+        while (bitmapsDecoded.hasNext()) {
+            final Bitmap b = bitmapsDecoded.next();
+
+            // It's possible to receive null, most likely due to OOM or a zero-sized image,
+            // from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options)
+            if (b != null) {
+                iconMap.put(b.getWidth(), b);
+                sizes.add(b.getWidth());
+            }
+        }
+
+        int bestSize = Favicons.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);
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java
@@ -10,17 +10,17 @@ import java.util.LinkedList;
 import java.util.List;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.distribution.PartnerBookmarksProviderClient;
+import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
 import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
 import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
 import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 import android.app.Activity;
@@ -230,17 +230,17 @@ public class BookmarksPanel extends Home
 
             final ContentResolver contentResolver = getContext().getContentResolver();
 
             Cursor partnerCursor = null;
             Cursor userCursor = null;
 
             if (GeckoSharedPrefs.forProfile(getContext()).getBoolean(GeckoPreferences.PREFS_READ_PARTNER_BOOKMARKS_PROVIDER, false)
                     && (isRootFolder || mFolderInfo.id <= Bookmarks.FAKE_PARTNER_BOOKMARKS_START)) {
-                partnerCursor = PartnerBookmarksProviderClient.getBookmarksInFolder(contentResolver, mFolderInfo.id);
+                partnerCursor = contentResolver.query(PartnerBookmarksProviderProxy.getUriForBookmarks(getContext(), mFolderInfo.id), null, null, null, null, null);
             }
 
             if (isRootFolder || mFolderInfo.id > Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
                 userCursor = mDB.getBookmarksInFolder(contentResolver, mFolderInfo.id);
             }
 
 
             if (partnerCursor == null && userCursor == null) {
--- a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
@@ -4,16 +4,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import java.lang.ref.WeakReference;
 
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
+import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
@@ -265,18 +268,18 @@ public class TwoLinePageRow extends Line
      */
     public void update(String title, String url) {
         update(title, url, 0, false);
     }
 
     protected void update(String title, String url, long bookmarkId, boolean hasReaderCacheItem) {
         if (mShowIcons) {
             // The bookmark id will be 0 (null in database) when the url
-            // is not a bookmark.
-            final boolean isBookmark = bookmarkId != 0;
+            // is not a bookmark and negative for 'fake' bookmarks.
+            final boolean isBookmark = bookmarkId > 0;
 
             updateStatusIcon(isBookmark, hasReaderCacheItem);
         } else {
             updateStatusIcon(false, false);
         }
 
         // Use the URL instead of an empty title for consistency with the normal URL
         // bar view - this is the equivalent of getDisplayTitle() in Tab.java
@@ -288,19 +291,33 @@ public class TwoLinePageRow extends Line
         }
 
         // Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB.
         mFavicon.clearImage();
         Favicons.cancelFaviconLoad(mLoadFaviconJobId);
 
         // Displayed RecentTabsPanel URLs may refer to pages opened in reader mode, so we
         // remove the about:reader prefix to ensure the Favicon loads properly.
-        final String pageURL = AboutPages.isAboutReader(url) ?
-            ReaderModeUtils.getUrlFromAboutReader(url) : url;
-        mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(getContext(), pageURL, mFaviconListener);
+        final String pageURL = AboutPages.isAboutReader(url) ? ReaderModeUtils.getUrlFromAboutReader(url) : url;
+
+        if (bookmarkId < BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
+            mLoadFaviconJobId = Favicons.getSizedFavicon(
+                    getContext(),
+                    pageURL,
+                    PartnerBookmarksProviderProxy.getUriForIcon(getContext(), bookmarkId).toString(),
+                    Favicons.LoadType.PRIVILEGED,
+                    Favicons.defaultFaviconSize,
+                    // We want to load the favicon from the content provider but we do not want the
+                    // favicon loader to fallback to loading a favicon from the web using a guessed
+                    // default URL.
+                    LoadFaviconTask.FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL,
+                    mFaviconListener);
+        } else {
+            mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(getContext(), pageURL, mFaviconListener);
+        }
 
         updateDisplayedUrl(url, hasReaderCacheItem);
     }
 
     /**
      * Update the data displayed by this row.
      * <p>
      * This method must be invoked on the UI thread.
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -255,17 +255,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'db/URLMetadataTable.java',
     'delegates/BookmarkStateChangeDelegate.java',
     'delegates/BrowserAppDelegate.java',
     'delegates/BrowserAppDelegateWithReference.java',
     'delegates/ScreenshotDelegate.java',
     'DevToolsAuthHelper.java',
     'distribution/Distribution.java',
     'distribution/DistributionStoreCallback.java',
-    'distribution/PartnerBookmarksProviderClient.java',
+    'distribution/PartnerBookmarksProviderProxy.java',
     'distribution/PartnerBrowserCustomizationsClient.java',
     'distribution/ReferrerDescriptor.java',
     'distribution/ReferrerReceiver.java',
     'dlc/BaseAction.java',
     'dlc/catalog/DownloadContent.java',
     'dlc/catalog/DownloadContentBootstrap.java',
     'dlc/catalog/DownloadContentBuilder.java',
     'dlc/catalog/DownloadContentCatalog.java',