/* * Copyright (C) 2017 Team Gateship-One * (Hendrik Borghorst & Frederik Luetkes) * * The AUTHORS.md file contains a detailed contributors list: * <https://github.com/gateship-one/odyssey/blob/master/AUTHORS.md> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package org.gateshipone.odyssey.artworkdatabase; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.preference.PreferenceManager; import android.util.Log; import com.android.volley.NetworkResponse; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.Response; import com.android.volley.VolleyError; import org.gateshipone.odyssey.R; import org.gateshipone.odyssey.artworkdatabase.network.LimitingRequestQueue; import org.gateshipone.odyssey.artworkdatabase.network.artprovider.FanartTVManager; import org.gateshipone.odyssey.artworkdatabase.network.artprovider.LastFMManager; import org.gateshipone.odyssey.artworkdatabase.network.artprovider.MusicBrainzManager; import org.gateshipone.odyssey.artworkdatabase.network.responses.AlbumFetchError; import org.gateshipone.odyssey.artworkdatabase.network.responses.AlbumImageResponse; import org.gateshipone.odyssey.artworkdatabase.network.responses.ArtistFetchError; import org.gateshipone.odyssey.artworkdatabase.network.responses.ArtistImageResponse; import org.gateshipone.odyssey.models.AlbumModel; import org.gateshipone.odyssey.models.ArtistModel; import org.gateshipone.odyssey.models.TrackModel; import org.gateshipone.odyssey.utils.MusicLibraryHelper; import org.json.JSONException; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.List; public class ArtworkManager implements ArtistFetchError, AlbumFetchError { private static final String TAG = ArtworkManager.class.getSimpleName(); /** * Maximmum size for either x or y of an image */ private static final int MAXIMUM_IMAGE_SIZE = 500; /** * Compression level if images are rescaled */ private static final int IMAGE_COMPRESSION_SETTING = 80; /** * Manager for the SQLite database handling */ private ArtworkDatabaseManager mDBManager; /** * List of observers that needs updating if a new ArtistImage is downloaded. */ private final ArrayList<onNewArtistImageListener> mArtistListeners; /** * List of observers that needs updating if a new AlbumImage is downloaded. */ private final ArrayList<onNewAlbumImageListener> mAlbumListeners; /** * Private static singleton instance that can be used by other classes via the * getInstance method. */ private static ArtworkManager mInstance; /** * Lists of {@link AlbumModel} objects used for bulk downloading. */ private final List<AlbumModel> mAlbumList = new ArrayList<>(); /** * Lists of {@link ArtistModel} objects used for bulk downloading. */ private final List<ArtistModel> mArtistList = new ArrayList<>(); /** * Current {@link AlbumModel} handled by the bulk downloading */ private AlbumModel mCurrentBulkAlbum = null; /** * Current {@link ArtistModel} handled by the bulk downloading */ private ArtistModel mCurrentBulkArtist = null; /** * Callback for the bulkdownload observer (s. {@link BulkDownloadService}) */ private BulkLoadingProgressCallback mBulkProgressCallback; /** * Settings string which artist download provider to use */ private String mArtistProvider; /** * Settings string which album download provider to use */ private String mAlbumProvider; /** * Settings value if artwork download is only allowed via wifi/wired connection. */ private boolean mWifiOnly; /* * Broadcast constants */ public static final String ACTION_NEW_ARTWORK_READY = "org.gateshipone.odyssey.action_new_artwork_ready"; public static final String INTENT_EXTRA_KEY_ARTIST_ID = "org.gateshipone.odyssey.extra.artist_id"; public static final String INTENT_EXTRA_KEY_ARTIST_NAME = "org.gateshipone.odyssey.extra.artist_name"; public static final String INTENT_EXTRA_KEY_ALBUM_ID = "org.gateshipone.odyssey.extra.album_id"; public static final String INTENT_EXTRA_KEY_ALBUM_NAME = "org.gateshipone.odyssey.extra.album_name"; public static final String INTENT_EXTRA_KEY_ALBUM_KEY = "org.gateshipone.odyssey.extra.album_key"; private ArtworkManager(Context context) { mDBManager = ArtworkDatabaseManager.getInstance(context); mArtistListeners = new ArrayList<>(); mAlbumListeners = new ArrayList<>(); ConnectionStateReceiver receiver = new ConnectionStateReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(receiver, filter); SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); mArtistProvider = sharedPref.getString(context.getString(R.string.pref_artist_provider_key), context.getString(R.string.pref_artwork_provider_artist_default)); mAlbumProvider = sharedPref.getString(context.getString(R.string.pref_album_provider_key), context.getString(R.string.pref_artwork_provider_album_default)); mWifiOnly = sharedPref.getBoolean(context.getString(R.string.pref_download_wifi_only_key), context.getResources().getBoolean(R.bool.pref_download_wifi_default)); } public static synchronized ArtworkManager getInstance(Context context) { if (null == mInstance) { mInstance = new ArtworkManager(context); } return mInstance; } public void setWifiOnly(boolean wifiOnly) { mWifiOnly = wifiOnly; } public void setAlbumProvider(String albumProvider) { mAlbumProvider = albumProvider; } public void setArtistProvider(String artistProvider) { mArtistProvider = artistProvider; } public void initialize(String artistProvider, String albumProvider, boolean wifiOnly) { mArtistProvider = artistProvider; mAlbumProvider = albumProvider; mWifiOnly = wifiOnly; } /** * Removes the image for the album and tries to reload it from the internet * @param album {@link AlbumModel} to reload the image for */ public void resetAlbumImage(final AlbumModel album, final Context context) { if (null == album) { return; } // Clear the old image mDBManager.removeAlbumImage(album); // Reload the image from the internet fetchAlbumImage(album,context); } /** * Removes the image for the artist and tries to reload it from the internet * @param artist {@link ArtistModel} to reload the image for */ public void resetArtistImage(final ArtistModel artist, final Context context) { if (null == artist) { return; } // Clear the old image mDBManager.removeArtistImage(artist); // Reload the image from the internet fetchArtistImage(artist, context); } public Bitmap getArtistImage(final ArtistModel artist) throws ImageNotFoundException { if (null == artist) { return null; } long artistID = artist.getArtistID(); byte[] image; /** * If no artist id is set for the album (possible with data set of Odyssey) check * the artist with name instead of id. */ if (artistID == -1) { image = mDBManager.getArtistImage(artist.getArtistName()); } else { image = mDBManager.getArtistImage(artistID); } // Checks if the database has an image for the requested artist if (null != image) { // Create a bitmap from the data blob in the database return BitmapFactory.decodeByteArray(image, 0, image.length); } return null; } public Bitmap getAlbumImage(final AlbumModel album) throws ImageNotFoundException { if (null == album) { return null; } // Get the id for the album, used to check in database long albumID = album.getAlbumID(); byte[] image; if (albumID == -1) { // Check if ID is available (should be the case). If not use the album name for // lookup. // FIXME use artistname also image = mDBManager.getAlbumImage(album.getAlbumName()); } else { // If id is available use it. image = mDBManager.getAlbumImage(album.getAlbumID()); } // Checks if the database has an image for the requested album if (null != image) { // Create a bitmap from the data blob in the database return BitmapFactory.decodeByteArray(image, 0, image.length); } return null; } public Bitmap getAlbumImage(final TrackModel track) throws ImageNotFoundException { if (null == track) { return null; } byte[] image; // FIXME use album artist as well. image = mDBManager.getAlbumImage(track.getTrackAlbumName()); // Checks if the database has an image for the requested album if (null != image) { // Create a bitmap from the data blob in the database return BitmapFactory.decodeByteArray(image, 0, image.length); } return null; } /** * Starts an asynchronous fetch for the image of the given artist. * * @param artist Artist to fetch an image for. */ public void fetchArtistImage(final ArtistModel artist, final Context context) { if (!isDownloadAllowed(context)) { return; } if (mArtistProvider.equals(context.getString(R.string.pref_artwork_provider_lastfm_key))) { LastFMManager.getInstance(context).fetchArtistImage(artist, context, new Response.Listener<ArtistImageResponse>() { @Override public void onResponse(ArtistImageResponse response) { new InsertArtistImageTask(context).execute(response); } }, this); } else if (mArtistProvider.equals(context.getString(R.string.pref_artwork_provider_fanarttv_key))) { FanartTVManager.getInstance(context).fetchArtistImage(artist, context, new Response.Listener<ArtistImageResponse>() { @Override public void onResponse(ArtistImageResponse response) { new InsertArtistImageTask(context).execute(response); } }, this); } } /** * Starts an asynchronous fetch for the image of the given album * * @param album Album to fetch an image for. */ public void fetchAlbumImage(final AlbumModel album, final Context context) { if (!isDownloadAllowed(context)) { return; } if (mAlbumProvider.equals(context.getString(R.string.pref_artwork_provider_musicbrainz_key))) { MusicBrainzManager.getInstance(context).fetchAlbumImage(album, context, new Response.Listener<AlbumImageResponse>() { @Override public void onResponse(AlbumImageResponse response) { new InsertAlbumImageTask(context).execute(response); } }, this); } else if (mAlbumProvider.equals(context.getString(R.string.pref_artwork_provider_lastfm_key))) { LastFMManager.getInstance(context).fetchAlbumImage(album, context, new Response.Listener<AlbumImageResponse>() { @Override public void onResponse(AlbumImageResponse response) { new InsertAlbumImageTask(context).execute(response); } }, this); } } /** * Starts an asynchronous fetch for the image of the given album * * @param track Track to be used for image fetching */ public void fetchAlbumImage(final TrackModel track, final Context context) { if (!isDownloadAllowed(context)) { return; } // Create a dummy album AlbumModel album = new AlbumModel(track.getTrackAlbumName(), null, track.getTrackArtistName(), track.getTrackAlbumKey(), MusicLibraryHelper.getAlbumIDFromKey(track.getTrackAlbumKey(), context)); if (mAlbumProvider.equals(context.getString(R.string.pref_artwork_provider_musicbrainz_key))) { MusicBrainzManager.getInstance(context).fetchAlbumImage(album, context, new Response.Listener<AlbumImageResponse>() { @Override public void onResponse(AlbumImageResponse response) { new InsertAlbumImageTask(context).execute(response); } }, this); } else if (mAlbumProvider.equals(context.getString(R.string.pref_artwork_provider_lastfm_key))) { LastFMManager.getInstance(context).fetchAlbumImage(album, context, new Response.Listener<AlbumImageResponse>() { @Override public void onResponse(AlbumImageResponse response) { new InsertAlbumImageTask(context).execute(response); } }, this); } } /** * Registers a listener that gets notified when a new artist image was added to the dataset. * * @param listener Listener to register */ public void registerOnNewArtistImageListener(onNewArtistImageListener listener) { if (null != listener) { synchronized (mArtistListeners) { mArtistListeners.add(listener); } } } /** * Unregisters a listener that got notified when a new artist image was added to the dataset. * * @param listener Listener to unregister */ public void unregisterOnNewArtistImageListener(onNewArtistImageListener listener) { if (null != listener) { synchronized (mArtistListeners) { mArtistListeners.remove(listener); } } } /** * Registers a listener that gets notified when a new album image was added to the dataset. * * @param listener Listener to register */ public void registerOnNewAlbumImageListener(onNewAlbumImageListener listener) { if (null != listener) { synchronized (mArtistListeners) { mAlbumListeners.add(listener); } } } /** * Unregisters a listener that got notified when a new album image was added to the dataset. * * @param listener Listener to unregister */ public void unregisterOnNewAlbumImageListener(onNewAlbumImageListener listener) { if (null != listener) { synchronized (mArtistListeners) { mAlbumListeners.remove(listener); } } } /** * Interface implementation to handle errors during fetching of album images * * @param album Album that resulted in a fetch error */ @Override public void fetchJSONException(AlbumModel album, Context context, JSONException exception) { Log.e(TAG, "JSONException for album: " + album.getAlbumName() + "-" + album.getArtistName()); AlbumImageResponse imageResponse = new AlbumImageResponse(); imageResponse.album = album; imageResponse.image = null; imageResponse.url = null; new InsertAlbumImageTask(context).execute(imageResponse); } /** * Interface implementation to handle errors during fetching of album images * * @param album Album that resulted in a fetch error */ @Override public void fetchVolleyError(AlbumModel album, Context context, VolleyError error) { Log.e(TAG, "VolleyError for album: " + album.getAlbumName() + "-" + album.getArtistName()); if (error != null) { NetworkResponse networkResponse = error.networkResponse; if (networkResponse != null && networkResponse.statusCode == 503) { mAlbumList.clear(); cancelAllRequests(context); synchronized (mArtistList) { mArtistList.clear(); } if (mBulkProgressCallback != null) { mBulkProgressCallback.finishedLoading(); } return; } } AlbumImageResponse imageResponse = new AlbumImageResponse(); imageResponse.album = album; imageResponse.image = null; imageResponse.url = null; new InsertAlbumImageTask(context).execute(imageResponse); } /** * Interface implementation to handle errors during fetching of artist images * * @param artist Artist that resulted in a fetch error */ @Override public void fetchJSONException(ArtistModel artist, Context context, JSONException exception) { Log.e(TAG, "JSONException fetching: " + artist.getArtistName()); ArtistImageResponse imageResponse = new ArtistImageResponse(); imageResponse.artist = artist; imageResponse.image = null; imageResponse.url = null; new InsertArtistImageTask(context).execute(imageResponse); } /** * Interface implementation to handle errors during fetching of artist images * * @param artist Artist that resulted in a fetch error */ @Override public void fetchVolleyError(ArtistModel artist, Context context, VolleyError error) { Log.e(TAG, "VolleyError fetching: " + artist.getArtistName()); if (error != null) { NetworkResponse networkResponse = error.networkResponse; if (networkResponse != null && networkResponse.statusCode == 503) { mArtistList.clear(); cancelAllRequests(context); synchronized (mAlbumList) { mAlbumList.clear(); } if (mBulkProgressCallback != null) { mBulkProgressCallback.finishedLoading(); } return; } } ArtistImageResponse imageResponse = new ArtistImageResponse(); imageResponse.artist = artist; imageResponse.image = null; imageResponse.url = null; new InsertArtistImageTask(context).execute(imageResponse); } /** * Checks the current network state if an artwork download is allowed. * * @param context The current context to resolve the networkinfo * @return true if a download is allowed else false */ private boolean isDownloadAllowed(final Context context) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = cm.getActiveNetworkInfo(); if (networkInfo == null) { return false; } else { boolean isWifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET; return !(mWifiOnly && !isWifi); } } /** * AsyncTask to insert the images to the SQLdatabase. This is necessary as the Volley response * is handled in the UI thread. */ private class InsertArtistImageTask extends AsyncTask<ArtistImageResponse, Object, ArtistModel> { private final Context mContext; public InsertArtistImageTask(Context context) { mContext = context; } /** * Inserts the image to the database. * * @param params Pair of byte[] (containing the image itself) and ArtistModel for which the image is for * @return the artist model that was inserted to the database. */ @Override protected ArtistModel doInBackground(ArtistImageResponse... params) { ArtistImageResponse response = params[0]; if (mCurrentBulkArtist == response.artist) { fetchNextBulkArtist(mContext); } if (response.image == null) { mDBManager.insertArtistImage(response.artist, null, mContext); return response.artist; } // Rescale them if to big BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(response.image, 0, response.image.length, options); if ((options.outHeight > MAXIMUM_IMAGE_SIZE || options.outWidth > MAXIMUM_IMAGE_SIZE)) { options.inJustDecodeBounds = false; Bitmap bm = BitmapFactory.decodeByteArray(response.image, 0, response.image.length, options); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); Bitmap.createScaledBitmap(bm, MAXIMUM_IMAGE_SIZE, MAXIMUM_IMAGE_SIZE, true).compress(Bitmap.CompressFormat.JPEG, IMAGE_COMPRESSION_SETTING, byteStream); mDBManager.insertArtistImage(response.artist, byteStream.toByteArray(), mContext); } else { mDBManager.insertArtistImage(response.artist, response.image, mContext); } broadcastNewArtistImageInfo(response, mContext); return response.artist; } /** * Notifies the listeners about a change in the image dataset. Called in the UI thread. * * @param result Artist that was inserted in the database */ protected void onPostExecute(ArtistModel result) { synchronized (mArtistListeners) { for (onNewArtistImageListener artistListener : mArtistListeners) { artistListener.newArtistImage(result); } } } } /** * AsyncTask to insert the images to the SQLdatabase. This is necessary as the Volley response * is handled in the UI thread. */ private class InsertAlbumImageTask extends AsyncTask<AlbumImageResponse, Object, AlbumModel> { private final Context mContext; public InsertAlbumImageTask(Context context) { mContext = context; } /** * Inserts the image to the database. * * @param params Pair of byte[] (containing the image itself) and AlbumModel for which the image is for * @return the album model that was inserted to the database. */ @Override protected AlbumModel doInBackground(AlbumImageResponse... params) { AlbumImageResponse response = params[0]; if (mCurrentBulkAlbum == response.album) { fetchNextBulkAlbum(mContext); } if (response.image == null) { mDBManager.insertAlbumImage(response.album, null); return response.album; } // Rescale them if to big BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(response.image, 0, response.image.length, options); if ((options.outHeight > MAXIMUM_IMAGE_SIZE || options.outWidth > MAXIMUM_IMAGE_SIZE)) { options.inJustDecodeBounds = false; Bitmap bm = BitmapFactory.decodeByteArray(response.image, 0, response.image.length, options); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); Bitmap.createScaledBitmap(bm, MAXIMUM_IMAGE_SIZE, MAXIMUM_IMAGE_SIZE, true).compress(Bitmap.CompressFormat.JPEG, IMAGE_COMPRESSION_SETTING, byteStream); mDBManager.insertAlbumImage(response.album, byteStream.toByteArray()); } else { mDBManager.insertAlbumImage(response.album, response.image); } broadcastNewAlbumImageInfo(response, mContext); return response.album; } /** * Notifies the listeners about a change in the image dataset. Called in the UI thread. * * @param result Album that was inserted in the database */ protected void onPostExecute(AlbumModel result) { synchronized (mAlbumListeners) { for (onNewAlbumImageListener albumListener : mAlbumListeners) { albumListener.newAlbumImage(result); } } } } /** * Used to broadcast information about new available artwork to {@link BroadcastReceiver} like * the {@link org.gateshipone.odyssey.widget.OdysseyWidgetProvider} to reload its artwork. * * @param artistImage Image response containing the artist that an image was inserted for. * @param context Context used for broadcasting */ private void broadcastNewArtistImageInfo(ArtistImageResponse artistImage, Context context) { Intent newImageIntent = new Intent(ACTION_NEW_ARTWORK_READY); newImageIntent.putExtra(INTENT_EXTRA_KEY_ARTIST_ID, artistImage.artist.getArtistID()); newImageIntent.putExtra(INTENT_EXTRA_KEY_ARTIST_NAME, artistImage.artist.getArtistName()); context.sendBroadcast(newImageIntent); } /** * Used to broadcast information about new available artwork to {@link BroadcastReceiver} like * the {@link org.gateshipone.odyssey.widget.OdysseyWidgetProvider} to reload its artwork. * * @param albumImage Image response containing the albums that an image was inserted for. * @param context Context used for broadcasting */ private void broadcastNewAlbumImageInfo(AlbumImageResponse albumImage, Context context) { Intent newImageIntent = new Intent(ACTION_NEW_ARTWORK_READY); newImageIntent.putExtra(INTENT_EXTRA_KEY_ALBUM_ID, albumImage.album.getAlbumID()); newImageIntent.putExtra(INTENT_EXTRA_KEY_ALBUM_KEY, albumImage.album.getAlbumKey()); newImageIntent.putExtra(INTENT_EXTRA_KEY_ALBUM_NAME, albumImage.album.getAlbumName()); context.sendBroadcast(newImageIntent); } /** * Interface used for adapters to be notified about data set changes */ public interface onNewArtistImageListener { void newArtistImage(ArtistModel artist); } /** * Interface used for adapters to be notified about data set changes */ public interface onNewAlbumImageListener { void newAlbumImage(AlbumModel album); } private class ConnectionStateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!isDownloadAllowed(context)) { // Cancel all downloads Log.v(TAG, "Cancel all downloads because of connection change"); cancelAllRequests(context); } } } /** * This will cancel the last used album/artist image providers. To make this useful on connection change * it is important to cancel all requests when changing the provider in settings. */ public void cancelAllRequests(Context context) { LimitingRequestQueue.getInstance(context).cancelAll(new RequestQueue.RequestFilter() { @Override public boolean apply(Request<?> request) { return true; } }); } public void bulkLoadImages(BulkLoadingProgressCallback progressCallback, Context context) { if (progressCallback == null) { return; } mBulkProgressCallback = progressCallback; mArtistList.clear(); mAlbumList.clear(); Log.v(TAG, "Start bulk loading"); if (!mAlbumProvider.equals(context.getString((R.string.pref_artwork_provider_none_key)))) { List<AlbumModel> albums = MusicLibraryHelper.getAllAlbums(context); new ParseAlbumListTask(context).execute(albums); } if (!mArtistProvider.equals(context.getString((R.string.pref_artwork_provider_none_key)))) { List<ArtistModel> artists = MusicLibraryHelper.getAllArtists(false, context); new ParseArtistListTask(context).execute(artists); } } private class ParseAlbumListTask extends AsyncTask<List<AlbumModel>, Object, Object> { private final Context mContext; public ParseAlbumListTask(Context context) { mContext = context; } @SafeVarargs @Override protected final Object doInBackground(List<AlbumModel>... lists) { List<AlbumModel> albumList = lists[0]; mBulkProgressCallback.startAlbumLoading(albumList.size()); Log.v(TAG, "Received " + albumList.size() + " albums for bulk loading"); synchronized (mAlbumList) { mAlbumList.clear(); mAlbumList.addAll(albumList); } fetchNextBulkAlbum(mContext); return null; } } private class ParseArtistListTask extends AsyncTask<List<ArtistModel>, Object, Object> { private final Context mContext; public ParseArtistListTask(Context context) { mContext = context; } @SafeVarargs @Override protected final Object doInBackground(List<ArtistModel>... lists) { List<ArtistModel> artistList = lists[0]; Log.v(TAG, "Received " + artistList.size() + " artists for bulk loading"); mBulkProgressCallback.startArtistLoading(artistList.size()); synchronized (mArtistList) { mArtistList.clear(); mArtistList.addAll(artistList); } fetchNextBulkArtist(mContext); return null; } } private void fetchNextBulkAlbum(Context context) { boolean isEmpty; synchronized (mAlbumList) { isEmpty = mAlbumList.isEmpty(); } while (!isEmpty) { AlbumModel album; synchronized (mAlbumList) { album = mAlbumList.remove(0); Log.v(TAG, "Bulk load next album: " + album.getAlbumName() + ":" + album.getArtistName() + " remaining: " + mAlbumList.size()); mBulkProgressCallback.albumsRemaining(mAlbumList.size()); } mCurrentBulkAlbum = album; if (album.getAlbumArtURL() == null || album.getAlbumArtURL().isEmpty()) { // Check if image already there try { if (album.getAlbumID() != -1) { mDBManager.getAlbumImage(album.getAlbumID()); } else { mDBManager.getAlbumImage(album.getAlbumName()); } // If this does not throw the exception it already has an image. } catch (ImageNotFoundException e) { fetchAlbumImage(album, context); return; } } synchronized (mAlbumList) { isEmpty = mAlbumList.isEmpty(); } } if (mArtistList.isEmpty()) { mBulkProgressCallback.finishedLoading(); } } private void fetchNextBulkArtist(Context context) { boolean isEmpty; synchronized (mArtistList) { isEmpty = mArtistList.isEmpty(); } while (!isEmpty) { ArtistModel artist; synchronized (mArtistList) { artist = mArtistList.remove(0); Log.v(TAG, "Bulk load next artist: " + artist.getArtistName() + " remaining: " + mArtistList.size()); mBulkProgressCallback.artistsRemaining(mArtistList.size()); } mCurrentBulkArtist = artist; // Check if image already there try { if (artist.getArtistID() != -1) { mDBManager.getArtistImage(artist.getArtistID()); } else { mDBManager.getArtistImage(artist.getArtistName()); } // If this does not throw the exception it already has an image. } catch (ImageNotFoundException e) { fetchArtistImage(artist, context); return; } synchronized (mArtistList) { isEmpty = mArtistList.isEmpty(); } } if (mAlbumList.isEmpty()) { mBulkProgressCallback.finishedLoading(); } } public interface BulkLoadingProgressCallback { void startAlbumLoading(int albumCount); void startArtistLoading(int artistCount); void albumsRemaining(int remainingAlbums); void artistsRemaining(int remainingArtists); void finishedLoading(); } }