michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.favicons; michael@0: michael@0: import org.mozilla.gecko.AboutPages; michael@0: import org.mozilla.gecko.AppConstants; michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.Tab; michael@0: import org.mozilla.gecko.Tabs; michael@0: import org.mozilla.gecko.db.BrowserDB; michael@0: import org.mozilla.gecko.favicons.cache.FaviconCache; michael@0: import org.mozilla.gecko.util.GeckoJarReader; michael@0: import org.mozilla.gecko.util.NonEvictingLruCache; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.content.Context; michael@0: import android.content.res.Resources; michael@0: import android.graphics.Bitmap; michael@0: import android.graphics.BitmapFactory; michael@0: import android.text.TextUtils; michael@0: import android.util.Log; michael@0: import android.util.SparseArray; michael@0: michael@0: import java.io.File; michael@0: import java.net.URI; michael@0: import java.net.URISyntaxException; michael@0: import java.util.Arrays; michael@0: import java.util.Iterator; michael@0: import java.util.List; michael@0: michael@0: public class Favicons { michael@0: private static final String LOGTAG = "GeckoFavicons"; michael@0: michael@0: // A magic URL representing the app's own favicon, used for about: pages. michael@0: private static final String BUILT_IN_FAVICON_URL = "about:favicon"; michael@0: michael@0: // Size of the favicon bitmap cache, in bytes (Counting payload only). michael@0: public static final int FAVICON_CACHE_SIZE_BYTES = 512 * 1024; michael@0: michael@0: // Number of URL mappings from page URL to Favicon URL to cache in memory. michael@0: public static final int NUM_PAGE_URL_MAPPINGS_TO_STORE = 128; michael@0: michael@0: public static final int NOT_LOADING = 0; michael@0: public static final int LOADED = 1; michael@0: public static final int FLAG_PERSIST = 2; michael@0: public static final int FLAG_SCALE = 4; michael@0: michael@0: protected static Context context; michael@0: michael@0: // The default Favicon to show if no other can be found. michael@0: public static Bitmap defaultFavicon; michael@0: michael@0: // The density-adjusted default Favicon dimensions. michael@0: public static int defaultFaviconSize; michael@0: michael@0: // The density-adjusted maximum Favicon dimensions. michael@0: public static int largestFaviconSize; michael@0: michael@0: private static final SparseArray loadTasks = new SparseArray(); michael@0: michael@0: // Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when michael@0: // doing so is not necessary. michael@0: private static final NonEvictingLruCache pageURLMappings = new NonEvictingLruCache(NUM_PAGE_URL_MAPPINGS_TO_STORE); michael@0: michael@0: public static String getFaviconURLForPageURLFromCache(String pageURL) { michael@0: return pageURLMappings.get(pageURL); michael@0: } michael@0: michael@0: /** michael@0: * Insert the given pageUrl->faviconUrl mapping into the memory cache of such mappings. michael@0: * Useful for short-circuiting local database access. michael@0: */ michael@0: public static void putFaviconURLForPageURLInCache(String pageURL, String faviconURL) { michael@0: pageURLMappings.put(pageURL, faviconURL); michael@0: } michael@0: michael@0: private static FaviconCache faviconsCache; michael@0: michael@0: /** michael@0: * Returns either NOT_LOADING, or LOADED if the onFaviconLoaded call could michael@0: * be made on the main thread. michael@0: * If no listener is provided, NOT_LOADING is returned. michael@0: */ michael@0: static int dispatchResult(final String pageUrl, final String faviconURL, final Bitmap image, michael@0: final OnFaviconLoadedListener listener) { michael@0: if (listener == null) { michael@0: return NOT_LOADING; michael@0: } michael@0: michael@0: if (ThreadUtils.isOnUiThread()) { michael@0: listener.onFaviconLoaded(pageUrl, faviconURL, image); michael@0: return LOADED; michael@0: } michael@0: michael@0: // We want to always run the listener on UI thread. michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: listener.onFaviconLoaded(pageUrl, faviconURL, image); michael@0: } michael@0: }); michael@0: return NOT_LOADING; michael@0: } michael@0: michael@0: /** michael@0: * Only returns a non-null Bitmap if the entire path is cached -- the michael@0: * page URL to favicon URL, and the favicon URL to in-memory bitmaps. michael@0: * michael@0: * Returns null otherwise. michael@0: */ michael@0: public static Bitmap getSizedFaviconForPageFromCache(final String pageURL, int targetSize) { michael@0: final String faviconURL = pageURLMappings.get(pageURL); michael@0: if (faviconURL == null) { michael@0: return null; michael@0: } michael@0: return getSizedFaviconFromCache(faviconURL, targetSize); michael@0: } michael@0: michael@0: /** michael@0: * Get a Favicon as close as possible to the target dimensions for the URL provided. michael@0: * If a result is instantly available from the cache, it is returned and the listener is invoked. michael@0: * Otherwise, the result is drawn from the database or network and the listener invoked when the michael@0: * result becomes available. michael@0: * michael@0: * @param pageURL Page URL for which a Favicon is desired. michael@0: * @param faviconURL URL of the Favicon to be downloaded, if known. If none provided, an educated michael@0: * guess is made by the system. michael@0: * @param targetSize Target size of the returned Favicon michael@0: * @param listener Listener to call with the result of the load operation, if the result is not michael@0: * immediately available. michael@0: * @return The id of the asynchronous task created, NOT_LOADING if none is created, or michael@0: * LOADED if the value could be dispatched on the current thread. michael@0: */ michael@0: public static int getSizedFavicon(String pageURL, String faviconURL, int targetSize, int flags, OnFaviconLoadedListener listener) { michael@0: // Do we know the favicon URL for this page already? michael@0: String cacheURL = faviconURL; michael@0: if (cacheURL == null) { michael@0: cacheURL = pageURLMappings.get(pageURL); michael@0: } michael@0: michael@0: // If there's no favicon URL given, try and hit the cache with the default one. michael@0: if (cacheURL == null) { michael@0: cacheURL = guessDefaultFaviconURL(pageURL); michael@0: } michael@0: michael@0: // If it's something we can't even figure out a default URL for, just give up. michael@0: if (cacheURL == null) { michael@0: return dispatchResult(pageURL, null, defaultFavicon, listener); michael@0: } michael@0: michael@0: Bitmap cachedIcon = getSizedFaviconFromCache(cacheURL, targetSize); michael@0: if (cachedIcon != null) { michael@0: return dispatchResult(pageURL, cacheURL, cachedIcon, listener); michael@0: } michael@0: michael@0: // Check if favicon has failed. michael@0: if (faviconsCache.isFailedFavicon(cacheURL)) { michael@0: return dispatchResult(pageURL, cacheURL, defaultFavicon, listener); michael@0: } michael@0: michael@0: // Failing that, try and get one from the database or internet. michael@0: return loadUncachedFavicon(pageURL, faviconURL, flags, targetSize, listener); michael@0: } michael@0: michael@0: /** michael@0: * Returns the cached Favicon closest to the target size if any exists or is coercible. Returns michael@0: * null otherwise. Does not query the database or network for the Favicon is the result is not michael@0: * immediately available. michael@0: * michael@0: * @param faviconURL URL of the Favicon to query for. michael@0: * @param targetSize The desired size of the returned Favicon. michael@0: * @return The cached Favicon, rescaled to be as close as possible to the target size, if any exists. michael@0: * null if no applicable Favicon exists in the cache. michael@0: */ michael@0: public static Bitmap getSizedFaviconFromCache(String faviconURL, int targetSize) { michael@0: return faviconsCache.getFaviconForDimensions(faviconURL, targetSize); michael@0: } michael@0: michael@0: /** michael@0: * Attempts to find a Favicon for the provided page URL from either the mem cache or the database. michael@0: * Does not need an explicit favicon URL, since, as we are accessing the database anyway, we michael@0: * can query the history DB for the Favicon URL. michael@0: * Handy for easing the transition from caching with page URLs to caching with Favicon URLs. michael@0: * michael@0: * A null result is passed to the listener if no value is locally available. The Favicon is not michael@0: * added to the failure cache. michael@0: * michael@0: * @param pageURL Page URL for which a Favicon is wanted. michael@0: * @param targetSize Target size of the desired Favicon to pass to the cache query michael@0: * @param callback Callback to fire with the result. michael@0: * @return The job ID of the spawned async task, if any. michael@0: */ michael@0: public static int getSizedFaviconForPageFromLocal(final String pageURL, final int targetSize, final OnFaviconLoadedListener callback) { michael@0: // Firstly, try extremely hard to cheat. michael@0: // Have we cached this favicon URL? If we did, we can consult the memcache right away. michael@0: String targetURL = pageURLMappings.get(pageURL); michael@0: if (targetURL != null) { michael@0: // Check if favicon has failed. michael@0: if (faviconsCache.isFailedFavicon(targetURL)) { michael@0: return dispatchResult(pageURL, targetURL, null, callback); michael@0: } michael@0: michael@0: // Do we have a Favicon in the cache for this favicon URL? michael@0: Bitmap result = getSizedFaviconFromCache(targetURL, targetSize); michael@0: if (result != null) { michael@0: // Victory - immediate response! michael@0: return dispatchResult(pageURL, targetURL, result, callback); michael@0: } michael@0: } michael@0: michael@0: // No joy using in-memory resources. Go to background thread and ask the database. michael@0: LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageURL, targetURL, 0, callback, targetSize, true); michael@0: int taskId = task.getId(); michael@0: synchronized(loadTasks) { michael@0: loadTasks.put(taskId, task); michael@0: } michael@0: task.execute(); michael@0: return taskId; michael@0: } michael@0: michael@0: public static int getSizedFaviconForPageFromLocal(final String pageURL, final OnFaviconLoadedListener callback) { michael@0: return getSizedFaviconForPageFromLocal(pageURL, defaultFaviconSize, callback); michael@0: } michael@0: michael@0: /** michael@0: * Helper method to determine the URL of the Favicon image for a given page URL by querying the michael@0: * history database. Should only be called from the background thread - does database access. michael@0: * michael@0: * @param pageURL The URL of a webpage with a Favicon. michael@0: * @return The URL of the Favicon used by that webpage, according to either the History database michael@0: * or a somewhat educated guess. michael@0: */ michael@0: public static String getFaviconURLForPageURL(String pageURL) { michael@0: // Attempt to determine the Favicon URL from the Tabs datastructure. Can dodge having to use michael@0: // the database sometimes by doing this. michael@0: String targetURL; michael@0: Tab theTab = Tabs.getInstance().getFirstTabForUrl(pageURL); michael@0: if (theTab != null) { michael@0: targetURL = theTab.getFaviconURL(); michael@0: if (targetURL != null) { michael@0: return targetURL; michael@0: } michael@0: } michael@0: michael@0: targetURL = BrowserDB.getFaviconUrlForHistoryUrl(context.getContentResolver(), pageURL); michael@0: if (targetURL == null) { michael@0: // Nothing in the history database. Fall back to the default URL and hope for the best. michael@0: targetURL = guessDefaultFaviconURL(pageURL); michael@0: } michael@0: return targetURL; michael@0: } michael@0: michael@0: /** michael@0: * Helper function to create an async job to load a Favicon which does not exist in the memcache. michael@0: * Contains logic to prevent the repeated loading of Favicons which have previously failed. michael@0: * There is no support for recovery from transient failures. michael@0: * michael@0: * @param pageUrl URL of the page for which to load a Favicon. If null, no job is created. michael@0: * @param faviconUrl The URL of the Favicon to load. If null, an attempt to infer the value from michael@0: * the history database will be made, and ultimately an attempt to guess will michael@0: * be made. michael@0: * @param flags Flags to be used by the LoadFaviconTask while loading. Currently only one flag michael@0: * is supported, LoadFaviconTask.FLAG_PERSIST. michael@0: * If FLAG_PERSIST is set and the Favicon is ultimately loaded from the internet, michael@0: * the downloaded Favicon is subsequently stored in the local database. michael@0: * If FLAG_PERSIST is unset, the downloaded Favicon is stored only in the memcache. michael@0: * FLAG_PERSIST has no effect on loads which come from the database. michael@0: * @param listener The OnFaviconLoadedListener to invoke with the result of this Favicon load. michael@0: * @return The id of the LoadFaviconTask handling this job. michael@0: */ michael@0: private static int loadUncachedFavicon(String pageUrl, String faviconUrl, int flags, int targetSize, OnFaviconLoadedListener listener) { michael@0: // Handle the case where we have no page url. michael@0: if (TextUtils.isEmpty(pageUrl)) { michael@0: dispatchResult(null, null, null, listener); michael@0: return NOT_LOADING; michael@0: } michael@0: michael@0: LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageUrl, faviconUrl, flags, listener, targetSize, false); michael@0: michael@0: int taskId = task.getId(); michael@0: synchronized(loadTasks) { michael@0: loadTasks.put(taskId, task); michael@0: } michael@0: michael@0: task.execute(); michael@0: michael@0: return taskId; michael@0: } michael@0: michael@0: public static void putFaviconInMemCache(String pageUrl, Bitmap image) { michael@0: faviconsCache.putSingleFavicon(pageUrl, image); michael@0: } michael@0: michael@0: /** michael@0: * Adds the bitmaps given by the specified iterator to the cache associated with the url given. michael@0: * Future requests for images will be able to select the least larger image than the target michael@0: * size from this new set of images. michael@0: * michael@0: * @param pageUrl The URL to associate the new favicons with. michael@0: * @param images An iterator over the new favicons to put in the cache. michael@0: */ michael@0: public static void putFaviconsInMemCache(String pageUrl, Iterator images, boolean permanently) { michael@0: faviconsCache.putFavicons(pageUrl, images, permanently); michael@0: } michael@0: michael@0: public static void putFaviconsInMemCache(String pageUrl, Iterator images) { michael@0: putFaviconsInMemCache(pageUrl, images, false); michael@0: } michael@0: michael@0: public static void clearMemCache() { michael@0: faviconsCache.evictAll(); michael@0: pageURLMappings.evictAll(); michael@0: } michael@0: michael@0: public static void putFaviconInFailedCache(String faviconURL) { michael@0: faviconsCache.putFailed(faviconURL); michael@0: } michael@0: michael@0: public static boolean cancelFaviconLoad(int taskId) { michael@0: if (taskId == NOT_LOADING) { michael@0: return false; michael@0: } michael@0: michael@0: boolean cancelled; michael@0: synchronized (loadTasks) { michael@0: if (loadTasks.indexOfKey(taskId) < 0) { michael@0: return false; michael@0: } michael@0: michael@0: Log.v(LOGTAG, "Cancelling favicon load " + taskId + "."); michael@0: michael@0: LoadFaviconTask task = loadTasks.get(taskId); michael@0: cancelled = task.cancel(false); michael@0: } michael@0: return cancelled; michael@0: } michael@0: michael@0: public static void close() { michael@0: Log.d(LOGTAG, "Closing Favicons database"); michael@0: michael@0: // Cancel any pending tasks michael@0: synchronized (loadTasks) { michael@0: final int count = loadTasks.size(); michael@0: for (int i = 0; i < count; i++) { michael@0: cancelFaviconLoad(loadTasks.keyAt(i)); michael@0: } michael@0: loadTasks.clear(); michael@0: } michael@0: michael@0: LoadFaviconTask.closeHTTPClient(); michael@0: } michael@0: michael@0: /** michael@0: * Get the dominant colour of the Favicon at the URL given, if any exists in the cache. michael@0: * michael@0: * @param url The URL of the Favicon, to be used as the cache key for the colour value. michael@0: * @return The dominant colour of the provided Favicon. michael@0: */ michael@0: public static int getFaviconColor(String url) { michael@0: return faviconsCache.getDominantColor(url); michael@0: } michael@0: michael@0: /** michael@0: * Called by GeckoApp on startup to pass this class a reference to the GeckoApp object used as michael@0: * the application's Context. michael@0: * Consider replacing with references to a staticly held reference to the GeckoApp object. michael@0: * michael@0: * @param context A reference to the GeckoApp instance. michael@0: */ michael@0: public static void attachToContext(Context context) throws Exception { michael@0: final Resources res = context.getResources(); michael@0: Favicons.context = context; michael@0: michael@0: // Decode the default Favicon ready for use. michael@0: defaultFavicon = BitmapFactory.decodeResource(res, R.drawable.favicon); michael@0: if (defaultFavicon == null) { michael@0: throw new Exception("Null default favicon was returned from the resources system!"); michael@0: } michael@0: michael@0: defaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg); michael@0: michael@0: // Screen-density-adjusted upper limit on favicon size. Favicons larger than this are michael@0: // downscaled to this size or discarded. michael@0: largestFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size); michael@0: faviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, largestFaviconSize); michael@0: michael@0: // Initialize page mappings for each of our special pages. michael@0: for (String url : AboutPages.getDefaultIconPages()) { michael@0: pageURLMappings.putWithoutEviction(url, BUILT_IN_FAVICON_URL); michael@0: } michael@0: michael@0: // Load and cache the built-in favicon in each of its sizes. michael@0: // TODO: don't open the zip twice! michael@0: List toInsert = Arrays.asList(loadBrandingBitmap(context, "favicon64.png"), michael@0: loadBrandingBitmap(context, "favicon32.png")); michael@0: michael@0: putFaviconsInMemCache(BUILT_IN_FAVICON_URL, toInsert.iterator(), true); michael@0: } michael@0: michael@0: /** michael@0: * Compute a string like: michael@0: * "jar:jar:file:///data/app/org.mozilla.firefox-1.apk!/assets/omni.ja!/chrome/chrome/content/branding/favicon64.png" michael@0: */ michael@0: private static String getBrandingBitmapPath(Context context, String name) { michael@0: final String apkPath = context.getPackageResourcePath(); michael@0: return "jar:jar:" + new File(apkPath).toURI() + "!/" + michael@0: AppConstants.OMNIJAR_NAME + "!/" + michael@0: "chrome/chrome/content/branding/" + name; michael@0: } michael@0: michael@0: private static Bitmap loadBrandingBitmap(Context context, String name) { michael@0: Bitmap b = GeckoJarReader.getBitmap(context.getResources(), michael@0: getBrandingBitmapPath(context, name)); michael@0: if (b == null) { michael@0: throw new IllegalStateException("Bitmap " + name + " missing from JAR!"); michael@0: } michael@0: return b; michael@0: } michael@0: michael@0: /** michael@0: * Helper method to get the default Favicon URL for a given pageURL. Generally: somewhere.com/favicon.ico michael@0: * michael@0: * @param pageURL Page URL for which a default Favicon URL is requested michael@0: * @return The default Favicon URL. michael@0: */ michael@0: public static String guessDefaultFaviconURL(String pageURL) { michael@0: // Special-casing for about: pages. The favicon for about:pages which don't provide a link tag michael@0: // is bundled in the database, keyed only by page URL, hence the need to return the page URL michael@0: // here. If the database ever migrates to stop being silly in this way, this can plausibly michael@0: // be removed. michael@0: if (AboutPages.isAboutPage(pageURL) || pageURL.startsWith("jar:")) { michael@0: return pageURL; michael@0: } michael@0: michael@0: try { michael@0: // Fall back to trying "someScheme:someDomain.someExtension/favicon.ico". michael@0: URI u = new URI(pageURL); michael@0: return new URI(u.getScheme(), michael@0: u.getAuthority(), michael@0: "/favicon.ico", null, michael@0: null).toString(); michael@0: } catch (URISyntaxException e) { michael@0: Log.e(LOGTAG, "URISyntaxException getting default favicon URL", e); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public static void removeLoadTask(int taskId) { michael@0: synchronized(loadTasks) { michael@0: loadTasks.delete(taskId); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Method to wrap FaviconCache.isFailedFavicon for use by LoadFaviconTask. michael@0: * michael@0: * @param faviconURL Favicon URL to check for failure. michael@0: */ michael@0: static boolean isFailedFavicon(String faviconURL) { michael@0: return faviconsCache.isFailedFavicon(faviconURL); michael@0: } michael@0: michael@0: /** michael@0: * Sidestep the cache and get, from either the database or the internet, a favicon michael@0: * suitable for use as an app icon for the provided URL. michael@0: * michael@0: * Useful for creating homescreen shortcuts without being limited michael@0: * by possibly low-resolution values in the cache. michael@0: * michael@0: * Deduces the favicon URL from the browser database, guessing if necessary. michael@0: * michael@0: * @param url page URL to get a large favicon image for. michael@0: * @param onFaviconLoadedListener listener to call back with the result. michael@0: */ michael@0: public static void getPreferredSizeFaviconForPage(String url, OnFaviconLoadedListener onFaviconLoadedListener) { michael@0: int preferredSize = GeckoAppShell.getPreferredIconSize(); michael@0: loadUncachedFavicon(url, null, 0, preferredSize, onFaviconLoadedListener); michael@0: } michael@0: }