summaryrefslogtreecommitdiffstats
path: root/src/com/cyanogenmod/eleven/cache
diff options
context:
space:
mode:
authorlinus_lee <llee@cyngn.com>2014-11-20 16:39:38 -0800
committerlinus_lee <llee@cyngn.com>2014-12-09 12:23:20 -0800
commit71810ebb2bf8fd792c92487fe87f9dbebefc8541 (patch)
tree42a4d11ba03a4c7af843edc0b45375b17c64053c /src/com/cyanogenmod/eleven/cache
parentf199f983c9a5e2f4434b85273d1da0d609c33228 (diff)
downloadandroid_packages_apps_Eleven-71810ebb2bf8fd792c92487fe87f9dbebefc8541.tar.gz
android_packages_apps_Eleven-71810ebb2bf8fd792c92487fe87f9dbebefc8541.tar.bz2
android_packages_apps_Eleven-71810ebb2bf8fd792c92487fe87f9dbebefc8541.zip
Update Eleven headers and namespace for open source
Change-Id: I82caf2ebf991998e67f546ff2ac7eaf2b30dc6be
Diffstat (limited to 'src/com/cyanogenmod/eleven/cache')
-rw-r--r--src/com/cyanogenmod/eleven/cache/BitmapWorkerTask.java149
-rw-r--r--src/com/cyanogenmod/eleven/cache/BlurBitmapWorkerTask.java180
-rw-r--r--src/com/cyanogenmod/eleven/cache/DiskLruCache.java969
-rw-r--r--src/com/cyanogenmod/eleven/cache/ICacheListener.java20
-rw-r--r--src/com/cyanogenmod/eleven/cache/ImageCache.java827
-rw-r--r--src/com/cyanogenmod/eleven/cache/ImageFetcher.java322
-rw-r--r--src/com/cyanogenmod/eleven/cache/ImageWorker.java586
-rw-r--r--src/com/cyanogenmod/eleven/cache/LruCache.java333
-rw-r--r--src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java383
-rw-r--r--src/com/cyanogenmod/eleven/cache/SimpleBitmapWorkerTask.java88
10 files changed, 3857 insertions, 0 deletions
diff --git a/src/com/cyanogenmod/eleven/cache/BitmapWorkerTask.java b/src/com/cyanogenmod/eleven/cache/BitmapWorkerTask.java
new file mode 100644
index 0000000..c7ab842
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/BitmapWorkerTask.java
@@ -0,0 +1,149 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package com.cyanogenmod.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.cyanogenmod.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;
+
+ protected boolean mScaleImgToView;
+
+ /**
+ * 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) {
+ this(key, imageView, imageType, fromDrawable, context, false);
+ }
+
+ /**
+ * 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
+ * @param scaleImgToView flag to scale the bitmap to the image view bounds
+ */
+ public BitmapWorkerTask(final String key, final ImageView imageView, final ImageType imageType,
+ final Drawable fromDrawable, final Context context, final boolean scaleImgToView) {
+ 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;
+
+ mScaleImgToView = scaleImgToView;
+ }
+
+ /**
+ * @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/cyanogenmod/eleven/cache/BlurBitmapWorkerTask.java b/src/com/cyanogenmod/eleven/cache/BlurBitmapWorkerTask.java
new file mode 100644
index 0000000..2fe88ea
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/BlurBitmapWorkerTask.java
@@ -0,0 +1,180 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package com.cyanogenmod.eleven.cache;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+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.cyanogenmod.eleven.cache.ImageWorker.ImageType;
+import com.cyanogenmod.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.cyanogenmod.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;
+ }
+
+ // Set the scrim color to be 50% gray
+ result.mPaletteColor = 0x7f000000;
+
+ // create the bitmap transition drawable
+ result.mImageViewBitmapDrawable = createImageTransitionDrawable(output,
+ ImageWorker.FADE_IN_TIME_SLOW, true, true);
+
+ return result;
+ }
+
+ return null;
+ }
+
+ /**
+ * {@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(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/cyanogenmod/eleven/cache/DiskLruCache.java b/src/com/cyanogenmod/eleven/cache/DiskLruCache.java
new file mode 100644
index 0000000..4d29ded
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/DiskLruCache.java
@@ -0,0 +1,969 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyanogenmod.eleven.cache;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ ****************************************************************************** Taken from the JB source code, can be found in:
+ * libcore/luni/src/main/java/libcore/io/DiskLruCache.java or direct link:
+ * https:
+ * //android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/
+ * main/java/libcore/io/DiskLruCache.java A cache that uses a bounded amount of
+ * space on a filesystem. Each cache entry has a string key and a fixed number
+ * of values. Values are byte sequences, accessible as streams or files. Each
+ * value must be between {@code 0} and {@code Integer.MAX_VALUE} bytes in
+ * length.
+ * <p>
+ * The cache stores its data in a directory on the filesystem. This directory
+ * must be exclusive to the cache; the cache may delete or overwrite files from
+ * its directory. It is an error for multiple processes to use the same cache
+ * directory at the same time.
+ * <p>
+ * This cache limits the number of bytes that it will store on the filesystem.
+ * When the number of stored bytes exceeds the limit, the cache will remove
+ * entries in the background until the limit is satisfied. The limit is not
+ * strict: the cache may temporarily exceed it while waiting for files to be
+ * deleted. The limit does not include filesystem overhead or the cache journal
+ * so space-sensitive applications should set a conservative limit.
+ * <p>
+ * Clients call {@link #edit} to create or update the values of an entry. An
+ * entry may have only one editor at one time; if a value is not available to be
+ * edited then {@link #edit} will return null.
+ * <ul>
+ * <li>When an entry is being <strong>created</strong> it is necessary to supply
+ * a full set of values; the empty value should be used as a placeholder if
+ * necessary.
+ * <li>When an entry is being <strong>edited</strong>, it is not necessary to
+ * supply data for every value; values default to their previous value.
+ * </ul>
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
+ * or {@link Editor#abort}. Committing is atomic: a read observes the full set
+ * of values as they were before or after the commit, but never a mix of values.
+ * <p>
+ * Clients call {@link #get} to read a snapshot of an entry. The read will
+ * observe the value at the time that {@link #get} was called. Updates and
+ * removals after the call do not impact ongoing reads.
+ * <p>
+ * This class is tolerant of some I/O errors. If files are missing from the
+ * filesystem, the corresponding entries will be dropped from the cache. If an
+ * error occurs while writing a cache value, the edit will fail silently.
+ * Callers should handle other problems by catching {@code IOException} and
+ * responding appropriately.
+ */
+public final class DiskLruCache implements Closeable {
+ static final String JOURNAL_FILE = "journal";
+
+ static final String JOURNAL_FILE_TMP = "journal.tmp";
+
+ static final String MAGIC = "libcore.io.DiskLruCache";
+
+ static final String VERSION_1 = "1";
+
+ static final long ANY_SEQUENCE_NUMBER = -1;
+
+ private static final String CLEAN = "CLEAN";
+
+ private static final String DIRTY = "DIRTY";
+
+ private static final String REMOVE = "REMOVE";
+
+ private static final String READ = "READ";
+
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ private static final int IO_BUFFER_SIZE = 8 * 1024;
+
+ /*
+ * This cache uses a journal file named "journal". A typical journal file
+ * looks like this: libcore.io.DiskLruCache 1 100 2 CLEAN
+ * 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 DIRTY
+ * 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52
+ * 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY
+ * 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a
+ * 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ
+ * 3400330d1dfc7f3f7f4b8d4d803dfcf6 The first five lines of the journal form
+ * its header. They are the constant string "libcore.io.DiskLruCache", the
+ * disk cache's version, the application's version, the value count, and a
+ * blank line. Each of the subsequent lines in the file is a record of the
+ * state of a cache entry. Each line contains space-separated values: a
+ * state, a key, and optional state-specific values. o DIRTY lines track
+ * that an entry is actively being created or updated. Every successful
+ * DIRTY action should be followed by a CLEAN or REMOVE action. DIRTY lines
+ * without a matching CLEAN or REMOVE indicate that temporary files may need
+ * to be deleted. o CLEAN lines track a cache entry that has been
+ * successfully published and may be read. A publish line is followed by the
+ * lengths of each of its values. o READ lines track accesses for LRU. o
+ * REMOVE lines track entries that have been deleted. The journal file is
+ * appended to as cache operations occur. The journal may occasionally be
+ * compacted by dropping redundant lines. A temporary file named
+ * "journal.tmp" will be used during compaction; that file should be deleted
+ * if it exists when the cache is opened.
+ */
+
+ private final File directory;
+
+ private final File journalFile;
+
+ private final File journalFileTmp;
+
+ private final int appVersion;
+
+ private final long maxSize;
+
+ private final int valueCount;
+
+ private long size = 0;
+
+ private Writer journalWriter;
+
+ private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0,
+ 0.75f, true);
+
+ private int redundantOpCount;
+
+ /**
+ * To differentiate between old and current snapshots, each entry is given a
+ * sequence number each time an edit is committed. A snapshot is stale if
+ * its sequence number is not equal to its entry's sequence number.
+ */
+ private long nextSequenceNumber = 0;
+
+ /* From java.util.Arrays */
+ @SuppressWarnings("unchecked")
+ private static <T> T[] copyOfRange(final T[] original, final int start, final int end) {
+ final int originalLength = original.length; // For exception priority
+ // compatibility.
+ if (start > end) {
+ throw new IllegalArgumentException();
+ }
+ if (start < 0 || start > originalLength) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ final int resultLength = end - start;
+ final int copyLength = Math.min(resultLength, originalLength - start);
+ final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(),
+ resultLength);
+ System.arraycopy(original, start, result, 0, copyLength);
+ return result;
+ }
+
+ /**
+ * Returns the remainder of 'reader' as a string, closing it when done.
+ */
+ public static String readFully(final Reader reader) throws IOException {
+ try {
+ final StringWriter writer = new StringWriter();
+ final char[] buffer = new char[1024];
+ int count;
+ while ((count = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, count);
+ }
+ return writer.toString();
+ } finally {
+ reader.close();
+ }
+ }
+
+ /**
+ * Returns the ASCII characters up to but not including the next "\r\n", or
+ * "\n".
+ *
+ * @throws java.io.EOFException if the stream is exhausted before the next
+ * newline character.
+ */
+ public static String readAsciiLine(final InputStream in) throws IOException {
+ // TODO: support UTF-8 here instead
+
+ final StringBuilder result = new StringBuilder(80);
+ while (true) {
+ final int c = in.read();
+ if (c == -1) {
+ throw new EOFException();
+ } else if (c == '\n') {
+ break;
+ }
+
+ result.append((char)c);
+ }
+ final int length = result.length();
+ if (length > 0 && result.charAt(length - 1) == '\r') {
+ result.setLength(length - 1);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Closes 'closeable', ignoring any checked exceptions. Does nothing if
+ * 'closeable' is null.
+ */
+ public static void closeQuietly(final Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (final RuntimeException rethrown) {
+ throw rethrown;
+ } catch (final Exception ignored) {
+ }
+ }
+ }
+
+ /**
+ * Recursively delete everything in {@code dir}.
+ */
+ // TODO: this should specify paths as Strings rather than as Files
+ public static void deleteContents(final File dir) throws IOException {
+ final File[] files = dir.listFiles();
+ if (files == null) {
+ throw new IllegalArgumentException("not a directory: " + dir);
+ }
+ for (final File file : files) {
+ if (file.isDirectory()) {
+ deleteContents(file);
+ }
+ if (!file.delete()) {
+ throw new IOException("failed to delete file: " + file);
+ }
+ }
+ }
+
+ /** This cache uses a single background thread to evict entries. */
+ private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L,
+ TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+
+ private final Callable<Void> cleanupCallable = new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ synchronized (DiskLruCache.this) {
+ if (journalWriter == null) {
+ return null; // closed
+ }
+ trimToSize();
+ if (journalRebuildRequired()) {
+ rebuildJournal();
+ redundantOpCount = 0;
+ }
+ }
+ return null;
+ }
+ };
+
+ private DiskLruCache(final File directory, final int appVersion, final int valueCount,
+ final long maxSize) {
+ this.directory = directory;
+ this.appVersion = appVersion;
+ journalFile = new File(directory, JOURNAL_FILE);
+ journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+ this.valueCount = valueCount;
+ this.maxSize = maxSize;
+ }
+
+ /**
+ * Opens the cache in {@code directory}, creating a cache if none exists
+ * there.
+ *
+ * @param directory a writable directory
+ * @param appVersion
+ * @param valueCount the number of values per cache entry. Must be positive.
+ * @param maxSize the maximum number of bytes this cache should use to store
+ * @throws IOException if reading or writing the cache directory fails
+ */
+ public static DiskLruCache open(final File directory, final int appVersion,
+ final int valueCount, final long maxSize) throws IOException {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ if (valueCount <= 0) {
+ throw new IllegalArgumentException("valueCount <= 0");
+ }
+
+ // prefer to pick up where we left off
+ DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ if (cache.journalFile.exists()) {
+ try {
+ cache.readJournal();
+ cache.processJournal();
+ cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
+ IO_BUFFER_SIZE);
+ return cache;
+ } catch (final IOException journalIsCorrupt) {
+ // System.logW("DiskLruCache " + directory + " is corrupt: "
+ // + journalIsCorrupt.getMessage() + ", removing");
+ cache.delete();
+ }
+ }
+
+ // create a new empty cache
+ directory.mkdirs();
+ cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ cache.rebuildJournal();
+ return cache;
+ }
+
+ private void readJournal() throws IOException {
+ final InputStream in = new BufferedInputStream(new FileInputStream(journalFile),
+ IO_BUFFER_SIZE);
+ try {
+ final String magic = readAsciiLine(in);
+ final String version = readAsciiLine(in);
+ final String appVersionString = readAsciiLine(in);
+ final String valueCountString = readAsciiLine(in);
+ final String blank = readAsciiLine(in);
+ if (!MAGIC.equals(magic) || !VERSION_1.equals(version)
+ || !Integer.toString(appVersion).equals(appVersionString)
+ || !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank)) {
+ throw new IOException("unexpected journal header: [" + magic + ", " + version
+ + ", " + valueCountString + ", " + blank + "]");
+ }
+
+ while (true) {
+ try {
+ readJournalLine(readAsciiLine(in));
+ } catch (final EOFException endOfJournal) {
+ break;
+ }
+ }
+ } finally {
+ closeQuietly(in);
+ }
+ }
+
+ private void readJournalLine(final String line) throws IOException {
+ final String[] parts = line.split(" ");
+ if (parts.length < 2) {
+ throw new IOException("unexpected journal line: " + line);
+ }
+
+ final String key = parts[1];
+ if (parts[0].equals(REMOVE) && parts.length == 2) {
+ lruEntries.remove(key);
+ return;
+ }
+
+ Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ }
+
+ if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+ entry.readable = true;
+ entry.currentEditor = null;
+ entry.setLengths(copyOfRange(parts, 2, parts.length));
+ } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+ entry.currentEditor = new Editor(entry);
+ } else if (parts[0].equals(READ) && parts.length == 2) {
+ // this work was already done by calling lruEntries.get()
+ } else {
+ throw new IOException("unexpected journal line: " + line);
+ }
+ }
+
+ /**
+ * Computes the initial size and collects garbage as a part of opening the
+ * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+ */
+ private void processJournal() throws IOException {
+ deleteIfExists(journalFileTmp);
+ for (final Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) {
+ final Entry entry = i.next();
+ if (entry.currentEditor == null) {
+ for (int t = 0; t < valueCount; t++) {
+ size += entry.lengths[t];
+ }
+ } else {
+ entry.currentEditor = null;
+ for (int t = 0; t < valueCount; t++) {
+ deleteIfExists(entry.getCleanFile(t));
+ deleteIfExists(entry.getDirtyFile(t));
+ }
+ i.remove();
+ }
+ }
+ }
+
+ /**
+ * Creates a new journal that omits redundant information. This replaces the
+ * current journal if it exists.
+ */
+ private synchronized void rebuildJournal() throws IOException {
+ if (journalWriter != null) {
+ journalWriter.close();
+ }
+
+ final Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
+ writer.write(MAGIC);
+ writer.write("\n");
+ writer.write(VERSION_1);
+ writer.write("\n");
+ writer.write(Integer.toString(appVersion));
+ writer.write("\n");
+ writer.write(Integer.toString(valueCount));
+ writer.write("\n");
+ writer.write("\n");
+
+ for (final Entry entry : lruEntries.values()) {
+ if (entry.currentEditor != null) {
+ writer.write(DIRTY + ' ' + entry.key + '\n');
+ } else {
+ writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ }
+ }
+
+ writer.close();
+ journalFileTmp.renameTo(journalFile);
+ journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
+ }
+
+ private static void deleteIfExists(final File file) throws IOException {
+ // try {
+ // Libcore.os.remove(file.getPath());
+ // } catch (ErrnoException errnoException) {
+ // if (errnoException.errno != OsConstants.ENOENT) {
+ // throw errnoException.rethrowAsIOException();
+ // }
+ // }
+ if (file.exists() && !file.delete()) {
+ throw new IOException();
+ }
+ }
+
+ /**
+ * Returns a snapshot of the entry named {@code key}, or null if it doesn't
+ * exist is not currently readable. If a value is returned, it is moved to
+ * the head of the LRU queue.
+ */
+ public synchronized Snapshot get(final String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ final Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ return null;
+ }
+
+ if (!entry.readable) {
+ return null;
+ }
+
+ /*
+ * Open all streams eagerly to guarantee that we see a single published
+ * snapshot. If we opened streams lazily then the streams could come
+ * from different edits.
+ */
+ final InputStream[] ins = new InputStream[valueCount];
+ try {
+ for (int i = 0; i < valueCount; i++) {
+ ins[i] = new FileInputStream(entry.getCleanFile(i));
+ }
+ } catch (final FileNotFoundException e) {
+ // a file must have been deleted manually!
+ return null;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(READ + ' ' + key + '\n');
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return new Snapshot(key, entry.sequenceNumber, ins);
+ }
+
+ /**
+ * Returns an editor for the entry named {@code key}, or null if another
+ * edit is in progress.
+ */
+ public Editor edit(final String key) throws IOException {
+ return edit(key, ANY_SEQUENCE_NUMBER);
+ }
+
+ private synchronized Editor edit(final String key, final long expectedSequenceNumber)
+ throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
+ && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
+ return null; // snapshot is stale
+ }
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ } else if (entry.currentEditor != null) {
+ return null; // another edit is in progress
+ }
+
+ final Editor editor = new Editor(entry);
+ entry.currentEditor = editor;
+
+ // flush the journal before creating files to prevent file leaks
+ journalWriter.write(DIRTY + ' ' + key + '\n');
+ journalWriter.flush();
+ return editor;
+ }
+
+ /**
+ * Returns the directory where this cache stores its data.
+ */
+ public File getDirectory() {
+ return directory;
+ }
+
+ /**
+ * Returns the maximum number of bytes that this cache should use to store
+ * its data.
+ */
+ public long maxSize() {
+ return maxSize;
+ }
+
+ /**
+ * Returns the number of bytes currently being used to store the values in
+ * this cache. This may be greater than the max size if a background
+ * deletion is pending.
+ */
+ public synchronized long size() {
+ return size;
+ }
+
+ private synchronized void completeEdit(final Editor editor, final boolean success)
+ throws IOException {
+ final Entry entry = editor.entry;
+ if (entry.currentEditor != editor) {
+ throw new IllegalStateException();
+ }
+
+ // if this edit is creating the entry for the first time, every index
+ // must have a value
+ if (success && !entry.readable) {
+ for (int i = 0; i < valueCount; i++) {
+ if (!entry.getDirtyFile(i).exists()) {
+ editor.abort();
+ throw new IllegalStateException("edit didn't create file " + i);
+ }
+ }
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ final File dirty = entry.getDirtyFile(i);
+ if (success) {
+ if (dirty.exists()) {
+ final File clean = entry.getCleanFile(i);
+ dirty.renameTo(clean);
+ final long oldLength = entry.lengths[i];
+ final long newLength = clean.length();
+ entry.lengths[i] = newLength;
+ size = size - oldLength + newLength;
+ }
+ } else {
+ deleteIfExists(dirty);
+ }
+ }
+
+ redundantOpCount++;
+ entry.currentEditor = null;
+ if (entry.readable | success) {
+ entry.readable = true;
+ journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ if (success) {
+ entry.sequenceNumber = nextSequenceNumber++;
+ }
+ } else {
+ lruEntries.remove(entry.key);
+ journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+ }
+
+ if (size > maxSize || journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+ }
+
+ /**
+ * We only rebuild the journal when it will halve the size of the journal
+ * and eliminate at least 2000 ops.
+ */
+ private boolean journalRebuildRequired() {
+ final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
+ return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
+ && redundantOpCount >= lruEntries.size();
+ }
+
+ /**
+ * Drops the entry for {@code key} if it exists and can be removed. Entries
+ * actively being edited cannot be removed.
+ *
+ * @return true if an entry was removed.
+ */
+ public synchronized boolean remove(final String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ final Entry entry = lruEntries.get(key);
+ if (entry == null || entry.currentEditor != null) {
+ return false;
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ final File file = entry.getCleanFile(i);
+ if (!file.delete()) {
+ throw new IOException("failed to delete " + file);
+ }
+ size -= entry.lengths[i];
+ entry.lengths[i] = 0;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(REMOVE + ' ' + key + '\n');
+ lruEntries.remove(key);
+
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if this cache has been closed.
+ */
+ public boolean isClosed() {
+ return journalWriter == null;
+ }
+
+ private void checkNotClosed() {
+ if (journalWriter == null) {
+ throw new IllegalStateException("cache is closed");
+ }
+ }
+
+ /**
+ * Force buffered operations to the filesystem.
+ */
+ public synchronized void flush() throws IOException {
+ checkNotClosed();
+ trimToSize();
+ journalWriter.flush();
+ }
+
+ /**
+ * Closes this cache. Stored values will remain on the filesystem.
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ if (journalWriter == null) {
+ return; // already closed
+ }
+ for (final Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.abort();
+ }
+ }
+ trimToSize();
+ journalWriter.close();
+ journalWriter = null;
+ }
+
+ private void trimToSize() throws IOException {
+ while (size > maxSize) {
+ // Map.Entry<String, Entry> toEvict = lruEntries.eldest();
+ final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
+ remove(toEvict.getKey());
+ }
+ }
+
+ /**
+ * Closes the cache and deletes all of its stored values. This will delete
+ * all files in the cache directory including files that weren't created by
+ * the cache.
+ */
+ public void delete() throws IOException {
+ close();
+ deleteContents(directory);
+ }
+
+ private void validateKey(final String key) {
+ if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
+ throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key
+ + "\"");
+ }
+ }
+
+ private static String inputStreamToString(final InputStream in) throws IOException {
+ return readFully(new InputStreamReader(in, UTF_8));
+ }
+
+ /**
+ * A snapshot of the values for an entry.
+ */
+ public final class Snapshot implements Closeable {
+ private final String key;
+
+ private final long sequenceNumber;
+
+ private final InputStream[] ins;
+
+ private Snapshot(final String key, final long sequenceNumber, final InputStream[] ins) {
+ this.key = key;
+ this.sequenceNumber = sequenceNumber;
+ this.ins = ins;
+ }
+
+ /**
+ * Returns an editor for this snapshot's entry, or null if either the
+ * entry has changed since this snapshot was created or if another edit
+ * is in progress.
+ */
+ public Editor edit() throws IOException {
+ return DiskLruCache.this.edit(key, sequenceNumber);
+ }
+
+ /**
+ * Returns the unbuffered stream with the value for {@code index}.
+ */
+ public InputStream getInputStream(final int index) {
+ return ins[index];
+ }
+
+ /**
+ * Returns the string value for {@code index}.
+ */
+ public String getString(final int index) throws IOException {
+ return inputStreamToString(getInputStream(index));
+ }
+
+ @Override
+ public void close() {
+ for (final InputStream in : ins) {
+ closeQuietly(in);
+ }
+ }
+ }
+
+ /**
+ * Edits the values for an entry.
+ */
+ public final class Editor {
+ private final Entry entry;
+
+ private boolean hasErrors;
+
+ private Editor(final Entry entry) {
+ this.entry = entry;
+ }
+
+ /**
+ * Returns an unbuffered input stream to read the last committed value,
+ * or null if no value has been committed.
+ */
+ public InputStream newInputStream(final int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ return null;
+ }
+ return new FileInputStream(entry.getCleanFile(index));
+ }
+ }
+
+ /**
+ * Returns the last committed value as a string, or null if no value has
+ * been committed.
+ */
+ public String getString(final int index) throws IOException {
+ final InputStream in = newInputStream(index);
+ return in != null ? inputStreamToString(in) : null;
+ }
+
+ /**
+ * Returns a new unbuffered output stream to write the value at
+ * {@code index}. If the underlying output stream encounters errors when
+ * writing to the filesystem, this edit will be aborted when
+ * {@link #commit} is called. The returned output stream does not throw
+ * IOExceptions.
+ */
+ public OutputStream newOutputStream(final int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+ }
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}.
+ */
+ public void set(final int index, final String value) throws IOException {
+ Writer writer = null;
+ try {
+ writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
+ writer.write(value);
+ } finally {
+ closeQuietly(writer);
+ }
+ }
+
+ /**
+ * Commits this edit so it is visible to readers. This releases the edit
+ * lock so another edit may be started on the same key.
+ */
+ public void commit() throws IOException {
+ if (hasErrors) {
+ completeEdit(this, false);
+ remove(entry.key); // the previous entry is stale
+ } else {
+ completeEdit(this, true);
+ }
+ }
+
+ /**
+ * Aborts this edit. This releases the edit lock so another edit may be
+ * started on the same key.
+ */
+ public void abort() throws IOException {
+ completeEdit(this, false);
+ }
+
+ private class FaultHidingOutputStream extends FilterOutputStream {
+ private FaultHidingOutputStream(final OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(final int oneByte) {
+ try {
+ out.write(oneByte);
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void write(final byte[] buffer, final int offset, final int length) {
+ try {
+ out.write(buffer, offset, length);
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ out.close();
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void flush() {
+ try {
+ out.flush();
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+ }
+ }
+
+ private final class Entry {
+ private final String key;
+
+ /** Lengths of this entry's files. */
+ private final long[] lengths;
+
+ /** True if this entry has ever been published */
+ private boolean readable;
+
+ /** The ongoing edit or null if this entry is not being edited. */
+ private Editor currentEditor;
+
+ /**
+ * The sequence number of the most recently committed edit to this
+ * entry.
+ */
+ private long sequenceNumber;
+
+ private Entry(final String key) {
+ this.key = key;
+ lengths = new long[valueCount];
+ }
+
+ public String getLengths() throws IOException {
+ final StringBuilder result = new StringBuilder();
+ for (final long size : lengths) {
+ result.append(' ').append(size);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Set lengths using decimal numbers like "10123".
+ */
+ private void setLengths(final String[] strings) throws IOException {
+ if (strings.length != valueCount) {
+ throw invalidLengths(strings);
+ }
+
+ try {
+ for (int i = 0; i < strings.length; i++) {
+ lengths[i] = Long.parseLong(strings[i]);
+ }
+ } catch (final NumberFormatException e) {
+ throw invalidLengths(strings);
+ }
+ }
+
+ private IOException invalidLengths(final String[] strings) throws IOException {
+ throw new IOException("unexpected journal line: " + Arrays.toString(strings));
+ }
+
+ public File getCleanFile(final int i) {
+ return new File(directory, key + "." + i);
+ }
+
+ public File getDirtyFile(final int i) {
+ return new File(directory, key + "." + i + ".tmp");
+ }
+ }
+}
diff --git a/src/com/cyanogenmod/eleven/cache/ICacheListener.java b/src/com/cyanogenmod/eleven/cache/ICacheListener.java
new file mode 100644
index 0000000..89b88ac
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/ICacheListener.java
@@ -0,0 +1,20 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package com.cyanogenmod.eleven.cache;
+
+public interface ICacheListener {
+ void onCacheUnpaused();
+}
diff --git a/src/com/cyanogenmod/eleven/cache/ImageCache.java b/src/com/cyanogenmod/eleven/cache/ImageCache.java
new file mode 100644
index 0000000..0859054
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/ImageCache.java
@@ -0,0 +1,827 @@
+/*
+ * Copyright (C) 2012 Andrew Neal
+ * Copyright (C) 2014 The CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyanogenmod.eleven.cache;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.ComponentCallbacks2;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.cyanogenmod.eleven.utils.ApolloUtils;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashSet;
+
+/**
+ * This class holds the memory and disk bitmap caches.
+ */
+public final class ImageCache {
+
+ private static final String TAG = ImageCache.class.getSimpleName();
+
+ /**
+ * The {@link Uri} used to retrieve album art
+ */
+ private static final Uri mArtworkUri;
+
+ /**
+ * Default memory cache size as a percent of device memory class
+ */
+ private static final float MEM_CACHE_DIVIDER = 0.25f;
+
+ /**
+ * Default disk cache size 10MB
+ */
+ private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
+
+ /**
+ * Compression settings when writing images to disk cache
+ */
+ private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
+
+ /**
+ * Disk cache index to read from
+ */
+ private static final int DISK_CACHE_INDEX = 0;
+
+ /**
+ * Image compression quality
+ */
+ private static final int COMPRESS_QUALITY = 98;
+
+ /**
+ * LRU cache
+ */
+ private MemoryCache mLruCache;
+
+ /**
+ * Disk LRU cache
+ */
+ private DiskLruCache mDiskCache;
+
+ /**
+ * listeners to the cache state
+ */
+ private HashSet<ICacheListener> mListeners = new HashSet<ICacheListener>();
+
+ private static ImageCache sInstance;
+
+ /**
+ * Used to temporarily pause the disk cache while scrolling
+ */
+ public boolean mPauseDiskAccess = false;
+ private Object mPauseLock = new Object();
+
+ static {
+ mArtworkUri = Uri.parse("content://media/external/audio/albumart");
+ }
+
+ /**
+ * Constructor of <code>ImageCache</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public ImageCache(final Context context) {
+ init(context);
+ }
+
+ /**
+ * Used to create a singleton of {@link ImageCache}
+ *
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public final static ImageCache getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new ImageCache(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Initialize the cache, providing all parameters.
+ *
+ * @param context The {@link Context} to use
+ * @param cacheParams The cache parameters to initialize the cache
+ */
+ private void init(final Context context) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ // Initialize the disk cahe in a background thread
+ initDiskCache(context);
+ return null;
+ }
+ }, (Void[])null);
+ // Set up the memory cache
+ initLruCache(context);
+ }
+
+ /**
+ * Initializes the disk cache. Note that this includes disk access so this
+ * should not be executed on the main/UI thread. By default an ImageCache
+ * does not initialize the disk cache when it is created, instead you should
+ * call initDiskCache() to initialize it on a background thread.
+ *
+ * @param context The {@link Context} to use
+ */
+ private synchronized void initDiskCache(final Context context) {
+ // Set up disk cache
+ if (mDiskCache == null || mDiskCache.isClosed()) {
+ File diskCacheDir = getDiskCacheDir(context, TAG);
+ if (diskCacheDir != null) {
+ if (!diskCacheDir.exists()) {
+ diskCacheDir.mkdirs();
+ }
+ if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
+ try {
+ mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
+ } catch (final IOException e) {
+ diskCacheDir = null;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets up the Lru cache
+ *
+ * @param context The {@link Context} to use
+ */
+ @SuppressLint("NewApi")
+ public void initLruCache(final Context context) {
+ final ActivityManager activityManager = (ActivityManager)context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
+ * 1024 * 1024);
+ mLruCache = new MemoryCache(lruCacheSize);
+
+ // Release some memory as needed
+ context.registerComponentCallbacks(new ComponentCallbacks2() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTrimMemory(final int level) {
+ if (level >= TRIM_MEMORY_MODERATE) {
+ evictAll();
+ } else if (level >= TRIM_MEMORY_BACKGROUND) {
+ mLruCache.trimToSize(mLruCache.size() / 2);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLowMemory() {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onConfigurationChanged(final Configuration newConfig) {
+ // Nothing to do
+ }
+ });
+ }
+
+ /**
+ * Find and return an existing ImageCache stored in a {@link RetainFragment}
+ * , if not found a new one is created using the supplied params and saved
+ * to a {@link RetainFragment}
+ *
+ * @param activity The calling {@link FragmentActivity}
+ * @return An existing retained ImageCache object or a new one if one did
+ * not exist
+ */
+ public static final ImageCache findOrCreateCache(final Activity activity) {
+
+ // Search for, or create an instance of the non-UI RetainFragment
+ final RetainFragment retainFragment = findOrCreateRetainFragment(
+ activity.getFragmentManager());
+
+ // See if we already have an ImageCache stored in RetainFragment
+ ImageCache cache = (ImageCache)retainFragment.getObject();
+
+ // No existing ImageCache, create one and store it in RetainFragment
+ if (cache == null) {
+ cache = getInstance(activity);
+ retainFragment.setObject(cache);
+ }
+ return cache;
+ }
+
+ /**
+ * Locate an existing instance of this {@link Fragment} or if not found,
+ * create and add it using {@link FragmentManager}
+ *
+ * @param fm The {@link FragmentManager} to use
+ * @return The existing instance of the {@link Fragment} or the new instance
+ * if just created
+ */
+ public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
+ // Check to see if we have retained the worker fragment
+ RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
+
+ // If not retained, we need to create and add it
+ if (retainFragment == null) {
+ retainFragment = new RetainFragment();
+ fm.beginTransaction().add(retainFragment, TAG).commit();
+ }
+ return retainFragment;
+ }
+
+ /**
+ * 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
+ */
+ 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, replace);
+
+ // Add to disk cache
+ if (mDiskCache != null && !mDiskCache.isClosed()) {
+ final String key = hashKeyForDisk(data);
+ OutputStream out = null;
+ try {
+ final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
+ 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);
+ bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
+ editor.commit();
+ out.close();
+ flush();
+ }
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ } catch (final IllegalStateException e) {
+ // if the user clears the cache while we have an async task going we could try
+ // writing to the disk cache while it isn't ready. Catching here will silently
+ // fail instead
+ Log.e(TAG, "addBitmapToCache - " + e);
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ out = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ } catch (final IllegalStateException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called to add a new image to the memory cache
+ *
+ * @param data The key identifier
+ * @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 (replace || getBitmapFromMemCache(data) == null) {
+ mLruCache.put(data, bitmap);
+ }
+ }
+
+ /**
+ * Fetches a cached image from the memory cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getBitmapFromMemCache(final String data) {
+ if (data == null) {
+ return null;
+ }
+ if (mLruCache != null) {
+ final Bitmap lruBitmap = mLruCache.get(data);
+ if (lruBitmap != null) {
+ return lruBitmap;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Fetches a cached image from the disk cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getBitmapFromDiskCache(final String data) {
+ if (data == null) {
+ return null;
+ }
+
+ // Check in the memory cache here to avoid going to the disk cache less
+ // often
+ if (getBitmapFromMemCache(data) != null) {
+ return getBitmapFromMemCache(data);
+ }
+
+ waitUntilUnpaused();
+ final String key = hashKeyForDisk(data);
+ if (mDiskCache != null) {
+ InputStream inputStream = null;
+ try {
+ final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
+ if (snapshot != null) {
+ inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
+ if (inputStream != null) {
+ final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ if (bitmap != null) {
+ return bitmap;
+ }
+ }
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "getBitmapFromDiskCache - " + e);
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (final IOException e) {
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Tries to return a cached image from memory cache before fetching from the
+ * disk cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getCachedBitmap(final String data) {
+ if (data == null) {
+ return null;
+ }
+ Bitmap cachedImage = getBitmapFromMemCache(data);
+ if (cachedImage == null) {
+ cachedImage = getBitmapFromDiskCache(data);
+ }
+ if (cachedImage != null) {
+ addBitmapToMemCache(data, cachedImage);
+ return cachedImage;
+ }
+ return null;
+ }
+
+ /**
+ * Tries to return the album art from memory cache and disk cache, before
+ * calling {@code #getArtworkFromFile(Context, String)} again
+ *
+ * @param context The {@link Context} to use
+ * @param data The name of the album art
+ * @param id The ID of the album to find artwork for
+ * @return The artwork for an album
+ */
+ public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
+ if (context == null || data == null) {
+ return null;
+ }
+ Bitmap cachedImage = getCachedBitmap(data);
+ if (cachedImage == null && id >= 0) {
+ cachedImage = getArtworkFromFile(context, id);
+ }
+ if (cachedImage != null) {
+ addBitmapToMemCache(data, cachedImage);
+ return cachedImage;
+ }
+ return null;
+ }
+
+ /**
+ * Used to fetch the artwork for an album locally from the user's device
+ *
+ * @param context The {@link Context} to use
+ * @param albumID The ID of the album to find artwork for
+ * @return The artwork for an album
+ */
+ public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
+ if (albumId < 0) {
+ return null;
+ }
+ Bitmap artwork = null;
+ waitUntilUnpaused();
+ try {
+ final Uri uri = ContentUris.withAppendedId(mArtworkUri, albumId);
+ final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver()
+ .openFileDescriptor(uri, "r");
+ if (parcelFileDescriptor != null) {
+ final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+ artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
+ }
+ } catch (final IllegalStateException e) {
+ // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
+ } catch (final FileNotFoundException e) {
+ // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
+ } catch (final OutOfMemoryError evict) {
+ // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
+ evictAll();
+ }
+ return artwork;
+ }
+
+ /**
+ * flush() is called to synchronize up other methods that are accessing the
+ * cache first
+ */
+ public void flush() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ if (mDiskCache != null) {
+ try {
+ if (!mDiskCache.isClosed()) {
+ mDiskCache.flush();
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "flush - " + e);
+ }
+ }
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Clears the disk and memory caches
+ */
+ public void clearCaches() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ // Clear the disk cache
+ try {
+ if (mDiskCache != null) {
+ mDiskCache.delete();
+ mDiskCache = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "clearCaches - " + e);
+ }
+ // Clear the memory cache
+ evictAll();
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Closes the disk cache associated with this ImageCache object. Note that
+ * this includes disk access so this should not be executed on the main/UI
+ * thread.
+ */
+ public void close() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ if (mDiskCache != null) {
+ try {
+ if (!mDiskCache.isClosed()) {
+ mDiskCache.close();
+ mDiskCache = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "close - " + e);
+ }
+ }
+ return null;
+ }
+ }, (Void[]) null);
+ }
+
+ /**
+ * Evicts all of the items from the memory cache and lets the system know
+ * now would be a good time to garbage collect
+ */
+ public void evictAll() {
+ if (mLruCache != null) {
+ mLruCache.evictAll();
+ }
+ System.gc();
+ }
+
+ /**
+ * @param key The key used to identify which cache entries to delete.
+ */
+ public void removeFromCache(final String key) {
+ if (key == null) {
+ return;
+ }
+ // Remove the Lru entry
+ if (mLruCache != null) {
+ mLruCache.remove(key);
+ }
+
+ try {
+ // Remove the disk entry
+ if (mDiskCache != null) {
+ mDiskCache.remove(hashKeyForDisk(key));
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "remove - " + e);
+ }
+ flush();
+ }
+
+ /**
+ * Used to temporarily pause the disk cache while the user is scrolling to
+ * improve scrolling.
+ *
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ synchronized (mPauseLock) {
+ if (mPauseDiskAccess != pause) {
+ mPauseDiskAccess = pause;
+ if (!pause) {
+ mPauseLock.notify();
+
+ for (ICacheListener listener : mListeners) {
+ listener.onCacheUnpaused();
+ }
+ }
+ }
+ }
+ }
+
+ private void waitUntilUnpaused() {
+ synchronized (mPauseLock) {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ while (mPauseDiskAccess) {
+ try {
+ mPauseLock.wait();
+ } catch (InterruptedException e) {
+ // ignored, we'll start waiting again
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @return True if the user is scrolling, false otherwise.
+ */
+ public boolean isDiskCachePaused() {
+ return mPauseDiskAccess;
+ }
+
+ public void addCacheListener(ICacheListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeCacheListener(ICacheListener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Get a usable cache directory (external if available, internal otherwise)
+ *
+ * @param context The {@link Context} to use
+ * @param uniqueName A unique directory name to append to the cache
+ * directory
+ * @return The cache directory
+ */
+ public static final File getDiskCacheDir(final Context context, final String uniqueName) {
+ // getExternalCacheDir(context) returns null if external storage is not ready
+ final String cachePath = getExternalCacheDir(context) != null
+ ? getExternalCacheDir(context).getPath()
+ : context.getCacheDir().getPath();
+ return new File(cachePath, uniqueName);
+ }
+
+ /**
+ * Check if external storage is built-in or removable
+ *
+ * @return True if external storage is removable (like an SD card), false
+ * otherwise
+ */
+ public static final boolean isExternalStorageRemovable() {
+ return Environment.isExternalStorageRemovable();
+ }
+
+ /**
+ * Get the external app cache directory
+ *
+ * @param context The {@link Context} to use
+ * @return The external cache directory
+ */
+ public static final File getExternalCacheDir(final Context context) {
+ return context.getExternalCacheDir();
+ }
+
+ /**
+ * Check how much usable space is available at a given path.
+ *
+ * @param path The path to check
+ * @return The space available in bytes
+ */
+ public static final long getUsableSpace(final File path) {
+ return path.getUsableSpace();
+ }
+
+ /**
+ * A hashing method that changes a string (like a URL) into a hash suitable
+ * for using as a disk filename.
+ *
+ * @param key The key used to store the file
+ */
+ public static final String hashKeyForDisk(final String key) {
+ String cacheKey;
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("MD5");
+ digest.update(key.getBytes());
+ cacheKey = bytesToHexString(digest.digest());
+ } catch (final NoSuchAlgorithmException e) {
+ cacheKey = String.valueOf(key.hashCode());
+ }
+ return cacheKey;
+ }
+
+ /**
+ * http://stackoverflow.com/questions/332079
+ *
+ * @param bytes The bytes to convert.
+ * @return A {@link String} converted from the bytes of a hashable key used
+ * to store a filename on the disk, to hex digits.
+ */
+ private static final String bytesToHexString(final byte[] bytes) {
+ final StringBuilder builder = new StringBuilder();
+ for (final byte b : bytes) {
+ final String hex = Integer.toHexString(0xFF & b);
+ if (hex.length() == 1) {
+ builder.append('0');
+ }
+ builder.append(hex);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * A simple non-UI Fragment that stores a single Object and is retained over
+ * configuration changes. In this sample it will be used to retain an
+ * {@link ImageCache} object.
+ */
+ public static final class RetainFragment extends Fragment {
+
+ /**
+ * The object to be stored
+ */
+ private Object mObject;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public RetainFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Make sure this Fragment is retained over a configuration change
+ setRetainInstance(true);
+ }
+
+ /**
+ * Store a single object in this {@link Fragment}
+ *
+ * @param object The object to store
+ */
+ public void setObject(final Object object) {
+ mObject = object;
+ }
+
+ /**
+ * Get the stored object
+ *
+ * @return The stored object
+ */
+ public Object getObject() {
+ return mObject;
+ }
+ }
+
+ /**
+ * Used to cache images via {@link LruCache}.
+ */
+ public static final class MemoryCache extends LruCache<String, Bitmap> {
+
+ /**
+ * Constructor of <code>MemoryCache</code>
+ *
+ * @param maxSize The allowed size of the {@link LruCache}
+ */
+ public MemoryCache(final int maxSize) {
+ super(maxSize);
+ }
+
+ /**
+ * Get the size in bytes of a bitmap.
+ */
+ public static final int getBitmapSize(final Bitmap bitmap) {
+ return bitmap.getByteCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
+ return getBitmapSize(paramBitmap);
+ }
+
+ }
+
+}
diff --git a/src/com/cyanogenmod/eleven/cache/ImageFetcher.java b/src/com/cyanogenmod/eleven/cache/ImageFetcher.java
new file mode 100644
index 0000000..d6300bd
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/ImageFetcher.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2012 Andrew Neal
+ * Copyright (C) 2014 The CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyanogenmod.eleven.cache;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.widget.ImageView;
+import com.cyanogenmod.eleven.Config;
+import com.cyanogenmod.eleven.MusicPlaybackService;
+import com.cyanogenmod.eleven.cache.PlaylistWorkerTask.PlaylistWorkerType;
+import com.cyanogenmod.eleven.utils.MusicUtils;
+import com.cyanogenmod.eleven.widgets.BlurScrimImage;
+import com.cyanogenmod.eleven.widgets.LetterTileDrawable;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A subclass of {@link ImageWorker} that fetches images from a URL.
+ */
+public class ImageFetcher extends ImageWorker {
+
+ private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
+
+ private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
+
+ private static ImageFetcher sInstance = null;
+
+ /**
+ * Creates a new instance of {@link ImageFetcher}.
+ *
+ * @param context The {@link Context} to use.
+ */
+ public ImageFetcher(final Context context) {
+ super(context);
+ }
+
+ /**
+ * Used to create a singleton of the image fetcher
+ *
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public static final ImageFetcher getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new ImageFetcher(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Loads a playlist's most played song's artist image
+ * @param playlistId id of the playlist
+ * @param imageView imageview to load into
+ */
+ public void loadPlaylistArtistImage(final long playlistId, final ImageView imageView) {
+ loadPlaylistImage(playlistId, PlaylistWorkerType.Artist, imageView);
+ }
+
+ /**
+ * 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
+ */
+ 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) {
+ loadImage(generateAlbumCacheKey(albumName, artistName), artistName, albumName, albumId, imageView,
+ ImageType.ALBUM);
+ }
+
+ /**
+ * Used to fetch the current artwork.
+ */
+ public void loadCurrentArtwork(final ImageView imageView) {
+ loadImage(getCurrentCacheKey(),
+ MusicUtils.getArtistName(), MusicUtils.getAlbumName(), MusicUtils.getCurrentAlbumId(),
+ imageView, ImageType.ALBUM);
+ }
+
+ /**
+ * Used to fetch the current artwork blurred.
+ */
+ public void loadCurrentBlurredArtwork(final BlurScrimImage image) {
+ loadBlurImage(getCurrentCacheKey(),
+ MusicUtils.getArtistName(), MusicUtils.getAlbumName(), MusicUtils.getCurrentAlbumId(),
+ image, ImageType.ALBUM);
+ }
+
+ public static String getCurrentCacheKey() {
+ return generateAlbumCacheKey(MusicUtils.getAlbumName(), MusicUtils.getArtistName());
+ }
+
+ /**
+ * Used to fetch artist images.
+ */
+ public void loadArtistImage(final String key, final ImageView imageView) {
+ loadImage(key, key, null, -1, imageView, ImageType.ARTIST);
+ }
+
+ /**
+ * Used to fetch artist images. It also scales the image to fit the image view, if necessary.
+ */
+ public void loadArtistImage(final String key, final ImageView imageView, boolean scaleImgToView) {
+ loadImage(key, key, null, -1, imageView, ImageType.ARTIST, scaleImgToView);
+ }
+
+ /**
+ * Used to fetch the current artist image.
+ */
+ public void loadCurrentArtistImage(final ImageView imageView) {
+ loadImage(MusicUtils.getArtistName(), MusicUtils.getArtistName(), null, -1, imageView,
+ ImageType.ARTIST);
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageCache != null) {
+ mImageCache.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * Clears the disk and memory caches
+ */
+ public void clearCaches() {
+ if (mImageCache != null) {
+ mImageCache.clearCaches();
+ }
+
+ // clear the keys of images we've already downloaded
+ sKeys.clear();
+ }
+
+ public void addCacheListener(ICacheListener listener) {
+ if (mImageCache != null) {
+ mImageCache.addCacheListener(listener);
+ }
+ }
+
+ public void removeCacheListener(ICacheListener listener) {
+ if (mImageCache != null) {
+ mImageCache.removeCacheListener(listener);
+ }
+ }
+
+ /**
+ * @param key The key used to find the image to remove
+ */
+ public void removeFromCache(final String key) {
+ if (mImageCache != null) {
+ mImageCache.removeFromCache(key);
+ }
+ }
+
+ /**
+ * 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 artistName The album artist in case we should have to download
+ * missing artwork
+ * @param smallArtwork Get the small version of the default artwork if no artwork exists
+ * @return The album art as an {@link Bitmap}
+ */
+ public Bitmap getArtwork(final String albumName, final long albumId, final String artistName,
+ boolean smallArtwork) {
+ // Check the disk cache
+ Bitmap artwork = null;
+ String key = albumName;
+
+ if (artwork == null && albumName != null && mImageCache != null) {
+ key = generateAlbumCacheKey(albumName, artistName);
+ artwork = mImageCache.getBitmapFromDiskCache(key);
+ }
+ if (artwork == null && albumId >= 0 && mImageCache != null) {
+ // Check for local artwork
+ artwork = mImageCache.getArtworkFromFile(mContext, albumId);
+ }
+ if (artwork != null) {
+ return artwork;
+ }
+
+ return LetterTileDrawable.createDefaultBitmap(mContext, key, ImageType.ALBUM, false,
+ smallArtwork);
+ }
+
+ /**
+ * 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 artistName The artist name the cache key needs to be generated.
+ * @return
+ */
+ public static String generateAlbumCacheKey(final String albumName, final String artistName) {
+ if (albumName == null || artistName == null) {
+ return null;
+ }
+ return new StringBuilder(albumName)
+ .append("_")
+ .append(artistName)
+ .append("_")
+ .append(Config.ALBUM_ART_SUFFIX)
+ .toString();
+ }
+
+ /**
+ * Decode and sample down a {@link Bitmap} from a Uri.
+ *
+ * @param selectedImage Uri of the Image to decode
+ * @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 decodeSampledBitmapFromUri(ContentResolver cr, final Uri selectedImage) {
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ try {
+ InputStream input = cr.openInputStream(selectedImage);
+ BitmapFactory.decodeStream(input, null, options);
+ input.close();
+
+ if (options.outHeight == -1 || options.outWidth == -1) {
+ return null;
+ }
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, DEFAULT_MAX_IMAGE_WIDTH,
+ DEFAULT_MAX_IMAGE_HEIGHT);
+
+ // Decode bitmap with inSampleSize set
+ options.inJustDecodeBounds = false;
+ input = cr.openInputStream(selectedImage);
+ return BitmapFactory.decodeStream(input, null, options);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/src/com/cyanogenmod/eleven/cache/ImageWorker.java b/src/com/cyanogenmod/eleven/cache/ImageWorker.java
new file mode 100644
index 0000000..e8ea9d8
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/ImageWorker.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2012 Andrew Neal
+ * Copyright (C) 2014 The CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyanogenmod.eleven.cache;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.support.v8.renderscript.RenderScript;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.cyanogenmod.eleven.R;
+import com.cyanogenmod.eleven.provider.PlaylistArtworkStore;
+import com.cyanogenmod.eleven.utils.ApolloUtils;
+import com.cyanogenmod.eleven.utils.ImageUtils;
+import com.cyanogenmod.eleven.widgets.BlurScrimImage;
+import com.cyanogenmod.eleven.cache.PlaylistWorkerTask.PlaylistWorkerType;
+import com.cyanogenmod.eleven.widgets.LetterTileDrawable;
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * This class wraps up completing some arbitrary long running work when loading
+ * a {@link Bitmap} to an {@link ImageView}. It handles things like using a
+ * memory and disk cache, running the work in a background thread and setting a
+ * placeholder image.
+ */
+public abstract class ImageWorker {
+
+ /**
+ * Render script
+ */
+ public static RenderScript sRenderScript = null;
+
+ /**
+ * Tracks which images we've tried to download and prevents it from trying again
+ * In the future we might want to throw this into a db
+ */
+ public static Set<String> sKeys = Collections.synchronizedSet(new HashSet<String>());
+
+ /**
+ * Default transition drawable fade time
+ */
+ public static final int FADE_IN_TIME = 200;
+
+ /**
+ * Default transition drawable fade time slow
+ */
+ public static final int FADE_IN_TIME_SLOW = 1000;
+
+ /**
+ * The resources to use
+ */
+ private final Resources mResources;
+
+ /**
+ * First layer of the transition drawable
+ */
+ private final ColorDrawable mTransparentDrawable;
+
+ /**
+ * The Context to use
+ */
+ protected Context mContext;
+
+ /**
+ * Disk and memory caches
+ */
+ protected ImageCache mImageCache;
+
+ /**
+ * Constructor of <code>ImageWorker</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ protected ImageWorker(final Context context) {
+ mContext = context.getApplicationContext();
+
+ if (sRenderScript == null) {
+ sRenderScript = RenderScript.create(mContext);
+ }
+
+ mResources = mContext.getResources();
+ // Create the transparent layer for the transition drawable
+ mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT);
+ }
+
+ /**
+ * Set the {@link ImageCache} object to use with this ImageWorker.
+ *
+ * @param cacheCallback new {@link ImageCache} object.
+ */
+ public void setImageCache(final ImageCache cacheCallback) {
+ mImageCache = cacheCallback;
+ }
+
+ /**
+ * Closes the disk cache associated with this ImageCache object. Note that
+ * this includes disk access so this should not be executed on the main/UI
+ * thread.
+ */
+ public void close() {
+ if (mImageCache != null) {
+ mImageCache.close();
+ }
+ }
+
+ /**
+ * flush() is called to synchronize up other methods that are accessing the
+ * cache first
+ */
+ public void flush() {
+ if (mImageCache != null) {
+ mImageCache.flush();
+ }
+ }
+
+ /**
+ * 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
+ */
+ public void addBitmapToCache(final String key, final Bitmap bitmap) {
+ if (mImageCache != null) {
+ mImageCache.addBitmapToCache(key, bitmap);
+ }
+ }
+
+ /**
+ * @return A new drawable of the default artwork
+ */
+ public Drawable getNewDrawable(ImageType imageType, String name,
+ String identifier) {
+ LetterTileDrawable letterTileDrawable = new LetterTileDrawable(mContext);
+ letterTileDrawable.setTileDetails(name, identifier, imageType);
+ letterTileDrawable.setIsCircular(false);
+ return letterTileDrawable;
+ }
+
+ 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;
+
+ // First, check the disk cache for the image
+ if (key != null && imageCache != null) {
+ bitmap = imageCache.getCachedBitmap(key);
+ }
+
+ // 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);
+ }
+
+ // Third, by now we need to download the image
+ if (bitmap == null && ApolloUtils.isOnline(context) && !sKeys.contains(key)) {
+ // 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);
+ }
+ }
+
+ // Fourth, add the new image to the cache
+ if (bitmap != null && key != null && imageCache != null) {
+ imageCache.addBitmapToCache(key, bitmap);
+ }
+
+ sKeys.add(key);
+
+ return bitmap;
+ }
+
+ /**
+ * Parses the drawable for instances of TransitionDrawable and breaks them open until it finds
+ * a drawable that isn't a transition drawable
+ * @param drawable to parse
+ * @return the target drawable that isn't a TransitionDrawable
+ */
+ public static Drawable getTopDrawable(final Drawable drawable) {
+ if (drawable == null) {
+ return null;
+ }
+
+ Drawable retDrawable = drawable;
+ while (retDrawable instanceof TransitionDrawable) {
+ TransitionDrawable transition = (TransitionDrawable) retDrawable;
+ retDrawable = transition.getDrawable(transition.getNumberOfLayers() - 1);
+ }
+
+ return retDrawable;
+ }
+
+ /**
+ * Creates a transition drawable to Bitmap with params
+ * @param resources Android Resources!
+ * @param fromDrawable the drawable to transition from
+ * @param bitmap the bitmap to transition to
+ * @param fadeTime the fade time in MS to fade in
+ * @param dither setting
+ * @param force force create a transition even if bitmap == null (fade to transparent)
+ * @return the drawable if created, null otherwise
+ */
+ public static TransitionDrawable createImageTransitionDrawable(final Resources resources,
+ final Drawable fromDrawable, final Bitmap bitmap, final int fadeTime,
+ final boolean dither, final boolean force) {
+ if (bitmap != null || force) {
+ final Drawable[] arrayDrawable = new Drawable[2];
+ arrayDrawable[0] = getTopDrawable(fromDrawable);
+
+ // Add the transition to drawable
+ Drawable layerTwo;
+ if (bitmap != null) {
+ layerTwo = new BitmapDrawable(resources, bitmap);
+ layerTwo.setFilterBitmap(false);
+ layerTwo.setDither(dither);
+ } else {
+ // if no bitmap (forced) then transition to transparent
+ layerTwo = new ColorDrawable(Color.TRANSPARENT);
+ }
+
+ arrayDrawable[1] = layerTwo;
+
+ // Finally, return the image
+ final TransitionDrawable result = new TransitionDrawable(arrayDrawable);
+ result.setCrossFadeEnabled(true);
+ result.startTransition(fadeTime);
+ return result;
+ }
+
+ return null;
+ }
+
+ /**
+ * This will create the palette transition from the original color to the new one
+ * @param scrimImage the container to change the color for
+ * @param color the color to transition to
+ * @return the transition to run
+ */
+ public static TransitionDrawable createPaletteTransition(BlurScrimImage scrimImage, int color) {
+ final Drawable[] arrayDrawable = new Drawable[2];
+ arrayDrawable[0] = getTopDrawable(scrimImage.getBackground());
+
+ if (arrayDrawable[0] == null) {
+ arrayDrawable[0] = new ColorDrawable(Color.TRANSPARENT);
+ }
+
+ arrayDrawable[1] = new ColorDrawable(color);
+
+ // create the transition
+ final TransitionDrawable result = new TransitionDrawable(arrayDrawable);
+ result.setCrossFadeEnabled(true);
+ result.startTransition(FADE_IN_TIME_SLOW);
+ return result;
+ }
+
+ /**
+ * Cancels and clears out any pending bitmap worker tasks on this image view
+ * @param image ImageView/BlurScrimImage to check
+ */
+ public static final void cancelWork(final View 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);
+ }
+
+ // clear out the tag
+ image.setTag(null);
+ }
+ }
+
+ /**
+ * Returns false if the existing async task is loading the same key value
+ * Returns true otherwise and also cancels the async task if one exists
+ */
+ public static final boolean executePotentialWork(final String key, final View view) {
+ final AsyncTaskContainer asyncTaskContainer = getAsyncTaskContainer(view);
+ if (asyncTaskContainer != null) {
+ // we are trying to reload the same image, return false to indicate no work is needed
+ if (asyncTaskContainer.getKey().equals(key)) {
+ return false;
+ }
+
+ // since we don't match, cancel the work and switch to the new worker task
+ cancelWork(view);
+ }
+
+ return true;
+ }
+
+ /**
+ * Used to determine if the current image drawable has an instance of
+ * {@link AsyncTaskContainer}
+ *
+ * @param view Any {@link View} that either is or contains an ImageView.
+ * @return Retrieve the AsyncTaskContainer assigned to the {@link View}. null if there is no
+ * such task.
+ */
+ public static final AsyncTaskContainer getAsyncTaskContainer(final View view) {
+ if (view != null) {
+ if (view.getTag() instanceof AsyncTaskContainer) {
+ return (AsyncTaskContainer) view.getTag();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Used to determine if the current image drawable has an instance of
+ * {@link BitmapWorkerTask}. A {@link BitmapWorkerTask} may not exist even if the {@link
+ * AsyncTaskContainer} does as it may have finished its work
+ *
+ * @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 View}. null if there is no such task.
+ */
+ public static final BitmapWorkerTask getBitmapWorkerTask(final View view) {
+ AsyncTaskContainer asyncTask = getAsyncTaskContainer(view);
+ if (asyncTask != null) {
+ return asyncTask.getBitmapWorkerTask();
+ }
+
+ return null;
+ }
+
+ /**
+ * A custom {@link BitmapDrawable} that will be attached to the
+ * {@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.
+ */
+ public static final class AsyncTaskContainer {
+
+ private final WeakReference<BitmapWorkerTask> mBitmapWorkerTaskReference;
+ // keep a copy of the key in case the worker task mBitmapWorkerTaskReference is released
+ // after completion
+ private String mKey;
+
+ /**
+ * Constructor of <code>AsyncDrawable</code>
+ */
+ public AsyncTaskContainer(final BitmapWorkerTask bitmapWorkerTask) {
+ mBitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
+ mKey = bitmapWorkerTask.mKey;
+ }
+
+ /**
+ * @return The {@link BitmapWorkerTask} associated with this drawable
+ */
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return mBitmapWorkerTaskReference.get();
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+ }
+
+ /**
+ * Loads the default image into the image view given the image type
+ * @param imageView The {@link ImageView}
+ * @param imageType The type of image
+ */
+ public void loadDefaultImage(final ImageView imageView, final ImageType imageType,
+ final String name, final String identifier) {
+ if (imageView != null) {
+ // if an existing letter drawable exists, re-use it
+ Drawable existingDrawable = imageView.getDrawable();
+ if (existingDrawable != null && existingDrawable instanceof LetterTileDrawable) {
+ ((LetterTileDrawable)existingDrawable).setTileDetails(name, identifier, imageType);
+ } else {
+ imageView.setImageDrawable(getNewDrawable(imageType, name,
+ identifier));
+ }
+ }
+ }
+
+ /**
+ * 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.
+ * @param albumName The album name for the Last.fm API.
+ * @param albumId The album art index, to check for missing artwork.
+ * @param imageView The {@link ImageView} used to set the cached
+ * {@link Bitmap}.
+ * @param imageType The type of image URL to fetch for.
+ */
+ protected void loadImage(final String key, final String artistName, final String albumName,
+ final long albumId, final ImageView imageView, final ImageType imageType) {
+
+ loadImage(key, artistName, albumName, albumId, imageView, imageType, false);
+ }
+
+ /**
+ * 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.
+ * @param albumName The album name for the Last.fm API.
+ * @param albumId The album art index, to check for missing artwork.
+ * @param imageView The {@link ImageView} used to set the cached
+ * {@link Bitmap}.
+ * @param imageType The type of image URL to fetch for.
+ * @param scaleImgToView config option to scale the image to the image view's dimensions
+ */
+ protected void loadImage(final String key, final String artistName, final String albumName,
+ final long albumId, final ImageView imageView,
+ final ImageType imageType, final boolean scaleImgToView) {
+
+ if (key == null || mImageCache == null || imageView == null) {
+ return;
+ }
+
+ // First, check the memory for the image
+ final Bitmap lruBitmap = mImageCache.getBitmapFromMemCache(key);
+ if (lruBitmap != null) { // Bitmap found in memory cache
+ // scale image if necessary
+ if (scaleImgToView) {
+ imageView.setImageBitmap(ImageUtils.scaleBitmapForImageView(lruBitmap, imageView));
+ } else {
+ imageView.setImageBitmap(lruBitmap);
+ }
+ } else {
+ // load the default image
+ if (imageType == ImageType.ARTIST) {
+ loadDefaultImage(imageView, imageType, artistName, key);
+ } else if (imageType == ImageType.ALBUM) {
+ // don't show letters for albums so pass in null as the display string
+ // because an album could have multiple artists, use the album id as the key here
+ loadDefaultImage(imageView, imageType, null, String.valueOf(albumId));
+ } else {
+ // don't show letters for playlists so pass in null as the display string
+ loadDefaultImage(imageView, imageType, null, key);
+ }
+
+ if (executePotentialWork(key, imageView)
+ && imageView != null && !mImageCache.isDiskCachePaused()) {
+ Drawable fromDrawable = imageView.getDrawable();
+ if (fromDrawable == null) {
+ fromDrawable = mTransparentDrawable;
+ }
+
+ // Otherwise run the worker task
+ final SimpleBitmapWorkerTask bitmapWorkerTask = new SimpleBitmapWorkerTask(key,
+ imageView, imageType, fromDrawable, mContext, scaleImgToView);
+
+ final AsyncTaskContainer asyncTaskContainer = new AsyncTaskContainer(bitmapWorkerTask);
+ imageView.setTag(asyncTaskContainer);
+ try {
+ ApolloUtils.execute(false, bitmapWorkerTask,
+ artistName, albumName, String.valueOf(albumId));
+ } catch (RejectedExecutionException e) {
+ // 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 {
+ // load the default image
+ loadDefaultImage(imageView, ImageType.PLAYLIST, null, String.valueOf(playlistId));
+ }
+
+ // 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()) {
+ // since a playlist's image can change based on changes to the playlist
+ // set the from drawable to be the existing image (if it exists) instead of transparent
+ // and fade from there
+ Drawable fromDrawable = imageView.getDrawable();
+ if (fromDrawable == null) {
+ fromDrawable = mTransparentDrawable;
+ }
+
+ // Otherwise run the worker task
+ final PlaylistWorkerTask bitmapWorkerTask = new PlaylistWorkerTask(key, playlistId, type,
+ lruBitmap != null, imageView, fromDrawable, 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.
+ * @param artistName The artist name for the Last.fm API.
+ * @param albumName The album name for the Last.fm API.
+ * @param albumId The album art index, to check for missing artwork.
+ * @param blurScrimImage The {@link BlurScrimImage} used to set the cached
+ * {@link Bitmap}.
+ * @param imageType The type of image URL to fetch for.
+ */
+ protected void loadBlurImage(final String key, final String artistName, final String albumName,
+ final long albumId, final BlurScrimImage blurScrimImage, final ImageType imageType) {
+ if (key == null || mImageCache == null || blurScrimImage == null) {
+ return;
+ }
+
+ if (executePotentialWork(key, blurScrimImage) && !mImageCache.isDiskCachePaused()) {
+ // Otherwise run the worker task
+ 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, artistName, albumName, String.valueOf(albumId));
+ } catch (RejectedExecutionException e) {
+ // Executor has exhausted queue space, show default artwork
+ blurScrimImage.transitionToDefaultState();
+ }
+ }
+ }
+
+ /**
+ * Used to define what type of image URL to fetch for, artist or album.
+ */
+ public enum ImageType {
+ ARTIST, ALBUM, PLAYLIST;
+ }
+}
diff --git a/src/com/cyanogenmod/eleven/cache/LruCache.java b/src/com/cyanogenmod/eleven/cache/LruCache.java
new file mode 100644
index 0000000..2144b93
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/LruCache.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.cyanogenmod.eleven.cache;
+
+// NOTE: upstream of this class is android.util.LruCache, changes below
+// expose trimToSize() to be called externally.
+
+import android.annotation.SuppressLint;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Static library version of {@link android.util.LruCache}. Used to write apps
+ * that run on API levels prior to 12. When running on API level 12 or above,
+ * this implementation is still used; it does not try to switch to the
+ * framework's implementation. See the framework SDK documentation for a class
+ * overview.
+ */
+public class LruCache<K, V> {
+
+ private final LinkedHashMap<K, V> map;
+
+ private final int maxSize;
+
+ /** Size of this cache in units. Not necessarily the number of elements. */
+ private int size;
+
+ private int putCount;
+
+ private int createCount;
+
+ private int evictionCount;
+
+ private int hitCount;
+
+ private int missCount;
+
+ /**
+ * @param maxSize for caches that do not override {@link #sizeOf}, this is
+ * the maximum number of entries in the cache. For all other
+ * caches, this is the maximum sum of the sizes of the entries in
+ * this cache.
+ */
+ public LruCache(final int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ this.maxSize = maxSize;
+ this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
+ }
+
+ /**
+ * Returns the value for {@code key} if it exists in the cache or can be
+ * created by {@code #create}. If a value was returned, it is moved to the
+ * head of the queue. This returns null if a value is not cached and cannot
+ * be created.
+ */
+ public final V get(final K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V mapValue;
+ synchronized (this) {
+ mapValue = map.get(key);
+ if (mapValue != null) {
+ this.hitCount++;
+ return mapValue;
+ }
+ this.missCount++;
+ }
+
+ /*
+ * Attempt to create a value. This may take a long time, and the map may
+ * be different when create() returns. If a conflicting value was added
+ * to the map while create() was working, we leave that value in the map
+ * and release the created value.
+ */
+
+ final V createdValue = create(key);
+ if (createdValue == null) {
+ return null;
+ }
+
+ synchronized (this) {
+ this.createCount++;
+ mapValue = map.put(key, createdValue);
+
+ if (mapValue != null) {
+ /* There was a conflict so undo that last put */
+ this.map.put(key, mapValue);
+ } else {
+ this.size += safeSizeOf(key, createdValue);
+ }
+ }
+
+ if (mapValue != null) {
+ entryRemoved(false, key, createdValue, mapValue);
+ return mapValue;
+ } else {
+ trimToSize(maxSize);
+ return createdValue;
+ }
+ }
+
+ /**
+ * Caches {@code value} for {@code key}. The value is moved to the head of
+ * the queue.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V put(final K key, final V value) {
+ if (key == null || value == null) {
+ throw new NullPointerException("key == null || value == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ this.putCount++;
+ this.size += safeSizeOf(key, value);
+ previous = this.map.put(key, value);
+ if (previous != null) {
+ this.size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, value);
+ }
+
+ trimToSize(maxSize);
+ return previous;
+ }
+
+ /**
+ * @param maxSize the maximum size of the cache before returning. May be -1
+ * to evict even 0-sized elements.
+ */
+ public void trimToSize(final int maxSize) {
+ while (true) {
+ K key;
+ V value;
+ synchronized (this) {
+ if (this.size < 0 || this.map.isEmpty() && size != 0) {
+ throw new IllegalStateException(getClass().getName()
+ + ".sizeOf() is reporting inconsistent results!");
+ }
+
+ if (this.size <= maxSize || this.map.isEmpty()) {
+ break;
+ }
+
+ final Map.Entry<K, V> toEvict = this.map.entrySet().iterator().next();
+ key = toEvict.getKey();
+ value = toEvict.getValue();
+ this.map.remove(key);
+ this.size -= safeSizeOf(key, value);
+ this.evictionCount++;
+ }
+
+ entryRemoved(true, key, value, null);
+ }
+ }
+
+ /**
+ * Removes the entry for {@code key} if it exists.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V remove(final K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ previous = this.map.remove(key);
+ if (previous != null) {
+ this.size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, null);
+ }
+
+ return previous;
+ }
+
+ /**
+ * Called for entries that have been evicted or removed. This method is
+ * invoked when a value is evicted to make space, removed by a call to
+ * {@link #remove}, or replaced by a call to {@link #put}. The default
+ * implementation does nothing.
+ * <p>
+ * The method is called without synchronization: other threads may access
+ * the cache while this method is executing.
+ *
+ * @param evicted true if the entry is being removed to make space, false if
+ * the removal was caused by a {@link #put} or {@link #remove}.
+ * @param newValue the new value for {@code key}, if it exists. If non-null,
+ * this removal was caused by a {@link #put}. Otherwise it was
+ * caused by an eviction or a {@link #remove}.
+ */
+ protected void entryRemoved(final boolean evicted, final K key, final V oldValue,
+ final V newValue) {
+ }
+
+ /**
+ * Called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The
+ * default implementation returns null.
+ * <p>
+ * The method is called without synchronization: other threads may access
+ * the cache while this method is executing.
+ * <p>
+ * If a value for {@code key} exists in the cache when this method returns,
+ * the created value will be released with {@link #entryRemoved} and
+ * discarded. This can occur when multiple threads request the same key at
+ * the same time (causing multiple values to be created), or when one thread
+ * calls {@link #put} while another is creating a value for the same key.
+ */
+ protected V create(final K key) {
+ return null;
+ }
+
+ private int safeSizeOf(final K key, final V value) {
+ final int result = sizeOf(key, value);
+ if (result < 0) {
+ throw new IllegalStateException("Negative size: " + key + "=" + value);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the size of the entry for {@code key} and {@code value} in
+ * user-defined units. The default implementation returns 1 so that size is
+ * the number of entries and max size is the maximum number of entries.
+ * <p>
+ * An entry's size must not change while it is in the cache.
+ */
+ protected int sizeOf(final K key, final V value) {
+ return 1;
+ }
+
+ /**
+ * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+ */
+ public final void evictAll() {
+ trimToSize(-1); // -1 will evict 0-sized elements
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the number
+ * of entries in the cache. For all other caches, this returns the sum of
+ * the sizes of the entries in this cache.
+ */
+ public synchronized final int size() {
+ return this.size;
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the maximum
+ * number of entries in the cache. For all other caches, this returns the
+ * maximum sum of the sizes of the entries in this cache.
+ */
+ public synchronized final int maxSize() {
+ return this.maxSize;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned a value.
+ */
+ public synchronized final int hitCount() {
+ return this.hitCount;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned null or required a new
+ * value to be created.
+ */
+ public synchronized final int missCount() {
+ return this.missCount;
+ }
+
+ /**
+ * Returns the number of times {@link #create(Object)} returned a value.
+ */
+ public synchronized final int createCount() {
+ return this.createCount;
+ }
+
+ /**
+ * Returns the number of times {@link #put} was called.
+ */
+ public synchronized final int putCount() {
+ return this.putCount;
+ }
+
+ /**
+ * Returns the number of values that have been evicted.
+ */
+ public synchronized final int evictionCount() {
+ return this.evictionCount;
+ }
+
+ /**
+ * Returns a copy of the current contents of the cache, ordered from least
+ * recently accessed to most recently accessed.
+ */
+ public synchronized final Map<K, V> snapshot() {
+ return new LinkedHashMap<K, V>(this.map);
+ }
+
+ @SuppressLint("DefaultLocale")
+ @Override
+ public synchronized final String toString() {
+ final int accesses = this.hitCount + this.missCount;
+ final int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0;
+ return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize,
+ this.hitCount, this.missCount, hitPercent);
+ }
+}
diff --git a/src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java b/src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java
new file mode 100644
index 0000000..c35ad00
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java
@@ -0,0 +1,383 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package com.cyanogenmod.eleven.cache;
+
+import android.content.Context;
+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.cyanogenmod.eleven.R;
+import com.cyanogenmod.eleven.cache.ImageWorker.ImageType;
+import com.cyanogenmod.eleven.loaders.PlaylistSongLoader;
+import com.cyanogenmod.eleven.loaders.SortedCursor;
+import com.cyanogenmod.eleven.provider.PlaylistArtworkStore;
+import com.cyanogenmod.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;
+
+ // because a cached image can be loaded, we use this flag to signal to remove that default image
+ protected boolean mFallbackToDefaultImage;
+
+ /**
+ * 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;
+ mFallbackToDefaultImage = false;
+ }
+
+ /**
+ * {@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 don't need an update, return something
+ if (!needsUpdate) {
+ if (bitmap != null) {
+ // if we found a bitmap, return it
+ return createImageTransitionDrawable(bitmap);
+ } else {
+ // otherwise return null since we don't need an update
+ return null;
+ }
+ }
+
+ // otherwise re-run the logic to get the bitmap
+ Cursor sortedCursor = null;
+
+ try {
+ // get the top songs for our playlist
+ sortedCursor = getTopSongsForPlaylist();
+
+ if (isCancelled()) {
+ return null;
+ }
+
+ if (sortedCursor == null || sortedCursor.getCount() == 0) {
+ // if all songs were removed from the playlist, update the last updated time
+ // and reset to the default art
+ if (mWorkerType == PlaylistWorkerType.Artist) {
+ // update the timestamp
+ mPlaylistStore.updateArtistArt(mPlaylistId);
+ // remove the cached image
+ mImageCache.removeFromCache(PlaylistArtworkStore.getArtistCacheKey(mPlaylistId));
+ // revert back to default image
+ mFallbackToDefaultImage = true;
+ } else if (mWorkerType == PlaylistWorkerType.CoverArt) {
+ // update the timestamp
+ mPlaylistStore.updateCoverArt(mPlaylistId);
+ // remove the cached image
+ mImageCache.removeFromCache(PlaylistArtworkStore.getCoverCacheKey(mPlaylistId));
+ // revert back to default image
+ mFallbackToDefaultImage = true;
+ }
+ } else 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);
+
+ if (bitmap == null) {
+ // if we can't find any artist images, try loading the top songs image
+ bitmap = mImageCache.getCachedBitmap(
+ PlaylistArtworkStore.getCoverCacheKey(mPlaylistId));
+ }
+
+ if (bitmap != null) {
+ // add the image to the cache
+ mImageCache.addBitmapToCache(mKey, bitmap, true);
+ } else {
+ mImageCache.removeFromCache(mKey);
+ mFallbackToDefaultImage = true;
+ }
+
+ // store the fact that we ran this code into the db to prevent multiple re-runs
+ 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);
+
+ // 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.release();
+ combinedCanvas = null;
+ bitmap = combinedBitmap;
+ }
+ }
+
+ // store the fact that we ran this code into the db to prevent multiple re-runs
+ mPlaylistStore.updateCoverArt(mPlaylistId);
+
+ if (bitmap != null) {
+ // add the image to the cache
+ mImageCache.addBitmapToCache(mKey, bitmap, true);
+ } else {
+ mImageCache.removeFromCache(mKey);
+ mFallbackToDefaultImage = true;
+ }
+
+ return bitmap;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPostExecute(TransitionDrawable transitionDrawable) {
+ final ImageView imageView = getAttachedImageView();
+ if (imageView != null) {
+ if (transitionDrawable != null) {
+ imageView.setImageDrawable(transitionDrawable);
+ } else if (mFallbackToDefaultImage) {
+ ImageFetcher.getInstance(mContext).loadDefaultImage(imageView,
+ ImageType.PLAYLIST, null, String.valueOf(mPlaylistId));
+ }
+ }
+ }
+}
diff --git a/src/com/cyanogenmod/eleven/cache/SimpleBitmapWorkerTask.java b/src/com/cyanogenmod/eleven/cache/SimpleBitmapWorkerTask.java
new file mode 100644
index 0000000..027eb7f
--- /dev/null
+++ b/src/com/cyanogenmod/eleven/cache/SimpleBitmapWorkerTask.java
@@ -0,0 +1,88 @@
+/*
+* Copyright (C) 2014 The CyanogenMod Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package com.cyanogenmod.eleven.cache;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.widget.ImageView;
+import com.cyanogenmod.eleven.cache.ImageWorker.ImageType;
+import com.cyanogenmod.eleven.utils.ImageUtils;
+
+/**
+ * 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);
+ }
+
+ /**
+ * 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
+ * @param scaleImgToView flag to scale the bitmap to the image view bounds
+ */
+ public SimpleBitmapWorkerTask(final String key, final ImageView imageView, final ImageType imageType,
+ final Drawable fromDrawable, final Context context, final boolean scaleImgToView) {
+ super(key, imageView, imageType, fromDrawable, context, scaleImgToView);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected TransitionDrawable doInBackground(final String... params) {
+ if (isCancelled()) {
+ return null;
+ }
+
+ final Bitmap bitmap = getBitmapInBackground(params);
+ if (mScaleImgToView) {
+ Bitmap scaledBitmap = ImageUtils.scaleBitmapForImageView(bitmap, getAttachedImageView());
+ return createImageTransitionDrawable(scaledBitmap);
+ }
+ else
+ return createImageTransitionDrawable(bitmap);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPostExecute(TransitionDrawable transitionDrawable) {
+ final ImageView imageView = getAttachedImageView();
+ if (transitionDrawable != null && imageView != null) {
+ imageView.setImageDrawable(transitionDrawable);
+ } else if (imageView != null) {
+ imageView.setImageDrawable(mFromDrawable);
+ }
+ }
+}