Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
5 package org.mozilla.gecko.favicons;
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;
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;
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";
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>();
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;
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;
63 private final boolean onlyFromLocal;
65 // Assuming square favicons, judging by width only is acceptable.
66 protected int targetWidth;
67 private LinkedList<LoadFaviconTask> chainees;
68 private boolean isChaining;
70 static AndroidHttpClient httpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString());
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);
82 id = nextFaviconLoadId.incrementAndGet();
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 }
92 // Runs in background thread
93 private LoadFaviconResult loadFaviconFromDb() {
94 ContentResolver resolver = context.getContentResolver();
95 return BrowserDB.getFaviconForFaviconUrl(resolver, faviconURL);
96 }
98 // Runs in background thread
99 private void saveFaviconToDb(final byte[] encodedFavicon) {
100 if (encodedFavicon == null) {
101 return;
102 }
104 if ((flags & FLAG_PERSIST) == 0) {
105 return;
106 }
108 ContentResolver resolver = context.getContentResolver();
109 BrowserDB.updateFaviconForUrl(resolver, pageUrl, encodedFavicon, faviconURL);
110 }
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 }
127 HttpGet request = new HttpGet(faviconURI);
128 HttpResponse response = httpClient.execute(request);
129 if (response == null) {
130 return null;
131 }
133 if (response.getStatusLine() != null) {
135 // Was the response a failure?
136 int status = response.getStatusLine().getStatusCode();
138 // Handle HTTP status codes requesting a redirect.
139 if (status >= 300 && status < 400) {
140 Header header = response.getFirstHeader("Location");
142 // Handle mad webservers.
143 if (header == null) {
144 return null;
145 }
147 String newURI = header.getValue();
148 if (newURI == null || newURI.equals(faviconURI.toString())) {
149 return null;
150 }
152 if (visited.contains(newURI)) {
153 // Already been redirected here - abort.
154 return null;
155 }
157 visited.add(newURI);
158 return tryDownloadRecurse(new URI(newURI), visited);
159 }
161 if (status >= 400) {
162 return null;
163 }
164 }
165 return response;
166 }
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 }
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 }
196 // Only get favicons for HTTP/HTTPS.
197 String scheme = targetFaviconURI.getScheme();
198 if (!"http".equals(scheme) && !"https".equals(scheme)) {
199 return null;
200 }
202 LoadFaviconResult result = null;
204 try {
205 result = downloadAndDecodeImage(targetFaviconURI);
206 } catch (Exception e) {
207 Log.e(LOGTAG, "Error reading favicon", e);
208 }
210 return result;
211 }
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 }
231 HttpEntity entity = response.getEntity();
232 if (entity == null) {
233 return null;
234 }
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 }
248 // Allocate a buffer to hold the raw favicon data downloaded.
249 byte[] buffer = new byte[bufferSize];
251 // The offset of the start of the buffer's free space.
252 int bPointer = 0;
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;
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];
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 }
279 // Having downloaded the image, decode it.
280 return FaviconDecoder.decodeFavicon(buffer, 0, bPointer + 1);
281 }
283 @Override
284 protected Bitmap doInBackground(Void... unused) {
285 if (isCancelled()) {
286 return null;
287 }
289 String storedFaviconUrl;
290 boolean isUsingDefaultURL = false;
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);
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 }
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);
314 if (TextUtils.isEmpty(faviconURL)) {
315 return null;
316 }
317 isUsingDefaultURL = true;
318 }
319 }
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 }
327 if (isCancelled()) {
328 return null;
329 }
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;
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 }
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 }
351 if (isCancelled()) {
352 return null;
353 }
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 }
361 if (onlyFromLocal || isCancelled()) {
362 return null;
363 }
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 }
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 }
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 }
390 if (isUsingDefaultURL) {
391 Favicons.putFaviconInFailedCache(faviconURL);
392 return null;
393 }
395 if (isCancelled()) {
396 return null;
397 }
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 }
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 }
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 }
420 if (loadedBitmaps != null) {
421 saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
422 return pushToCacheAndGetResult(loadedBitmaps);
423 }
425 return null;
426 }
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 }
443 private static boolean imageIsValid(final Bitmap image) {
444 return image != null &&
445 image.getWidth() > 0 &&
446 image.getHeight() > 0;
447 }
449 @Override
450 protected void onPostExecute(Bitmap image) {
451 if (isChaining) {
452 return;
453 }
455 // Process the result, scale for the listener, etc.
456 processResult(image);
458 synchronized (loadsInFlight) {
459 // Prevent any other tasks from chaining on this one.
460 loadsInFlight.remove(faviconURL);
461 }
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.)
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.
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 }
482 private void processResult(Bitmap image) {
483 Favicons.removeLoadTask(id);
484 Bitmap scaled = image;
486 // Notify listeners, scaling if required.
487 if (targetWidth != -1 && image != null && image.getWidth() != targetWidth) {
488 scaled = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth);
489 }
491 Favicons.dispatchResult(pageUrl, faviconURL, scaled, listener);
492 }
494 @Override
495 protected void onCancelled() {
496 Favicons.removeLoadTask(id);
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 }
515 // Note that we don't call the listener callback if the
516 // favicon load is cancelled.
517 }
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 }
532 chainees.add(aChainee);
533 }
535 int getId() {
536 return id;
537 }
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 }
550 ThreadUtils.postToBackgroundThread(new Runnable() {
551 @Override
552 public void run() {
553 LoadFaviconTask.closeHTTPClient();
554 }
555 });
556 }
557 }