summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorlinus_lee <llee@cyngn.com>2014-09-18 17:18:38 -0700
committerlinus_lee <llee@cyngn.com>2014-11-20 12:03:03 -0800
commit963653311eac17ad0822462014580a26e31ccc95 (patch)
tree921ed4a007ee5d3e330433efe12744b27f4deb0a
parent2a707a9079440f06809a212847bf0969ef7b3509 (diff)
downloadandroid_packages_apps_Eleven-963653311eac17ad0822462014580a26e31ccc95.tar.gz
android_packages_apps_Eleven-963653311eac17ad0822462014580a26e31ccc95.tar.bz2
android_packages_apps_Eleven-963653311eac17ad0822462014580a26e31ccc95.zip
Eleven: This adds the playlist top artist and song artwork logic
Reshuffles a lot of code from ImageWorker/ImageFetcher into ImageUtils and Separate worker tasks. The artwork logic leverages the top songs db to figure the top artist as well as the top songs for a playlist and builds images based on them. There is some basic logic to prevent this code from running all the time https://cyanogen.atlassian.net/browse/MUSIC-27 Change-Id: I05d832d4f0810b71ed0c0df184a372771537ccc1
-rw-r--r--res/drawable-xxhdpi/recently_added.pngbin0 -> 619 bytes
-rw-r--r--src/com/cyngn/eleven/adapters/PlaylistAdapter.java15
-rw-r--r--src/com/cyngn/eleven/cache/BitmapWorkerTask.java120
-rw-r--r--src/com/cyngn/eleven/cache/BlurBitmapWorkerTask.java221
-rw-r--r--src/com/cyngn/eleven/cache/ImageCache.java34
-rw-r--r--src/com/cyngn/eleven/cache/ImageFetcher.java242
-rw-r--r--src/com/cyngn/eleven/cache/ImageWorker.java563
-rw-r--r--src/com/cyngn/eleven/cache/PlaylistWorkerTask.java332
-rw-r--r--src/com/cyngn/eleven/cache/SimpleBitmapWorkerTask.java55
-rw-r--r--src/com/cyngn/eleven/loaders/SortedCursor.java (renamed from src/com/cyngn/eleven/loaders/TopTracksCursor.java)19
-rw-r--r--src/com/cyngn/eleven/loaders/TopTracksLoader.java4
-rw-r--r--src/com/cyngn/eleven/provider/PlaylistArtworkStore.java258
-rw-r--r--src/com/cyngn/eleven/provider/SongPlayCount.java83
-rw-r--r--src/com/cyngn/eleven/recycler/RecycleHolder.java2
-rw-r--r--src/com/cyngn/eleven/ui/activities/PlaylistDetailActivity.java4
-rw-r--r--src/com/cyngn/eleven/ui/activities/ProfileActivity.java3
-rw-r--r--src/com/cyngn/eleven/utils/ImageUtils.java241
17 files changed, 1521 insertions, 675 deletions
diff --git a/res/drawable-xxhdpi/recently_added.png b/res/drawable-xxhdpi/recently_added.png
new file mode 100644
index 0000000..e626b05
--- /dev/null
+++ b/res/drawable-xxhdpi/recently_added.png
Binary files differ
diff --git a/src/com/cyngn/eleven/adapters/PlaylistAdapter.java b/src/com/cyngn/eleven/adapters/PlaylistAdapter.java
index cff604c..6dd742f 100644
--- a/src/com/cyngn/eleven/adapters/PlaylistAdapter.java
+++ b/src/com/cyngn/eleven/adapters/PlaylistAdapter.java
@@ -21,6 +21,7 @@ import android.widget.ArrayAdapter;
import com.cyngn.eleven.Config.SmartPlaylistType;
import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
import com.cyngn.eleven.model.Playlist;
import com.cyngn.eleven.ui.MusicHolder;
import com.cyngn.eleven.ui.MusicHolder.DataHolder;
@@ -92,10 +93,13 @@ public class PlaylistAdapter extends ArrayAdapter<Playlist> {
SmartPlaylistType type = SmartPlaylistType.getTypeById(dataHolder.mItemId);
if (type != null) {
+ // Clear any drawables
+ holder.mImage.get().setBackground(null);
+
+ // Set the image resource based on the icon
switch (type) {
case LastAdded:
- // TOOD: Replace with Last Added Icon
- holder.mImage.get().setImageResource(R.drawable.recent_icon);
+ holder.mImage.get().setImageResource(R.drawable.recently_added);
break;
case TopTracks:
default:
@@ -103,10 +107,15 @@ public class PlaylistAdapter extends ArrayAdapter<Playlist> {
break;
}
+ // set the special background color
convertView.setBackgroundColor(getContext().getResources().
getColor(R.color.smart_playlist_item_background));
} else {
- holder.mImage.get().setImageResource(R.drawable.default_playlist);
+ // load the image
+ ImageFetcher.getInstance(getContext()).loadPlaylistCoverArtImage(
+ dataHolder.mItemId, holder.mImage.get());
+
+ // clear the background color
convertView.setBackgroundColor(Color.TRANSPARENT);
}
diff --git a/src/com/cyngn/eleven/cache/BitmapWorkerTask.java b/src/com/cyngn/eleven/cache/BitmapWorkerTask.java
new file mode 100644
index 0000000..aa354f4
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/BitmapWorkerTask.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.cache;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.AsyncTask;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.cache.ImageWorker.ImageType;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * The actual {@link android.os.AsyncTask} that will process the image.
+ */
+public abstract class BitmapWorkerTask<Params, Progress, Result>
+ extends AsyncTask<Params, Progress, Result> {
+ /**
+ * The {@link android.widget.ImageView} used to set the result
+ */
+ protected final WeakReference<ImageView> mImageReference;
+
+ /**
+ * Type of URL to download
+ */
+ protected final ImageWorker.ImageType mImageType;
+
+ /**
+ * Layer drawable used to cross fade the result from the worker
+ */
+ protected Drawable mFromDrawable;
+
+ protected final Context mContext;
+
+ protected final ImageCache mImageCache;
+
+ protected final Resources mResources;
+
+ /**
+ * The key used to store cached entries
+ */
+ public String mKey;
+
+ /**
+ * Constructor of <code>BitmapWorkerTask</code>
+ * @param key used for caching the image
+ * @param imageView The {@link ImageView} to use.
+ * @param imageType The type of image URL to fetch for.
+ * @param fromDrawable what drawable to transition from
+ */
+ public BitmapWorkerTask(final String key, final ImageView imageView, final ImageType imageType,
+ final Drawable fromDrawable, final Context context) {
+ mKey = key;
+
+ mContext = context;
+ mImageCache = ImageCache.getInstance(mContext);
+ mResources = mContext.getResources();
+
+ mImageReference = new WeakReference<ImageView>(imageView);
+ mImageType = imageType;
+
+ // A transparent image (layer 0) and the new result (layer 1)
+ mFromDrawable = fromDrawable;
+ }
+
+ /**
+ * @return The {@link ImageView} associated with this task as long as
+ * the ImageView's task still points to this task as well.
+ * Returns null otherwise.
+ */
+ protected ImageView getAttachedImageView() {
+ final ImageView imageView = mImageReference.get();
+ if (imageView != null) {
+ final BitmapWorkerTask bitmapWorkerTask = ImageWorker.getBitmapWorkerTask(imageView);
+ if (this == bitmapWorkerTask) {
+ return imageView;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the bitmap given the input params
+ * @param params artistName, albumName, albumId
+ * @return Bitmap
+ */
+ protected Bitmap getBitmapInBackground(final String... params) {
+ return ImageWorker.getBitmapInBackground(mContext, mImageCache, mKey,
+ params[1], params[0], Long.valueOf(params[2]), mImageType);
+ }
+
+ /**
+ * Creates a transition drawable with default parameters
+ * @param bitmap the bitmap to transition to
+ * @return the transition drawable
+ */
+ protected TransitionDrawable createImageTransitionDrawable(final Bitmap bitmap) {
+ return createImageTransitionDrawable(bitmap, ImageWorker.FADE_IN_TIME, false, false);
+ }
+
+ /**
+ * Creates a transition drawable
+ * @param bitmap to transition to
+ * @param fadeTime the time to fade in ms
+ * @param dither setting
+ * @param force force create a transition even if bitmap == null (fade to transparent)
+ * @return the transition drawable
+ */
+ protected TransitionDrawable createImageTransitionDrawable(final Bitmap bitmap,
+ final int fadeTime, final boolean dither, final boolean force) {
+ return ImageWorker.createImageTransitionDrawable(mResources, mFromDrawable, bitmap,
+ fadeTime, dither, force);
+ }
+}
diff --git a/src/com/cyngn/eleven/cache/BlurBitmapWorkerTask.java b/src/com/cyngn/eleven/cache/BlurBitmapWorkerTask.java
new file mode 100644
index 0000000..ac4cc59
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/BlurBitmapWorkerTask.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.cache;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.support.v7.graphics.Palette;
+import android.support.v7.graphics.PaletteItem;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.Element;
+import android.support.v8.renderscript.RenderScript;
+import android.support.v8.renderscript.ScriptIntrinsicBlur;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.cache.ImageWorker.ImageType;
+import com.cyngn.eleven.widgets.BlurScrimImage;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This will download the image (if needed) and create a blur and set the scrim as well on the
+ * BlurScrimImage
+ */
+public class BlurBitmapWorkerTask extends BitmapWorkerTask<String, Void, BlurBitmapWorkerTask.ResultContainer> {
+ // if the image is too small, the blur will look bad post scale up so we use the min size
+ // to scale up before bluring
+ private static final int MIN_BITMAP_SIZE = 500;
+ // number of times to run the blur
+ private static final int NUM_BLUR_RUNS = 8;
+ // 25f is the max blur radius possible
+ private static final float BLUR_RADIUS = 25f;
+
+ // container for the result
+ public static class ResultContainer {
+ public TransitionDrawable mImageViewBitmapDrawable;
+ public int mPaletteColor;
+ }
+
+ /**
+ * The {@link com.cyngn.eleven.widgets.BlurScrimImage} used to set the result
+ */
+ private final WeakReference<BlurScrimImage> mBlurScrimImage;
+
+ /**
+ * RenderScript used to blur the image
+ */
+ protected final RenderScript mRenderScript;
+
+ /**
+ * Constructor of <code>BlurBitmapWorkerTask</code>
+ * @param key used for caching the image
+ * @param blurScrimImage The {@link BlurScrimImage} to use.
+ * @param imageType The type of image URL to fetch for.
+ * @param fromDrawable what drawable to transition from
+ */
+ public BlurBitmapWorkerTask(final String key, final BlurScrimImage blurScrimImage,
+ final ImageType imageType, final Drawable fromDrawable,
+ final Context context, final RenderScript renderScript) {
+ super(key, blurScrimImage.getImageView(), imageType, fromDrawable, context);
+ mBlurScrimImage = new WeakReference<BlurScrimImage>(blurScrimImage);
+ mRenderScript = renderScript;
+
+ // use the existing image as the drawable and if it doesn't exist fallback to transparent
+ mFromDrawable = blurScrimImage.getImageView().getDrawable();
+ if (mFromDrawable == null) {
+ mFromDrawable = fromDrawable;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected ResultContainer doInBackground(final String... params) {
+ if (isCancelled()) {
+ return null;
+ }
+
+ Bitmap bitmap = getBitmapInBackground(params);
+
+ ResultContainer result = new ResultContainer();
+
+ Bitmap output = null;
+
+ if (bitmap != null) {
+ // now create the blur bitmap
+ Bitmap input = bitmap;
+
+ // if the image is too small, scale it up before running through the blur
+ if (input.getWidth() < MIN_BITMAP_SIZE || input.getHeight() < MIN_BITMAP_SIZE) {
+ float multiplier = Math.max(MIN_BITMAP_SIZE / (float)input.getWidth(),
+ MIN_BITMAP_SIZE / (float)input.getHeight());
+ input = input.createScaledBitmap(bitmap, (int)(input.getWidth() * multiplier),
+ (int)(input.getHeight() * multiplier), true);
+ // since we created a new bitmap, we can re-use the bitmap for our output
+ output = input;
+ } else {
+ // if we aren't creating a new bitmap, create a new output bitmap
+ output = Bitmap.createBitmap(input.getWidth(), input.getHeight(), input.getConfig());
+ }
+
+ // run the blur multiple times
+ for (int i = 0; i < NUM_BLUR_RUNS; i++) {
+ final Allocation inputAlloc = Allocation.createFromBitmap(mRenderScript, input);
+ final Allocation outputAlloc = Allocation.createTyped(mRenderScript,
+ inputAlloc.getType());
+ final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(mRenderScript,
+ Element.U8_4(mRenderScript));
+
+ script.setRadius(BLUR_RADIUS);
+ script.setInput(inputAlloc);
+ script.forEach(outputAlloc);
+ outputAlloc.copyTo(output);
+
+ // if we run more than 1 blur, the new input should be the old output
+ input = output;
+ }
+
+ // calculate the palette color
+ result.mPaletteColor = getPaletteColorInBackground(output);
+
+ // create the bitmap transition drawable
+ result.mImageViewBitmapDrawable = createImageTransitionDrawable(output,
+ ImageWorker.FADE_IN_TIME_SLOW, true, true);
+
+ return result;
+ }
+
+ return null;
+ }
+
+ /**
+ * This will get the most vibrant palette color for a bitmap
+ * @param input to process
+ * @return the most vibrant color or transparent if none found
+ */
+ private int getPaletteColorInBackground(Bitmap input) {
+ int color = Color.TRANSPARENT;
+
+ if (input != null) {
+ Palette palette = Palette.generate(input);
+ PaletteItem paletteItem = palette.getVibrantColor();
+
+ // keep walking through the palette items to find a color if we don't have any
+ if (paletteItem == null) {
+ paletteItem = palette.getLightVibrantColor();
+ }
+
+ if (paletteItem == null) {
+ paletteItem = palette.getLightMutedColor();
+ }
+
+ if (paletteItem == null) {
+ paletteItem = palette.getLightMutedColor();
+ }
+
+ if (paletteItem == null) {
+ paletteItem = palette.getDarkVibrantColor();
+ }
+
+ if (paletteItem == null) {
+ paletteItem = palette.getMutedColor();
+ }
+
+ if (paletteItem == null) {
+ paletteItem = palette.getDarkMutedColor();
+ }
+
+ if (paletteItem != null) {
+ // grab the rgb values
+ color = paletteItem.getRgb() | 0xFFFFFF;
+
+ // make it 20% opacity
+ color &= 0x33000000;
+ }
+ }
+
+ return color;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPostExecute(ResultContainer resultContainer) {
+ BlurScrimImage blurScrimImage = mBlurScrimImage.get();
+ if (blurScrimImage != null) {
+ if (resultContainer == null) {
+ // if we have no image, then signal the transition to the default state
+ blurScrimImage.transitionToDefaultState();
+ } else {
+ // create the palette transition
+ TransitionDrawable paletteTransition = ImageWorker.createPaletteTransition(
+ blurScrimImage,
+ resultContainer.mPaletteColor);
+
+ // set the transition drawable
+ blurScrimImage.setTransitionDrawable(false,
+ resultContainer.mImageViewBitmapDrawable, paletteTransition);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected final ImageView getAttachedImageView() {
+ final BlurScrimImage blurImage = mBlurScrimImage.get();
+ final BitmapWorkerTask bitmapWorkerTask = ImageWorker.getBitmapWorkerTask(blurImage);
+ if (this == bitmapWorkerTask) {
+ return blurImage.getImageView();
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/com/cyngn/eleven/cache/ImageCache.java b/src/com/cyngn/eleven/cache/ImageCache.java
index f8b66ac..9bdf9b9 100644
--- a/src/com/cyngn/eleven/cache/ImageCache.java
+++ b/src/com/cyngn/eleven/cache/ImageCache.java
@@ -271,12 +271,23 @@ public final class ImageCache {
* @param bitmap The {@link Bitmap} to cache
*/
public void addBitmapToCache(final String data, final Bitmap bitmap) {
+ addBitmapToCache(data, bitmap, false);
+ }
+
+ /**
+ * Adds a new image to the memory and disk caches
+ *
+ * @param data The key used to store the image
+ * @param bitmap The {@link Bitmap} to cache
+ * @param replace force a replace even if the bitmap exists in the cache
+ */
+ public void addBitmapToCache(final String data, final Bitmap bitmap, final boolean replace) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
- addBitmapToMemCache(data, bitmap);
+ addBitmapToMemCache(data, bitmap, replace);
// Add to disk cache
if (mDiskCache != null) {
@@ -284,7 +295,11 @@ public final class ImageCache {
OutputStream out = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
- if (snapshot == null) {
+ if (snapshot != null) {
+ snapshot.getInputStream(DISK_CACHE_INDEX).close();
+ }
+
+ if (snapshot == null || replace) {
final DiskLruCache.Editor editor = mDiskCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
@@ -293,8 +308,6 @@ public final class ImageCache {
out.close();
flush();
}
- } else {
- snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
@@ -320,11 +333,22 @@ public final class ImageCache {
* @param bitmap The {@link Bitmap} to cache
*/
public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
+ addBitmapToMemCache(data, bitmap, false);
+ }
+
+ /**
+ * Called to add a new image to the memory cache
+ *
+ * @param data The key identifier
+ * @param bitmap The {@link Bitmap} to cache
+ * @param replace whether to force a replace if it already exists
+ */
+ public void addBitmapToMemCache(final String data, final Bitmap bitmap, final boolean replace) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
- if (getBitmapFromMemCache(data) == null) {
+ if (replace || getBitmapFromMemCache(data) == null) {
mLruCache.put(data, bitmap);
}
}
diff --git a/src/com/cyngn/eleven/cache/ImageFetcher.java b/src/com/cyngn/eleven/cache/ImageFetcher.java
index e647693..3832c6e 100644
--- a/src/com/cyngn/eleven/cache/ImageFetcher.java
+++ b/src/com/cyngn/eleven/cache/ImageFetcher.java
@@ -13,42 +13,19 @@ package com.cyngn.eleven.cache;
import android.content.Context;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.text.TextUtils;
import android.widget.ImageView;
import com.cyngn.eleven.Config;
import com.cyngn.eleven.MusicPlaybackService;
-import com.cyngn.eleven.lastfm.Album;
-import com.cyngn.eleven.lastfm.Artist;
-import com.cyngn.eleven.lastfm.MusicEntry;
-import com.cyngn.eleven.lastfm.ImageSize;
+import com.cyngn.eleven.cache.PlaylistWorkerTask.PlaylistWorkerType;
import com.cyngn.eleven.utils.MusicUtils;
-import com.cyngn.eleven.utils.PreferenceUtils;
import com.cyngn.eleven.widgets.BlurScrimImage;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
/**
* A subclass of {@link ImageWorker} that fetches images from a URL.
*/
public class ImageFetcher extends ImageWorker {
- public static final int IO_BUFFER_SIZE_BYTES = 1024;
-
- private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
-
- private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
-
- private static final String DEFAULT_HTTP_CACHE_DIR = "http"; //$NON-NLS-1$
-
private static ImageFetcher sInstance = null;
/**
@@ -74,79 +51,28 @@ public class ImageFetcher extends ImageWorker {
}
/**
- * {@inheritDoc}
+ * Loads a playlist's most played song's artist image
+ * @param playlistId id of the playlist
+ * @param imageView imageview to load into
*/
- @Override
- protected Bitmap processBitmap(final String url) {
- if (url == null) {
- return null;
- }
- final File file = downloadBitmapToFile(mContext, url, DEFAULT_HTTP_CACHE_DIR);
- if (file != null) {
- // Return a sampled down version
- final Bitmap bitmap = decodeSampledBitmapFromFile(file.toString());
- file.delete();
- if (bitmap != null) {
- return bitmap;
- }
- }
- return null;
- }
-
- private static String getBestImage(MusicEntry e) {
- final ImageSize[] QUALITY = {ImageSize.EXTRALARGE, ImageSize.LARGE, ImageSize.MEDIUM,
- ImageSize.SMALL, ImageSize.UNKNOWN};
- for(ImageSize q : QUALITY) {
- String url = e.getImageURL(q);
- if (url != null) {
- return url;
- }
- }
- return null;
+ public void loadPlaylistArtistImage(final long playlistId, final ImageView imageView) {
+ loadPlaylistImage(playlistId, PlaylistWorkerType.Artist, imageView);
}
/**
- * {@inheritDoc}
+ * Loads a playlist's most played songs into a combined image, or show 1 if not enough images
+ * @param playlistId id of the playlist
+ * @param imageView imageview to load into
*/
- @Override
- protected String processImageUrl(final String artistName, final String albumName,
- final ImageType imageType) {
- switch (imageType) {
- case ARTIST:
- if (!TextUtils.isEmpty(artistName)) {
- if (PreferenceUtils.getInstance(mContext).downloadMissingArtistImages()) {
- final Artist artist = Artist.getInfo(mContext, artistName);
- if (artist != null) {
- return getBestImage(artist);
- }
- }
- }
- break;
- case ALBUM:
- if (!TextUtils.isEmpty(artistName) && !TextUtils.isEmpty(albumName)) {
- if (PreferenceUtils.getInstance(mContext).downloadMissingArtwork()) {
- final Artist correction = Artist.getCorrection(mContext, artistName);
- if (correction != null) {
- final Album album = Album.getInfo(mContext, correction.getName(),
- albumName);
- if (album != null) {
- return getBestImage(album);
- }
- }
- }
- }
- break;
- default:
- break;
- }
- return null;
+ public void loadPlaylistCoverArtImage(final long playlistId, final ImageView imageView) {
+ loadPlaylistImage(playlistId, PlaylistWorkerType.CoverArt, imageView);
}
/**
* Used to fetch album images.
*/
public void loadAlbumImage(final String artistName, final String albumName, final long albumId,
- final ImageView imageView) {
+ final ImageView imageView) {
loadImage(generateAlbumCacheKey(albumName, artistName), artistName, albumName, albumId, imageView,
ImageType.ALBUM);
}
@@ -222,7 +148,7 @@ public class ImageFetcher extends ImageWorker {
}
/**
- * @param keyAlbum The key (album name) used to find the album art to return
+ * @param keyAlbum The key (album name) used to find the album art to return
* @param keyArtist The key (artist name) used to find the album art to return
*/
public Bitmap getCachedArtwork(final String keyAlbum, final String keyArtist) {
@@ -231,12 +157,12 @@ public class ImageFetcher extends ImageWorker {
}
/**
- * @param keyAlbum The key (album name) used to find the album art to return
+ * @param keyAlbum The key (album name) used to find the album art to return
* @param keyArtist The key (artist name) used to find the album art to return
- * @param keyId The key (album id) used to find the album art to return
+ * @param keyId The key (album id) used to find the album art to return
*/
public Bitmap getCachedArtwork(final String keyAlbum, final String keyArtist,
- final long keyId) {
+ final long keyId) {
if (mImageCache != null) {
return mImageCache.getCachedArtwork(mContext,
generateAlbumCacheKey(keyAlbum, keyArtist),
@@ -249,10 +175,10 @@ public class ImageFetcher extends ImageWorker {
* Finds cached or downloads album art. Used in {@link MusicPlaybackService}
* to set the current album art in the notification and lock screen
*
- * @param albumName The name of the current album
- * @param albumId The ID of the current album
+ * @param albumName The name of the current album
+ * @param albumId The ID of the current album
* @param artistName The album artist in case we should have to download
- * missing artwork
+ * missing artwork
* @return The album art as an {@link Bitmap}
*/
public Bitmap getArtwork(final String albumName, final long albumId, final String artistName) {
@@ -274,139 +200,11 @@ public class ImageFetcher extends ImageWorker {
}
/**
- * Download a {@link Bitmap} from a URL, write it to a disk and return the
- * File pointer. This implementation uses a simple disk cache.
- *
- * @param context The context to use
- * @param urlString The URL to fetch
- * @return A {@link File} pointing to the fetched bitmap
- */
- public static final File downloadBitmapToFile(final Context context, final String urlString,
- final String uniqueName) {
- final File cacheDir = ImageCache.getDiskCacheDir(context, uniqueName);
-
- if (!cacheDir.exists()) {
- cacheDir.mkdir();
- }
-
- HttpURLConnection urlConnection = null;
- BufferedOutputStream out = null;
-
- try {
- final File tempFile = File.createTempFile("bitmap", null, cacheDir); //$NON-NLS-1$
-
- final URL url = new URL(urlString);
- urlConnection = (HttpURLConnection)url.openConnection();
- if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
- return null;
- }
- final InputStream in = new BufferedInputStream(urlConnection.getInputStream(),
- IO_BUFFER_SIZE_BYTES);
- out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);
-
- int oneByte;
- while ((oneByte = in.read()) != -1) {
- out.write(oneByte);
- }
- return tempFile;
- } catch (final IOException ignored) {
- } finally {
- if (urlConnection != null) {
- urlConnection.disconnect();
- }
- if (out != null) {
- try {
- out.close();
- } catch (final IOException ignored) {
- }
- }
- }
- return null;
- }
-
- /**
- * Decode and sample down a {@link Bitmap} from a file to the requested
- * width and height.
- *
- * @param filename The full path of the file to decode
- * @param reqWidth The requested width of the resulting bitmap
- * @param reqHeight The requested height of the resulting bitmap
- * @return A {@link Bitmap} sampled down from the original with the same
- * aspect ratio and dimensions that are equal to or greater than the
- * requested width and height
- */
- public static Bitmap decodeSampledBitmapFromFile(final String filename) {
-
- // First decode with inJustDecodeBounds=true to check dimensions
- final BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(filename, options);
-
- // Calculate inSampleSize
- options.inSampleSize = calculateInSampleSize(options, DEFAULT_MAX_IMAGE_WIDTH,
- DEFAULT_MAX_IMAGE_HEIGHT);
-
- // Decode bitmap with inSampleSize set
- options.inJustDecodeBounds = false;
- return BitmapFactory.decodeFile(filename, options);
- }
-
- /**
- * Calculate an inSampleSize for use in a
- * {@link android.graphics.BitmapFactory.Options} object when decoding
- * bitmaps using the decode* methods from {@link BitmapFactory}. This
- * implementation calculates the closest inSampleSize that will result in
- * the final decoded bitmap having a width and height equal to or larger
- * than the requested width and height. This implementation does not ensure
- * a power of 2 is returned for inSampleSize which can be faster when
- * decoding but results in a larger bitmap which isn't as useful for caching
- * purposes.
- *
- * @param options An options object with out* params already populated (run
- * through a decode* method with inJustDecodeBounds==true
- * @param reqWidth The requested width of the resulting bitmap
- * @param reqHeight The requested height of the resulting bitmap
- * @return The value to be used for inSampleSize
- */
- public static final int calculateInSampleSize(final BitmapFactory.Options options,
- final int reqWidth, final int reqHeight) {
- /* Raw height and width of image */
- final int height = options.outHeight;
- final int width = options.outWidth;
- int inSampleSize = 1;
-
- if (height > reqHeight || width > reqWidth) {
- if (width > height) {
- inSampleSize = Math.round((float)height / (float)reqHeight);
- } else {
- inSampleSize = Math.round((float)width / (float)reqWidth);
- }
-
- // This offers some additional logic in case the image has a strange
- // aspect ratio. For example, a panorama may have a much larger
- // width than height. In these cases the total pixels might still
- // end up being too large to fit comfortably in memory, so we should
- // be more aggressive with sample down the image (=larger
- // inSampleSize).
-
- final float totalPixels = width * height;
-
- /* More than 2x the requested pixels we'll sample down further */
- final float totalReqPixelsCap = reqWidth * reqHeight * 2;
-
- while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
- inSampleSize++;
- }
- }
- return inSampleSize;
- }
-
- /**
* Generates key used by album art cache. It needs both album name and artist name
* to let to select correct image for the case when there are two albums with the
* same artist.
*
- * @param albumName The album name the cache key needs to be generated.
+ * @param albumName The album name the cache key needs to be generated.
* @param artistName The artist name the cache key needs to be generated.
* @return
*/
diff --git a/src/com/cyngn/eleven/cache/ImageWorker.java b/src/com/cyngn/eleven/cache/ImageWorker.java
index aa78a79..b31b2f1 100644
--- a/src/com/cyngn/eleven/cache/ImageWorker.java
+++ b/src/com/cyngn/eleven/cache/ImageWorker.java
@@ -19,18 +19,16 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
-import android.os.AsyncTask;
-import android.support.v7.graphics.Palette;
-import android.support.v7.graphics.PaletteItem;
-import android.support.v8.renderscript.Allocation;
-import android.support.v8.renderscript.Element;
import android.support.v8.renderscript.RenderScript;
-import android.support.v8.renderscript.ScriptIntrinsicBlur;
+import android.view.View;
import android.widget.ImageView;
import com.cyngn.eleven.R;
+import com.cyngn.eleven.provider.PlaylistArtworkStore;
import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.ImageUtils;
import com.cyngn.eleven.widgets.BlurScrimImage;
+import com.cyngn.eleven.cache.PlaylistWorkerTask.PlaylistWorkerType;
import java.lang.ref.WeakReference;
import java.util.concurrent.RejectedExecutionException;
@@ -46,7 +44,7 @@ public abstract class ImageWorker {
/**
* Render script
*/
- private static RenderScript sRenderScript = null;
+ public static RenderScript sRenderScript = null;
/**
* Default transition drawable fade time
@@ -183,6 +181,10 @@ public abstract class ImageWorker {
targetBitmap = mDefaultArtist;
break;
+ case PLAYLIST:
+ targetBitmap = mDefaultPlaylist;
+ break;
+
case ALBUM:
default:
targetBitmap = mDefault;
@@ -197,344 +199,38 @@ public abstract class ImageWorker {
return bitmapDrawable;
}
- /**
- * The actual {@link AsyncTask} that will process the image.
- */
- private class BitmapWorkerTask extends AsyncTask<String, Void, Object> {
-
- /**
- * The {@link ImageView} used to set the result
- */
- private final WeakReference<ImageView> mImageReference;
-
- /**
- * Type of URL to download
- */
- private final ImageType mImageType;
-
- /**
- * The key used to store cached entries
- */
- private String mKey;
-
- /**
- * Artist name param
- */
- private String mArtistName;
-
- /**
- * Album name parm
- */
- private String mAlbumName;
-
- /**
- * The album ID used to find the corresponding artwork
- */
- private long mAlbumId;
-
- /**
- * The URL of an image to download
- */
- private String mUrl;
-
- /**
- * Layer drawable used to cross fade the result from the worker
- */
- protected Drawable mFromDrawable;
-
- /**
- * Constructor of <code>BitmapWorkerTask</code>
- *
- * @param imageView The {@link ImageView} to use.
- * @param imageType The type of image URL to fetch for.
- */
- @SuppressWarnings("deprecation")
- public BitmapWorkerTask(final ImageView imageView, final ImageType imageType) {
- mImageReference = new WeakReference<ImageView>(imageView);
- mImageType = imageType;
-
- // A transparent image (layer 0) and the new result (layer 1)
- mFromDrawable = mTransparentDrawable;
- }
-
- protected Bitmap getBitmapInBackground(final String... params) {
- // Define the key
- mKey = params[0];
-
- // The result
- Bitmap bitmap = null;
-
- // First, check the disk cache for the image
- if (mKey != null && mImageCache != null && !isCancelled()
- && getAttachedImageView() != null) {
- bitmap = mImageCache.getCachedBitmap(mKey);
- }
-
- // Define the album id now
- mAlbumId = Long.valueOf(params[3]);
-
- // Second, if we're fetching artwork, check the device for the image
- if (bitmap == null && mImageType.equals(ImageType.ALBUM) && mAlbumId >= 0
- && mKey != null && !isCancelled() && getAttachedImageView() != null
- && mImageCache != null) {
- bitmap = mImageCache.getCachedArtwork(mContext, mKey, mAlbumId);
- }
-
- // Third, by now we need to download the image
- if (bitmap == null && ApolloUtils.isOnline(mContext) && !isCancelled()
- && getAttachedImageView() != null) {
- // Now define what the artist name, album name, and url are.
- mArtistName = params[1];
- mAlbumName = params[2];
- mUrl = processImageUrl(mArtistName, mAlbumName, mImageType);
- if (mUrl != null) {
- bitmap = processBitmap(mUrl);
- }
- }
-
- // Fourth, add the new image to the cache
- if (bitmap != null && mKey != null && mImageCache != null) {
- addBitmapToCache(mKey, bitmap);
- }
-
- return bitmap;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected Object doInBackground(final String... params) {
- final Bitmap bitmap = getBitmapInBackground(params);
- return createImageTransitionDrawable(mResources, mFromDrawable, bitmap,
- FADE_IN_TIME, false, false);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected void onPostExecute(Object result) {
- if (isCancelled()) {
- return;
- }
-
- TransitionDrawable transitionDrawable = (TransitionDrawable)result;
-
- final ImageView imageView = getAttachedImageView();
- if (transitionDrawable != null && imageView != null) {
- imageView.setImageDrawable(transitionDrawable);
- }
- }
-
- /**
- * @return The {@link ImageView} associated with this task as long as
- * the ImageView's task still points to this task as well.
- * Returns null otherwise.
- */
- protected ImageView getAttachedImageView() {
- final ImageView imageView = mImageReference.get();
- final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
- if (this == bitmapWorkerTask) {
- return imageView;
- }
- return null;
- }
- }
-
- /**
- * This will download the image (if needed) and create a blur and set the scrim as well on the
- * BlurScrimImage
- */
- private class BlurBitmapWorkerTask extends BitmapWorkerTask {
- // if the image is too small, the blur will look bad post scale up so we use the min size
- // to scale up before bluring
- private static final int MIN_BITMAP_SIZE = 500;
- // number of times to run the blur
- private static final int NUM_BLUR_RUNS = 8;
- // 25f is the max blur radius possible
- private static final float BLUR_RADIUS = 25f;
-
- // container for the result
- private class ResultContainer {
- public TransitionDrawable mImageViewBitmapDrawable;
- public int mPaletteColor;
- }
-
- /**
- * The {@link BlurScrimImage} used to set the result
- */
- private final WeakReference<BlurScrimImage> mBlurScrimImage;
+ public static Bitmap getBitmapInBackground(final Context context, final ImageCache imageCache,
+ final String key, final String albumName, final String artistName,
+ final long albumId, final ImageType imageType) {
+ // The result
+ Bitmap bitmap = null;
- /**
- * Constructor of <code>BitmapWorkerTask</code>
- *
- * @param blurScrimImage The {@link BlurScrimImage} to use.
- * @param imageType The type of image URL to fetch for.
- */
- @SuppressWarnings("deprecation")
- public BlurBitmapWorkerTask(final BlurScrimImage blurScrimImage, final ImageType imageType) {
- super(blurScrimImage.getImageView(), imageType);
- mBlurScrimImage = new WeakReference<BlurScrimImage>(blurScrimImage);
-
- // use the existing image as the drawable and if it doesn't exist fallback to transparent
- mFromDrawable = blurScrimImage.getImageView().getDrawable();
- if (mFromDrawable == null) {
- mFromDrawable = mTransparentDrawable;
- }
+ // First, check the disk cache for the image
+ if (key != null && imageCache != null) {
+ bitmap = imageCache.getCachedBitmap(key);
}
- /**
- * {@inheritDoc}
- */
- @Override
- protected Object doInBackground(final String... params) {
- Bitmap bitmap = getBitmapInBackground(params);
-
- ResultContainer result = new ResultContainer();
-
- Bitmap output = null;
-
- if (bitmap != null) {
- // now create the blur bitmap
- Bitmap input = bitmap;
-
- // if the image is too small, scale it up before running through the blur
- if (input.getWidth() < MIN_BITMAP_SIZE || input.getHeight() < MIN_BITMAP_SIZE) {
- float multiplier = Math.max(MIN_BITMAP_SIZE / (float)input.getWidth(),
- MIN_BITMAP_SIZE / (float)input.getHeight());
- input = input.createScaledBitmap(bitmap, (int)(input.getWidth() * multiplier),
- (int)(input.getHeight() * multiplier), true);
- // since we created a new bitmap, we can re-use the bitmap for our output
- output = input;
- } else {
- // if we aren't creating a new bitmap, create a new output bitmap
- output = Bitmap.createBitmap(input.getWidth(), input.getHeight(), input.getConfig());
- }
-
- // run the blur multiple times
- for (int i = 0; i < NUM_BLUR_RUNS; i++) {
- final Allocation inputAlloc = Allocation.createFromBitmap(sRenderScript, input);
- final Allocation outputAlloc = Allocation.createTyped(sRenderScript,
- inputAlloc.getType());
- final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(sRenderScript,
- Element.U8_4(sRenderScript));
-
- script.setRadius(BLUR_RADIUS);
- script.setInput(inputAlloc);
- script.forEach(outputAlloc);
- outputAlloc.copyTo(output);
-
- // if we run more than 1 blur, the new input should be the old output
- input = output;
- }
-
- // calculate the palette color
- result.mPaletteColor = getPaletteColorInBackground(output);
-
- // create the bitmap transition drawable
- result.mImageViewBitmapDrawable = createImageTransitionDrawable(mResources, mFromDrawable,
- output, FADE_IN_TIME_SLOW, true, true);
-
- return result;
- }
-
- return null;
+ // Second, if we're fetching artwork, check the device for the image
+ if (bitmap == null && imageType.equals(ImageType.ALBUM) && albumId >= 0
+ && key != null && imageCache != null) {
+ bitmap = imageCache.getCachedArtwork(context, key, albumId);
}
- /**
- * This will get the most vibrant palette color for a bitmap
- * @param input to process
- * @return the most vibrant color or transparent if none found
- */
- private int getPaletteColorInBackground(Bitmap input) {
- int color = Color.TRANSPARENT;
-
- if (input != null) {
- Palette palette = Palette.generate(input);
- PaletteItem paletteItem = palette.getVibrantColor();
-
- // keep walking through the palette items to find a color if we don't have any
- if (paletteItem == null) {
- paletteItem = palette.getVibrantColor();
- }
-
- if (paletteItem == null) {
- paletteItem = palette.getLightVibrantColor();
- }
-
- if (paletteItem == null) {
- paletteItem = palette.getLightMutedColor();
- }
-
- if (paletteItem == null) {
- paletteItem = palette.getLightMutedColor();
- }
-
- if (paletteItem == null) {
- paletteItem = palette.getDarkVibrantColor();
- }
-
- if (paletteItem == null) {
- paletteItem = palette.getMutedColor();
- }
-
- if (paletteItem == null) {
- paletteItem = palette.getDarkMutedColor();
- }
-
- if (paletteItem != null) {
- // grab the rgb values
- color = paletteItem.getRgb() | 0xFFFFFF;
-
- // make it 20% opacity
- color &= 0x33000000;
- }
+ // Third, by now we need to download the image
+ if (bitmap == null && ApolloUtils.isOnline(context)) {
+ // Now define what the artist name, album name, and url are.
+ String url = ImageUtils.processImageUrl(context, artistName, albumName, imageType);
+ if (url != null) {
+ bitmap = ImageUtils.processBitmap(context, url);
}
-
- return color;
}
- /**
- * {@inheritDoc}
- */
- @Override
- protected void onPostExecute(Object result) {
- if (isCancelled()) {
- return;
- }
-
- BlurScrimImage blurScrimImage = mBlurScrimImage.get();
- if (blurScrimImage != null) {
- if (result == null) {
- // if we have no image, then signal the transition to the default state
- blurScrimImage.transitionToDefaultState();
- } else {
- ResultContainer resultContainer = (ResultContainer)result;
-
- // create the palette transition
- TransitionDrawable paletteTransition = createPaletteTransition(blurScrimImage,
- resultContainer.mPaletteColor);
-
- // set the transition drawable
- blurScrimImage.setTransitionDrawable(false,
- resultContainer.mImageViewBitmapDrawable, paletteTransition);
- }
- }
+ // Fourth, add the new image to the cache
+ if (bitmap != null && key != null && imageCache != null) {
+ imageCache.addBitmapToCache(key, bitmap);
}
- /**
- * {@inheritDoc}
- */
- @Override
- protected final ImageView getAttachedImageView() {
- final BlurScrimImage blurImage = mBlurScrimImage.get();
- final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(blurImage);
- if (this == bitmapWorkerTask) {
- return blurImage.getImageView();
- }
- return null;
- }
+ return bitmap;
}
/**
@@ -601,25 +297,21 @@ public abstract class ImageWorker {
}
/**
- * Calls {@code cancel()} in the worker task
- *
- * @param imageView the {@link ImageView} to use
+ * Cancels and clears out any pending bitmap worker tasks on this image view
+ * @param image ImageView to check
*/
- public static final void cancelWork(final ImageView imageView) {
- final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
- if (bitmapWorkerTask != null) {
- bitmapWorkerTask.cancel(true);
- }
- }
+ public static final void cancelWork(final ImageView image) {
+ Object tag = image.getTag();
+ if (tag != null && tag instanceof AsyncTaskContainer) {
+ AsyncTaskContainer asyncTaskContainer = (AsyncTaskContainer)tag;
+ BitmapWorkerTask bitmapWorkerTask = asyncTaskContainer.getBitmapWorkerTask();
+ if (bitmapWorkerTask != null) {
+ bitmapWorkerTask.cancel(false);
+ }
- /**
- * Returns true if the current work has been canceled or if there was no
- * work in progress on this image view. Returns false if the work in
- * progress deals with the same data. The work is not stopped in that case.
- */
- public static final boolean executePotentialWork(final Object data, final ImageView imageView) {
- final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
- return executePotentialWork(data, bitmapWorkerTask);
+ // clear out the tag
+ image.setTag(null);
+ }
}
/**
@@ -627,8 +319,8 @@ public abstract class ImageWorker {
* work in progress on this image view. Returns false if the work in
* progress deals with the same data. The work is not stopped in that case.
*/
- public static final boolean executePotentialWork(final Object data, final BlurScrimImage image) {
- final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(image);
+ public static final boolean executePotentialWork(final Object data, final View view) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(view);
return executePotentialWork(data, bitmapWorkerTask);
}
@@ -655,34 +347,16 @@ public abstract class ImageWorker {
* Used to determine if the current image drawable has an instance of
* {@link BitmapWorkerTask}
*
- * @param imageView Any {@link ImageView}.
- * @return Retrieve the currently active work task (if any) associated with
- * this {@link ImageView}. null if there is no such task.
- */
- private static final BitmapWorkerTask getBitmapWorkerTask(final ImageView imageView) {
- if (imageView != null) {
- final Drawable drawable = imageView.getDrawable();
- if (drawable instanceof AsyncDrawable) {
- final AsyncDrawable asyncDrawable = (AsyncDrawable)drawable;
- return asyncDrawable.getBitmapWorkerTask();
- }
- }
- return null;
- }
-
- /**
- * Used to determine if the current image drawable has an instance of
- * {@link BitmapWorkerTask}
- *
- * @param image Any {@link BlurScrimImage}.
+ * @param view Any {@link View} that either is or contains an ImageView.
* @return Retrieve the currently active work task (if any) associated with
- * this {@link BlurScrimImage}. null if there is no such task.
- */
- private static final BitmapWorkerTask getBitmapWorkerTask(final BlurScrimImage image) {
- if (image != null) {
- final AsyncDrawable asyncDrawable = (AsyncDrawable)image.getTag();
- if (asyncDrawable != null) {
- return asyncDrawable.getBitmapWorkerTask();
+ * this {@link View}. null if there is no such task.
+ */
+ public static final BitmapWorkerTask getBitmapWorkerTask(final View view) {
+ if (view != null) {
+ final Object tag = view.getTag();
+ if (tag instanceof AsyncTaskContainer) {
+ final AsyncTaskContainer asyncTaskContainer = (AsyncTaskContainer)tag;
+ return asyncTaskContainer.getBitmapWorkerTask();
}
}
return null;
@@ -690,21 +364,19 @@ public abstract class ImageWorker {
/**
* A custom {@link BitmapDrawable} that will be attached to the
- * {@link ImageView} while the work is in progress. Contains a reference to
- * the actual worker task, so that it can be stopped if a new binding is
+ * {@link View} which either is or contains an {@link ImageView} while the work is in progress.
+ * Contains a reference to the actual worker task, so that it can be stopped if a new binding is
* required, and makes sure that only the last started worker process can
* bind its result, independently of the finish order.
*/
- private static final class AsyncDrawable extends ColorDrawable {
+ public static final class AsyncTaskContainer {
private final WeakReference<BitmapWorkerTask> mBitmapWorkerTaskReference;
/**
* Constructor of <code>AsyncDrawable</code>
*/
- public AsyncDrawable(final Resources res, final Bitmap bitmap,
- final BitmapWorkerTask mBitmapWorkerTask) {
- super(Color.TRANSPARENT);
+ public AsyncTaskContainer(final BitmapWorkerTask mBitmapWorkerTask) {
mBitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(mBitmapWorkerTask);
}
@@ -717,7 +389,7 @@ public abstract class ImageWorker {
}
/**
- * Called to fetch the artist or ablum art.
+ * Called to fetch the artist or album art.
*
* @param key The unique identifier for the image.
* @param artistName The artist name for the Last.fm API.
@@ -748,31 +420,79 @@ public abstract class ImageWorker {
if (executePotentialWork(key, imageView)
&& imageView != null && !mImageCache.isDiskCachePaused()) {
// cancel the old task if any
- final Drawable previousDrawable = imageView.getDrawable();
- if (previousDrawable != null && previousDrawable instanceof AsyncDrawable) {
- BitmapWorkerTask workerTask = ((AsyncDrawable)previousDrawable).getBitmapWorkerTask();
- if (workerTask != null) {
- workerTask.cancel(false);
- }
- }
+ cancelWork(imageView);
// Otherwise run the worker task
- final BitmapWorkerTask bitmapWorkerTask = new BitmapWorkerTask(imageView, imageType);
- final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mDefault,
- bitmapWorkerTask);
- imageView.setImageDrawable(asyncDrawable);
+ final SimpleBitmapWorkerTask bitmapWorkerTask = new SimpleBitmapWorkerTask(key,
+ imageView, imageType, mTransparentDrawable, mContext);
+ final AsyncTaskContainer asyncTaskContainer = new AsyncTaskContainer(bitmapWorkerTask);
+ imageView.setTag(asyncTaskContainer);
try {
- ApolloUtils.execute(false, bitmapWorkerTask, key,
+ ApolloUtils.execute(false, bitmapWorkerTask,
artistName, albumName, String.valueOf(albumId));
} catch (RejectedExecutionException e) {
- // Executor has exhausted queue space, show default artwork
- imageView.setImageBitmap(getDefaultArtwork());
+ // Executor has exhausted queue space
}
}
}
}
/**
+ * Called to fetch a playlist's top artist or cover art
+ * @param playlistId playlist identifier
+ * @param type of work to get (Artist or CoverArt)
+ * @param imageView to set the image to
+ */
+ public void loadPlaylistImage(final long playlistId, final PlaylistWorkerType type,
+ final ImageView imageView) {
+ if (mImageCache == null || imageView == null) {
+ return;
+ }
+
+ String key = null;
+ switch (type) {
+ case Artist:
+ key = PlaylistArtworkStore.getArtistCacheKey(playlistId);
+ break;
+ case CoverArt:
+ key = PlaylistArtworkStore.getCoverCacheKey(playlistId);
+ break;
+ }
+
+ // First, check the memory for the image
+ final Bitmap lruBitmap = mImageCache.getBitmapFromMemCache(key);
+ if (lruBitmap != null) {
+ // Bitmap found in memory cache
+ imageView.setImageBitmap(lruBitmap);
+ } else {
+ // if a background drawable hasn't been set, create one so that even if
+ // the disk cache is paused we see something
+ if (imageView.getBackground() == null) {
+ imageView.setBackgroundDrawable(getNewDefaultBitmapDrawable(ImageType.PLAYLIST));
+ }
+ }
+
+ // even though we may have found the image in the cache, we want to check if the playlist
+ // has been updated, or it's been too long since the last update and change the image
+ // accordingly
+ if (executePotentialWork(key, imageView) && !mImageCache.isDiskCachePaused()) {
+ // cancel the old task if any
+ cancelWork(imageView);
+
+ // Otherwise run the worker task
+ final PlaylistWorkerTask bitmapWorkerTask = new PlaylistWorkerTask(key, playlistId, type,
+ lruBitmap != null, imageView, mTransparentDrawable, mContext);
+ final AsyncTaskContainer asyncTaskContainer = new AsyncTaskContainer(bitmapWorkerTask);
+ imageView.setTag(asyncTaskContainer);
+ try {
+ ApolloUtils.execute(false, bitmapWorkerTask);
+ } catch (RejectedExecutionException e) {
+ // Executor has exhausted queue space
+ }
+ }
+ }
+
+ /**
* Called to fetch the blurred artist or album art.
*
* @param key The unique identifier for the image.
@@ -795,7 +515,7 @@ public abstract class ImageWorker {
if (executePotentialWork(blurKey, blurScrimImage)
&& blurScrimImage != null && !mImageCache.isDiskCachePaused()) {
// cancel the old task if any
- final AsyncDrawable previousDrawable = (AsyncDrawable)blurScrimImage.getTag();
+ final AsyncTaskContainer previousDrawable = (AsyncTaskContainer)blurScrimImage.getTag();
if (previousDrawable != null) {
BitmapWorkerTask workerTask = previousDrawable.getBitmapWorkerTask();
if (workerTask != null) {
@@ -804,14 +524,13 @@ public abstract class ImageWorker {
}
// Otherwise run the worker task
- final BlurBitmapWorkerTask blurWorkerTask = new BlurBitmapWorkerTask(blurScrimImage, imageType);
- final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mDefault,
- blurWorkerTask);
- blurScrimImage.setTag(asyncDrawable);
+ final BlurBitmapWorkerTask blurWorkerTask = new BlurBitmapWorkerTask(key, blurScrimImage,
+ imageType, mTransparentDrawable, mContext, sRenderScript);
+ final AsyncTaskContainer asyncTaskContainer = new AsyncTaskContainer(blurWorkerTask);
+ blurScrimImage.setTag(asyncTaskContainer);
try {
- ApolloUtils.execute(false, blurWorkerTask, key,
- artistName, albumName, String.valueOf(albumId));
+ ApolloUtils.execute(false, blurWorkerTask, artistName, albumName, String.valueOf(albumId));
} catch (RejectedExecutionException e) {
// Executor has exhausted queue space, show default artwork
blurScrimImage.transitionToDefaultState();
@@ -820,33 +539,9 @@ public abstract class ImageWorker {
}
/**
- * Subclasses should override this to define any processing or work that
- * must happen to produce the final {@link Bitmap}. This will be executed in
- * a background thread and be long running.
- *
- * @param key The key to identify which image to process, as provided by
- * {@link ImageWorker#loadImage(mKey, ImageView)}
- * @return The processed {@link Bitmap}.
- */
- protected abstract Bitmap processBitmap(String key);
-
- /**
- * Subclasses should override this to define any processing or work that
- * must happen to produce the URL needed to fetch the final {@link Bitmap}.
- *
- * @param artistName The artist name param used in the Last.fm API.
- * @param albumName The album name param used in the Last.fm API.
- * @param imageType The type of image URL to fetch for.
- * @return The image URL for an artist image or album image.
- */
- protected abstract String processImageUrl(String artistName, String albumName,
- ImageType imageType);
-
- /**
* Used to define what type of image URL to fetch for, artist or album.
*/
public enum ImageType {
- ARTIST, ALBUM;
+ ARTIST, ALBUM, PLAYLIST;
}
-
}
diff --git a/src/com/cyngn/eleven/cache/PlaylistWorkerTask.java b/src/com/cyngn/eleven/cache/PlaylistWorkerTask.java
new file mode 100644
index 0000000..880c450
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/PlaylistWorkerTask.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.cache;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.provider.MediaStore;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.cache.ImageWorker.ImageType;
+import com.cyngn.eleven.loaders.PlaylistSongLoader;
+import com.cyngn.eleven.loaders.SortedCursor;
+import com.cyngn.eleven.provider.PlaylistArtworkStore;
+import com.cyngn.eleven.provider.SongPlayCount;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+/**
+ * The playlistWorkerTask will load either the top artist image or the cover art (a combination of
+ * up to 4 of the top song's album images) into the designated ImageView. If not enough time has
+ * elapsed since the last update or if the # of songs in the playlist hasn't changed, no new images
+ * will be loaded.
+ */
+public class PlaylistWorkerTask extends BitmapWorkerTask<Void, Void, TransitionDrawable> {
+ // the work type
+ public enum PlaylistWorkerType {
+ Artist, CoverArt
+ }
+
+ // number of images to load in the cover art
+ private static final int MAX_NUM_BITMAPS_TO_LOAD = 4;
+
+ protected final long mPlaylistId;
+ protected final PlaylistArtworkStore mPlaylistStore;
+ protected final PlaylistWorkerType mWorkerType;
+
+ // if we've found it in the cache, don't do any more logic unless enough time has elapsed or
+ // if the playlist has changed
+ protected final boolean mFoundInCache;
+
+ /**
+ * Constructor of <code>PlaylistWorkerTask</code>
+ * @param key the key of the image to store to
+ * @param playlistId the playlist identifier
+ * @param type Artist or CoverArt?
+ * @param foundInCache does this exist in the memory cache already
+ * @param imageView The {@link ImageView} to use.
+ * @param fromDrawable what drawable to transition from
+ */
+ public PlaylistWorkerTask(final String key, final long playlistId, final PlaylistWorkerType type,
+ final boolean foundInCache, final ImageView imageView,
+ final Drawable fromDrawable, final Context context) {
+ super(key, imageView, ImageType.PLAYLIST, fromDrawable, context);
+
+ mPlaylistId = playlistId;
+ mWorkerType = type;
+ mPlaylistStore = PlaylistArtworkStore.getInstance(mContext);
+ mFoundInCache = foundInCache;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected TransitionDrawable doInBackground(final Void... params) {
+ if (isCancelled()) {
+ return null;
+ }
+
+ Bitmap bitmap = null;
+
+ // See if we need to update the image
+ boolean needsUpdate = false;
+ if (mWorkerType == PlaylistWorkerType.Artist
+ && mPlaylistStore.needsArtistArtUpdate(mPlaylistId)) {
+ needsUpdate = true;
+ } else if (mWorkerType == PlaylistWorkerType.CoverArt
+ && mPlaylistStore.needsCoverArtUpdate(mPlaylistId)) {
+ needsUpdate = true;
+ }
+
+ // if we don't need to update and we've already found it in the cache, then return
+ if (!needsUpdate && mFoundInCache) {
+ return null;
+ }
+
+ // if we didn't find it in memory cache, try the disk cache
+ if (!mFoundInCache) {
+ bitmap = mImageCache.getCachedBitmap(mKey);
+ }
+
+ // if we found a bitmap and we don't need an update, return it
+ if (bitmap != null && !needsUpdate) {
+ return createImageTransitionDrawable(bitmap);
+ }
+
+ // otherwise re-run the logic to get the bitmap
+ Cursor sortedCursor = null;
+
+ try {
+ // get the top songs for our playlist
+ sortedCursor = getTopSongsForPlaylist();
+
+ if (sortedCursor == null || sortedCursor.getCount() == 0 || isCancelled()) {
+ return null;
+ }
+
+ // run the appropriate logic
+ if (mWorkerType == PlaylistWorkerType.Artist) {
+ bitmap = loadTopArtist(sortedCursor);
+ } else {
+ bitmap = loadTopSongs(sortedCursor);
+ }
+ } finally {
+ if (sortedCursor != null) {
+ sortedCursor.close();
+ }
+ }
+
+ // if we have a bitmap create a transition drawable
+ if (bitmap != null) {
+ return createImageTransitionDrawable(bitmap);
+ }
+
+ return null;
+ }
+
+ /**
+ * This gets the sorted cursor of the songs from a playlist based on play count
+ * @return Cursor containing the sorted list
+ */
+ protected Cursor getTopSongsForPlaylist() {
+ Cursor playlistCursor = null;
+ SortedCursor sortedCursor = null;
+
+ try {
+ // gets the songs in the playlist
+ playlistCursor = PlaylistSongLoader.makePlaylistSongCursor(mContext, mPlaylistId);
+ if (playlistCursor == null || !playlistCursor.moveToFirst()) {
+ return null;
+ }
+
+ // get all the ids in the list
+ long[] songIds = new long[playlistCursor.getCount()];
+ do {
+ long id = playlistCursor.getLong(playlistCursor.getColumnIndex(
+ MediaStore.Audio.Playlists.Members.AUDIO_ID));
+
+ songIds[playlistCursor.getPosition()] = id;
+ } while (playlistCursor.moveToNext());
+
+ if (isCancelled()) {
+ return null;
+ }
+
+ // find the sorted order for the playlist based on the top songs database
+ long[] order = SongPlayCount.getInstance(mContext).getTopPlayedResultsForList(songIds);
+
+ // create a new cursor that takes the playlist cursor and the sorted order
+ sortedCursor = new SortedCursor(playlistCursor, order,
+ MediaStore.Audio.Playlists.Members.AUDIO_ID);
+
+ // since this cursor is now wrapped by SortedTracksCursor, remove the reference here
+ // so we don't accidentally close it in the finally loop
+ playlistCursor = null;
+ } finally {
+ // if we quit early from isCancelled(), close our cursor
+ if (playlistCursor != null) {
+ playlistCursor.close();
+ playlistCursor = null;
+ }
+ }
+
+ return sortedCursor;
+ }
+
+ /**
+ * Gets the most played song's artist image
+ * @param sortedCursor the sorted playlist song cursor
+ * @return Bitmap of the artist
+ */
+ protected Bitmap loadTopArtist(Cursor sortedCursor) {
+ if (sortedCursor == null || !sortedCursor.moveToFirst()) {
+ return null;
+ }
+
+ Bitmap bitmap = null;
+ int artistIndex = sortedCursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST);
+ String artistName = null;
+
+ do {
+ if (isCancelled()) {
+ return null;
+ }
+
+ artistName = sortedCursor.getString(artistIndex);
+ // try to load the bitmap
+ bitmap = ImageWorker.getBitmapInBackground(mContext, mImageCache, artistName,
+ null, artistName, -1, ImageType.ARTIST);
+ } while (sortedCursor.moveToNext() && bitmap == null);
+
+ sortedCursor.close();
+
+ if (bitmap != null) {
+ // add the image to the cache
+ mImageCache.addBitmapToCache(mKey, bitmap, true);
+
+ // store this artist name into the db
+ mPlaylistStore.updateArtistArt(mPlaylistId);
+ }
+
+ return bitmap;
+ }
+
+ /**
+ * Gets the Cover Art of the playlist, which is a combination of the top song's album image
+ * @param sortedCursor the sorted playlist song cursor
+ * @return Bitmap of the artist
+ */
+ protected Bitmap loadTopSongs(Cursor sortedCursor) {
+ if (sortedCursor == null || !sortedCursor.moveToFirst()) {
+ return null;
+ }
+
+ ArrayList<Bitmap> loadedBitmaps = new ArrayList<Bitmap>(MAX_NUM_BITMAPS_TO_LOAD);
+
+ final int artistIdx = sortedCursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST);
+ final int albumIdIdx = sortedCursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM_ID);
+ final int albumIdx = sortedCursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM);
+
+ Bitmap bitmap = null;
+ String artistName = null;
+ String albumName = null;
+ long albumId = -1;
+
+ // create a hashset of the keys so we don't load images from the same album multiple times
+ HashSet<String> keys = new HashSet<String>(sortedCursor.getCount());
+
+ do {
+ if (isCancelled()) {
+ return null;
+ }
+
+ artistName = sortedCursor.getString(artistIdx);
+ albumName = sortedCursor.getString(albumIdx);
+ albumId = sortedCursor.getLong(albumIdIdx);
+
+ String key = ImageFetcher.generateAlbumCacheKey(albumName, artistName);
+
+ // if we successfully added the key (ie the key didn't previously exist)
+ if (keys.add(key)) {
+ // try to load the bitmap
+ bitmap = ImageWorker.getBitmapInBackground(mContext, mImageCache,
+ key, albumName, artistName, albumId, ImageType.ALBUM);
+
+ // if we got the bitmap, add it to the list
+ if (bitmap != null) {
+ loadedBitmaps.add(bitmap);
+ bitmap = null;
+ }
+ }
+ } while (sortedCursor.moveToNext() && loadedBitmaps.size() < MAX_NUM_BITMAPS_TO_LOAD);
+
+ sortedCursor.close();
+
+ // if we found at least 1 bitmap
+ if (loadedBitmaps.size() > 0) {
+ // get the first bitmap
+ bitmap = loadedBitmaps.get(0);
+
+ // if we have many bitmaps
+ if (loadedBitmaps.size() == MAX_NUM_BITMAPS_TO_LOAD) {
+ // create a combined bitmap of the 4 images
+ final int width = bitmap.getWidth();
+ final int height = bitmap.getHeight();
+ Bitmap combinedBitmap = Bitmap.createBitmap(width, height,
+ bitmap.getConfig());
+ Canvas combinedCanvas = new Canvas(combinedBitmap);
+
+ // top left
+ combinedCanvas.drawBitmap(loadedBitmaps.get(0), null,
+ new Rect(0, 0, width / 2, height / 2), null);
+
+ // top right
+ combinedCanvas.drawBitmap(loadedBitmaps.get(1), null,
+ new Rect(width / 2, 0, width, height / 2), null);
+
+ // bottom left
+ combinedCanvas.drawBitmap(loadedBitmaps.get(2), null,
+ new Rect(0, height / 2, width / 2, height), null);
+
+ // bottom right
+ combinedCanvas.drawBitmap(loadedBitmaps.get(3), null,
+ new Rect(width / 2, height / 2, width, height), null);
+
+ combinedCanvas = null;
+ bitmap = combinedBitmap;
+ }
+
+ if (bitmap != null) {
+ // add the image to the cache
+ mImageCache.addBitmapToCache(mKey, bitmap, true);
+
+ // store this artist name into the db
+ mPlaylistStore.updateCoverArt(mPlaylistId);
+ }
+
+ return bitmap;
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPostExecute(TransitionDrawable transitionDrawable) {
+ final ImageView imageView = getAttachedImageView();
+ if (transitionDrawable != null && imageView != null) {
+ imageView.setImageDrawable(transitionDrawable);
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/cache/SimpleBitmapWorkerTask.java b/src/com/cyngn/eleven/cache/SimpleBitmapWorkerTask.java
new file mode 100644
index 0000000..0e1c3bd
--- /dev/null
+++ b/src/com/cyngn/eleven/cache/SimpleBitmapWorkerTask.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.cache;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.widget.ImageView;
+import com.cyngn.eleven.cache.ImageWorker.ImageType;
+
+/**
+ * The actual {@link android.os.AsyncTask} that will process the image.
+ */
+public class SimpleBitmapWorkerTask extends BitmapWorkerTask<String, Void, TransitionDrawable> {
+
+ /**
+ * Constructor of <code>BitmapWorkerTask</code>
+ *
+ * @param key the key of the image to store to
+ * @param imageView The {@link ImageView} to use.
+ * @param imageType The type of image URL to fetch for.
+ * @param fromDrawable what drawable to transition from
+ */
+ public SimpleBitmapWorkerTask(final String key, final ImageView imageView, final ImageType imageType,
+ final Drawable fromDrawable, final Context context) {
+ super(key, imageView, imageType, fromDrawable, context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected TransitionDrawable doInBackground(final String... params) {
+ if (isCancelled()) {
+ return null;
+ }
+
+ final Bitmap bitmap = getBitmapInBackground(params);
+ return createImageTransitionDrawable(bitmap);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPostExecute(TransitionDrawable transitionDrawable) {
+ final ImageView imageView = getAttachedImageView();
+ if (transitionDrawable != null && imageView != null) {
+ imageView.setImageDrawable(transitionDrawable);
+ }
+ }
+}
diff --git a/src/com/cyngn/eleven/loaders/TopTracksCursor.java b/src/com/cyngn/eleven/loaders/SortedCursor.java
index 70e3054..ffd43b4 100644
--- a/src/com/cyngn/eleven/loaders/TopTracksCursor.java
+++ b/src/com/cyngn/eleven/loaders/SortedCursor.java
@@ -5,7 +5,6 @@ package com.cyngn.eleven.loaders;
import android.database.AbstractCursor;
import android.database.Cursor;
-import android.provider.BaseColumns;
import java.util.ArrayList;
import java.util.HashMap;
@@ -15,7 +14,7 @@ import java.util.HashMap;
* contents of the cursor. It wraps the Cursor and simulates the internal cursor being sorted
* by moving the point to the appropriate spot
*/
-public class TopTracksCursor extends AbstractCursor {
+public class SortedCursor extends AbstractCursor {
// cursor to wrap
private final Cursor mCursor;
// the map of external indices to internal indices
@@ -24,14 +23,15 @@ public class TopTracksCursor extends AbstractCursor {
/**
* @param cursor to wrap
* @param order the list of ids in sorted order to display
+ * @param columnName the column name of the id to look up in the internal cursor
*/
- public TopTracksCursor(final Cursor cursor, final long[] order) {
+ public SortedCursor(final Cursor cursor, final long[] order, final String columnName) {
if (cursor == null) {
throw new IllegalArgumentException("Non-null cursor is needed");
}
mCursor = cursor;
- buildCursorPositionMapping(order);
+ buildCursorPositionMapping(order, columnName);
}
/**
@@ -39,11 +39,11 @@ public class TopTracksCursor extends AbstractCursor {
* on the order passed in
* @param order the target order of the internal cursor
*/
- private void buildCursorPositionMapping(final long[] order) {
+ private void buildCursorPositionMapping(final long[] order, final String columnName) {
mOrderedPositions = new ArrayList<Integer>(mCursor.getCount());
HashMap<Long, Integer> mapCursorPositions = new HashMap<Long, Integer>(mCursor.getCount());
- final int idPosition = mCursor.getColumnIndex(BaseColumns._ID);
+ final int idPosition = mCursor.getColumnIndex(columnName);
if (mCursor.moveToFirst()) {
// first figure out where each of the ids are in the cursor
@@ -62,6 +62,13 @@ public class TopTracksCursor extends AbstractCursor {
}
@Override
+ public void close() {
+ mCursor.close();
+
+ super.close();
+ }
+
+ @Override
public int getCount() {
return mOrderedPositions.size();
}
diff --git a/src/com/cyngn/eleven/loaders/TopTracksLoader.java b/src/com/cyngn/eleven/loaders/TopTracksLoader.java
index 4e1940e..175eec0 100644
--- a/src/com/cyngn/eleven/loaders/TopTracksLoader.java
+++ b/src/com/cyngn/eleven/loaders/TopTracksLoader.java
@@ -54,11 +54,11 @@ public class TopTracksLoader extends SongLoader {
selection.append(")");
- // get a list of songs with the data given the selection statemtn
+ // get a list of songs with the data given the selection statment
Cursor songCursor = makeSongCursor(context, selection.toString());
if (songCursor != null) {
// now return the wrapped TopTracksCursor to handle sorting given order
- return new TopTracksCursor(songCursor, order);
+ return new SortedCursor(songCursor, order, BaseColumns._ID);
}
}
diff --git a/src/com/cyngn/eleven/provider/PlaylistArtworkStore.java b/src/com/cyngn/eleven/provider/PlaylistArtworkStore.java
new file mode 100644
index 0000000..2751a1e
--- /dev/null
+++ b/src/com/cyngn/eleven/provider/PlaylistArtworkStore.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.cyngn.eleven.utils.MusicUtils;
+
+/**
+ * This db stores the details to generate the playlist artwork including when it was
+ * last updated and the # of songs in the playlist when it last updated
+ */
+public class PlaylistArtworkStore extends SQLiteOpenHelper {
+ /* Version constant to increment when the database should be rebuilt */
+ private static final int VERSION = 1;
+
+ private static final long ONE_DAY_IN_MS = 1000 * 60 * 60 * 24;
+
+ /* Name of database file */
+ public static final String DATABASENAME = "playlistdetail.db";
+
+ private static PlaylistArtworkStore sInstance = null;
+
+ /**
+ * @param context The {@link android.content.Context} to use
+ * @return A new instance of this class.
+ */
+ public static final synchronized PlaylistArtworkStore getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new PlaylistArtworkStore(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * @param playlistId playlist identifier
+ * @return the key used for the imagae cache for the cover art
+ */
+ public static final String getCoverCacheKey(final long playlistId) {
+ return "playlist_cover_" + playlistId;
+ }
+
+ /**
+ * @param playlistId playlist identifier
+ * @return the key used for the imagae cache for the top artist image
+ */
+ public static final String getArtistCacheKey(final long playlistId) {
+ return "playlist_artist_" + playlistId;
+ }
+
+ private final Context mContext;
+
+ /**
+ * Constructor of <code>RecentStore</code>
+ *
+ * @param context The {@link android.content.Context} to use
+ */
+ public PlaylistArtworkStore(final Context context) {
+ super(context, DATABASENAME, null, VERSION);
+
+ mContext = context;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ // create the table
+ StringBuilder builder = new StringBuilder();
+ builder.append("CREATE TABLE IF NOT EXISTS ");
+ builder.append(PlaylistArtworkStoreColumns.NAME);
+ builder.append("(");
+ builder.append(PlaylistArtworkStoreColumns.ID);
+ builder.append(" INT UNIQUE,");
+
+ builder.append(PlaylistArtworkStoreColumns.LAST_UPDATE_ARTIST);
+ builder.append(" LONG DEFAULT 0,");
+
+ builder.append(PlaylistArtworkStoreColumns.NUM_SONGS_LAST_UPDATE_ARTIST);
+ builder.append(" INT DEFAULT 0,");
+
+ builder.append(PlaylistArtworkStoreColumns.LAST_UPDATE_COVER);
+ builder.append(" LONG DEFAULT 0,");
+
+ builder.append(PlaylistArtworkStoreColumns.NUM_SONGS_LAST_UPDATE_COVER);
+ builder.append(" INT DEFAULT 0);");
+
+ db.execSQL(builder.toString());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ // If we ever have upgrade, this code should be changed to handle this more gracefully
+ db.execSQL("DROP TABLE IF EXISTS " + PlaylistArtworkStoreColumns.NAME);
+ onCreate(db);
+ }
+
+ /**
+ * @param playlistId playlist identifier
+ * @return true if the artist artwork should be updated based on time since last update and
+ * whether the # of songs for the playlist has changed
+ */
+ public boolean needsArtistArtUpdate(final long playlistId) {
+ return needsUpdate(playlistId, PlaylistArtworkStoreColumns.LAST_UPDATE_ARTIST,
+ PlaylistArtworkStoreColumns.NUM_SONGS_LAST_UPDATE_ARTIST);
+ }
+
+ /**
+ * @param playlistId playlist identifier
+ * @return true if the cover artwork should be updated based on time since last update and
+ * whether the # of songs for the playlist has changed
+ */
+ public boolean needsCoverArtUpdate(final long playlistId) {
+ return needsUpdate(playlistId, PlaylistArtworkStoreColumns.LAST_UPDATE_COVER,
+ PlaylistArtworkStoreColumns.NUM_SONGS_LAST_UPDATE_COVER);
+ }
+
+ /**
+ * Updates the time and the # of songs in the db for the artist section of the table
+ * @param playlistId playlist identifier
+ */
+ public void updateArtistArt(final long playlistId) {
+ updateOrInsertTime(playlistId, PlaylistArtworkStoreColumns.LAST_UPDATE_ARTIST,
+ PlaylistArtworkStoreColumns.NUM_SONGS_LAST_UPDATE_ARTIST);
+ }
+
+ /**
+ * Updates the time and the # of songs in the db for the cover art of the table
+ * @param playlistId playlist identifier
+ */
+ public void updateCoverArt(final long playlistId) {
+ updateOrInsertTime(playlistId, PlaylistArtworkStoreColumns.LAST_UPDATE_COVER,
+ PlaylistArtworkStoreColumns.NUM_SONGS_LAST_UPDATE_COVER);
+ }
+
+ /**
+ * Internal function to update the entry for the columns passed in
+ * @param playlistId playlist identifier
+ * @param columnName the column to update to the current time
+ * @param countColumnName the column to set the # of songs to based on the playlist
+ */
+ private void updateOrInsertTime(final long playlistId, final String columnName, final String countColumnName) {
+ SQLiteDatabase database = getWritableDatabase();
+
+ database.beginTransaction();
+
+ // gets the existing values for the entry if it exists
+ ContentValues values = getExistingContentValues(playlistId);
+ boolean existingEntry = values.size() > 0;
+ // update the values
+ values.put(PlaylistArtworkStoreColumns.ID, playlistId);
+ values.put(columnName, System.currentTimeMillis());
+ values.put(countColumnName, MusicUtils.getSongCountForPlaylist(mContext, playlistId));
+
+ // if it is an existing entry, update, otherwise insert
+ if (existingEntry) {
+ database.update(PlaylistArtworkStoreColumns.NAME, values,
+ PlaylistArtworkStoreColumns.ID + "=" + playlistId, null);
+ } else {
+ database.insert(PlaylistArtworkStoreColumns.NAME, null, values);
+ }
+
+ database.setTransactionSuccessful();
+ database.endTransaction();
+ }
+
+ /**
+ * Internal function to get the existing values for a playlist entry
+ * @param playlistId playlist identifier
+ * @return the content values
+ */
+ private ContentValues getExistingContentValues(final long playlistId) {
+ ContentValues values = new ContentValues(4);
+ Cursor c = getEntry(playlistId);
+ if (c != null && c.moveToFirst()) {
+ values.put(PlaylistArtworkStoreColumns.ID, c.getLong(0));
+ values.put(PlaylistArtworkStoreColumns.LAST_UPDATE_ARTIST, c.getLong(1));
+ values.put(PlaylistArtworkStoreColumns.NUM_SONGS_LAST_UPDATE_ARTIST, c.getInt(2));
+ values.put(PlaylistArtworkStoreColumns.LAST_UPDATE_COVER, c.getLong(3));
+ values.put(PlaylistArtworkStoreColumns.NUM_SONGS_LAST_UPDATE_COVER, c.getInt(4));
+ c.close();
+ c = null;
+ }
+
+ return values;
+ }
+
+ /**
+ * Internal function to return whether the columns show that this needs an update
+ * @param playlistId playlist identifier
+ * @param columnName the column to inspect
+ * @param countColumnName the column count to inspect
+ * @return
+ */
+ private boolean needsUpdate(final long playlistId, final String columnName, final String countColumnName) {
+ // get the entry
+ Cursor c = getEntry(playlistId);
+
+ if (c != null && c.moveToFirst()) {
+ final long lastUpdate = c.getLong(c.getColumnIndex(columnName));
+ final long msSinceEpoch = System.currentTimeMillis();
+ final int songCount = MusicUtils.getSongCountForPlaylist(mContext, playlistId);
+ final int lastUpdatedSongCount = c.getInt(c.getColumnIndex(countColumnName));
+
+ c.close();
+ c = null;
+
+ // if the elapsed time since our last update is less than a day and the
+ // number of songs in the playlist hasn't changed, then don't update
+ if (msSinceEpoch - lastUpdate < ONE_DAY_IN_MS &&
+ songCount == lastUpdatedSongCount) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Internal function to get the cursor entry for the playlist
+ * @param playlistId playlist identifier
+ * @return cursor
+ */
+ private Cursor getEntry(final long playlistId) {
+ SQLiteDatabase db = getReadableDatabase();
+ return db.query(PlaylistArtworkStoreColumns.NAME, null,
+ PlaylistArtworkStoreColumns.ID + "=" + playlistId, null, null, null, null);
+ }
+
+ public interface PlaylistArtworkStoreColumns {
+ /* Table name */
+ public static final String NAME = "playlist_details";
+
+ /* Playlist ID column */
+ public static final String ID = "playlistid";
+
+ /* When the top artist was last updated */
+ public static final String LAST_UPDATE_ARTIST = "last_updated_artist";
+
+ /* The number of songs when we last updated the artist */
+ public static final String NUM_SONGS_LAST_UPDATE_ARTIST = "num_songs_last_updated_artist";
+
+ /* When the cover art was last updated */
+ public static final String LAST_UPDATE_COVER = "last_updated_cover";
+
+ /* The number of songs when we last updated the cover */
+ public static final String NUM_SONGS_LAST_UPDATE_COVER = "num_songs_last_updated_cover";
+ }
+}
diff --git a/src/com/cyngn/eleven/provider/SongPlayCount.java b/src/com/cyngn/eleven/provider/SongPlayCount.java
index 0ec54dd..61012a6 100644
--- a/src/com/cyngn/eleven/provider/SongPlayCount.java
+++ b/src/com/cyngn/eleven/provider/SongPlayCount.java
@@ -12,6 +12,10 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
/**
* This database tracks the number of play counts for an individual song. This is used to drive
* the top played tracks as well as the playlist images
@@ -264,6 +268,85 @@ public class SongPlayCount extends SQLiteOpenHelper {
}
/**
+ * Given a list of ids, it sorts the results based on the most played results
+ * @param ids list
+ * @return sorted list - this may be smaller than the list passed in for performance reasons
+ */
+ public long[] getTopPlayedResultsForList(long[] ids) {
+ final int MAX_NUMBER_SONGS_TO_ANALYZE = 250;
+
+ if (ids == null || ids.length == 0) {
+ return null;
+ }
+
+ HashSet<Long> uniqueIds = new HashSet<Long>(ids.length);
+
+ // create the list of ids to select against
+ StringBuilder selection = new StringBuilder();
+ selection.append(SongPlayCountColumns.ID);
+ selection.append(" IN (");
+
+ // add the first element to handle the separator case for the first element
+ uniqueIds.add(ids[0]);
+ selection.append(ids[0]);
+
+ for (int i = 1; i < ids.length; i++) {
+ // if the new id doesn't exist
+ if (uniqueIds.add(ids[i])) {
+ // append a separator
+ selection.append(",");
+
+ // append the id
+ selection.append(ids[i]);
+
+ // for performance reasons, only look at a certain number of songs
+ // in case their playlist is ridiculously large
+ if (uniqueIds.size() >= MAX_NUMBER_SONGS_TO_ANALYZE) {
+ break;
+ }
+ }
+ }
+
+ // close out the selection
+ selection.append(")");
+
+ long[] sortedList = new long[uniqueIds.size()];
+
+ // now query for the songs
+ final SQLiteDatabase database = getReadableDatabase();
+ Cursor topSongsCursor = null;
+ int idx = 0;
+
+ try {
+ topSongsCursor = database.query(SongPlayCountColumns.NAME,
+ new String[]{ SongPlayCountColumns.ID }, selection.toString(), null, null,
+ null, SongPlayCountColumns.PLAYCOUNTSCORE + " DESC");
+
+ if (topSongsCursor != null && topSongsCursor.moveToFirst()) {
+ do {
+ // for each id found, add it to the list and remove it from the unique ids
+ long id = topSongsCursor.getLong(0);
+ sortedList[idx++] = id;
+ uniqueIds.remove(id);
+ } while (topSongsCursor.moveToNext());
+ }
+ } finally {
+ if (topSongsCursor != null) {
+ topSongsCursor.close();
+ topSongsCursor = null;
+ }
+ }
+
+ // append the remaining items - these are songs that haven't been played recently
+ Iterator<Long> iter = uniqueIds.iterator();
+ while (iter.hasNext()) {
+ sortedList[idx++] = iter.next();
+ }
+
+ return sortedList;
+ }
+
+ /**
* This updates all the results for the getTopPlayedResults so that we can get an
* accurate list of the top played results
*/
diff --git a/src/com/cyngn/eleven/recycler/RecycleHolder.java b/src/com/cyngn/eleven/recycler/RecycleHolder.java
index 07fdabf..5d86239 100644
--- a/src/com/cyngn/eleven/recycler/RecycleHolder.java
+++ b/src/com/cyngn/eleven/recycler/RecycleHolder.java
@@ -14,6 +14,7 @@ package com.cyngn.eleven.recycler;
import android.view.View;
import android.widget.AbsListView.RecyclerListener;
+import com.cyngn.eleven.cache.ImageWorker;
import com.cyngn.eleven.ui.MusicHolder;
/**
@@ -42,6 +43,7 @@ public class RecycleHolder implements RecyclerListener {
// Release mImage's reference
if (holder.mImage.get() != null) {
+ ImageWorker.cancelWork(holder.mImage.get());
holder.mImage.get().setImageDrawable(null);
holder.mImage.get().setImageBitmap(null);
}
diff --git a/src/com/cyngn/eleven/ui/activities/PlaylistDetailActivity.java b/src/com/cyngn/eleven/ui/activities/PlaylistDetailActivity.java
index 4bab42b..13f3b29 100644
--- a/src/com/cyngn/eleven/ui/activities/PlaylistDetailActivity.java
+++ b/src/com/cyngn/eleven/ui/activities/PlaylistDetailActivity.java
@@ -22,6 +22,7 @@ import android.widget.TextView;
import com.cyngn.eleven.Config;
import com.cyngn.eleven.R;
import com.cyngn.eleven.adapters.ProfileSongAdapter;
+import com.cyngn.eleven.cache.ImageFetcher;
import com.cyngn.eleven.dragdrop.DragSortListView;
import com.cyngn.eleven.dragdrop.DragSortListView.DragScrollProfile;
import com.cyngn.eleven.dragdrop.DragSortListView.DropListener;
@@ -116,8 +117,7 @@ public class PlaylistDetailActivity extends DetailActivity implements
mNumberOfSongs = (TextView)findViewById(R.id.number_of_songs_text);
mDurationOfPlaylist = (TextView)findViewById(R.id.duration_text);
- // TODO: Get the top artist image - do this in the next patch
- // ImageFetcher.getInstance(this).loadCurrentArtwork(mPlaylistImageView);
+ ImageFetcher.getInstance(this).loadPlaylistArtistImage(mPlaylistId, mPlaylistImageView);
}
private void setupSongList(ViewGroup root) {
diff --git a/src/com/cyngn/eleven/ui/activities/ProfileActivity.java b/src/com/cyngn/eleven/ui/activities/ProfileActivity.java
index 6b7951d..0fb8d4e 100644
--- a/src/com/cyngn/eleven/ui/activities/ProfileActivity.java
+++ b/src/com/cyngn/eleven/ui/activities/ProfileActivity.java
@@ -40,6 +40,7 @@ import com.cyngn.eleven.ui.fragments.profile.ArtistSongFragment;
import com.cyngn.eleven.ui.fragments.profile.GenreSongFragment;
import com.cyngn.eleven.ui.fragments.profile.LastAddedFragment;
import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.ImageUtils;
import com.cyngn.eleven.utils.MusicUtils;
import com.cyngn.eleven.utils.NavUtils;
import com.cyngn.eleven.utils.PreferenceUtils;
@@ -489,7 +490,7 @@ public class ProfileActivity extends SlidingPanelActivity implements OnPageChang
key = ImageFetcher.generateAlbumCacheKey(mProfileName, mArtistName);
}
- final Bitmap bitmap = ImageFetcher.decodeSampledBitmapFromFile(picturePath);
+ final Bitmap bitmap = ImageUtils.decodeSampledBitmapFromFile(picturePath);
mImageFetcher.addBitmapToCache(key, bitmap);
if (isAlbum()) {
mTabCarousel.getAlbumArt().setImageBitmap(bitmap);
diff --git a/src/com/cyngn/eleven/utils/ImageUtils.java b/src/com/cyngn/eleven/utils/ImageUtils.java
new file mode 100644
index 0000000..69675bf
--- /dev/null
+++ b/src/com/cyngn/eleven/utils/ImageUtils.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.widget.ImageView;
+
+import com.cyngn.eleven.cache.BitmapWorkerTask;
+import com.cyngn.eleven.cache.ImageCache;
+import com.cyngn.eleven.cache.ImageWorker;
+import com.cyngn.eleven.lastfm.Album;
+import com.cyngn.eleven.lastfm.Artist;
+import com.cyngn.eleven.lastfm.ImageSize;
+import com.cyngn.eleven.lastfm.MusicEntry;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class ImageUtils {
+ private static final String DEFAULT_HTTP_CACHE_DIR = "http"; //$NON-NLS-1$
+
+ public static final int IO_BUFFER_SIZE_BYTES = 1024;
+
+ private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
+
+ private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
+
+ /**
+ * Gets the image url based on the imageType
+ * @param artistName The artist name param used in the Last.fm API.
+ * @param albumName The album name param used in the Last.fm API.
+ * @param imageType The type of image URL to fetch for.
+ * @return The image URL for an artist image or album image.
+ */
+ public static String processImageUrl(final Context context, final String artistName,
+ final String albumName, final ImageWorker.ImageType imageType) {
+ switch (imageType) {
+ case ARTIST:
+ if (!TextUtils.isEmpty(artistName)) {
+ if (PreferenceUtils.getInstance(context).downloadMissingArtistImages()) {
+ final Artist artist = Artist.getInfo(context, artistName);
+ if (artist != null) {
+ return getBestImage(artist);
+ }
+ }
+ }
+ break;
+ case ALBUM:
+ if (!TextUtils.isEmpty(artistName) && !TextUtils.isEmpty(albumName)) {
+ if (PreferenceUtils.getInstance(context).downloadMissingArtwork()) {
+ final Artist correction = Artist.getCorrection(context, artistName);
+ if (correction != null) {
+ final Album album = Album.getInfo(context, correction.getName(),
+ albumName);
+ if (album != null) {
+ return getBestImage(album);
+ }
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ return null;
+ }
+
+ /**
+ * Downloads the bitmap from the url and returns it after some processing
+ *
+ * @param key The key to identify which image to process, as provided by
+ * {@link ImageWorker#loadImage(mKey, android.widget.ImageView)}
+ * @return The processed {@link Bitmap}.
+ */
+ public static Bitmap processBitmap(final Context context, final String url) {
+ if (url == null) {
+ return null;
+ }
+ final File file = downloadBitmapToFile(context, url, DEFAULT_HTTP_CACHE_DIR);
+ if (file != null) {
+ // Return a sampled down version
+ final Bitmap bitmap = decodeSampledBitmapFromFile(file.toString());
+ file.delete();
+ if (bitmap != null) {
+ return bitmap;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Decode and sample down a {@link Bitmap} from a file to the requested
+ * width and height.
+ *
+ * @param filename The full path of the file to decode
+ * @param reqWidth The requested width of the resulting bitmap
+ * @param reqHeight The requested height of the resulting bitmap
+ * @return A {@link Bitmap} sampled down from the original with the same
+ * aspect ratio and dimensions that are equal to or greater than the
+ * requested width and height
+ */
+ public static Bitmap decodeSampledBitmapFromFile(final String filename) {
+
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(filename, options);
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, DEFAULT_MAX_IMAGE_WIDTH,
+ DEFAULT_MAX_IMAGE_HEIGHT);
+
+ // Decode bitmap with inSampleSize set
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeFile(filename, options);
+ }
+
+ /**
+ * Calculate an inSampleSize for use in a
+ * {@link android.graphics.BitmapFactory.Options} object when decoding
+ * bitmaps using the decode* methods from {@link BitmapFactory}. This
+ * implementation calculates the closest inSampleSize that will result in
+ * the final decoded bitmap having a width and height equal to or larger
+ * than the requested width and height. This implementation does not ensure
+ * a power of 2 is returned for inSampleSize which can be faster when
+ * decoding but results in a larger bitmap which isn't as useful for caching
+ * purposes.
+ *
+ * @param options An options object with out* params already populated (run
+ * through a decode* method with inJustDecodeBounds==true
+ * @param reqWidth The requested width of the resulting bitmap
+ * @param reqHeight The requested height of the resulting bitmap
+ * @return The value to be used for inSampleSize
+ */
+ public static final int calculateInSampleSize(final BitmapFactory.Options options,
+ final int reqWidth, final int reqHeight) {
+ /* Raw height and width of image */
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ if (width > height) {
+ inSampleSize = Math.round((float)height / (float)reqHeight);
+ } else {
+ inSampleSize = Math.round((float)width / (float)reqWidth);
+ }
+
+ // This offers some additional logic in case the image has a strange
+ // aspect ratio. For example, a panorama may have a much larger
+ // width than height. In these cases the total pixels might still
+ // end up being too large to fit comfortably in memory, so we should
+ // be more aggressive with sample down the image (=larger
+ // inSampleSize).
+
+ final float totalPixels = width * height;
+
+ /* More than 2x the requested pixels we'll sample down further */
+ final float totalReqPixelsCap = reqWidth * reqHeight * 2;
+
+ while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
+ inSampleSize++;
+ }
+ }
+ return inSampleSize;
+ }
+
+ private static String getBestImage(MusicEntry e) {
+ final ImageSize[] QUALITY = {ImageSize.EXTRALARGE, ImageSize.LARGE, ImageSize.MEDIUM,
+ ImageSize.SMALL, ImageSize.UNKNOWN};
+ for(ImageSize q : QUALITY) {
+ String url = e.getImageURL(q);
+ if (url != null) {
+ return url;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Download a {@link Bitmap} from a URL, write it to a disk and return the
+ * File pointer. This implementation uses a simple disk cache.
+ *
+ * @param context The context to use
+ * @param urlString The URL to fetch
+ * @return A {@link File} pointing to the fetched bitmap
+ */
+ public static final File downloadBitmapToFile(final Context context, final String urlString,
+ final String uniqueName) {
+ final File cacheDir = ImageCache.getDiskCacheDir(context, uniqueName);
+
+ if (!cacheDir.exists()) {
+ cacheDir.mkdir();
+ }
+
+ HttpURLConnection urlConnection = null;
+ BufferedOutputStream out = null;
+
+ try {
+ final File tempFile = File.createTempFile("bitmap", null, cacheDir); //$NON-NLS-1$
+
+ final URL url = new URL(urlString);
+ urlConnection = (HttpURLConnection)url.openConnection();
+ if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ return null;
+ }
+ final InputStream in = new BufferedInputStream(urlConnection.getInputStream(),
+ IO_BUFFER_SIZE_BYTES);
+ out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);
+
+ int oneByte;
+ while ((oneByte = in.read()) != -1) {
+ out.write(oneByte);
+ }
+ return tempFile;
+ } catch (final IOException ignored) {
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ if (out != null) {
+ try {
+ out.close();
+ } catch (final IOException ignored) {
+ }
+ }
+ }
+ return null;
+ }
+}