2 * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
3 * (the "License"); you may not use this file except in compliance with the
4 * License. You may obtain a copy of the License at
5 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6 * or agreed to in writing, software distributed under the License is
7 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 * KIND, either express or implied. See the License for the specific language
9 * governing permissions and limitations under the License.
12 package com.andrew.apollo.cache;
14 import android.content.Context;
15 import android.graphics.Bitmap;
16 import android.graphics.BitmapFactory;
17 import android.text.TextUtils;
18 import android.widget.ImageView;
20 import com.andrew.apollo.Config;
21 import com.andrew.apollo.MusicPlaybackService;
22 import com.andrew.apollo.lastfm.Album;
23 import com.andrew.apollo.lastfm.Artist;
24 import com.andrew.apollo.lastfm.MusicEntry;
25 import com.andrew.apollo.lastfm.ImageSize;
26 import com.andrew.apollo.utils.MusicUtils;
27 import com.andrew.apollo.utils.PreferenceUtils;
29 import java.io.BufferedInputStream;
30 import java.io.BufferedOutputStream;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.net.HttpURLConnection;
39 * A subclass of {@link ImageWorker} that fetches images from a URL.
41 public class ImageFetcher extends ImageWorker {
43 public static final int IO_BUFFER_SIZE_BYTES = 1024;
45 private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
47 private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
49 private static final String DEFAULT_HTTP_CACHE_DIR = "http"; //$NON-NLS-1$
51 private static ImageFetcher sInstance = null;
54 * Creates a new instance of {@link ImageFetcher}.
56 * @param context The {@link Context} to use.
58 public ImageFetcher(final Context context) {
63 * Used to create a singleton of the image fetcher
65 * @param context The {@link Context} to use
66 * @return A new instance of this class.
68 public static final ImageFetcher getInstance(final Context context) {
69 if (sInstance == null) {
70 sInstance = new ImageFetcher(context.getApplicationContext());
79 protected Bitmap processBitmap(final String url) {
83 final File file = downloadBitmapToFile(mContext, url, DEFAULT_HTTP_CACHE_DIR);
85 // Return a sampled down version
86 final Bitmap bitmap = decodeSampledBitmapFromFile(file.toString());
95 private static String getBestImage(MusicEntry e) {
96 final ImageSize[] QUALITY = {ImageSize.EXTRALARGE, ImageSize.LARGE, ImageSize.MEDIUM,
97 ImageSize.SMALL, ImageSize.UNKNOWN};
98 for(ImageSize q : QUALITY) {
99 String url = e.getImageURL(q);
111 protected String processImageUrl(final String artistName, final String albumName,
112 final ImageType imageType) {
115 if (!TextUtils.isEmpty(artistName)) {
116 if (PreferenceUtils.getInstance(mContext).downloadMissingArtistImages()) {
117 final Artist artist = Artist.getInfo(mContext, artistName);
118 if (artist != null) {
119 return getBestImage(artist);
125 if (!TextUtils.isEmpty(artistName) && !TextUtils.isEmpty(albumName)) {
126 if (PreferenceUtils.getInstance(mContext).downloadMissingArtwork()) {
127 final Artist correction = Artist.getCorrection(mContext, artistName);
128 if (correction != null) {
129 final Album album = Album.getInfo(mContext, correction.getName(),
132 return getBestImage(album);
145 * Used to fetch album images.
147 public void loadAlbumImage(final String artistName, final String albumName, final long albumId,
148 final ImageView imageView) {
149 loadImage(generateAlbumCacheKey(albumName, artistName), artistName, albumName, albumId, imageView,
154 * Used to fetch the current artwork.
156 public void loadCurrentArtwork(final ImageView imageView) {
157 loadImage(generateAlbumCacheKey(MusicUtils.getAlbumName(), MusicUtils.getArtistName()),
158 MusicUtils.getArtistName(), MusicUtils.getAlbumName(), MusicUtils.getCurrentAlbumId(),
159 imageView, ImageType.ALBUM);
163 * Used to fetch artist images.
165 public void loadArtistImage(final String key, final ImageView imageView) {
166 loadImage(key, key, null, -1, imageView, ImageType.ARTIST);
170 * Used to fetch the current artist image.
172 public void loadCurrentArtistImage(final ImageView imageView) {
173 loadImage(MusicUtils.getArtistName(), MusicUtils.getArtistName(), null, -1, imageView,
178 * @param pause True to temporarily pause the disk cache, false otherwise.
180 public void setPauseDiskCache(final boolean pause) {
181 if (mImageCache != null) {
182 mImageCache.setPauseDiskCache(pause);
187 * Clears the disk and memory caches
189 public void clearCaches() {
190 if (mImageCache != null) {
191 mImageCache.clearCaches();
196 * @param key The key used to find the image to remove
198 public void removeFromCache(final String key) {
199 if (mImageCache != null) {
200 mImageCache.removeFromCache(key);
205 * @param key The key used to find the image to return
207 public Bitmap getCachedBitmap(final String key) {
208 if (mImageCache != null) {
209 return mImageCache.getCachedBitmap(key);
211 return getDefaultArtwork();
215 * @param keyAlbum The key (album name) used to find the album art to return
216 * @param keyArtist The key (artist name) used to find the album art to return
218 public Bitmap getCachedArtwork(final String keyAlbum, final String keyArtist) {
219 return getCachedArtwork(keyAlbum, keyArtist,
220 MusicUtils.getIdForAlbum(mContext, keyAlbum, keyArtist));
224 * @param keyAlbum The key (album name) used to find the album art to return
225 * @param keyArtist The key (artist name) used to find the album art to return
226 * @param keyId The key (album id) used to find the album art to return
228 public Bitmap getCachedArtwork(final String keyAlbum, final String keyArtist,
230 if (mImageCache != null) {
231 return mImageCache.getCachedArtwork(mContext,
232 generateAlbumCacheKey(keyAlbum, keyArtist),
235 return getDefaultArtwork();
239 * Finds cached or downloads album art. Used in {@link MusicPlaybackService}
240 * to set the current album art in the notification and lock screen
242 * @param albumName The name of the current album
243 * @param albumId The ID of the current album
244 * @param artistName The album artist in case we should have to download
246 * @return The album art as an {@link Bitmap}
248 public Bitmap getArtwork(final String albumName, final long albumId, final String artistName) {
249 // Check the disk cache
250 Bitmap artwork = null;
252 if (artwork == null && albumName != null && mImageCache != null) {
253 artwork = mImageCache.getBitmapFromDiskCache(
254 generateAlbumCacheKey(albumName, artistName));
256 if (artwork == null && albumId >= 0 && mImageCache != null) {
257 // Check for local artwork
258 artwork = mImageCache.getArtworkFromFile(mContext, albumId);
260 if (artwork != null) {
263 return getDefaultArtwork();
267 * Download a {@link Bitmap} from a URL, write it to a disk and return the
268 * File pointer. This implementation uses a simple disk cache.
270 * @param context The context to use
271 * @param urlString The URL to fetch
272 * @return A {@link File} pointing to the fetched bitmap
274 public static final File downloadBitmapToFile(final Context context, final String urlString,
275 final String uniqueName) {
276 final File cacheDir = ImageCache.getDiskCacheDir(context, uniqueName);
278 if (!cacheDir.exists()) {
282 HttpURLConnection urlConnection = null;
283 BufferedOutputStream out = null;
286 final File tempFile = File.createTempFile("bitmap", null, cacheDir); //$NON-NLS-1$
288 final URL url = new URL(urlString);
289 urlConnection = (HttpURLConnection)url.openConnection();
290 if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
293 final InputStream in = new BufferedInputStream(urlConnection.getInputStream(),
294 IO_BUFFER_SIZE_BYTES);
295 out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);
298 while ((oneByte = in.read()) != -1) {
302 } catch (final IOException ignored) {
304 if (urlConnection != null) {
305 urlConnection.disconnect();
310 } catch (final IOException ignored) {
318 * Decode and sample down a {@link Bitmap} from a file to the requested
321 * @param filename The full path of the file to decode
322 * @param reqWidth The requested width of the resulting bitmap
323 * @param reqHeight The requested height of the resulting bitmap
324 * @return A {@link Bitmap} sampled down from the original with the same
325 * aspect ratio and dimensions that are equal to or greater than the
326 * requested width and height
328 public static Bitmap decodeSampledBitmapFromFile(final String filename) {
330 // First decode with inJustDecodeBounds=true to check dimensions
331 final BitmapFactory.Options options = new BitmapFactory.Options();
332 options.inJustDecodeBounds = true;
333 BitmapFactory.decodeFile(filename, options);
335 // Calculate inSampleSize
336 options.inSampleSize = calculateInSampleSize(options, DEFAULT_MAX_IMAGE_WIDTH,
337 DEFAULT_MAX_IMAGE_HEIGHT);
339 // Decode bitmap with inSampleSize set
340 options.inJustDecodeBounds = false;
341 return BitmapFactory.decodeFile(filename, options);
345 * Calculate an inSampleSize for use in a
346 * {@link android.graphics.BitmapFactory.Options} object when decoding
347 * bitmaps using the decode* methods from {@link BitmapFactory}. This
348 * implementation calculates the closest inSampleSize that will result in
349 * the final decoded bitmap having a width and height equal to or larger
350 * than the requested width and height. This implementation does not ensure
351 * a power of 2 is returned for inSampleSize which can be faster when
352 * decoding but results in a larger bitmap which isn't as useful for caching
355 * @param options An options object with out* params already populated (run
356 * through a decode* method with inJustDecodeBounds==true
357 * @param reqWidth The requested width of the resulting bitmap
358 * @param reqHeight The requested height of the resulting bitmap
359 * @return The value to be used for inSampleSize
361 public static final int calculateInSampleSize(final BitmapFactory.Options options,
362 final int reqWidth, final int reqHeight) {
363 /* Raw height and width of image */
364 final int height = options.outHeight;
365 final int width = options.outWidth;
366 int inSampleSize = 1;
368 if (height > reqHeight || width > reqWidth) {
369 if (width > height) {
370 inSampleSize = Math.round((float)height / (float)reqHeight);
372 inSampleSize = Math.round((float)width / (float)reqWidth);
375 // This offers some additional logic in case the image has a strange
376 // aspect ratio. For example, a panorama may have a much larger
377 // width than height. In these cases the total pixels might still
378 // end up being too large to fit comfortably in memory, so we should
379 // be more aggressive with sample down the image (=larger
382 final float totalPixels = width * height;
384 /* More than 2x the requested pixels we'll sample down further */
385 final float totalReqPixelsCap = reqWidth * reqHeight * 2;
387 while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
395 * Generates key used by album art cache. It needs both album name and artist name
396 * to let to select correct image for the case when there are two albums with the
399 * @param albumName The album name the cache key needs to be generated.
400 * @param artistName The artist name the cache key needs to be generated.
403 public static String generateAlbumCacheKey(final String albumName, final String artistName) {
404 if (albumName == null || artistName == null) {
407 return new StringBuilder(albumName)
411 .append(Config.ALBUM_ART_SUFFIX)