2 * Copyright (C) 2014 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org.lineageos.eleven.cache;
18 import android.content.Context;
19 import android.database.Cursor;
20 import android.graphics.Bitmap;
21 import android.graphics.Canvas;
22 import android.graphics.Rect;
23 import android.graphics.drawable.Drawable;
24 import android.graphics.drawable.TransitionDrawable;
25 import android.provider.MediaStore;
26 import android.widget.ImageView;
28 import org.lineageos.eleven.cache.ImageWorker.ImageType;
29 import org.lineageos.eleven.loaders.PlaylistSongLoader;
30 import org.lineageos.eleven.loaders.SortedCursor;
31 import org.lineageos.eleven.provider.PlaylistArtworkStore;
32 import org.lineageos.eleven.provider.SongPlayCount;
34 import java.util.ArrayList;
35 import java.util.HashSet;
38 * The playlistWorkerTask will load either the top artist image or the cover art (a combination of
39 * up to 4 of the top song's album images) into the designated ImageView. If not enough time has
40 * elapsed since the last update or if the # of songs in the playlist hasn't changed, no new images
43 public class PlaylistWorkerTask extends BitmapWorkerTask<Void, Void, TransitionDrawable> {
45 public enum PlaylistWorkerType {
49 // number of images to load in the cover art
50 private static final int MAX_NUM_BITMAPS_TO_LOAD = 4;
52 protected final long mPlaylistId;
53 protected final PlaylistArtworkStore mPlaylistStore;
54 protected final PlaylistWorkerType mWorkerType;
56 // if we've found it in the cache, don't do any more logic unless enough time has elapsed or
57 // if the playlist has changed
58 protected final boolean mFoundInCache;
60 // because a cached image can be loaded, we use this flag to signal to remove that default image
61 protected boolean mFallbackToDefaultImage;
64 * Constructor of <code>PlaylistWorkerTask</code>
65 * @param key the key of the image to store to
66 * @param playlistId the playlist identifier
67 * @param type Artist or CoverArt?
68 * @param foundInCache does this exist in the memory cache already
69 * @param imageView The {@link ImageView} to use.
70 * @param fromDrawable what drawable to transition from
72 public PlaylistWorkerTask(final String key, final long playlistId, final PlaylistWorkerType type,
73 final boolean foundInCache, final ImageView imageView,
74 final Drawable fromDrawable, final Context context) {
75 super(key, imageView, ImageType.PLAYLIST, fromDrawable, context);
77 mPlaylistId = playlistId;
79 mPlaylistStore = PlaylistArtworkStore.getInstance(mContext);
80 mFoundInCache = foundInCache;
81 mFallbackToDefaultImage = false;
88 protected TransitionDrawable doInBackground(final Void... params) {
95 // See if we need to update the image
96 boolean needsUpdate = false;
97 if (mWorkerType == PlaylistWorkerType.Artist
98 && mPlaylistStore.needsArtistArtUpdate(mPlaylistId)) {
100 } else if (mWorkerType == PlaylistWorkerType.CoverArt
101 && mPlaylistStore.needsCoverArtUpdate(mPlaylistId)) {
105 // if we don't need to update and we've already found it in the cache, then return
106 if (!needsUpdate && mFoundInCache) {
110 // if we didn't find it in memory cache, try the disk cache
111 if (!mFoundInCache) {
112 bitmap = mImageCache.getCachedBitmap(mKey);
115 // if we don't need an update, return something
117 if (bitmap != null) {
118 // if we found a bitmap, return it
119 return createImageTransitionDrawable(bitmap);
121 // otherwise return null since we don't need an update
126 // otherwise re-run the logic to get the bitmap
127 try (Cursor sortedCursor = getTopSongsForPlaylist()) {
128 // get the top songs for our playlist
134 if (sortedCursor == null || sortedCursor.getCount() == 0) {
135 // if all songs were removed from the playlist, update the last updated time
136 // and reset to the default art
137 if (mWorkerType == PlaylistWorkerType.Artist) {
138 // update the timestamp
139 mPlaylistStore.updateArtistArt(mPlaylistId);
140 // remove the cached image
141 mImageCache.removeFromCache(PlaylistArtworkStore.getArtistCacheKey(mPlaylistId));
142 // revert back to default image
143 mFallbackToDefaultImage = true;
144 } else if (mWorkerType == PlaylistWorkerType.CoverArt) {
145 // update the timestamp
146 mPlaylistStore.updateCoverArt(mPlaylistId);
147 // remove the cached image
148 mImageCache.removeFromCache(PlaylistArtworkStore.getCoverCacheKey(mPlaylistId));
149 // revert back to default image
150 mFallbackToDefaultImage = true;
152 } else if (mWorkerType == PlaylistWorkerType.Artist) {
153 bitmap = loadTopArtist(sortedCursor);
155 bitmap = loadTopSongs(sortedCursor);
159 // if we have a bitmap create a transition drawable
160 if (bitmap != null) {
161 return createImageTransitionDrawable(bitmap);
168 * This gets the sorted cursor of the songs from a playlist based on play count
169 * @return Cursor containing the sorted list
171 protected Cursor getTopSongsForPlaylist() {
172 Cursor playlistCursor = null;
173 SortedCursor sortedCursor = null;
176 // gets the songs in the playlist
177 playlistCursor = PlaylistSongLoader.makePlaylistSongCursor(mContext, mPlaylistId);
178 if (playlistCursor == null || !playlistCursor.moveToFirst()) {
182 // get all the ids in the list
183 long[] songIds = new long[playlistCursor.getCount()];
185 long id = playlistCursor.getLong(playlistCursor.getColumnIndex(
186 MediaStore.Audio.Playlists.Members.AUDIO_ID));
188 songIds[playlistCursor.getPosition()] = id;
189 } while (playlistCursor.moveToNext());
195 // find the sorted order for the playlist based on the top songs database
196 long[] order = SongPlayCount.getInstance(mContext).getTopPlayedResultsForList(songIds);
198 // create a new cursor that takes the playlist cursor and the sorted order
199 sortedCursor = new SortedCursor(playlistCursor, order,
200 MediaStore.Audio.Playlists.Members.AUDIO_ID, null);
202 // since this cursor is now wrapped by SortedTracksCursor, remove the reference here
203 // so we don't accidentally close it in the finally loop
204 playlistCursor = null;
206 // if we quit early from isCancelled(), close our cursor
207 if (playlistCursor != null) {
208 playlistCursor.close();
209 playlistCursor = null;
217 * Gets the most played song's artist image
218 * @param sortedCursor the sorted playlist song cursor
219 * @return Bitmap of the artist
221 protected Bitmap loadTopArtist(Cursor sortedCursor) {
222 if (sortedCursor == null || !sortedCursor.moveToFirst()) {
226 Bitmap bitmap = null;
227 int artistIndex = sortedCursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST);
228 String artistName = null;
235 artistName = sortedCursor.getString(artistIndex);
236 // try to load the bitmap
237 bitmap = ImageWorker.getBitmapInBackground(mContext, mImageCache, artistName,
238 null, artistName, -1, ImageType.ARTIST);
239 } while (sortedCursor.moveToNext() && bitmap == null);
241 if (bitmap == null) {
242 // if we can't find any artist images, try loading the top songs image
243 bitmap = mImageCache.getCachedBitmap(
244 PlaylistArtworkStore.getCoverCacheKey(mPlaylistId));
247 if (bitmap != null) {
248 // add the image to the cache
249 mImageCache.addBitmapToCache(mKey, bitmap, true);
251 mImageCache.removeFromCache(mKey);
252 mFallbackToDefaultImage = true;
255 // store the fact that we ran this code into the db to prevent multiple re-runs
256 mPlaylistStore.updateArtistArt(mPlaylistId);
262 * Gets the Cover Art of the playlist, which is a combination of the top song's album image
263 * @param sortedCursor the sorted playlist song cursor
264 * @return Bitmap of the artist
266 protected Bitmap loadTopSongs(Cursor sortedCursor) {
267 if (sortedCursor == null || !sortedCursor.moveToFirst()) {
271 ArrayList<Bitmap> loadedBitmaps = new ArrayList<>(MAX_NUM_BITMAPS_TO_LOAD);
273 final int artistIdx = sortedCursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST);
274 final int albumIdIdx = sortedCursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM_ID);
275 final int albumIdx = sortedCursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM);
277 Bitmap bitmap = null;
278 String artistName = null;
279 String albumName = null;
282 // create a hashset of the keys so we don't load images from the same album multiple times
283 HashSet<String> keys = new HashSet<>(sortedCursor.getCount());
290 artistName = sortedCursor.getString(artistIdx);
291 albumName = sortedCursor.getString(albumIdx);
292 albumId = sortedCursor.getLong(albumIdIdx);
294 String key = ImageFetcher.generateAlbumCacheKey(albumName, artistName);
296 // if we successfully added the key (ie the key didn't previously exist)
298 // try to load the bitmap
299 bitmap = ImageWorker.getBitmapInBackground(mContext, mImageCache,
300 key, albumName, artistName, albumId, ImageType.ALBUM);
302 // if we got the bitmap, add it to the list
303 if (bitmap != null) {
304 loadedBitmaps.add(bitmap);
308 } while (sortedCursor.moveToNext() && loadedBitmaps.size() < MAX_NUM_BITMAPS_TO_LOAD);
310 // if we found at least 1 bitmap
311 if (loadedBitmaps.size() > 0) {
312 // get the first bitmap
313 bitmap = loadedBitmaps.get(0);
315 // if we have many bitmaps
316 if (loadedBitmaps.size() == MAX_NUM_BITMAPS_TO_LOAD) {
317 // create a combined bitmap of the 4 images
318 final int width = bitmap.getWidth();
319 final int height = bitmap.getHeight();
320 Bitmap combinedBitmap = Bitmap.createBitmap(width, height,
322 Canvas combinedCanvas = new Canvas(combinedBitmap);
325 combinedCanvas.drawBitmap(loadedBitmaps.get(0), null,
326 new Rect(0, 0, width / 2, height / 2), null);
329 combinedCanvas.drawBitmap(loadedBitmaps.get(1), null,
330 new Rect(width / 2, 0, width, height / 2), null);
333 combinedCanvas.drawBitmap(loadedBitmaps.get(2), null,
334 new Rect(0, height / 2, width / 2, height), null);
337 combinedCanvas.drawBitmap(loadedBitmaps.get(3), null,
338 new Rect(width / 2, height / 2, width, height), null);
340 combinedCanvas = null;
341 bitmap = combinedBitmap;
345 // store the fact that we ran this code into the db to prevent multiple re-runs
346 mPlaylistStore.updateCoverArt(mPlaylistId);
348 if (bitmap != null) {
349 // add the image to the cache
350 mImageCache.addBitmapToCache(mKey, bitmap, true);
352 mImageCache.removeFromCache(mKey);
353 mFallbackToDefaultImage = true;
363 protected void onPostExecute(TransitionDrawable transitionDrawable) {
364 final ImageView imageView = getAttachedImageView();
365 if (imageView != null) {
366 if (transitionDrawable != null) {
367 imageView.setImageDrawable(transitionDrawable);
368 } else if (mFallbackToDefaultImage) {
369 ImageFetcher.getInstance(mContext).loadDefaultImage(imageView,
370 ImageType.PLAYLIST, null, String.valueOf(mPlaylistId));