1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/favicons/Favicons.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,483 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.favicons; 1.10 + 1.11 +import org.mozilla.gecko.AboutPages; 1.12 +import org.mozilla.gecko.AppConstants; 1.13 +import org.mozilla.gecko.GeckoAppShell; 1.14 +import org.mozilla.gecko.R; 1.15 +import org.mozilla.gecko.Tab; 1.16 +import org.mozilla.gecko.Tabs; 1.17 +import org.mozilla.gecko.db.BrowserDB; 1.18 +import org.mozilla.gecko.favicons.cache.FaviconCache; 1.19 +import org.mozilla.gecko.util.GeckoJarReader; 1.20 +import org.mozilla.gecko.util.NonEvictingLruCache; 1.21 +import org.mozilla.gecko.util.ThreadUtils; 1.22 + 1.23 +import android.content.Context; 1.24 +import android.content.res.Resources; 1.25 +import android.graphics.Bitmap; 1.26 +import android.graphics.BitmapFactory; 1.27 +import android.text.TextUtils; 1.28 +import android.util.Log; 1.29 +import android.util.SparseArray; 1.30 + 1.31 +import java.io.File; 1.32 +import java.net.URI; 1.33 +import java.net.URISyntaxException; 1.34 +import java.util.Arrays; 1.35 +import java.util.Iterator; 1.36 +import java.util.List; 1.37 + 1.38 +public class Favicons { 1.39 + private static final String LOGTAG = "GeckoFavicons"; 1.40 + 1.41 + // A magic URL representing the app's own favicon, used for about: pages. 1.42 + private static final String BUILT_IN_FAVICON_URL = "about:favicon"; 1.43 + 1.44 + // Size of the favicon bitmap cache, in bytes (Counting payload only). 1.45 + public static final int FAVICON_CACHE_SIZE_BYTES = 512 * 1024; 1.46 + 1.47 + // Number of URL mappings from page URL to Favicon URL to cache in memory. 1.48 + public static final int NUM_PAGE_URL_MAPPINGS_TO_STORE = 128; 1.49 + 1.50 + public static final int NOT_LOADING = 0; 1.51 + public static final int LOADED = 1; 1.52 + public static final int FLAG_PERSIST = 2; 1.53 + public static final int FLAG_SCALE = 4; 1.54 + 1.55 + protected static Context context; 1.56 + 1.57 + // The default Favicon to show if no other can be found. 1.58 + public static Bitmap defaultFavicon; 1.59 + 1.60 + // The density-adjusted default Favicon dimensions. 1.61 + public static int defaultFaviconSize; 1.62 + 1.63 + // The density-adjusted maximum Favicon dimensions. 1.64 + public static int largestFaviconSize; 1.65 + 1.66 + private static final SparseArray<LoadFaviconTask> loadTasks = new SparseArray<LoadFaviconTask>(); 1.67 + 1.68 + // Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when 1.69 + // doing so is not necessary. 1.70 + private static final NonEvictingLruCache<String, String> pageURLMappings = new NonEvictingLruCache<String, String>(NUM_PAGE_URL_MAPPINGS_TO_STORE); 1.71 + 1.72 + public static String getFaviconURLForPageURLFromCache(String pageURL) { 1.73 + return pageURLMappings.get(pageURL); 1.74 + } 1.75 + 1.76 + /** 1.77 + * Insert the given pageUrl->faviconUrl mapping into the memory cache of such mappings. 1.78 + * Useful for short-circuiting local database access. 1.79 + */ 1.80 + public static void putFaviconURLForPageURLInCache(String pageURL, String faviconURL) { 1.81 + pageURLMappings.put(pageURL, faviconURL); 1.82 + } 1.83 + 1.84 + private static FaviconCache faviconsCache; 1.85 + 1.86 + /** 1.87 + * Returns either NOT_LOADING, or LOADED if the onFaviconLoaded call could 1.88 + * be made on the main thread. 1.89 + * If no listener is provided, NOT_LOADING is returned. 1.90 + */ 1.91 + static int dispatchResult(final String pageUrl, final String faviconURL, final Bitmap image, 1.92 + final OnFaviconLoadedListener listener) { 1.93 + if (listener == null) { 1.94 + return NOT_LOADING; 1.95 + } 1.96 + 1.97 + if (ThreadUtils.isOnUiThread()) { 1.98 + listener.onFaviconLoaded(pageUrl, faviconURL, image); 1.99 + return LOADED; 1.100 + } 1.101 + 1.102 + // We want to always run the listener on UI thread. 1.103 + ThreadUtils.postToUiThread(new Runnable() { 1.104 + @Override 1.105 + public void run() { 1.106 + listener.onFaviconLoaded(pageUrl, faviconURL, image); 1.107 + } 1.108 + }); 1.109 + return NOT_LOADING; 1.110 + } 1.111 + 1.112 + /** 1.113 + * Only returns a non-null Bitmap if the entire path is cached -- the 1.114 + * page URL to favicon URL, and the favicon URL to in-memory bitmaps. 1.115 + * 1.116 + * Returns null otherwise. 1.117 + */ 1.118 + public static Bitmap getSizedFaviconForPageFromCache(final String pageURL, int targetSize) { 1.119 + final String faviconURL = pageURLMappings.get(pageURL); 1.120 + if (faviconURL == null) { 1.121 + return null; 1.122 + } 1.123 + return getSizedFaviconFromCache(faviconURL, targetSize); 1.124 + } 1.125 + 1.126 + /** 1.127 + * Get a Favicon as close as possible to the target dimensions for the URL provided. 1.128 + * If a result is instantly available from the cache, it is returned and the listener is invoked. 1.129 + * Otherwise, the result is drawn from the database or network and the listener invoked when the 1.130 + * result becomes available. 1.131 + * 1.132 + * @param pageURL Page URL for which a Favicon is desired. 1.133 + * @param faviconURL URL of the Favicon to be downloaded, if known. If none provided, an educated 1.134 + * guess is made by the system. 1.135 + * @param targetSize Target size of the returned Favicon 1.136 + * @param listener Listener to call with the result of the load operation, if the result is not 1.137 + * immediately available. 1.138 + * @return The id of the asynchronous task created, NOT_LOADING if none is created, or 1.139 + * LOADED if the value could be dispatched on the current thread. 1.140 + */ 1.141 + public static int getSizedFavicon(String pageURL, String faviconURL, int targetSize, int flags, OnFaviconLoadedListener listener) { 1.142 + // Do we know the favicon URL for this page already? 1.143 + String cacheURL = faviconURL; 1.144 + if (cacheURL == null) { 1.145 + cacheURL = pageURLMappings.get(pageURL); 1.146 + } 1.147 + 1.148 + // If there's no favicon URL given, try and hit the cache with the default one. 1.149 + if (cacheURL == null) { 1.150 + cacheURL = guessDefaultFaviconURL(pageURL); 1.151 + } 1.152 + 1.153 + // If it's something we can't even figure out a default URL for, just give up. 1.154 + if (cacheURL == null) { 1.155 + return dispatchResult(pageURL, null, defaultFavicon, listener); 1.156 + } 1.157 + 1.158 + Bitmap cachedIcon = getSizedFaviconFromCache(cacheURL, targetSize); 1.159 + if (cachedIcon != null) { 1.160 + return dispatchResult(pageURL, cacheURL, cachedIcon, listener); 1.161 + } 1.162 + 1.163 + // Check if favicon has failed. 1.164 + if (faviconsCache.isFailedFavicon(cacheURL)) { 1.165 + return dispatchResult(pageURL, cacheURL, defaultFavicon, listener); 1.166 + } 1.167 + 1.168 + // Failing that, try and get one from the database or internet. 1.169 + return loadUncachedFavicon(pageURL, faviconURL, flags, targetSize, listener); 1.170 + } 1.171 + 1.172 + /** 1.173 + * Returns the cached Favicon closest to the target size if any exists or is coercible. Returns 1.174 + * null otherwise. Does not query the database or network for the Favicon is the result is not 1.175 + * immediately available. 1.176 + * 1.177 + * @param faviconURL URL of the Favicon to query for. 1.178 + * @param targetSize The desired size of the returned Favicon. 1.179 + * @return The cached Favicon, rescaled to be as close as possible to the target size, if any exists. 1.180 + * null if no applicable Favicon exists in the cache. 1.181 + */ 1.182 + public static Bitmap getSizedFaviconFromCache(String faviconURL, int targetSize) { 1.183 + return faviconsCache.getFaviconForDimensions(faviconURL, targetSize); 1.184 + } 1.185 + 1.186 + /** 1.187 + * Attempts to find a Favicon for the provided page URL from either the mem cache or the database. 1.188 + * Does not need an explicit favicon URL, since, as we are accessing the database anyway, we 1.189 + * can query the history DB for the Favicon URL. 1.190 + * Handy for easing the transition from caching with page URLs to caching with Favicon URLs. 1.191 + * 1.192 + * A null result is passed to the listener if no value is locally available. The Favicon is not 1.193 + * added to the failure cache. 1.194 + * 1.195 + * @param pageURL Page URL for which a Favicon is wanted. 1.196 + * @param targetSize Target size of the desired Favicon to pass to the cache query 1.197 + * @param callback Callback to fire with the result. 1.198 + * @return The job ID of the spawned async task, if any. 1.199 + */ 1.200 + public static int getSizedFaviconForPageFromLocal(final String pageURL, final int targetSize, final OnFaviconLoadedListener callback) { 1.201 + // Firstly, try extremely hard to cheat. 1.202 + // Have we cached this favicon URL? If we did, we can consult the memcache right away. 1.203 + String targetURL = pageURLMappings.get(pageURL); 1.204 + if (targetURL != null) { 1.205 + // Check if favicon has failed. 1.206 + if (faviconsCache.isFailedFavicon(targetURL)) { 1.207 + return dispatchResult(pageURL, targetURL, null, callback); 1.208 + } 1.209 + 1.210 + // Do we have a Favicon in the cache for this favicon URL? 1.211 + Bitmap result = getSizedFaviconFromCache(targetURL, targetSize); 1.212 + if (result != null) { 1.213 + // Victory - immediate response! 1.214 + return dispatchResult(pageURL, targetURL, result, callback); 1.215 + } 1.216 + } 1.217 + 1.218 + // No joy using in-memory resources. Go to background thread and ask the database. 1.219 + LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageURL, targetURL, 0, callback, targetSize, true); 1.220 + int taskId = task.getId(); 1.221 + synchronized(loadTasks) { 1.222 + loadTasks.put(taskId, task); 1.223 + } 1.224 + task.execute(); 1.225 + return taskId; 1.226 + } 1.227 + 1.228 + public static int getSizedFaviconForPageFromLocal(final String pageURL, final OnFaviconLoadedListener callback) { 1.229 + return getSizedFaviconForPageFromLocal(pageURL, defaultFaviconSize, callback); 1.230 + } 1.231 + 1.232 + /** 1.233 + * Helper method to determine the URL of the Favicon image for a given page URL by querying the 1.234 + * history database. Should only be called from the background thread - does database access. 1.235 + * 1.236 + * @param pageURL The URL of a webpage with a Favicon. 1.237 + * @return The URL of the Favicon used by that webpage, according to either the History database 1.238 + * or a somewhat educated guess. 1.239 + */ 1.240 + public static String getFaviconURLForPageURL(String pageURL) { 1.241 + // Attempt to determine the Favicon URL from the Tabs datastructure. Can dodge having to use 1.242 + // the database sometimes by doing this. 1.243 + String targetURL; 1.244 + Tab theTab = Tabs.getInstance().getFirstTabForUrl(pageURL); 1.245 + if (theTab != null) { 1.246 + targetURL = theTab.getFaviconURL(); 1.247 + if (targetURL != null) { 1.248 + return targetURL; 1.249 + } 1.250 + } 1.251 + 1.252 + targetURL = BrowserDB.getFaviconUrlForHistoryUrl(context.getContentResolver(), pageURL); 1.253 + if (targetURL == null) { 1.254 + // Nothing in the history database. Fall back to the default URL and hope for the best. 1.255 + targetURL = guessDefaultFaviconURL(pageURL); 1.256 + } 1.257 + return targetURL; 1.258 + } 1.259 + 1.260 + /** 1.261 + * Helper function to create an async job to load a Favicon which does not exist in the memcache. 1.262 + * Contains logic to prevent the repeated loading of Favicons which have previously failed. 1.263 + * There is no support for recovery from transient failures. 1.264 + * 1.265 + * @param pageUrl URL of the page for which to load a Favicon. If null, no job is created. 1.266 + * @param faviconUrl The URL of the Favicon to load. If null, an attempt to infer the value from 1.267 + * the history database will be made, and ultimately an attempt to guess will 1.268 + * be made. 1.269 + * @param flags Flags to be used by the LoadFaviconTask while loading. Currently only one flag 1.270 + * is supported, LoadFaviconTask.FLAG_PERSIST. 1.271 + * If FLAG_PERSIST is set and the Favicon is ultimately loaded from the internet, 1.272 + * the downloaded Favicon is subsequently stored in the local database. 1.273 + * If FLAG_PERSIST is unset, the downloaded Favicon is stored only in the memcache. 1.274 + * FLAG_PERSIST has no effect on loads which come from the database. 1.275 + * @param listener The OnFaviconLoadedListener to invoke with the result of this Favicon load. 1.276 + * @return The id of the LoadFaviconTask handling this job. 1.277 + */ 1.278 + private static int loadUncachedFavicon(String pageUrl, String faviconUrl, int flags, int targetSize, OnFaviconLoadedListener listener) { 1.279 + // Handle the case where we have no page url. 1.280 + if (TextUtils.isEmpty(pageUrl)) { 1.281 + dispatchResult(null, null, null, listener); 1.282 + return NOT_LOADING; 1.283 + } 1.284 + 1.285 + LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageUrl, faviconUrl, flags, listener, targetSize, false); 1.286 + 1.287 + int taskId = task.getId(); 1.288 + synchronized(loadTasks) { 1.289 + loadTasks.put(taskId, task); 1.290 + } 1.291 + 1.292 + task.execute(); 1.293 + 1.294 + return taskId; 1.295 + } 1.296 + 1.297 + public static void putFaviconInMemCache(String pageUrl, Bitmap image) { 1.298 + faviconsCache.putSingleFavicon(pageUrl, image); 1.299 + } 1.300 + 1.301 + /** 1.302 + * Adds the bitmaps given by the specified iterator to the cache associated with the url given. 1.303 + * Future requests for images will be able to select the least larger image than the target 1.304 + * size from this new set of images. 1.305 + * 1.306 + * @param pageUrl The URL to associate the new favicons with. 1.307 + * @param images An iterator over the new favicons to put in the cache. 1.308 + */ 1.309 + public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images, boolean permanently) { 1.310 + faviconsCache.putFavicons(pageUrl, images, permanently); 1.311 + } 1.312 + 1.313 + public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) { 1.314 + putFaviconsInMemCache(pageUrl, images, false); 1.315 + } 1.316 + 1.317 + public static void clearMemCache() { 1.318 + faviconsCache.evictAll(); 1.319 + pageURLMappings.evictAll(); 1.320 + } 1.321 + 1.322 + public static void putFaviconInFailedCache(String faviconURL) { 1.323 + faviconsCache.putFailed(faviconURL); 1.324 + } 1.325 + 1.326 + public static boolean cancelFaviconLoad(int taskId) { 1.327 + if (taskId == NOT_LOADING) { 1.328 + return false; 1.329 + } 1.330 + 1.331 + boolean cancelled; 1.332 + synchronized (loadTasks) { 1.333 + if (loadTasks.indexOfKey(taskId) < 0) { 1.334 + return false; 1.335 + } 1.336 + 1.337 + Log.v(LOGTAG, "Cancelling favicon load " + taskId + "."); 1.338 + 1.339 + LoadFaviconTask task = loadTasks.get(taskId); 1.340 + cancelled = task.cancel(false); 1.341 + } 1.342 + return cancelled; 1.343 + } 1.344 + 1.345 + public static void close() { 1.346 + Log.d(LOGTAG, "Closing Favicons database"); 1.347 + 1.348 + // Cancel any pending tasks 1.349 + synchronized (loadTasks) { 1.350 + final int count = loadTasks.size(); 1.351 + for (int i = 0; i < count; i++) { 1.352 + cancelFaviconLoad(loadTasks.keyAt(i)); 1.353 + } 1.354 + loadTasks.clear(); 1.355 + } 1.356 + 1.357 + LoadFaviconTask.closeHTTPClient(); 1.358 + } 1.359 + 1.360 + /** 1.361 + * Get the dominant colour of the Favicon at the URL given, if any exists in the cache. 1.362 + * 1.363 + * @param url The URL of the Favicon, to be used as the cache key for the colour value. 1.364 + * @return The dominant colour of the provided Favicon. 1.365 + */ 1.366 + public static int getFaviconColor(String url) { 1.367 + return faviconsCache.getDominantColor(url); 1.368 + } 1.369 + 1.370 + /** 1.371 + * Called by GeckoApp on startup to pass this class a reference to the GeckoApp object used as 1.372 + * the application's Context. 1.373 + * Consider replacing with references to a staticly held reference to the GeckoApp object. 1.374 + * 1.375 + * @param context A reference to the GeckoApp instance. 1.376 + */ 1.377 + public static void attachToContext(Context context) throws Exception { 1.378 + final Resources res = context.getResources(); 1.379 + Favicons.context = context; 1.380 + 1.381 + // Decode the default Favicon ready for use. 1.382 + defaultFavicon = BitmapFactory.decodeResource(res, R.drawable.favicon); 1.383 + if (defaultFavicon == null) { 1.384 + throw new Exception("Null default favicon was returned from the resources system!"); 1.385 + } 1.386 + 1.387 + defaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg); 1.388 + 1.389 + // Screen-density-adjusted upper limit on favicon size. Favicons larger than this are 1.390 + // downscaled to this size or discarded. 1.391 + largestFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size); 1.392 + faviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, largestFaviconSize); 1.393 + 1.394 + // Initialize page mappings for each of our special pages. 1.395 + for (String url : AboutPages.getDefaultIconPages()) { 1.396 + pageURLMappings.putWithoutEviction(url, BUILT_IN_FAVICON_URL); 1.397 + } 1.398 + 1.399 + // Load and cache the built-in favicon in each of its sizes. 1.400 + // TODO: don't open the zip twice! 1.401 + List<Bitmap> toInsert = Arrays.asList(loadBrandingBitmap(context, "favicon64.png"), 1.402 + loadBrandingBitmap(context, "favicon32.png")); 1.403 + 1.404 + putFaviconsInMemCache(BUILT_IN_FAVICON_URL, toInsert.iterator(), true); 1.405 + } 1.406 + 1.407 + /** 1.408 + * Compute a string like: 1.409 + * "jar:jar:file:///data/app/org.mozilla.firefox-1.apk!/assets/omni.ja!/chrome/chrome/content/branding/favicon64.png" 1.410 + */ 1.411 + private static String getBrandingBitmapPath(Context context, String name) { 1.412 + final String apkPath = context.getPackageResourcePath(); 1.413 + return "jar:jar:" + new File(apkPath).toURI() + "!/" + 1.414 + AppConstants.OMNIJAR_NAME + "!/" + 1.415 + "chrome/chrome/content/branding/" + name; 1.416 + } 1.417 + 1.418 + private static Bitmap loadBrandingBitmap(Context context, String name) { 1.419 + Bitmap b = GeckoJarReader.getBitmap(context.getResources(), 1.420 + getBrandingBitmapPath(context, name)); 1.421 + if (b == null) { 1.422 + throw new IllegalStateException("Bitmap " + name + " missing from JAR!"); 1.423 + } 1.424 + return b; 1.425 + } 1.426 + 1.427 + /** 1.428 + * Helper method to get the default Favicon URL for a given pageURL. Generally: somewhere.com/favicon.ico 1.429 + * 1.430 + * @param pageURL Page URL for which a default Favicon URL is requested 1.431 + * @return The default Favicon URL. 1.432 + */ 1.433 + public static String guessDefaultFaviconURL(String pageURL) { 1.434 + // Special-casing for about: pages. The favicon for about:pages which don't provide a link tag 1.435 + // is bundled in the database, keyed only by page URL, hence the need to return the page URL 1.436 + // here. If the database ever migrates to stop being silly in this way, this can plausibly 1.437 + // be removed. 1.438 + if (AboutPages.isAboutPage(pageURL) || pageURL.startsWith("jar:")) { 1.439 + return pageURL; 1.440 + } 1.441 + 1.442 + try { 1.443 + // Fall back to trying "someScheme:someDomain.someExtension/favicon.ico". 1.444 + URI u = new URI(pageURL); 1.445 + return new URI(u.getScheme(), 1.446 + u.getAuthority(), 1.447 + "/favicon.ico", null, 1.448 + null).toString(); 1.449 + } catch (URISyntaxException e) { 1.450 + Log.e(LOGTAG, "URISyntaxException getting default favicon URL", e); 1.451 + return null; 1.452 + } 1.453 + } 1.454 + 1.455 + public static void removeLoadTask(int taskId) { 1.456 + synchronized(loadTasks) { 1.457 + loadTasks.delete(taskId); 1.458 + } 1.459 + } 1.460 + 1.461 + /** 1.462 + * Method to wrap FaviconCache.isFailedFavicon for use by LoadFaviconTask. 1.463 + * 1.464 + * @param faviconURL Favicon URL to check for failure. 1.465 + */ 1.466 + static boolean isFailedFavicon(String faviconURL) { 1.467 + return faviconsCache.isFailedFavicon(faviconURL); 1.468 + } 1.469 + 1.470 + /** 1.471 + * Sidestep the cache and get, from either the database or the internet, a favicon 1.472 + * suitable for use as an app icon for the provided URL. 1.473 + * 1.474 + * Useful for creating homescreen shortcuts without being limited 1.475 + * by possibly low-resolution values in the cache. 1.476 + * 1.477 + * Deduces the favicon URL from the browser database, guessing if necessary. 1.478 + * 1.479 + * @param url page URL to get a large favicon image for. 1.480 + * @param onFaviconLoadedListener listener to call back with the result. 1.481 + */ 1.482 + public static void getPreferredSizeFaviconForPage(String url, OnFaviconLoadedListener onFaviconLoadedListener) { 1.483 + int preferredSize = GeckoAppShell.getPreferredIconSize(); 1.484 + loadUncachedFavicon(url, null, 0, preferredSize, onFaviconLoadedListener); 1.485 + } 1.486 +}