/** 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);
    }
  }
  /**
   * 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;
  }
  /**
   * 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;
  }
  /**
   * 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 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);
    }
  }
  /**
   * 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;
  }
  /** 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 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);
    }
  }
 public List<Playlist> getAllMultiProviderPlaylists() {
   return mCache.getAllMultiProviderPlaylists();
 }
 /**
  * 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();
 }