mobile/android/base/favicons/Favicons.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko.favicons;
     8 import org.mozilla.gecko.AboutPages;
     9 import org.mozilla.gecko.AppConstants;
    10 import org.mozilla.gecko.GeckoAppShell;
    11 import org.mozilla.gecko.R;
    12 import org.mozilla.gecko.Tab;
    13 import org.mozilla.gecko.Tabs;
    14 import org.mozilla.gecko.db.BrowserDB;
    15 import org.mozilla.gecko.favicons.cache.FaviconCache;
    16 import org.mozilla.gecko.util.GeckoJarReader;
    17 import org.mozilla.gecko.util.NonEvictingLruCache;
    18 import org.mozilla.gecko.util.ThreadUtils;
    20 import android.content.Context;
    21 import android.content.res.Resources;
    22 import android.graphics.Bitmap;
    23 import android.graphics.BitmapFactory;
    24 import android.text.TextUtils;
    25 import android.util.Log;
    26 import android.util.SparseArray;
    28 import java.io.File;
    29 import java.net.URI;
    30 import java.net.URISyntaxException;
    31 import java.util.Arrays;
    32 import java.util.Iterator;
    33 import java.util.List;
    35 public class Favicons {
    36     private static final String LOGTAG = "GeckoFavicons";
    38     // A magic URL representing the app's own favicon, used for about: pages.
    39     private static final String BUILT_IN_FAVICON_URL = "about:favicon";
    41     // Size of the favicon bitmap cache, in bytes (Counting payload only).
    42     public static final int FAVICON_CACHE_SIZE_BYTES = 512 * 1024;
    44     // Number of URL mappings from page URL to Favicon URL to cache in memory.
    45     public static final int NUM_PAGE_URL_MAPPINGS_TO_STORE = 128;
    47     public static final int NOT_LOADING  = 0;
    48     public static final int LOADED       = 1;
    49     public static final int FLAG_PERSIST = 2;
    50     public static final int FLAG_SCALE   = 4;
    52     protected static Context context;
    54     // The default Favicon to show if no other can be found.
    55     public static Bitmap defaultFavicon;
    57     // The density-adjusted default Favicon dimensions.
    58     public static int defaultFaviconSize;
    60     // The density-adjusted maximum Favicon dimensions.
    61     public static int largestFaviconSize;
    63     private static final SparseArray<LoadFaviconTask> loadTasks = new SparseArray<LoadFaviconTask>();
    65     // Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when
    66     // doing so is not necessary.
    67     private static final NonEvictingLruCache<String, String> pageURLMappings = new NonEvictingLruCache<String, String>(NUM_PAGE_URL_MAPPINGS_TO_STORE);
    69     public static String getFaviconURLForPageURLFromCache(String pageURL) {
    70         return pageURLMappings.get(pageURL);
    71     }
    73     /**
    74      * Insert the given pageUrl->faviconUrl mapping into the memory cache of such mappings.
    75      * Useful for short-circuiting local database access.
    76      */
    77     public static void putFaviconURLForPageURLInCache(String pageURL, String faviconURL) {
    78         pageURLMappings.put(pageURL, faviconURL);
    79     }
    81     private static FaviconCache faviconsCache;
    83     /**
    84      * Returns either NOT_LOADING, or LOADED if the onFaviconLoaded call could
    85      * be made on the main thread.
    86      * If no listener is provided, NOT_LOADING is returned.
    87      */
    88     static int dispatchResult(final String pageUrl, final String faviconURL, final Bitmap image,
    89             final OnFaviconLoadedListener listener) {
    90         if (listener == null) {
    91             return NOT_LOADING;
    92         }
    94         if (ThreadUtils.isOnUiThread()) {
    95             listener.onFaviconLoaded(pageUrl, faviconURL, image);
    96             return LOADED;
    97         }
    99         // We want to always run the listener on UI thread.
   100         ThreadUtils.postToUiThread(new Runnable() {
   101             @Override
   102             public void run() {
   103                 listener.onFaviconLoaded(pageUrl, faviconURL, image);
   104             }
   105         });
   106         return NOT_LOADING;
   107     }
   109     /**
   110      * Only returns a non-null Bitmap if the entire path is cached -- the
   111      * page URL to favicon URL, and the favicon URL to in-memory bitmaps.
   112      *
   113      * Returns null otherwise.
   114      */
   115     public static Bitmap getSizedFaviconForPageFromCache(final String pageURL, int targetSize) {
   116         final String faviconURL = pageURLMappings.get(pageURL);
   117         if (faviconURL == null) {
   118             return null;
   119         }
   120         return getSizedFaviconFromCache(faviconURL, targetSize);
   121     }
   123     /**
   124      * Get a Favicon as close as possible to the target dimensions for the URL provided.
   125      * If a result is instantly available from the cache, it is returned and the listener is invoked.
   126      * Otherwise, the result is drawn from the database or network and the listener invoked when the
   127      * result becomes available.
   128      *
   129      * @param pageURL Page URL for which a Favicon is desired.
   130      * @param faviconURL URL of the Favicon to be downloaded, if known. If none provided, an educated
   131      *                    guess is made by the system.
   132      * @param targetSize Target size of the returned Favicon
   133      * @param listener Listener to call with the result of the load operation, if the result is not
   134      *                  immediately available.
   135      * @return The id of the asynchronous task created, NOT_LOADING if none is created, or
   136      *         LOADED if the value could be dispatched on the current thread.
   137      */
   138     public static int getSizedFavicon(String pageURL, String faviconURL, int targetSize, int flags, OnFaviconLoadedListener listener) {
   139         // Do we know the favicon URL for this page already?
   140         String cacheURL = faviconURL;
   141         if (cacheURL == null) {
   142             cacheURL = pageURLMappings.get(pageURL);
   143         }
   145         // If there's no favicon URL given, try and hit the cache with the default one.
   146         if (cacheURL == null)  {
   147             cacheURL = guessDefaultFaviconURL(pageURL);
   148         }
   150         // If it's something we can't even figure out a default URL for, just give up.
   151         if (cacheURL == null) {
   152             return dispatchResult(pageURL, null, defaultFavicon, listener);
   153         }
   155         Bitmap cachedIcon = getSizedFaviconFromCache(cacheURL, targetSize);
   156         if (cachedIcon != null) {
   157             return dispatchResult(pageURL, cacheURL, cachedIcon, listener);
   158         }
   160         // Check if favicon has failed.
   161         if (faviconsCache.isFailedFavicon(cacheURL)) {
   162             return dispatchResult(pageURL, cacheURL, defaultFavicon, listener);
   163         }
   165         // Failing that, try and get one from the database or internet.
   166         return loadUncachedFavicon(pageURL, faviconURL, flags, targetSize, listener);
   167     }
   169     /**
   170      * Returns the cached Favicon closest to the target size if any exists or is coercible. Returns
   171      * null otherwise. Does not query the database or network for the Favicon is the result is not
   172      * immediately available.
   173      *
   174      * @param faviconURL URL of the Favicon to query for.
   175      * @param targetSize The desired size of the returned Favicon.
   176      * @return The cached Favicon, rescaled to be as close as possible to the target size, if any exists.
   177      *         null if no applicable Favicon exists in the cache.
   178      */
   179     public static Bitmap getSizedFaviconFromCache(String faviconURL, int targetSize) {
   180         return faviconsCache.getFaviconForDimensions(faviconURL, targetSize);
   181     }
   183     /**
   184      * Attempts to find a Favicon for the provided page URL from either the mem cache or the database.
   185      * Does not need an explicit favicon URL, since, as we are accessing the database anyway, we
   186      * can query the history DB for the Favicon URL.
   187      * Handy for easing the transition from caching with page URLs to caching with Favicon URLs.
   188      *
   189      * A null result is passed to the listener if no value is locally available. The Favicon is not
   190      * added to the failure cache.
   191      *
   192      * @param pageURL Page URL for which a Favicon is wanted.
   193      * @param targetSize Target size of the desired Favicon to pass to the cache query
   194      * @param callback Callback to fire with the result.
   195      * @return The job ID of the spawned async task, if any.
   196      */
   197     public static int getSizedFaviconForPageFromLocal(final String pageURL, final int targetSize, final OnFaviconLoadedListener callback) {
   198         // Firstly, try extremely hard to cheat.
   199         // Have we cached this favicon URL? If we did, we can consult the memcache right away.
   200         String targetURL = pageURLMappings.get(pageURL);
   201         if (targetURL != null) {
   202             // Check if favicon has failed.
   203             if (faviconsCache.isFailedFavicon(targetURL)) {
   204                 return dispatchResult(pageURL, targetURL, null, callback);
   205             }
   207             // Do we have a Favicon in the cache for this favicon URL?
   208             Bitmap result = getSizedFaviconFromCache(targetURL, targetSize);
   209             if (result != null) {
   210                 // Victory - immediate response!
   211                 return dispatchResult(pageURL, targetURL, result, callback);
   212             }
   213         }
   215         // No joy using in-memory resources. Go to background thread and ask the database.
   216         LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageURL, targetURL, 0, callback, targetSize, true);
   217         int taskId = task.getId();
   218         synchronized(loadTasks) {
   219             loadTasks.put(taskId, task);
   220         }
   221         task.execute();
   222         return taskId;
   223     }
   225     public static int getSizedFaviconForPageFromLocal(final String pageURL, final OnFaviconLoadedListener callback) {
   226         return getSizedFaviconForPageFromLocal(pageURL, defaultFaviconSize, callback);
   227     }
   229     /**
   230      * Helper method to determine the URL of the Favicon image for a given page URL by querying the
   231      * history database. Should only be called from the background thread - does database access.
   232      *
   233      * @param pageURL The URL of a webpage with a Favicon.
   234      * @return The URL of the Favicon used by that webpage, according to either the History database
   235      *         or a somewhat educated guess.
   236      */
   237     public static String getFaviconURLForPageURL(String pageURL) {
   238         // Attempt to determine the Favicon URL from the Tabs datastructure. Can dodge having to use
   239         // the database sometimes by doing this.
   240         String targetURL;
   241         Tab theTab = Tabs.getInstance().getFirstTabForUrl(pageURL);
   242         if (theTab != null) {
   243             targetURL = theTab.getFaviconURL();
   244             if (targetURL != null) {
   245                 return targetURL;
   246             }
   247         }
   249         targetURL = BrowserDB.getFaviconUrlForHistoryUrl(context.getContentResolver(), pageURL);
   250         if (targetURL == null) {
   251             // Nothing in the history database. Fall back to the default URL and hope for the best.
   252             targetURL = guessDefaultFaviconURL(pageURL);
   253         }
   254         return targetURL;
   255     }
   257     /**
   258      * Helper function to create an async job to load a Favicon which does not exist in the memcache.
   259      * Contains logic to prevent the repeated loading of Favicons which have previously failed.
   260      * There is no support for recovery from transient failures.
   261      *
   262      * @param pageUrl URL of the page for which to load a Favicon. If null, no job is created.
   263      * @param faviconUrl The URL of the Favicon to load. If null, an attempt to infer the value from
   264      *                   the history database will be made, and ultimately an attempt to guess will
   265      *                   be made.
   266      * @param flags Flags to be used by the LoadFaviconTask while loading. Currently only one flag
   267      *              is supported, LoadFaviconTask.FLAG_PERSIST.
   268      *              If FLAG_PERSIST is set and the Favicon is ultimately loaded from the internet,
   269      *              the downloaded Favicon is subsequently stored in the local database.
   270      *              If FLAG_PERSIST is unset, the downloaded Favicon is stored only in the memcache.
   271      *              FLAG_PERSIST has no effect on loads which come from the database.
   272      * @param listener The OnFaviconLoadedListener to invoke with the result of this Favicon load.
   273      * @return The id of the LoadFaviconTask handling this job.
   274      */
   275     private static int loadUncachedFavicon(String pageUrl, String faviconUrl, int flags, int targetSize, OnFaviconLoadedListener listener) {
   276         // Handle the case where we have no page url.
   277         if (TextUtils.isEmpty(pageUrl)) {
   278             dispatchResult(null, null, null, listener);
   279             return NOT_LOADING;
   280         }
   282         LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageUrl, faviconUrl, flags, listener, targetSize, false);
   284         int taskId = task.getId();
   285         synchronized(loadTasks) {
   286             loadTasks.put(taskId, task);
   287         }
   289         task.execute();
   291         return taskId;
   292     }
   294     public static void putFaviconInMemCache(String pageUrl, Bitmap image) {
   295         faviconsCache.putSingleFavicon(pageUrl, image);
   296     }
   298     /**
   299      * Adds the bitmaps given by the specified iterator to the cache associated with the url given.
   300      * Future requests for images will be able to select the least larger image than the target
   301      * size from this new set of images.
   302      *
   303      * @param pageUrl The URL to associate the new favicons with.
   304      * @param images An iterator over the new favicons to put in the cache.
   305      */
   306     public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images, boolean permanently) {
   307         faviconsCache.putFavicons(pageUrl, images, permanently);
   308     }
   310     public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) {
   311         putFaviconsInMemCache(pageUrl, images, false);
   312     }
   314     public static void clearMemCache() {
   315         faviconsCache.evictAll();
   316         pageURLMappings.evictAll();
   317     }
   319     public static void putFaviconInFailedCache(String faviconURL) {
   320         faviconsCache.putFailed(faviconURL);
   321     }
   323     public static boolean cancelFaviconLoad(int taskId) {
   324         if (taskId == NOT_LOADING) {
   325             return false;
   326         }
   328         boolean cancelled;
   329         synchronized (loadTasks) {
   330             if (loadTasks.indexOfKey(taskId) < 0) {
   331                 return false;
   332             }
   334             Log.v(LOGTAG, "Cancelling favicon load " + taskId + ".");
   336             LoadFaviconTask task = loadTasks.get(taskId);
   337             cancelled = task.cancel(false);
   338         }
   339         return cancelled;
   340     }
   342     public static void close() {
   343         Log.d(LOGTAG, "Closing Favicons database");
   345         // Cancel any pending tasks
   346         synchronized (loadTasks) {
   347             final int count = loadTasks.size();
   348             for (int i = 0; i < count; i++) {
   349                 cancelFaviconLoad(loadTasks.keyAt(i));
   350             }
   351             loadTasks.clear();
   352         }
   354         LoadFaviconTask.closeHTTPClient();
   355     }
   357     /**
   358      * Get the dominant colour of the Favicon at the URL given, if any exists in the cache.
   359      *
   360      * @param url The URL of the Favicon, to be used as the cache key for the colour value.
   361      * @return The dominant colour of the provided Favicon.
   362      */
   363     public static int getFaviconColor(String url) {
   364         return faviconsCache.getDominantColor(url);
   365     }
   367     /**
   368      * Called by GeckoApp on startup to pass this class a reference to the GeckoApp object used as
   369      * the application's Context.
   370      * Consider replacing with references to a staticly held reference to the GeckoApp object.
   371      *
   372      * @param context A reference to the GeckoApp instance.
   373      */
   374     public static void attachToContext(Context context) throws Exception {
   375         final Resources res = context.getResources();
   376         Favicons.context = context;
   378         // Decode the default Favicon ready for use.
   379         defaultFavicon = BitmapFactory.decodeResource(res, R.drawable.favicon);
   380         if (defaultFavicon == null) {
   381             throw new Exception("Null default favicon was returned from the resources system!");
   382         }
   384         defaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg);
   386         // Screen-density-adjusted upper limit on favicon size. Favicons larger than this are
   387         // downscaled to this size or discarded.
   388         largestFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
   389         faviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, largestFaviconSize);
   391         // Initialize page mappings for each of our special pages.
   392         for (String url : AboutPages.getDefaultIconPages()) {
   393             pageURLMappings.putWithoutEviction(url, BUILT_IN_FAVICON_URL);
   394         }
   396         // Load and cache the built-in favicon in each of its sizes.
   397         // TODO: don't open the zip twice!
   398         List<Bitmap> toInsert = Arrays.asList(loadBrandingBitmap(context, "favicon64.png"),
   399                                               loadBrandingBitmap(context, "favicon32.png"));
   401         putFaviconsInMemCache(BUILT_IN_FAVICON_URL, toInsert.iterator(), true);
   402     }
   404     /**
   405      * Compute a string like:
   406      * "jar:jar:file:///data/app/org.mozilla.firefox-1.apk!/assets/omni.ja!/chrome/chrome/content/branding/favicon64.png"
   407      */
   408     private static String getBrandingBitmapPath(Context context, String name) {
   409         final String apkPath = context.getPackageResourcePath();
   410         return "jar:jar:" + new File(apkPath).toURI() + "!/" +
   411                AppConstants.OMNIJAR_NAME + "!/" +
   412                "chrome/chrome/content/branding/" + name;
   413     }
   415     private static Bitmap loadBrandingBitmap(Context context, String name) {
   416         Bitmap b = GeckoJarReader.getBitmap(context.getResources(),
   417                                             getBrandingBitmapPath(context, name));
   418         if (b == null) {
   419             throw new IllegalStateException("Bitmap " + name + " missing from JAR!");
   420         }
   421         return b;
   422     }
   424     /**
   425      * Helper method to get the default Favicon URL for a given pageURL. Generally: somewhere.com/favicon.ico
   426      *
   427      * @param pageURL Page URL for which a default Favicon URL is requested
   428      * @return The default Favicon URL.
   429      */
   430     public static String guessDefaultFaviconURL(String pageURL) {
   431         // Special-casing for about: pages. The favicon for about:pages which don't provide a link tag
   432         // is bundled in the database, keyed only by page URL, hence the need to return the page URL
   433         // here. If the database ever migrates to stop being silly in this way, this can plausibly
   434         // be removed.
   435         if (AboutPages.isAboutPage(pageURL) || pageURL.startsWith("jar:")) {
   436             return pageURL;
   437         }
   439         try {
   440             // Fall back to trying "someScheme:someDomain.someExtension/favicon.ico".
   441             URI u = new URI(pageURL);
   442             return new URI(u.getScheme(),
   443                            u.getAuthority(),
   444                            "/favicon.ico", null,
   445                            null).toString();
   446         } catch (URISyntaxException e) {
   447             Log.e(LOGTAG, "URISyntaxException getting default favicon URL", e);
   448             return null;
   449         }
   450     }
   452     public static void removeLoadTask(int taskId) {
   453         synchronized(loadTasks) {
   454             loadTasks.delete(taskId);
   455         }
   456     }
   458     /**
   459      * Method to wrap FaviconCache.isFailedFavicon for use by LoadFaviconTask.
   460      *
   461      * @param faviconURL Favicon URL to check for failure.
   462      */
   463     static boolean isFailedFavicon(String faviconURL) {
   464         return faviconsCache.isFailedFavicon(faviconURL);
   465     }
   467     /**
   468      * Sidestep the cache and get, from either the database or the internet, a favicon
   469      * suitable for use as an app icon for the provided URL.
   470      *
   471      * Useful for creating homescreen shortcuts without being limited
   472      * by possibly low-resolution values in the cache.
   473      *
   474      * Deduces the favicon URL from the browser database, guessing if necessary.
   475      *
   476      * @param url page URL to get a large favicon image for.
   477      * @param onFaviconLoadedListener listener to call back with the result.
   478      */
   479     public static void getPreferredSizeFaviconForPage(String url, OnFaviconLoadedListener onFaviconLoadedListener) {
   480         int preferredSize = GeckoAppShell.getPreferredIconSize();
   481         loadUncachedFavicon(url, null, 0, preferredSize, onFaviconLoadedListener);
   482     }
   483 }

mercurial