/* * Copyright (C) 2014 Fastboot Mobile, LLC. * * 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 com.fastbootmobile.encore.providers; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.DeadObjectException; import android.os.Handler; import android.os.HandlerThread; import android.os.RemoteException; import android.os.TransactionTooLargeException; import android.util.Log; import android.widget.Toast; import com.fastbootmobile.encore.app.R; import com.fastbootmobile.encore.framework.PluginsLookup; import com.fastbootmobile.encore.model.Album; import com.fastbootmobile.encore.model.Artist; import com.fastbootmobile.encore.model.Genre; import com.fastbootmobile.encore.model.Playlist; import com.fastbootmobile.encore.model.SearchResult; import com.fastbootmobile.encore.model.Song; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; /** * Class federating all the information from providers */ public class ProviderAggregator extends IProviderCallback.Stub { private static final String TAG = "ProviderAggregator"; private static final int PROPAGATION_DELAY = 200; private static final boolean DEBUG = false; private final Map<String, List<SearchResult>> mCachedSearches; private final List<ILocalCallback> mUpdateCallbacks; private final List<ProviderConnection> mProviders; private ProviderCache mCache; private Handler mMainHandler; private HandlerThread mBackHandlerThread; private Handler mBackHandler; private final List<Song> mPostedUpdateSongs = new ArrayList<>(); private final List<Album> mPostedUpdateAlbums = new ArrayList<>(); private final List<Artist> mPostedUpdateArtists = new ArrayList<>(); private final List<Playlist> mPostedUpdatePlaylists = new ArrayList<>(); private List<String> mRosettaStonePrefix = new ArrayList<>(); private Map<String, ProviderIdentifier> mRosettaStoneMap = new HashMap<>(); private ThreadPoolExecutor mExecutor = new ScheduledThreadPoolExecutor(4); private Context mContext; private boolean mIsOfflineMode = false; private List<OfflineModeListener> mOfflineModeListeners = new ArrayList<>(); private Runnable mPostSongsRunnable = new Runnable() { @Override public void run() { synchronized (mPostedUpdateSongs) { synchronized (mUpdateCallbacks) { for (ILocalCallback cb : mUpdateCallbacks) { cb.onSongUpdate(new ArrayList<>(mPostedUpdateSongs)); } } mPostedUpdateSongs.clear(); } } }; private Runnable mPostAlbumsRunnable = new Runnable() { @Override public void run() { synchronized (mPostedUpdateAlbums) { synchronized (mUpdateCallbacks) { for (ILocalCallback cb : mUpdateCallbacks) { cb.onAlbumUpdate(new ArrayList<>(mPostedUpdateAlbums)); } } mPostedUpdateAlbums.clear(); } } }; private Runnable mPostArtistsRunnable = new Runnable() { @Override public void run() { synchronized (mPostedUpdateArtists) { synchronized (mUpdateCallbacks) { for (ILocalCallback cb : mUpdateCallbacks) { cb.onArtistUpdate(new ArrayList<>(mPostedUpdateArtists)); } } mPostedUpdateArtists.clear(); } } }; private Runnable mPostPlaylistsRunnable = new Runnable() { @Override public void run() { synchronized (mPostedUpdatePlaylists) { synchronized (mUpdateCallbacks) { for (ILocalCallback cb : mUpdateCallbacks) { cb.onPlaylistUpdate(new ArrayList<>(mPostedUpdatePlaylists)); } } mPostedUpdatePlaylists.clear(); } } }; private Runnable mUpdatePlaylistsRunnable = new Runnable() { @Override public void run() { // We make a copy to avoid synchronization issues and needless locks ArrayList<ProviderConnection> providers; synchronized (mProviders) { providers = new ArrayList<>(mProviders); } // Then we query the providers for (ProviderConnection conn : providers) { try { IMusicProvider binder = conn.getBinder(); if (binder != null && binder.isSetup() && binder.isAuthenticated()) { List<Playlist> playlist = binder.getPlaylists(); ensurePlaylistsSongsCached(conn, playlist); // Cache all songs in batch int offset = 0; int limit = 100; boolean goForIt = true; while (goForIt) { try { List<Song> songs = binder.getSongs(offset, limit); if (songs == null || songs.size() == 0) { goForIt = false; } else { cacheSongs(conn, songs); if (songs.size() < limit) { goForIt = false; } offset += limit; } } catch (TransactionTooLargeException ignore) { limit -= 10; Log.w(TAG, "Got transaction size error, reducing limit to " + limit); } } try { cacheAlbums(conn, binder.getAlbums()); } catch (Exception e) { Log.e(TAG, "Provider " + conn.getProviderName() + " threw an exception in getAlbums", e); } try { cacheArtists(conn, binder.getArtists()); } catch (Exception e) { Log.e(TAG, "Provider " + conn.getProviderName() + " threw an exception in getArtists", e); } } else if (conn.getBinder() != null) { Log.i(TAG, "Skipping a providers because it is not setup or authenticated" + " ==> binder=" + binder + " ; isSetup=" + binder.isSetup() + " ; isAuthenticated=" + binder.isAuthenticated()); } else { unregisterProvider(conn); } } catch (RemoteException e) { Log.e(TAG, "Unable to get data from " + conn.getProviderName(), e); unregisterProvider(conn); } } } }; // Singleton private final static ProviderAggregator INSTANCE = new ProviderAggregator(); public static ProviderAggregator getDefault() { return INSTANCE; } /** * Default constructor */ private ProviderAggregator() { mUpdateCallbacks = new ArrayList<>(); mProviders = new ArrayList<>(); mCache = new ProviderCache(); mMainHandler = new Handler(); mCachedSearches = new HashMap<>(); mBackHandlerThread = new HandlerThread("ProviderAggregator"); mBackHandlerThread.start(); mBackHandler = new Handler(mBackHandlerThread.getLooper()); } @Override protected void finalize() throws Throwable { mBackHandlerThread.interrupt(); super.finalize(); } public void setContext(Context ctx) { mContext = ctx; } /** * @return The data cache */ public ProviderCache getCache() { return mCache; } /** * Registers a LocalCallback class, which will be called when various events happen from * any of the registered providers. * * @param cb The callback to add */ public void addUpdateCallback(ILocalCallback cb) { synchronized (mUpdateCallbacks) { mUpdateCallbacks.add(cb); } } /** * Unregisters a local update callback * * @param cb The callback to remove */ public void removeUpdateCallback(ILocalCallback cb) { synchronized (mUpdateCallbacks) { mUpdateCallbacks.remove(cb); } } public void cacheSongs(final ProviderConnection provider, final List<Song> songs) { if (provider == null) return; mExecutor.execute(new Runnable() { @Override public void run() { for (Song song : songs) { mCache.putSong(provider.getIdentifier(), song); } } }); } public void cacheAlbums(final ProviderConnection provider, final List<Album> albums) { if (provider == null) return; mExecutor.execute(new Runnable() { @Override public void run() { for (Album album : albums) { if (album.getProvider() == null) { Log.e(TAG, "Album " + album.getRef() + " is being cached with a null provider!"); } mCache.putAlbum(provider.getIdentifier(), album); } } }); } public void cacheArtists(final ProviderConnection provider, final List<Artist> artists) { if (provider == null) { return; } mExecutor.execute(new Runnable() { @Override public void run() { for (Artist artist : artists) { try { onArtistUpdate(provider.getIdentifier(), artist); } catch (RemoteException e) { // ignore } } } }); } /** * Retrieves a song from the provider, and put it in the cache * * @param ref The reference to the song * @param provider The provider from which retrieve the song (may be null to query cache only) * @return The song, or null if the provider says so */ public Song retrieveSong(final String ref, final ProviderIdentifier provider) { if (ref == null) { // Force get stack trace try { throw new RuntimeException(); } catch (RuntimeException e) { Log.e(TAG, "retrieveSong called with a null reference", e); } return null; } // Try from cache Song output = mCache.getSong(ref); if (output == null && provider != null) { // Get from provider then ProviderConnection pc = PluginsLookup.getDefault().getProvider(provider); if (pc != null) { IMusicProvider binder = pc.getBinder(); if (binder != null) { try { output = binder.getSong(ref); if (output != null) { onSongUpdate(provider, output); } } catch (DeadObjectException e) { Log.e(TAG, "Provider died while retrieving song"); return null; } catch (RemoteException e) { Log.e(TAG, "Unable to retrieve the song", e); return null; } } else { if (DEBUG) Log.e(TAG, "Binder null: provider not yet connected?"); } } else { Log.e(TAG, "Unknown provider identifier: " + provider); } } if (output == null && provider != null) { Log.d(TAG, "Unable to get song " + ref + " from " + provider.mName); } return output; } /** * Retrieves an artist from the provider, and put it in the cache * * @param ref The reference to the artist * @param provider The provider from which retrieve the artist * @return The artist, or null if the provider says so */ public Artist retrieveArtist(final String ref, final ProviderIdentifier provider) { if (ref == null) { // Force get stack trace try { throw new RuntimeException(); } catch (RuntimeException e) { Log.e(TAG, "retrieveArtist called with a null reference", e); } return null; } // Try from cache Artist output = mCache.getArtist(ref); if (output == null && provider != null) { ProviderConnection pc = PluginsLookup.getDefault().getProvider(provider); if (pc != null) { IMusicProvider binder = pc.getBinder(); if (binder != null) { try { output = binder.getArtist(ref); onArtistUpdate(provider, output); } catch (DeadObjectException e) { Log.e(TAG, "Provider died while retrieving artist"); return null; } catch (RemoteException e) { Log.e(TAG, "Unable to retrieve the artist", e); return null; } } } } return output; } /** * Retrieves an album from the provider, and put it in the cache * * @param ref The reference to the album * @param provider The provider from which retrieve the album * @return The album, or null if the provider says so */ public Album retrieveAlbum(final String ref, final ProviderIdentifier provider) { if (ref == null) { // Force get stack trace try { throw new RuntimeException(); } catch (RuntimeException e) { Log.e(TAG, "retrieveAlbum called with a null reference", e); } return null; } // Try from cache Album output = mCache.getAlbum(ref); if (output == null && provider != null) { ProviderConnection pc = PluginsLookup.getDefault().getProvider(provider); if (pc != null) { IMusicProvider binder = pc.getBinder(); if (binder != null) { try { output = binder.getAlbum(ref); onAlbumUpdate(provider, output); } catch (DeadObjectException e) { Log.e(TAG, "Provider died while retrieving album"); } catch (RemoteException e) { Log.e(TAG, "Unable to retrieve the album", e); } } } } return output; } /** * Retrieves an album from the provider, and put it in the cache * * @param ref The reference to the album * @param provider The provider from which retrieve the album * @return The album, or null if the provider says so */ public Playlist retrievePlaylist(final String ref, final ProviderIdentifier provider) { if (ref == null) { // Force get stack trace try { throw new RuntimeException(); } catch (RuntimeException e) { Log.e(TAG, "retrievePlaylist called with a null reference", e); } return null; } // Try from cache Playlist output = mCache.getPlaylist(ref); if (output == null && provider != null) { ProviderConnection pc = PluginsLookup.getDefault().getProvider(provider); if (pc != null) { IMusicProvider binder = pc.getBinder(); if (binder != null) { try { output = binder.getPlaylist(ref); onPlaylistAddedOrUpdated(provider, output); } catch (RemoteException e) { Log.e(TAG, "Unable to retrieve the playlist", e); } } } } return output; } /** * Queries in a thread the songs of the list of playlist passed in parameter, if needed * Note that this method is only valid for playlists that have been provided by a provider. * * @param provider The provider that provided these playlists * @param playlist The list of playlists to fetch */ private void ensurePlaylistsSongsCached(final ProviderConnection provider, final List<Playlist> playlist) { if (provider == null || playlist == null) { // playlist may be null if there are no playlists Log.w(TAG, "Bailing playlist song caching because provider or playlist is null"); return; } mExecutor.execute(new Runnable() { @Override public void run() { IMusicProvider binder = provider.getBinder(); if (binder == null) { return; } for (Playlist p : playlist) { if (p == null || p.getName() == null) { continue; } if (p.getProvider() == null) { Log.w(TAG, "Playlist '" + p.getRef() + "' cached without identifier!"); } mCache.putPlaylist(provider.getIdentifier(), p); // Make sure we have references to all the songs in the playlist Iterator<String> songs = p.songs(); while (songs.hasNext()) { String songRef = songs.next(); // We first check that we don't already have the song in the cache Song cachedSong = mCache.getSong(songRef); if (cachedSong != null && cachedSong.isLoaded()) { // We already have that song, continue to the next one continue; } // Get the song from the provider Song song = null; try { song = binder.getSong(songRef); } catch (RemoteException e) { // ignore, provider likely died, we just skip its song } if (song != null) { mCache.putSong(provider.getIdentifier(), song); // We call the songUpdate callback only if the track has been loaded. // If it's not, we assume that the provider will call songUpdated // here when it has the data for the track. if (song.isLoaded()) { postSongForUpdate(song); } } } } } }); } /** * Registers a new providers service that has been bound, and add the aggregator as a callback * * @param provider The providers that connected */ public void registerProvider(final ProviderConnection provider) { mBackHandler.post(new Runnable() { @Override public void run() { synchronized (mProviders) { mProviders.add(provider); } try { IMusicProvider binder = provider.getBinder(); if (binder != null) { // Register this class as callback binder.registerCallback(ProviderAggregator.this); // Add all rosetta prefixes and map it to this provider List<String> rosettaPrefixes = binder.getSupportedRosettaPrefix(); if (rosettaPrefixes != null) { for (String prefix : rosettaPrefixes) { mRosettaStoneMap.put(prefix, provider.getIdentifier()); if (!mRosettaStonePrefix.contains(prefix)) { mRosettaStonePrefix.add(prefix); } } } // Notify subclasses of the new provider synchronized (mUpdateCallbacks) { for (ILocalCallback cb : mUpdateCallbacks) { cb.onProviderConnected(binder); } } } } catch (RemoteException e) { // Maybe the service died already? Log.e(TAG, "Unable to register as a callback", e); } } }); } /** * Removes the connection to a providers. This may be called either if the connection to a * service has been lost (e.g. in case of a DeadObjectException if the service crashed), or * simply if the app closes and a service is not needed anymore. * * @param provider The providers to remove */ public void unregisterProvider(final ProviderConnection provider) { mBackHandler.post(new Runnable() { @Override public void run() { synchronized (mProviders) { mProviders.remove(provider); try { IMusicProvider binder = provider.getBinder(); if (binder != null) { binder.unregisterCallback(ProviderAggregator.this); } } catch (RemoteException e) { // This may perfectly happen if the provider died. } } mCache.purgeCacheForProvider(provider.getIdentifier()); } }); } /** * Notify the providers that the offline mode has changed. Unlike isOfflineMode, this method * is only called when the user toggles Offline mode in the main activity overflow menu. * * @param isEnabled true if offline mode is enabled, false otherwise */ public void notifyOfflineMode(final boolean isEnabled) { mIsOfflineMode = isEnabled; mBackHandler.post(new Runnable() { @Override public void run() { synchronized (mProviders) { for (ProviderConnection provider : mProviders) { IMusicProvider binder = provider.getBinder(); try { if (binder != null) { binder.setOfflineMode(isEnabled); } } catch (RemoteException e) { Log.e(TAG, "Cannot change offline mode on " + provider, e); } } } } }); for (OfflineModeListener listener : mOfflineModeListeners) { listener.onOfflineModeChange(isEnabled); } } /** * Returns whether or not the device is offline. The value will be true if either the user * checked "Offline mode" in the main overflow menu, or no Internet connection has been * detected. * @return true if the device is offline (or user toggled offline mode), false otherwise */ public boolean isOfflineMode() { return mIsOfflineMode || !hasNetworkConnectivity(); } public void registerOfflineModeListener(OfflineModeListener listener) { mOfflineModeListeners.add(listener); } public void unregisterOfflineModeListener(OfflineModeListener listener) { mOfflineModeListeners.remove(listener); } /** * @return true if the device has an active internet connectivity, false otherwise */ public boolean hasNetworkConnectivity() { // Check network connectivity final ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo ni = cm.getActiveNetworkInfo(); return ni != null && ni.isConnected(); } /** * Starts a search. Results will be given in onSearchResults * @param query The terms to look for */ public void startSearch(final String query) { List<ProviderConnection> providers = PluginsLookup.getDefault().getAvailableProviders(); for (ProviderConnection providerConnection : providers) { try { final IMusicProvider binder = providerConnection.getBinder(); if (binder != null) { binder.startSearch(query); } else { Log.e(TAG, "Null binder, cannot search on " + providerConnection.getIdentifier()); } } catch (RemoteException e) { Log.e(TAG, "Cannot run search on a provider", e); } } } /** * Returns the list of all cached playlists. At the same time, providers will be called for * updates and/or fetching playlists, and LocalCallbacks will be called when providers notify * this class of eventual new entries. * * @return A list of playlists */ public List<Playlist> getAllPlaylists() { mBackHandler.removeCallbacks(mUpdatePlaylistsRunnable); mBackHandler.post(mUpdatePlaylistsRunnable); return mCache.getAllPlaylists(); } public List<Playlist> getAllMultiProviderPlaylists() { return mCache.getAllMultiProviderPlaylists(); } public void postSongForUpdate(Song s) { mBackHandler.removeCallbacks(mPostSongsRunnable); synchronized (mPostedUpdateSongs) { mPostedUpdateSongs.add(s); } mBackHandler.postDelayed(mPostSongsRunnable, PROPAGATION_DELAY); } public void postAlbumForUpdate(Album a) { mBackHandler.removeCallbacks(mPostAlbumsRunnable); synchronized (mPostedUpdateAlbums) { mPostedUpdateAlbums.add(a); } mBackHandler.postDelayed(mPostAlbumsRunnable, PROPAGATION_DELAY); } public void postArtistForUpdate(Artist a) { mBackHandler.removeCallbacks(mPostArtistsRunnable); synchronized (mPostedUpdateArtists) { mPostedUpdateArtists.add(a); } mBackHandler.postDelayed(mPostArtistsRunnable, PROPAGATION_DELAY); } public void postPlaylistForUpdate(Playlist p) { mBackHandler.removeCallbacks(mPostPlaylistsRunnable); synchronized (mPostedUpdatePlaylists) { mPostedUpdatePlaylists.add(p); } mBackHandler.postDelayed(mPostPlaylistsRunnable, PROPAGATION_DELAY); } public List<String> getRosettaStonePrefix() { return mRosettaStonePrefix; } public String getPreferredRosettaStonePrefix() { if (mRosettaStonePrefix != null && mRosettaStonePrefix.size() > 0) { // TODO: Let user choose return mRosettaStonePrefix.get(0); } else { return null; } } public ProviderIdentifier getRosettaStoneIdentifier(final String identifier) { return mRosettaStoneMap.get(identifier); } @Override public int getIdentifier() throws RemoteException { return this.hashCode(); } /** * Called by the providers when a feedback is available about a login request * * @param success Whether or not the login succeeded */ @Override public void onLoggedIn(final ProviderIdentifier provider, boolean success) throws RemoteException { // Request playlists if we logged in Log.d(TAG, "onLoggedIn(" + success + ")"); if (success) { // Cache data mBackHandler.removeCallbacks(mUpdatePlaylistsRunnable); mBackHandler.post(mUpdatePlaylistsRunnable); } else { mMainHandler.post(new Runnable() { @Override public void run() { Toast.makeText(mContext, mContext.getString(R.string.cannot_login, provider.mName), Toast.LENGTH_LONG).show(); } }); } } /** * Called by the providers when the user login has expired, or has been kicked. */ @Override public void onLoggedOut(ProviderIdentifier provider) throws RemoteException { } /** * Called by the providers when a Playlist has been added or updated. The app's providers * syndicator will automatically update the local cache of playlists based on the playlist * name. */ @Override public void onPlaylistAddedOrUpdated(final ProviderIdentifier provider, final Playlist p) throws RemoteException { if (p == null || p.getRef() == null) { Log.w(TAG, "Provider returned a null playlist or a null-ref playlist"); return; } try { // We compare the provided copy with the one we have in cache. We only notify the callbacks // if it indeed changed. Playlist cached = mCache.getPlaylist(p.getRef()); boolean notify; if (cached == null) { mCache.putPlaylist(provider, p); cached = p; notify = true; } else { notify = !cached.isIdentical(p); // If the playlist isn't identical, update it if (notify) { // Update the name cached.setName(p.getName()); if (p.getName() == null) { Log.w(TAG, "Playlist " + p.getRef() + " updated, but name is null!"); } cached.setIsLoaded(p.isLoaded()); // Empty the playlist while (cached.getSongsCount() > 0) { cached.removeSong(0); } // Re-add the songs to it Iterator<String> songIt = p.songs(); while (songIt.hasNext()) { cached.addSong(songIt.next()); } // Set offline information cached.setOfflineCapable(p.isOfflineCapable()); cached.setOfflineStatus(p.getOfflineStatus()); } } final Playlist finalCachedPlaylist = cached; // If something has actually changed if (notify) { mExecutor.execute(new Runnable() { @Override public void run() { // First, we try to check if we need information for some of the songs // TODO(xplodwild): Is this really needed in a properly designed provider? Iterator<String> it = finalCachedPlaylist.songs(); while (it.hasNext()) { String ref = it.next(); retrieveSong(ref, provider); } // Then we notify the callbacks postPlaylistForUpdate(finalCachedPlaylist); } }); } } catch (Exception e) { Log.e(TAG, "FUUUU", e); } } /** * Called by the provider if a playlist has been removed from the user playlists container. * @param provider The provider * @param ref The reference of the playlist that has been removed * @throws RemoteException */ @Override public void onPlaylistRemoved(ProviderIdentifier provider, String ref) throws RemoteException { if (ref != null) { mCache.removePlaylist(ref); } synchronized (mUpdateCallbacks) { for (ILocalCallback cb : mUpdateCallbacks) { cb.onPlaylistRemoved(ref); } } } /** * Called by the providers when the details of a song have been updated. */ @Override public void onSongUpdate(ProviderIdentifier provider, final Song s) throws RemoteException { if (s == null) { Log.w(TAG, "Provider " + provider.mName + " sent in a null songUpdate"); return; } try { Song cached = mCache.getSong(s.getRef()); boolean wasLoaded = false; boolean changed = false; if (cached == null) { mCache.putSong(provider, s); changed = true; cached = s; } else { wasLoaded = cached.isLoaded(); if (s.isLoaded() && !cached.isIdentical(s)) { cached.setAlbum(s.getAlbum()); cached.setArtist(s.getArtist()); cached.setSourceLogo(s.getLogo()); cached.setDuration(s.getDuration()); cached.setTitle(s.getTitle()); cached.setYear(s.getYear()); cached.setOfflineStatus(s.getOfflineStatus()); cached.setAvailable(s.isAvailable()); cached.setIsLoaded(s.isLoaded()); changed = true; } } if (!wasLoaded && cached.isLoaded()) { // Match the album with the artist Artist artist = mCache.getArtist(s.getArtist()); if (artist == null && s.getArtist() != null) { artist = retrieveArtist(s.getArtist(), provider); } if (artist != null) { Album album = mCache.getAlbum(s.getAlbum()); if (album == null && s.getAlbum() != null) { album = retrieveAlbum(s.getAlbum(), provider); } if (album != null) { artist.addAlbum(album.getRef()); } } } if (changed) { postSongForUpdate(cached); } } catch (Exception e) { Log.e(TAG, "Exception while updating song data", e); } } @Override public void onGenreUpdate(ProviderIdentifier provider, final Genre g) throws RemoteException { } /** * Called by the providers when the details of an album have been updated. */ @Override public void onAlbumUpdate(ProviderIdentifier provider, final Album a) throws RemoteException { if (a == null) { Log.w(TAG, "Provider returned a null album"); return; } Album cached = mCache.getAlbum(a.getRef()); boolean modified = false; // See IProviderCallback.aidl in providerlib for more info about the logic of updating // the Album objects if (cached == null) { mCache.putAlbum(provider, a); cached = a; modified = true; } else if (!cached.isLoaded() || !cached.isIdentical(a)) { cached.setName(a.getName()); cached.setYear(a.getYear()); cached.setIsLoaded(a.isLoaded()); cached.setProvider(a.getProvider()); if (cached.getSongsCount() != a.getSongsCount()) { Iterator<String> songsIt = a.songs(); while (songsIt.hasNext()) { String songRef = songsIt.next(); cached.addSong(songRef); } } modified = true; } if (cached.getProvider() == null) { Log.e(TAG, "Provider for " + cached.getRef() + " is null!"); } if (modified) { // Add the album to each artist of the song (once) Iterator<String> songs = a.songs(); while (songs.hasNext()) { String songRef = songs.next(); Song song = retrieveSong(songRef, a.getProvider()); if (song != null && song.isLoaded()) { String artistRef = song.getArtist(); if (artistRef != null) { Artist artist = retrieveArtist(artistRef, song.getProvider()); if (artist != null) { artist.addAlbum(a.getRef()); } else { if (DEBUG) Log.e(TAG, "Artist is null!"); } } } else { if (DEBUG) Log.e(TAG, "Song is null!"); } } postAlbumForUpdate(cached); } } /** * Called by the providers when the details of an artist have been updated. */ @Override public void onArtistUpdate(ProviderIdentifier provider, Artist a) throws RemoteException { if (a == null) { Log.w(TAG, "Provider returned a null artist"); return; } Artist cached = mCache.getArtist(a.getRef()); if (cached == null) { mCache.putArtist(provider, a); postArtistForUpdate(a); } else if (!cached.isIdentical(a)) { cached.setName(a.getName()); Iterator<String> it = a.albums(); while (it.hasNext()) { cached.addAlbum(it.next()); } cached.setIsLoaded(a.isLoaded()); postArtistForUpdate(a); } } @Override public void onSongPlaying(ProviderIdentifier provider) throws RemoteException { } @Override public void onSongPaused(ProviderIdentifier provider) throws RemoteException { } @Override public void onTrackEnded(ProviderIdentifier provider) throws RemoteException { } @Override public void onSearchResult(SearchResult searchResult) { if (searchResult == null) { return; } if (searchResult.getIdentifier() == null) { Log.e(TAG, "Search result came with no source identifier!"); return; } Log.d(TAG, "Got new search results for '" + searchResult.getQuery() + "' from " + searchResult.getIdentifier().mName); final String query = searchResult.getQuery(); if (!mCachedSearches.containsKey(query)) { // No cached results for this query, add this one Log.d(TAG, "New search cache for '" + query + "'"); List<SearchResult> results = new ArrayList<>(); results.add(searchResult); mCachedSearches.put(query, results); // Feed results to the callback synchronized (mUpdateCallbacks) { for (ILocalCallback cb : mUpdateCallbacks) { cb.onSearchResult(results); } } } else { // We already have cached results for this query, add new results Log.d(TAG, "Updating search result for '" + query + "'"); List<SearchResult> cachedResults = mCachedSearches.get(query); SearchResult cachedResult = null; for (SearchResult tmpResult : cachedResults) { if (tmpResult.getIdentifier().equals(searchResult.getIdentifier())) { cachedResult = tmpResult; } } if (cachedResult == null) { cachedResults.add(searchResult); } else { cachedResult.setIdentifier(searchResult.getIdentifier()); for (String song : searchResult.getSongsList()) { if (!cachedResult.getSongsList().contains(song)) { cachedResult.getSongsList().add(song); } } for (String artist : searchResult.getArtistList()) { if (!cachedResult.getArtistList().contains(artist)) { cachedResult.getArtistList().add(artist); } } for (String album : searchResult.getAlbumsList()) { if (!cachedResult.getAlbumsList().contains(album)) { cachedResult.getAlbumsList().add(album); } } for (String playlist : searchResult.getPlaylistList()) { if (!cachedResult.getPlaylistList().contains(playlist)) { cachedResult.getPlaylistList().add(playlist); } } } // Feed updated results to the callbacks synchronized (mUpdateCallbacks) { for (ILocalCallback cb : mUpdateCallbacks) { cb.onSearchResult(cachedResults); } } } } /** * Interface for offline mode changes */ public interface OfflineModeListener { void onOfflineModeChange(boolean enabled); } }