Wed, 31 Dec 2014 07:22:50 +0100
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 }