package com.fastbootmobile.encore.providers.localprovider; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.*; import android.os.Process; import android.util.Log; import com.fastbootmobile.encore.app.R; import com.fastbootmobile.encore.model.Album; import com.fastbootmobile.encore.model.Artist; import com.fastbootmobile.encore.model.BoundEntity; 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 com.fastbootmobile.encore.providers.AudioClientSocket; import com.fastbootmobile.encore.providers.AudioSocket; import com.fastbootmobile.encore.providers.Constants; import com.fastbootmobile.encore.providers.IArtCallback; import com.fastbootmobile.encore.providers.IMusicProvider; import com.fastbootmobile.encore.providers.IProviderCallback; import com.fastbootmobile.encore.providers.ProviderIdentifier; import java.io.IOException; import java.util.ArrayList; import java.util.List; import omnimusic.Plugin; public class PluginService extends Service implements AudioSocket.ISocketCallback { private static final String TAG = "OmniMusic-LocalService"; public static final String LOGO_REF = "LOCAL_PROVIDER"; Handler mHandler = new Handler(); private ProviderIdentifier mIdentifier; private final List<IProviderCallback> mCallbacks; private final List<IProviderCallback> mCallbacksRemoval; private AudioClientSocket mAudioSocket; private LocalProvider mLocalProvider; private int mRate; private int mAudioWritten; private final Object mAudioWrittenLock = new Object(); private byte[] mAudioBuffer; private int mAudioBufferIndex = 0; private final Thread mWriteAudioThread = new Thread() { @Override public void run() { android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); while (!isInterrupted()) { synchronized (mWriteAudioThread) { try { mWriteAudioThread.wait(); } catch (InterruptedException e) { continue; } try { try { mAudioSocket.writeAudioData(mAudioBuffer, 0, mAudioBufferIndex); } catch (IOException e) { e.printStackTrace(); } } catch (Exception e) { Log.e(TAG, "Error while writing audio data", e); mAudioSocket = null; mLocalProvider.pause(false); } } } } }; public PluginService() { mCallbacks = new ArrayList<>(); mCallbacksRemoval = new ArrayList<>(); mRate = 0; } @Override public void onCreate() { super.onCreate(); Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Context context = getApplicationContext(); mLocalProvider = new LocalProvider(uri, getContentResolver(), providerCallback, context); mWriteAudioThread.start(); new Thread() { public void run() { mLocalProvider.poll(); } }.start(); } @Override public void onDestroy() { super.onDestroy(); mWriteAudioThread.interrupt(); } private void removeCallback(final IProviderCallback cb) { mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacksRemoval) { mCallbacksRemoval.add(cb); } } }); } private Runnable mRemoveCallbackRunnable = new Runnable() { @Override public void run() { synchronized (mCallbacksRemoval) { synchronized (mCallbacks) { mCallbacks.removeAll(mCallbacksRemoval); } mCallbacksRemoval.clear(); } } }; private LocalProvider.LocalCallback providerCallback = new LocalProvider.LocalCallback() { @Override public int musicDelivery(byte[] data, int frames, int channels, int sampleRate) { if (mAudioSocket == null) { Log.w(TAG, "Got music delivery without an audio socket set!"); return 0; } // If the sample rate changed, update it if (mRate != sampleRate) { mRate = sampleRate; try { mAudioSocket.writeFormatData(channels, sampleRate); } catch (IOException e) { Log.e(TAG, "IO error while pushing audio data", e); // Error while pushing audio to the socket, we stop the playback and // turn off the socket mAudioSocket = null; return 0; } } // First, we try to copy the frames to our local buffer. If the local buffer is full, // try to send it and empty it - and retry to write the current frames into the buffer // on next loop. //if (mAudioBufferIndex + frames < mAudioBuffer.length) { mAudioBuffer = data; mAudioBufferIndex = frames; // There's enough data in the buffer, try to send it over to the app mAudioWritten = -1; synchronized (mWriteAudioThread) { mWriteAudioThread.notifyAll(); } synchronized (mAudioWrittenLock) { if (mAudioWritten == -1) { // Wait 500ms for a reply try { mAudioWrittenLock.wait(500); } catch (InterruptedException e) { Log.e(TAG, "Interrupted while waiting for audio written response"); } } } // No response in time, assume the audio wasn't written if (mAudioWritten == -1) { mAudioWritten = 0; } if (mAudioWritten > 0) { // We could write it, so reset our local buffer mAudioBufferIndex = 0; } return mAudioWritten; } @Override public void artistUpdated(final Artist artist) { if (mIdentifier == null) { return; } artist.setProvider(mIdentifier); mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onArtistUpdate(mIdentifier, artist); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } @Override public void playlistUpdated(final Playlist playlist) { if (mIdentifier == null) { return; } playlist.setProvider(mIdentifier); mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onPlaylistAddedOrUpdated(mIdentifier, playlist); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } @Override public void playlistRemoved(final String playlistRef) { mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onPlaylistRemoved(mIdentifier, playlistRef); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } @Override public void albumUpdated(final Album album) { if (mIdentifier == null) { return; } album.setProvider(mIdentifier); mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onAlbumUpdate(mIdentifier, album); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } @Override public void songUpdated(final Song song) { if (song == null) { throw new IllegalArgumentException("Song cannot be null"); } if (mIdentifier == null) { return; } song.setProvider(mIdentifier); mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onSongUpdate(mIdentifier, song); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } @Override public void genreUpdated(final Genre genre) { mHandler.post(new Runnable() { @Override public void run() { /*synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { // cb.onGenreUpdate(mIdentifier, genre); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } }*/ } }); } @Override public void searchFinished(final SearchResult searchResult) { searchResult.setIdentifier(mIdentifier); mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onSearchResult(searchResult); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } @Override public void songFinished() { mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onTrackEnded(mIdentifier); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } @Override public void songPlaying() { mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onSongPlaying(mIdentifier); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } @Override public void songPaused() { mHandler.post(new Runnable() { @Override public void run() { synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onSongPaused(mIdentifier); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } }); } }; @Override public IBinder onBind(Intent intent) { return mBinder; } /* * Binder Stub implementation */ private IMusicProvider.Stub mBinder = new IMusicProvider.Stub() { /** * Returns the API Version of this providers. * The current API version is: 1 */ @Override public int getVersion() { return Constants.API_VERSION; } public ProviderIdentifier getIdentifier() { return mIdentifier; } @Override public void setIdentifier(ProviderIdentifier identifier) throws RemoteException { mIdentifier = identifier; mLocalProvider.notifyIdentifier(mIdentifier); } /** * Register a callback for the app to be notified of events. Remember that the providers calls * should all be asynchronous (every request must return immediately, and the result be posted * later on to all the callbacks registered here). */ @Override public void registerCallback(IProviderCallback cb) { synchronized (mCallbacks) { mCallbacks.add(cb); } } /** * Removes a registered callback */ public void unregisterCallback(IProviderCallback cb) { synchronized (mCallbacks) { mCallbacks.remove(cb); } } /** * Request authenticatication of the user against the providers. It is up to the providers to * store the credentials and grab them through a configuration activity. See providers.Constants * for more details about the configuration activity. * * @return true if the authentication request succeeded, false otherwise */ @Override public boolean login() throws RemoteException { return true; } /** * Returns whether or not the providers is fully setup and ready to use (for example, if the * user entered his login and password to authenticate to the service in the configuration * activity). * As long as this returns false, the app won't try to login or do any action on the providers. * * @return true if the providers is configured and ready to use */ @Override public boolean isSetup() throws RemoteException { return mLocalProvider.isSetup(); } /** * Indicates whether or not this providers has successfully authenticated against the remote * providers servers. * In case an authentication is not needed, this method should simply return true at all * times. No login attempt will be then made by the app. * * @return true if this providers is authenticated and ready to be used, false otherwise */ @Override public boolean isAuthenticated() throws RemoteException { return true; } /** * Informs whether or not this providers is infinite (ie. it's a cloud providers that allows * you to access a virtually unlimited number of tracks, such as Spotify or Deezer ; the local * storage or a simple storage providers would return false). * * @return true if there's no defined number of tracks, false otherwise */ @Override public boolean isInfinite() throws RemoteException { // This is not a cloud provider, thus is not infinite return false; } /** * Returns the list of all albums * This method call is only valid when isInfinite returns false * * @return A list of all the albums available on the providers */ @Override public List<Album> getAlbums() throws RemoteException { List<Album> albums = mLocalProvider.getAlbums(); for (Album album : albums) { if (album.getProvider() == null && mIdentifier != null) { album.setProvider(mIdentifier); } } return albums; } /** * Returns the list of all artists * This method call is only valid when isInfinite returns false * * @return A list of all the artists available on the providers */ @Override public List<Artist> getArtists() throws RemoteException { List<Artist> artists = mLocalProvider.getArtists(); for (Artist artist : artists) { if (artist.getProvider() == null && mIdentifier != null) { artist.setProvider(mIdentifier); } } return artists; } /** * Returns the list of all songs * This method call is only valid when isInfinite returns false * * @return A list of all songs available on the providers */ @Override public List<Song> getSongs(int offset, int range) throws RemoteException { List<Song> songs = mLocalProvider.getSongs(offset, range); for (Song song : songs) { if (song.getProvider() == null && mIdentifier != null) { song.setProvider(mIdentifier); } } return songs; } /** * Returns the list of all playlist on this provider * This method is valid for both infinite and defined providers. * * @return A list of all playlist on this providers */ @Override public List<Playlist> getPlaylists() throws RemoteException { List<Playlist> playlists = mLocalProvider.getPlaylists(); for (Playlist playlist : playlists) { if (playlist.getProvider() == null && mIdentifier != null) { playlist.setProvider(mIdentifier); } } return playlists; } /** * Returns the list of all genre on this provider * Since this is a finite provider, it will populate genre with songs,infinite provider may handle it differently * * @return a list of all genre */ @Override public List<Genre> getGenres() { return mLocalProvider.getGenres(); } @Override public void startSearch(String query) { mLocalProvider.startSearch(query); } @Override public Bitmap getLogo(String ref) throws RemoteException { if (LOGO_REF.equals(ref)) { return ((BitmapDrawable) getResources().getDrawable(R.drawable.ic_storage)).getBitmap(); } return null; } @Override public List<String> getSupportedRosettaPrefix() throws RemoteException { return null; } @Override public void setPlaylistOfflineMode(String ref, boolean offline) throws RemoteException { // ignore } @Override public void setOfflineMode(boolean offline) throws RemoteException { // ignore } /** * Returns a particular song * The providers may not return all the information immediately, and must set the IsLoaded * flag accordingly. * Song information should be then updated with onSongUpdate callback. * It must not return null however. * * @param ref The reference of the song */ @Override public Song getSong(String ref) throws RemoteException { Song s = mLocalProvider.getSong(ref); if (s != null) { s.setProvider(mIdentifier); s.setSourceLogo(LOGO_REF); s.setOfflineStatus(BoundEntity.OFFLINE_STATUS_READY); s.setAvailable(true); } else { Log.e(TAG, "Cannot find song " + ref); } return s; } @Override public Artist getArtist(String ref) throws RemoteException { Artist a = mLocalProvider.getArtist(ref); if (a != null) { a.setProvider(mIdentifier); a.setSourceLogo(LOGO_REF); } return a; } @Override public Album getAlbum(String ref) throws RemoteException { Album a = mLocalProvider.getAlbum(ref); if (a != null) { a.setProvider(mIdentifier); a.setSourceLogo(LOGO_REF); } return a; } @Override public Playlist getPlaylist(String ref) throws RemoteException { Playlist p = mLocalProvider.getPlaylist(ref); if (p != null) { p.setProvider(mIdentifier); p.setSourceLogo(LOGO_REF); } return p; } @Override public boolean getArtistArt(Artist entity, IArtCallback callback) throws RemoteException { return false; } @Override public boolean getAlbumArt(Album entity, IArtCallback callback) throws RemoteException { return mLocalProvider.getAlbumArt(entity.getRef(), callback); } @Override public boolean getPlaylistArt(Playlist entity, IArtCallback callback) throws RemoteException { return false; } @Override public boolean getSongArt(Song entity, IArtCallback callback) throws RemoteException { return mLocalProvider.getSongArt(entity.getRef(), callback); } @Override public boolean fetchArtistAlbums(String artistRef) { return false; } @Override public boolean fetchAlbumTracks(String albumRef) throws RemoteException { return false; } /** * Tells the providers the name of the local audio socket to use to push data. This string * should be passed to AudioSocket in order to push audio to the proper location. The app * manages audio crossfading and properly locks each socket to ensure a smooth playback * between the various providers. * * @param socketName The name of the socket to use */ @Override public void setAudioSocketName(final String socketName) { try { if (mAudioSocket == null) { mAudioSocket = new AudioClientSocket(); } mAudioSocket.connect(socketName); mAudioSocket.writeFormatData(2, 44100); mAudioSocket.setCallback(PluginService.this); } catch (IOException e) { Log.e(TAG, "Unable to open the audio socket!", e); } } /** * Returns the time, in milliseconds, the providers needs a call to prefetchSong() before the * end of the current song. * For instance, a cloud providers might need more time to prepare a song than a local providers * to ensure smooth and gapless playback. */ @Override public long getPrefetchDelay() throws RemoteException { // Let's say 10 seconds return 10 * 1000; } /** * Requests the providers to prepare the playback of a song (ie. start downloading it and/or * caching it in RAM), as it is likely the next song to be played. * Providers may choose to implement or not this method - it is called by the app so that * the providers can prepare the next song, but no particular result is expected. * * @param ref The unique reference of the song */ @Override public void prefetchSong(String ref) throws RemoteException { // TODO } /** * Requests the providers to play the song referenced by the provided ref string. * * @param ref The unique reference of the song * @return false in case of error, otherwise true */ @Override public boolean playSong(final String ref) throws RemoteException { mLocalProvider.playSong(ref); return true; } @Override public void pause() throws RemoteException { mLocalProvider.pause(true); synchronized (mCallbacks) { for (IProviderCallback cb : mCallbacks) { try { cb.onSongPaused(mIdentifier); } catch (DeadObjectException e) { removeCallback(cb); } catch (RemoteException e) { Log.e(TAG, "RemoteException when notifying a callback", e); } } } } @Override public void resume() throws RemoteException { mLocalProvider.resume(); } @Override public boolean onUserSwapPlaylistItem(int oldPosition, int newPosition, String playlistRef) { return mLocalProvider.onUserSwapPlaylistItem(oldPosition, newPosition, playlistRef); } @Override public boolean deletePlaylist(String playlistRef) { return mLocalProvider.deletePlaylist(playlistRef); } @Override public boolean renamePlaylist(String playlistRef, String title) throws RemoteException { boolean result = mLocalProvider.renamePlaylist(playlistRef, title); if (result) { providerCallback.playlistUpdated(getPlaylist(playlistRef)); } return result; } @Override public boolean deleteSongFromPlaylist(int songPosition, String playlistRef) { return mLocalProvider.deleteSongFromPlaylist(songPosition, playlistRef); } @Override public boolean addSongToPlaylist(String songRef, String playlistRef, ProviderIdentifier providerIdentifier) { return mLocalProvider.addSongToPlaylist(songRef, playlistRef); } @Override public String addPlaylist(String playlistName) { return mLocalProvider.addPlaylist(playlistName); } @Override public void seek(long timeMs) { mLocalProvider.seekTo(timeMs); } }; @Override public void onAudioData(AudioSocket socket, Plugin.AudioData.Builder message) { } @Override public void onAudioResponse(AudioSocket socket, Plugin.AudioResponse.Builder message) { synchronized (mAudioWrittenLock) { mAudioWritten = message.getWritten(); mAudioWrittenLock.notifyAll(); } } @Override public void onRequest(AudioSocket socket, Plugin.Request.Builder message) { } @Override public void onFormatInfo(AudioSocket socket, Plugin.FormatInfo.Builder message) { } @Override public void onBufferInfo(AudioSocket socket, Plugin.BufferInfo.Builder message) { } }