|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko.favicons; |
|
7 |
|
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; |
|
19 |
|
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; |
|
27 |
|
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; |
|
34 |
|
35 public class Favicons { |
|
36 private static final String LOGTAG = "GeckoFavicons"; |
|
37 |
|
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"; |
|
40 |
|
41 // Size of the favicon bitmap cache, in bytes (Counting payload only). |
|
42 public static final int FAVICON_CACHE_SIZE_BYTES = 512 * 1024; |
|
43 |
|
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; |
|
46 |
|
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; |
|
51 |
|
52 protected static Context context; |
|
53 |
|
54 // The default Favicon to show if no other can be found. |
|
55 public static Bitmap defaultFavicon; |
|
56 |
|
57 // The density-adjusted default Favicon dimensions. |
|
58 public static int defaultFaviconSize; |
|
59 |
|
60 // The density-adjusted maximum Favicon dimensions. |
|
61 public static int largestFaviconSize; |
|
62 |
|
63 private static final SparseArray<LoadFaviconTask> loadTasks = new SparseArray<LoadFaviconTask>(); |
|
64 |
|
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); |
|
68 |
|
69 public static String getFaviconURLForPageURLFromCache(String pageURL) { |
|
70 return pageURLMappings.get(pageURL); |
|
71 } |
|
72 |
|
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 } |
|
80 |
|
81 private static FaviconCache faviconsCache; |
|
82 |
|
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 } |
|
93 |
|
94 if (ThreadUtils.isOnUiThread()) { |
|
95 listener.onFaviconLoaded(pageUrl, faviconURL, image); |
|
96 return LOADED; |
|
97 } |
|
98 |
|
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 } |
|
108 |
|
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 } |
|
122 |
|
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 } |
|
144 |
|
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 } |
|
149 |
|
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 } |
|
154 |
|
155 Bitmap cachedIcon = getSizedFaviconFromCache(cacheURL, targetSize); |
|
156 if (cachedIcon != null) { |
|
157 return dispatchResult(pageURL, cacheURL, cachedIcon, listener); |
|
158 } |
|
159 |
|
160 // Check if favicon has failed. |
|
161 if (faviconsCache.isFailedFavicon(cacheURL)) { |
|
162 return dispatchResult(pageURL, cacheURL, defaultFavicon, listener); |
|
163 } |
|
164 |
|
165 // Failing that, try and get one from the database or internet. |
|
166 return loadUncachedFavicon(pageURL, faviconURL, flags, targetSize, listener); |
|
167 } |
|
168 |
|
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 } |
|
182 |
|
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 } |
|
206 |
|
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 } |
|
214 |
|
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 } |
|
224 |
|
225 public static int getSizedFaviconForPageFromLocal(final String pageURL, final OnFaviconLoadedListener callback) { |
|
226 return getSizedFaviconForPageFromLocal(pageURL, defaultFaviconSize, callback); |
|
227 } |
|
228 |
|
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 } |
|
248 |
|
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 } |
|
256 |
|
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 } |
|
281 |
|
282 LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageUrl, faviconUrl, flags, listener, targetSize, false); |
|
283 |
|
284 int taskId = task.getId(); |
|
285 synchronized(loadTasks) { |
|
286 loadTasks.put(taskId, task); |
|
287 } |
|
288 |
|
289 task.execute(); |
|
290 |
|
291 return taskId; |
|
292 } |
|
293 |
|
294 public static void putFaviconInMemCache(String pageUrl, Bitmap image) { |
|
295 faviconsCache.putSingleFavicon(pageUrl, image); |
|
296 } |
|
297 |
|
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 } |
|
309 |
|
310 public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) { |
|
311 putFaviconsInMemCache(pageUrl, images, false); |
|
312 } |
|
313 |
|
314 public static void clearMemCache() { |
|
315 faviconsCache.evictAll(); |
|
316 pageURLMappings.evictAll(); |
|
317 } |
|
318 |
|
319 public static void putFaviconInFailedCache(String faviconURL) { |
|
320 faviconsCache.putFailed(faviconURL); |
|
321 } |
|
322 |
|
323 public static boolean cancelFaviconLoad(int taskId) { |
|
324 if (taskId == NOT_LOADING) { |
|
325 return false; |
|
326 } |
|
327 |
|
328 boolean cancelled; |
|
329 synchronized (loadTasks) { |
|
330 if (loadTasks.indexOfKey(taskId) < 0) { |
|
331 return false; |
|
332 } |
|
333 |
|
334 Log.v(LOGTAG, "Cancelling favicon load " + taskId + "."); |
|
335 |
|
336 LoadFaviconTask task = loadTasks.get(taskId); |
|
337 cancelled = task.cancel(false); |
|
338 } |
|
339 return cancelled; |
|
340 } |
|
341 |
|
342 public static void close() { |
|
343 Log.d(LOGTAG, "Closing Favicons database"); |
|
344 |
|
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 } |
|
353 |
|
354 LoadFaviconTask.closeHTTPClient(); |
|
355 } |
|
356 |
|
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 } |
|
366 |
|
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; |
|
377 |
|
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 } |
|
383 |
|
384 defaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg); |
|
385 |
|
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); |
|
390 |
|
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 } |
|
395 |
|
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")); |
|
400 |
|
401 putFaviconsInMemCache(BUILT_IN_FAVICON_URL, toInsert.iterator(), true); |
|
402 } |
|
403 |
|
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 } |
|
414 |
|
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 } |
|
423 |
|
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 } |
|
438 |
|
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 } |
|
451 |
|
452 public static void removeLoadTask(int taskId) { |
|
453 synchronized(loadTasks) { |
|
454 loadTasks.delete(taskId); |
|
455 } |
|
456 } |
|
457 |
|
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 } |
|
466 |
|
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 } |