|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.favicons; |
|
6 |
|
7 |
|
8 import android.content.ContentResolver; |
|
9 import android.graphics.Bitmap; |
|
10 import android.net.http.AndroidHttpClient; |
|
11 import android.os.Handler; |
|
12 import android.text.TextUtils; |
|
13 import android.util.Log; |
|
14 import org.apache.http.Header; |
|
15 import org.apache.http.HttpEntity; |
|
16 import org.apache.http.HttpResponse; |
|
17 import org.apache.http.client.methods.HttpGet; |
|
18 import org.mozilla.gecko.GeckoAppShell; |
|
19 import org.mozilla.gecko.db.BrowserDB; |
|
20 import org.mozilla.gecko.favicons.decoders.FaviconDecoder; |
|
21 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; |
|
22 import org.mozilla.gecko.util.GeckoJarReader; |
|
23 import org.mozilla.gecko.util.ThreadUtils; |
|
24 import org.mozilla.gecko.util.UiAsyncTask; |
|
25 import static org.mozilla.gecko.favicons.Favicons.context; |
|
26 |
|
27 import java.io.IOException; |
|
28 import java.io.InputStream; |
|
29 import java.net.URI; |
|
30 import java.net.URISyntaxException; |
|
31 import java.util.HashMap; |
|
32 import java.util.HashSet; |
|
33 import java.util.LinkedList; |
|
34 import java.util.concurrent.atomic.AtomicInteger; |
|
35 |
|
36 /** |
|
37 * Class representing the asynchronous task to load a Favicon which is not currently in the in-memory |
|
38 * cache. |
|
39 * The implementation initially tries to get the Favicon from the database. Upon failure, the icon |
|
40 * is loaded from the internet. |
|
41 */ |
|
42 public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> { |
|
43 private static final String LOGTAG = "LoadFaviconTask"; |
|
44 |
|
45 // Access to this map needs to be synchronized prevent multiple jobs loading the same favicon |
|
46 // from executing concurrently. |
|
47 private static final HashMap<String, LoadFaviconTask> loadsInFlight = new HashMap<String, LoadFaviconTask>(); |
|
48 |
|
49 public static final int FLAG_PERSIST = 1; |
|
50 public static final int FLAG_SCALE = 2; |
|
51 private static final int MAX_REDIRECTS_TO_FOLLOW = 5; |
|
52 // The default size of the buffer to use for downloading Favicons in the event no size is given |
|
53 // by the server. |
|
54 private static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000; |
|
55 |
|
56 private static AtomicInteger nextFaviconLoadId = new AtomicInteger(0); |
|
57 private int id; |
|
58 private String pageUrl; |
|
59 private String faviconURL; |
|
60 private OnFaviconLoadedListener listener; |
|
61 private int flags; |
|
62 |
|
63 private final boolean onlyFromLocal; |
|
64 |
|
65 // Assuming square favicons, judging by width only is acceptable. |
|
66 protected int targetWidth; |
|
67 private LinkedList<LoadFaviconTask> chainees; |
|
68 private boolean isChaining; |
|
69 |
|
70 static AndroidHttpClient httpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString()); |
|
71 |
|
72 public LoadFaviconTask(Handler backgroundThreadHandler, |
|
73 String pageUrl, String faviconUrl, int flags, |
|
74 OnFaviconLoadedListener listener) { |
|
75 this(backgroundThreadHandler, pageUrl, faviconUrl, flags, listener, -1, false); |
|
76 } |
|
77 public LoadFaviconTask(Handler backgroundThreadHandler, |
|
78 String pageUrl, String faviconUrl, int flags, |
|
79 OnFaviconLoadedListener listener, int targetWidth, boolean onlyFromLocal) { |
|
80 super(backgroundThreadHandler); |
|
81 |
|
82 id = nextFaviconLoadId.incrementAndGet(); |
|
83 |
|
84 this.pageUrl = pageUrl; |
|
85 this.faviconURL = faviconUrl; |
|
86 this.listener = listener; |
|
87 this.flags = flags; |
|
88 this.targetWidth = targetWidth; |
|
89 this.onlyFromLocal = onlyFromLocal; |
|
90 } |
|
91 |
|
92 // Runs in background thread |
|
93 private LoadFaviconResult loadFaviconFromDb() { |
|
94 ContentResolver resolver = context.getContentResolver(); |
|
95 return BrowserDB.getFaviconForFaviconUrl(resolver, faviconURL); |
|
96 } |
|
97 |
|
98 // Runs in background thread |
|
99 private void saveFaviconToDb(final byte[] encodedFavicon) { |
|
100 if (encodedFavicon == null) { |
|
101 return; |
|
102 } |
|
103 |
|
104 if ((flags & FLAG_PERSIST) == 0) { |
|
105 return; |
|
106 } |
|
107 |
|
108 ContentResolver resolver = context.getContentResolver(); |
|
109 BrowserDB.updateFaviconForUrl(resolver, pageUrl, encodedFavicon, faviconURL); |
|
110 } |
|
111 |
|
112 /** |
|
113 * Helper method for trying the download request to grab a Favicon. |
|
114 * @param faviconURI URL of Favicon to try and download |
|
115 * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise. |
|
116 */ |
|
117 private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException { |
|
118 HashSet<String> visitedLinkSet = new HashSet<String>(); |
|
119 visitedLinkSet.add(faviconURI.toString()); |
|
120 return tryDownloadRecurse(faviconURI, visitedLinkSet); |
|
121 } |
|
122 private HttpResponse tryDownloadRecurse(URI faviconURI, HashSet<String> visited) throws URISyntaxException, IOException { |
|
123 if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) { |
|
124 return null; |
|
125 } |
|
126 |
|
127 HttpGet request = new HttpGet(faviconURI); |
|
128 HttpResponse response = httpClient.execute(request); |
|
129 if (response == null) { |
|
130 return null; |
|
131 } |
|
132 |
|
133 if (response.getStatusLine() != null) { |
|
134 |
|
135 // Was the response a failure? |
|
136 int status = response.getStatusLine().getStatusCode(); |
|
137 |
|
138 // Handle HTTP status codes requesting a redirect. |
|
139 if (status >= 300 && status < 400) { |
|
140 Header header = response.getFirstHeader("Location"); |
|
141 |
|
142 // Handle mad webservers. |
|
143 if (header == null) { |
|
144 return null; |
|
145 } |
|
146 |
|
147 String newURI = header.getValue(); |
|
148 if (newURI == null || newURI.equals(faviconURI.toString())) { |
|
149 return null; |
|
150 } |
|
151 |
|
152 if (visited.contains(newURI)) { |
|
153 // Already been redirected here - abort. |
|
154 return null; |
|
155 } |
|
156 |
|
157 visited.add(newURI); |
|
158 return tryDownloadRecurse(new URI(newURI), visited); |
|
159 } |
|
160 |
|
161 if (status >= 400) { |
|
162 return null; |
|
163 } |
|
164 } |
|
165 return response; |
|
166 } |
|
167 |
|
168 /** |
|
169 * Retrieve the specified favicon from the JAR, returning null if it's not |
|
170 * a JAR URI. |
|
171 */ |
|
172 private static Bitmap fetchJARFavicon(String uri) { |
|
173 if (uri == null) { |
|
174 return null; |
|
175 } |
|
176 if (uri.startsWith("jar:jar:")) { |
|
177 Log.d(LOGTAG, "Fetching favicon from JAR."); |
|
178 try { |
|
179 return GeckoJarReader.getBitmap(context.getResources(), uri); |
|
180 } catch (Exception e) { |
|
181 // Just about anything could happen here. |
|
182 Log.w(LOGTAG, "Error fetching favicon from JAR.", e); |
|
183 return null; |
|
184 } |
|
185 } |
|
186 return null; |
|
187 } |
|
188 |
|
189 // Runs in background thread. |
|
190 // Does not attempt to fetch from JARs. |
|
191 private LoadFaviconResult downloadFavicon(URI targetFaviconURI) { |
|
192 if (targetFaviconURI == null) { |
|
193 return null; |
|
194 } |
|
195 |
|
196 // Only get favicons for HTTP/HTTPS. |
|
197 String scheme = targetFaviconURI.getScheme(); |
|
198 if (!"http".equals(scheme) && !"https".equals(scheme)) { |
|
199 return null; |
|
200 } |
|
201 |
|
202 LoadFaviconResult result = null; |
|
203 |
|
204 try { |
|
205 result = downloadAndDecodeImage(targetFaviconURI); |
|
206 } catch (Exception e) { |
|
207 Log.e(LOGTAG, "Error reading favicon", e); |
|
208 } |
|
209 |
|
210 return result; |
|
211 } |
|
212 |
|
213 /** |
|
214 * Download the Favicon from the given URL and pass it to the decoder function. |
|
215 * |
|
216 * @param targetFaviconURL URL of the favicon to download. |
|
217 * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or |
|
218 * null if no or corrupt data ware received. |
|
219 * @throws IOException If attempts to fully read the stream result in such an exception, such as |
|
220 * in the event of a transient connection failure. |
|
221 * @throws URISyntaxException If the underlying call to tryDownload retries and raises such an |
|
222 * exception trying a fallback URL. |
|
223 */ |
|
224 private LoadFaviconResult downloadAndDecodeImage(URI targetFaviconURL) throws IOException, URISyntaxException { |
|
225 // Try the URL we were given. |
|
226 HttpResponse response = tryDownload(targetFaviconURL); |
|
227 if (response == null) { |
|
228 return null; |
|
229 } |
|
230 |
|
231 HttpEntity entity = response.getEntity(); |
|
232 if (entity == null) { |
|
233 return null; |
|
234 } |
|
235 |
|
236 // This may not be provided, but if it is, it's useful. |
|
237 final long entityReportedLength = entity.getContentLength(); |
|
238 int bufferSize; |
|
239 if (entityReportedLength > 0) { |
|
240 // The size was reported and sane, so let's use that. |
|
241 // Integer overflow should not be a problem for Favicon sizes... |
|
242 bufferSize = (int) entityReportedLength + 1; |
|
243 } else { |
|
244 // No declared size, so guess and reallocate later if it turns out to be too small. |
|
245 bufferSize = DEFAULT_FAVICON_BUFFER_SIZE; |
|
246 } |
|
247 |
|
248 // Allocate a buffer to hold the raw favicon data downloaded. |
|
249 byte[] buffer = new byte[bufferSize]; |
|
250 |
|
251 // The offset of the start of the buffer's free space. |
|
252 int bPointer = 0; |
|
253 |
|
254 // The quantity of bytes the last call to read yielded. |
|
255 int lastRead = 0; |
|
256 InputStream contentStream = entity.getContent(); |
|
257 try { |
|
258 // Fully read the entity into the buffer - decoding of streams is not supported |
|
259 // (and questionably pointful - what would one do with a half-decoded Favicon?) |
|
260 while (lastRead != -1) { |
|
261 // Read as many bytes as are currently available into the buffer. |
|
262 lastRead = contentStream.read(buffer, bPointer, buffer.length - bPointer); |
|
263 bPointer += lastRead; |
|
264 |
|
265 // If buffer has overflowed, double its size and carry on. |
|
266 if (bPointer == buffer.length) { |
|
267 bufferSize *= 2; |
|
268 byte[] newBuffer = new byte[bufferSize]; |
|
269 |
|
270 // Copy the contents of the old buffer into the new buffer. |
|
271 System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); |
|
272 buffer = newBuffer; |
|
273 } |
|
274 } |
|
275 } finally { |
|
276 contentStream.close(); |
|
277 } |
|
278 |
|
279 // Having downloaded the image, decode it. |
|
280 return FaviconDecoder.decodeFavicon(buffer, 0, bPointer + 1); |
|
281 } |
|
282 |
|
283 @Override |
|
284 protected Bitmap doInBackground(Void... unused) { |
|
285 if (isCancelled()) { |
|
286 return null; |
|
287 } |
|
288 |
|
289 String storedFaviconUrl; |
|
290 boolean isUsingDefaultURL = false; |
|
291 |
|
292 // Handle the case of malformed favicon URL. |
|
293 // If favicon is empty, fall back to the stored one. |
|
294 if (TextUtils.isEmpty(faviconURL)) { |
|
295 // Try to get the favicon URL from the memory cache. |
|
296 storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(pageUrl); |
|
297 |
|
298 // If that failed, try to get the URL from the database. |
|
299 if (storedFaviconUrl == null) { |
|
300 storedFaviconUrl = Favicons.getFaviconURLForPageURL(pageUrl); |
|
301 if (storedFaviconUrl != null) { |
|
302 // If that succeeded, cache the URL loaded from the database in memory. |
|
303 Favicons.putFaviconURLForPageURLInCache(pageUrl, storedFaviconUrl); |
|
304 } |
|
305 } |
|
306 |
|
307 // If we found a faviconURL - use it. |
|
308 if (storedFaviconUrl != null) { |
|
309 faviconURL = storedFaviconUrl; |
|
310 } else { |
|
311 // If we don't have a stored one, fall back to the default. |
|
312 faviconURL = Favicons.guessDefaultFaviconURL(pageUrl); |
|
313 |
|
314 if (TextUtils.isEmpty(faviconURL)) { |
|
315 return null; |
|
316 } |
|
317 isUsingDefaultURL = true; |
|
318 } |
|
319 } |
|
320 |
|
321 // Check if favicon has failed - if so, give up. We need this check because, sometimes, we |
|
322 // didn't know the real Favicon URL until we asked the database. |
|
323 if (Favicons.isFailedFavicon(faviconURL)) { |
|
324 return null; |
|
325 } |
|
326 |
|
327 if (isCancelled()) { |
|
328 return null; |
|
329 } |
|
330 |
|
331 Bitmap image; |
|
332 // Determine if there is already an ongoing task to fetch the Favicon we desire. |
|
333 // If there is, just join the queue and wait for it to finish. If not, we carry on. |
|
334 synchronized(loadsInFlight) { |
|
335 // Another load of the current Favicon is already underway |
|
336 LoadFaviconTask existingTask = loadsInFlight.get(faviconURL); |
|
337 if (existingTask != null && !existingTask.isCancelled()) { |
|
338 existingTask.chainTasks(this); |
|
339 isChaining = true; |
|
340 |
|
341 // If we are chaining, we want to keep the first task started to do this job as the one |
|
342 // in the hashmap so subsequent tasks will add themselves to its chaining list. |
|
343 return null; |
|
344 } |
|
345 |
|
346 // We do not want to update the hashmap if the task has chained - other tasks need to |
|
347 // chain onto the same parent task. |
|
348 loadsInFlight.put(faviconURL, this); |
|
349 } |
|
350 |
|
351 if (isCancelled()) { |
|
352 return null; |
|
353 } |
|
354 |
|
355 // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null. |
|
356 LoadFaviconResult loadedBitmaps = loadFaviconFromDb(); |
|
357 if (loadedBitmaps != null) { |
|
358 return pushToCacheAndGetResult(loadedBitmaps); |
|
359 } |
|
360 |
|
361 if (onlyFromLocal || isCancelled()) { |
|
362 return null; |
|
363 } |
|
364 |
|
365 // Let's see if it's in a JAR. |
|
366 image = fetchJARFavicon(faviconURL); |
|
367 if (imageIsValid(image)) { |
|
368 // We don't want to put this into the DB. |
|
369 Favicons.putFaviconInMemCache(faviconURL, image); |
|
370 return image; |
|
371 } |
|
372 |
|
373 try { |
|
374 loadedBitmaps = downloadFavicon(new URI(faviconURL)); |
|
375 } catch (URISyntaxException e) { |
|
376 Log.e(LOGTAG, "The provided favicon URL is not valid"); |
|
377 return null; |
|
378 } catch (Exception e) { |
|
379 Log.e(LOGTAG, "Couldn't download favicon.", e); |
|
380 } |
|
381 |
|
382 if (loadedBitmaps != null) { |
|
383 // Fetching bytes to store can fail. saveFaviconToDb will |
|
384 // do the right thing, but we still choose to cache the |
|
385 // downloaded icon in memory. |
|
386 saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage()); |
|
387 return pushToCacheAndGetResult(loadedBitmaps); |
|
388 } |
|
389 |
|
390 if (isUsingDefaultURL) { |
|
391 Favicons.putFaviconInFailedCache(faviconURL); |
|
392 return null; |
|
393 } |
|
394 |
|
395 if (isCancelled()) { |
|
396 return null; |
|
397 } |
|
398 |
|
399 // If we're not already trying the default URL, try it now. |
|
400 final String guessed = Favicons.guessDefaultFaviconURL(pageUrl); |
|
401 if (guessed == null) { |
|
402 Favicons.putFaviconInFailedCache(faviconURL); |
|
403 return null; |
|
404 } |
|
405 |
|
406 image = fetchJARFavicon(guessed); |
|
407 if (imageIsValid(image)) { |
|
408 // We don't want to put this into the DB. |
|
409 Favicons.putFaviconInMemCache(faviconURL, image); |
|
410 return image; |
|
411 } |
|
412 |
|
413 try { |
|
414 loadedBitmaps = downloadFavicon(new URI(guessed)); |
|
415 } catch (Exception e) { |
|
416 // Not interesting. It was an educated guess, anyway. |
|
417 return null; |
|
418 } |
|
419 |
|
420 if (loadedBitmaps != null) { |
|
421 saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage()); |
|
422 return pushToCacheAndGetResult(loadedBitmaps); |
|
423 } |
|
424 |
|
425 return null; |
|
426 } |
|
427 |
|
428 /** |
|
429 * Helper method to put the result of a favicon load into the memory cache and then query the |
|
430 * cache for the particular bitmap we want for this request. |
|
431 * This call is certain to succeed, provided there was enough memory to decode this favicon. |
|
432 * |
|
433 * @param loadedBitmaps LoadFaviconResult to store. |
|
434 * @return The optimal favicon available to satisfy this LoadFaviconTask's request, or null if |
|
435 * we are under extreme memory pressure and find ourselves dropping the cache immediately. |
|
436 */ |
|
437 private Bitmap pushToCacheAndGetResult(LoadFaviconResult loadedBitmaps) { |
|
438 Favicons.putFaviconsInMemCache(faviconURL, loadedBitmaps.getBitmaps()); |
|
439 Bitmap result = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth); |
|
440 return result; |
|
441 } |
|
442 |
|
443 private static boolean imageIsValid(final Bitmap image) { |
|
444 return image != null && |
|
445 image.getWidth() > 0 && |
|
446 image.getHeight() > 0; |
|
447 } |
|
448 |
|
449 @Override |
|
450 protected void onPostExecute(Bitmap image) { |
|
451 if (isChaining) { |
|
452 return; |
|
453 } |
|
454 |
|
455 // Process the result, scale for the listener, etc. |
|
456 processResult(image); |
|
457 |
|
458 synchronized (loadsInFlight) { |
|
459 // Prevent any other tasks from chaining on this one. |
|
460 loadsInFlight.remove(faviconURL); |
|
461 } |
|
462 |
|
463 // Since any update to chainees is done while holding the loadsInFlight lock, once we reach |
|
464 // this point no further updates to that list can possibly take place (As far as other tasks |
|
465 // are concerned, there is no longer a task to chain from. The above block will have waited |
|
466 // for any tasks that were adding themselves to the list before reaching this point.) |
|
467 |
|
468 // As such, I believe we're safe to do the following without holding the lock. |
|
469 // This is nice - we do not want to take the lock unless we have to anyway, and chaining rarely |
|
470 // actually happens outside of the strange situations unit tests create. |
|
471 |
|
472 // Share the result with all chained tasks. |
|
473 if (chainees != null) { |
|
474 for (LoadFaviconTask t : chainees) { |
|
475 // In the case that we just decoded multiple favicons, either we're passing the right |
|
476 // image now, or the call into the cache in processResult will fetch the right one. |
|
477 t.processResult(image); |
|
478 } |
|
479 } |
|
480 } |
|
481 |
|
482 private void processResult(Bitmap image) { |
|
483 Favicons.removeLoadTask(id); |
|
484 Bitmap scaled = image; |
|
485 |
|
486 // Notify listeners, scaling if required. |
|
487 if (targetWidth != -1 && image != null && image.getWidth() != targetWidth) { |
|
488 scaled = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth); |
|
489 } |
|
490 |
|
491 Favicons.dispatchResult(pageUrl, faviconURL, scaled, listener); |
|
492 } |
|
493 |
|
494 @Override |
|
495 protected void onCancelled() { |
|
496 Favicons.removeLoadTask(id); |
|
497 |
|
498 synchronized(loadsInFlight) { |
|
499 // Only remove from the hashmap if the task there is the one that's being canceled. |
|
500 // Cancellation of a task that would have chained is not interesting to the hashmap. |
|
501 final LoadFaviconTask primary = loadsInFlight.get(faviconURL); |
|
502 if (primary == this) { |
|
503 loadsInFlight.remove(faviconURL); |
|
504 return; |
|
505 } |
|
506 if (primary == null) { |
|
507 // This shouldn't happen. |
|
508 return; |
|
509 } |
|
510 if (primary.chainees != null) { |
|
511 primary.chainees.remove(this); |
|
512 } |
|
513 } |
|
514 |
|
515 // Note that we don't call the listener callback if the |
|
516 // favicon load is cancelled. |
|
517 } |
|
518 |
|
519 /** |
|
520 * When the result of this job is ready, also notify the chainee of the result. |
|
521 * Used for aggregating concurrent requests for the same Favicon into a single actual request. |
|
522 * (Don't want to download a hundred instances of Google's Favicon at once, for example). |
|
523 * The loadsInFlight lock must be held when calling this function. |
|
524 * |
|
525 * @param aChainee LoadFaviconTask |
|
526 */ |
|
527 private void chainTasks(LoadFaviconTask aChainee) { |
|
528 if (chainees == null) { |
|
529 chainees = new LinkedList<LoadFaviconTask>(); |
|
530 } |
|
531 |
|
532 chainees.add(aChainee); |
|
533 } |
|
534 |
|
535 int getId() { |
|
536 return id; |
|
537 } |
|
538 |
|
539 static void closeHTTPClient() { |
|
540 // This work must be done on a background thread because it shuts down |
|
541 // the connection pool, which typically involves closing a connection -- |
|
542 // which counts as network activity. |
|
543 if (ThreadUtils.isOnBackgroundThread()) { |
|
544 if (httpClient != null) { |
|
545 httpClient.close(); |
|
546 } |
|
547 return; |
|
548 } |
|
549 |
|
550 ThreadUtils.postToBackgroundThread(new Runnable() { |
|
551 @Override |
|
552 public void run() { |
|
553 LoadFaviconTask.closeHTTPClient(); |
|
554 } |
|
555 }); |
|
556 } |
|
557 } |