forked from mozilla/gecko-dev
-
Notifications
You must be signed in to change notification settings - Fork 2
/
LoadFaviconTask.java
481 lines (403 loc) · 16.5 KB
/
LoadFaviconTask.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.favicons;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.net.http.AndroidHttpClient;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.BufferedHttpEntity;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UiAsyncTask;
import static org.mozilla.gecko.favicons.Favicons.sContext;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Class representing the asynchronous task to load a Favicon which is not currently in the in-memory
* cache.
* The implementation initially tries to get the Favicon from the database. Upon failure, the icon
* is loaded from the internet.
*/
public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
private static final String LOGTAG = "LoadFaviconTask";
// Access to this map needs to be synchronized prevent multiple jobs loading the same favicon
// from executing concurrently.
private static final HashMap<String, LoadFaviconTask> loadsInFlight = new HashMap<String, LoadFaviconTask>();
public static final int FLAG_PERSIST = 1;
public static final int FLAG_SCALE = 2;
private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
private static AtomicInteger mNextFaviconLoadId = new AtomicInteger(0);
private int mId;
private String mPageUrl;
private String mFaviconUrl;
private OnFaviconLoadedListener mListener;
private int mFlags;
private final boolean mOnlyFromLocal;
// Assuming square favicons, judging by width only is acceptable.
protected int mTargetWidth;
private LinkedList<LoadFaviconTask> mChainees;
private boolean mIsChaining;
static AndroidHttpClient sHttpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString());
public LoadFaviconTask(Handler backgroundThreadHandler,
String pageUrl, String faviconUrl, int flags,
OnFaviconLoadedListener listener) {
this(backgroundThreadHandler, pageUrl, faviconUrl, flags, listener, -1, false);
}
public LoadFaviconTask(Handler backgroundThreadHandler,
String pageUrl, String faviconUrl, int flags,
OnFaviconLoadedListener aListener, int targetSize, boolean fromLocal) {
super(backgroundThreadHandler);
mId = mNextFaviconLoadId.incrementAndGet();
mPageUrl = pageUrl;
mFaviconUrl = faviconUrl;
mListener = aListener;
mFlags = flags;
mTargetWidth = targetSize;
mOnlyFromLocal = fromLocal;
}
// Runs in background thread
private Bitmap loadFaviconFromDb() {
ContentResolver resolver = sContext.getContentResolver();
return BrowserDB.getFaviconForFaviconUrl(resolver, mFaviconUrl);
}
// Runs in background thread
private void saveFaviconToDb(final Bitmap favicon) {
if ((mFlags & FLAG_PERSIST) == 0) {
return;
}
ContentResolver resolver = sContext.getContentResolver();
BrowserDB.updateFaviconForUrl(resolver, mPageUrl, favicon, mFaviconUrl);
}
/**
* Helper method for trying the download request to grab a Favicon.
* @param faviconURI URL of Favicon to try and download
* @return The HttpResponse containing the downloaded Favicon if successful, null otherwise.
*/
private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException {
HashSet<String> visitedLinkSet = new HashSet<String>();
visitedLinkSet.add(faviconURI.toString());
return tryDownloadRecurse(faviconURI, visitedLinkSet);
}
private HttpResponse tryDownloadRecurse(URI faviconURI, HashSet<String> visited) throws URISyntaxException, IOException {
if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) {
return null;
}
HttpGet request = new HttpGet(faviconURI);
HttpResponse response = sHttpClient.execute(request);
if (response == null) {
return null;
}
if (response.getStatusLine() != null) {
// Was the response a failure?
int status = response.getStatusLine().getStatusCode();
// Handle HTTP status codes requesting a redirect.
if (status >= 300 && status < 400) {
Header header = response.getFirstHeader("Location");
// Handle mad webservers.
if (header == null) {
return null;
}
String newURI = header.getValue();
if (newURI == null || newURI.equals(faviconURI.toString())) {
return null;
}
if (visited.contains(newURI)) {
// Already been redirected here - abort.
return null;
}
visited.add(newURI);
return tryDownloadRecurse(new URI(newURI), visited);
}
if (status >= 400) {
return null;
}
}
return response;
}
/**
* Retrieve the specified favicon from the JAR, returning null if it's not
* a JAR URI.
*/
private static Bitmap fetchJARFavicon(String uri) {
if (uri == null) {
return null;
}
if (uri.startsWith("jar:jar:")) {
Log.d(LOGTAG, "Fetching favicon from JAR.");
try {
return GeckoJarReader.getBitmap(sContext.getResources(), uri);
} catch (Exception e) {
// Just about anything could happen here.
Log.w(LOGTAG, "Error fetching favicon from JAR.", e);
return null;
}
}
return null;
}
// Runs in background thread.
// Does not attempt to fetch from JARs.
private Bitmap downloadFavicon(URI targetFaviconURI) {
if (targetFaviconURI == null) {
return null;
}
// Only get favicons for HTTP/HTTPS.
String scheme = targetFaviconURI.getScheme();
if (!"http".equals(scheme) && !"https".equals(scheme)) {
return null;
}
Bitmap image = null;
// skia decoder sometimes returns null; workaround is to use BufferedHttpEntity
// http://groups.google.com/group/android-developers/browse_thread/thread/171b8bf35dbbed96/c3ec5f45436ceec8?lnk=raot
try {
// Try the URL we were given.
HttpResponse response = tryDownload(targetFaviconURI);
if (response == null) {
return null;
}
HttpEntity entity = response.getEntity();
if (entity == null) {
return null;
}
BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity);
InputStream contentStream = null;
try {
contentStream = bufferedEntity.getContent();
image = BitmapUtils.decodeStream(contentStream);
contentStream.close();
} finally {
if (contentStream != null) {
contentStream.close();
}
}
} catch (Exception e) {
Log.e(LOGTAG, "Error reading favicon", e);
}
return image;
}
@Override
protected Bitmap doInBackground(Void... unused) {
if (isCancelled()) {
return null;
}
String storedFaviconUrl;
boolean isUsingDefaultURL = false;
// Handle the case of malformed favicon URL.
// If favicon is empty, fall back to the stored one.
if (TextUtils.isEmpty(mFaviconUrl)) {
// Try to get the favicon URL from the memory cache.
storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(mPageUrl);
// If that failed, try to get the URL from the database.
if (storedFaviconUrl == null) {
storedFaviconUrl = Favicons.getFaviconUrlForPageUrl(mPageUrl);
if (storedFaviconUrl != null) {
// If that succeeded, cache the URL loaded from the database in memory.
Favicons.putFaviconURLForPageURLInCache(mPageUrl, storedFaviconUrl);
}
}
// If we found a faviconURL - use it.
if (storedFaviconUrl != null) {
mFaviconUrl = storedFaviconUrl;
} else {
// If we don't have a stored one, fall back to the default.
mFaviconUrl = Favicons.guessDefaultFaviconURL(mPageUrl);
if (TextUtils.isEmpty(mFaviconUrl)) {
return null;
}
isUsingDefaultURL = true;
}
}
// Check if favicon has failed - if so, give up. We need this check because, sometimes, we
// didn't know the real Favicon URL until we asked the database.
if (Favicons.isFailedFavicon(mFaviconUrl)) {
return null;
}
if (isCancelled()) {
return null;
}
Bitmap image;
// Determine if there is already an ongoing task to fetch the Favicon we desire.
// If there is, just join the queue and wait for it to finish. If not, we carry on.
synchronized(loadsInFlight) {
// Another load of the current Favicon is already underway
LoadFaviconTask existingTask = loadsInFlight.get(mFaviconUrl);
if (existingTask != null && !existingTask.isCancelled()) {
existingTask.chainTasks(this);
mIsChaining = true;
// If we are chaining, we want to keep the first task started to do this job as the one
// in the hashmap so subsequent tasks will add themselves to its chaining list.
return null;
}
// We do not want to update the hashmap if the task has chained - other tasks need to
// chain onto the same parent task.
loadsInFlight.put(mFaviconUrl, this);
}
if (isCancelled()) {
return null;
}
image = loadFaviconFromDb();
if (imageIsValid(image)) {
return image;
}
if (mOnlyFromLocal || isCancelled()) {
return null;
}
// Let's see if it's in a JAR.
image = fetchJARFavicon(mFaviconUrl);
if (image != null) {
// We don't want to put this into the DB.
return image;
}
try {
image = downloadFavicon(new URI(mFaviconUrl));
} catch (URISyntaxException e) {
Log.e(LOGTAG, "The provided favicon URL is not valid");
return null;
} catch (Exception e) {
Log.e(LOGTAG, "Couldn't download favicon.", e);
}
if (imageIsValid(image)) {
saveFaviconToDb(image);
return image;
}
if (isUsingDefaultURL) {
Favicons.putFaviconInFailedCache(mFaviconUrl);
return null;
}
// If we're not already trying the default URL, try it now.
final String guessed = Favicons.guessDefaultFaviconURL(mPageUrl);
if (guessed == null) {
Favicons.putFaviconInFailedCache(mFaviconUrl);
return null;
}
image = fetchJARFavicon(guessed);
if (imageIsValid(image)) {
// We don't want to put this into the DB.
return image;
}
try {
image = downloadFavicon(new URI(guessed));
} catch (Exception e) {
// Not interesting. It was an educated guess, anyway.
return null;
}
if (imageIsValid(image)) {
saveFaviconToDb(image);
return image;
}
return null;
}
private static boolean imageIsValid(final Bitmap image) {
return image != null &&
image.getWidth() > 0 &&
image.getHeight() > 0;
}
@Override
protected void onPostExecute(Bitmap image) {
if (mIsChaining) {
return;
}
// Put what we got in the memcache.
Favicons.putFaviconInMemCache(mFaviconUrl, image);
// Process the result, scale for the listener, etc.
processResult(image);
synchronized (loadsInFlight) {
// Prevent any other tasks from chaining on this one.
loadsInFlight.remove(mFaviconUrl);
}
// Since any update to mChainees is done while holding the loadsInFlight lock, once we reach
// this point no further updates to that list can possibly take place (As far as other tasks
// are concerned, there is no longer a task to chain from. The above block will have waited
// for any tasks that were adding themselves to the list before reaching this point.)
// As such, I believe we're safe to do the following without holding the lock.
// This is nice - we do not want to take the lock unless we have to anyway, and chaining rarely
// actually happens outside of the strange situations unit tests create.
// Share the result with all chained tasks.
if (mChainees != null) {
for (LoadFaviconTask t : mChainees) {
t.processResult(image);
}
}
}
private void processResult(Bitmap image) {
Favicons.removeLoadTask(mId);
Bitmap scaled = image;
// Notify listeners, scaling if required.
if (mTargetWidth != -1 && image != null && image.getWidth() != mTargetWidth) {
scaled = Favicons.getSizedFaviconFromCache(mFaviconUrl, mTargetWidth);
}
Favicons.dispatchResult(mPageUrl, mFaviconUrl, scaled, mListener);
}
@Override
protected void onCancelled() {
Favicons.removeLoadTask(mId);
synchronized(loadsInFlight) {
// Only remove from the hashmap if the task there is the one that's being canceled.
// Cancellation of a task that would have chained is not interesting to the hashmap.
final LoadFaviconTask primary = loadsInFlight.get(mFaviconUrl);
if (primary == this) {
loadsInFlight.remove(mFaviconUrl);
return;
}
if (primary == null) {
// This shouldn't happen.
return;
}
if (primary.mChainees != null) {
primary.mChainees.remove(this);
}
}
// Note that we don't call the listener callback if the
// favicon load is cancelled.
}
/**
* When the result of this job is ready, also notify the chainee of the result.
* Used for aggregating concurrent requests for the same Favicon into a single actual request.
* (Don't want to download a hundred instances of Google's Favicon at once, for example).
* The loadsInFlight lock must be held when calling this function.
*
* @param aChainee LoadFaviconTask
*/
private void chainTasks(LoadFaviconTask aChainee) {
if (mChainees == null) {
mChainees = new LinkedList<LoadFaviconTask>();
}
mChainees.add(aChainee);
}
int getId() {
return mId;
}
static void closeHTTPClient() {
// This work must be done on a background thread because it shuts down
// the connection pool, which typically involves closing a connection --
// which counts as network activity.
if (ThreadUtils.isOnBackgroundThread()) {
if (sHttpClient != null) {
sHttpClient.close();
}
return;
}
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
LoadFaviconTask.closeHTTPClient();
}
});
}
}