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