/* * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org> * * 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 2, 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. */ package org.devtcg.five.service; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import org.devtcg.five.Constants; import org.devtcg.five.provider.Five; import org.devtcg.five.provider.util.SongItem; import org.devtcg.five.provider.util.Songs; import org.devtcg.five.provider.util.SourceItem; import org.devtcg.five.provider.util.Sources; import org.devtcg.five.receiver.MediaButton; import org.devtcg.five.service.CacheManager.CacheAllocationException; import org.devtcg.five.util.AuthHelper; import org.devtcg.five.util.streaming.DownloadManager; import org.devtcg.five.util.streaming.StreamMediaPlayer; import org.devtcg.five.util.streaming.TailStream; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.view.KeyEvent; public class PlaylistService extends Service implements MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener { public static final String TAG = "PlaylistService"; private static final String STATE_FILE = "playlist_state"; private static final String STATE_FILE_TMP = STATE_FILE + ".tmp"; private static final int STATE_FILE_FORMAT = 3; /* Lock synchronizing resource access from binder threads. This is more * of a hint than a rule as we know that only one thread will be making * changes to the playlist state at any time. */ final Object mBinderLock = new Object(); SongDownloadManager mManager; CacheManager mCacheMgr = null; StreamMediaPlayer mPlayer = null; final List<Long> mPlaylist = Collections.synchronizedList(new ArrayList<Long>(50)); volatile int mPosition = -1; volatile boolean mPlaying = false; volatile boolean mPaused = false; volatile boolean mPrepared = false; /** * Tracks whether there are activities currently bound to the service so * that we can determine when it would be safe to call stopSelf(). */ boolean mActive = false; IPlaylistChangeListenerCallbackList mChangeListeners; IPlaylistMoveListenerCallbackList mMoveListeners; IPlaylistDownloadListenerCallbackList mDownloadListeners; IPlaylistBufferListenerCallbackList mBufferListeners; PowerManager.WakeLock mWakeLock; volatile boolean mResumeAfterCall = false; @Override public void onCreate() { Log.d(TAG, "onCreate"); super.onCreate(); /* We use a wake lock for the entire time the service is alive. * When playback is explicitly stopped, we delay for a short while * then die, releasing the wake lock. */ PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, PlaylistService.class.getName()); mWakeLock.acquire(); /* Listen for incoming calls so we can temporarily pause playback. */ TelephonyManager tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); tm.listen(mPhoneListener, PhoneStateListener.LISTEN_CALL_STATE); mPlayer = new StreamMediaPlayer(); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mChangeListeners = new IPlaylistChangeListenerCallbackList(); mMoveListeners = new IPlaylistMoveListenerCallbackList(); mDownloadListeners = new IPlaylistDownloadListenerCallbackList(); mBufferListeners = new IPlaylistBufferListenerCallbackList(); mManager = new SongDownloadManager(this); mCacheMgr = CacheManager.getInstance(); /* When the service dies we attempt to serialize playlist state to * disk. Check for, and recover from, this state file. */ try { recoverState(); } catch (IOException e) { Log.e(TAG, "Couldn't recover state!", e); } /* Detect when the headphone jack is suddenly unplugged. */ registerReceiver(mNoisyReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); /* Detect changes in the network state to adjust behaviour accordingly. * For instance, if the connectivity leaves and then returns, restart * stalled or failed downloads. */ registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } @Override public void onDestroy() { Log.d(TAG, "onDestroy"); mManager.shutdown(); saveStateQuietly(); unregisterReceiver(mNoisyReceiver); unregisterReceiver(mConnectivityReceiver); TelephonyManager tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE); /* XXX: Synchronization may not be necessary here as onDestroy() is * likely to have been called after any binder threads were nuked. */ synchronized(mBinderLock) { mChangeListeners.kill(); mMoveListeners.kill(); mDownloadListeners.kill(); mBufferListeners.kill(); mPlayer.reset(); mPlayer.release(); mPlayer = null; } mWakeLock.release(); super.onDestroy(); } @Override public void onStart(Intent intent, int startId) { handleStart(intent, startId); } @Override public int onStartCommand(Intent intent, int flags, int startId) { handleStart(intent, startId); return START_NOT_STICKY; } private void handleStart(Intent intent, int startId) { if (intent == null) return; if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); try { MediaButton.handleMediaButtonEvent(mBinder, event); } catch (Exception e) { if (Constants.DEBUG) Log.d(Constants.TAG, "PlaylistService failed to start", e); } } } public void saveState() throws IOException { FileOutputStream outf = openFileOutput(STATE_FILE_TMP, MODE_PRIVATE); DataOutputStream out = null; try { int bufferSize = Math.min(18 + (mPlaylist.size() * 8), 2048); out = new DataOutputStream(new BufferedOutputStream(outf, bufferSize)); out.writeInt(STATE_FILE_FORMAT); out.writeInt(mPosition); out.writeBoolean(mPlaying); out.writeBoolean(mPaused); if (mPaused == true) out.writeInt(mPlayer.getCurrentPosition()); else out.writeInt(0); out.writeInt(mPlaylist.size()); for (Long songId: mPlaylist) out.writeLong(songId); } finally { if (out != null) out.close(); else outf.close(); } File tmp = getFileStreamPath(STATE_FILE_TMP); tmp.renameTo(getFileStreamPath(STATE_FILE)); } public void saveStateQuietly() { try { saveState(); } catch (IOException e) { Log.e(TAG, "Couldn't save state!", e); } } public boolean recoverState() throws IOException { FileInputStream inf; try { inf = openFileInput(STATE_FILE); } catch (FileNotFoundException e) { return false; } DataInputStream in = null; try { in = new DataInputStream(new BufferedInputStream(inf, 2048)); int version = in.readInt(); if (version != STATE_FILE_FORMAT) return false; int pos = in.readInt(); boolean playing = in.readBoolean(); boolean paused = in.readBoolean(); int playpos = in.readInt(); int playlist_length = in.readInt(); try { for (int i = 0; i < playlist_length; i++) mPlaylist.add(in.readLong()); } catch (IOException e) { mPlaylist.clear(); throw e; } mPosition = pos; mPlaying = playing; /* XXX: We don't support this yet. We would need to reload * the MediaPlayer to its original state and seek to playpos, * which will be tricky to guarantee. For now, we force * mPrepared false to ensure that a call to unpause will restart * from the beginning in either case. */ mPaused = paused; mPrepared = false; return true; } finally { if (in != null) in.close(); else inf.close(); } } private final BroadcastReceiver mNoisyReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { try { if (mBinder.isPlaying() == true && mBinder.isPaused() == false) mBinder.pause(); } catch (RemoteException e) {} } }; /* Logic taken from packages/apps/Music. Will pause when an incoming * call rings (volume > 0), or if a call (incoming or outgoing) is * connected. */ private PhoneStateListener mPhoneListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { try { switch (state) { case TelephonyManager.CALL_STATE_RINGING: AudioManager am = (AudioManager)getSystemService(AUDIO_SERVICE); /* Don't pause if the ringer isn't making any noise. */ int ringvol = am.getStreamVolume(AudioManager.STREAM_RING); if (ringvol <= 0) break; /* Fall through... */ case TelephonyManager.CALL_STATE_OFFHOOK: if (mBinder.isPlaying() == true && mBinder.isPaused() == false) { mResumeAfterCall = true; mBinder.pause(); } break; case TelephonyManager.CALL_STATE_IDLE: if (mResumeAfterCall == true) { mBinder.unpause(); mResumeAfterCall = false; } break; default: Log.d(TAG, "Unknown phone state=" + state); } } catch (RemoteException e) {} } }; private final DeferredStopHandler mHandler = new DeferredStopHandler(); private class DeferredStopHandler extends Handler { /* Wait 2 minutes before vanishing. */ public static final long DEFERRAL_DELAY = 2 * (60 * 1000); private static final int DEFERRED_STOP = 0; public void handleMessage(Message msg) { switch (msg.what) { case DEFERRED_STOP: stopSelf(); break; default: super.handleMessage(msg); } } public void deferredStopSelf() { if (Constants.DEBUG) Log.i(TAG, "Service stop scheduled " + (DEFERRAL_DELAY / 1000 / 60) + " minutes from now."); sendMessageDelayed(obtainMessage(DEFERRED_STOP), DEFERRAL_DELAY); saveStateQuietly(); } public void cancelStopSelf() { if (Constants.DEBUG && hasMessages(DEFERRED_STOP)) Log.i(TAG, "Service stop cancelled."); removeMessages(DEFERRED_STOP); } }; @Override public IBinder onBind(Intent intent) { mActive = true; return mBinder; } @Override public void onRebind(Intent intent) { mActive = true; mHandler.cancelStopSelf(); super.onRebind(intent); } @Override public boolean onUnbind(Intent intent) { mActive = false; if (mResumeAfterCall == true) return true; if (mPlaying == true && mPaused == false) return true; mHandler.deferredStopSelf(); return true; } /** * Previously used simply to control the notification displayed in the * status bar. Changes in Eclair now require that we tie this to the * foreground state of the service so that has been tacked on here. * <p> * This method is called to handle the play, pause/unpause, and stop events, * making it a good fit for foreground state control. */ private void notifySong(long songId) { if (songId < 0) PlayerNotification.getInstance().hideNotification(this); else PlayerNotification.getInstance().showNotification(this, songId); } private long getPlayingSong() { synchronized(mBinderLock) { if (mPlaying == false) return -1; if (mPosition < 0) return -1; return mPlaylist.get(mPosition); } } private BroadcastReceiver mConnectivityReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { NetworkInfo info = (NetworkInfo)intent.getParcelableExtra (ConnectivityManager.EXTRA_NETWORK_INFO); Log.v(TAG, "ConnectivityChange: info=" + info); /* Resume paused downloads if we get connected. Note that there is * no matching action when we've lost connectivity as we may * regain it before the established connection has timed out * and failed. */ if (info != null && info.isConnected() == true) mManager.resumeDownloads(); } }; private Cursor getContentCursor(long songId) { Uri songUri = ContentUris.withAppendedId(Five.Music.Songs.CONTENT_URI, songId); Cursor c = getContentResolver().query(songUri, new String[] { Five.Music.Songs._SYNC_ID, Five.Music.Songs.SOURCE_ID, Five.Music.Songs.CACHED_PATH, Five.Music.Songs.SIZE }, null, null, null); return c; } /** * Does all the heavy lifting to play a song. Checks the cache, * manages the local HTTP server / streaming, and (later) playback * events. * * @return * True if playback is starting; false if an unforeseen error has * occurred. */ private boolean playInternal(long songId) { mHandler.cancelStopSelf(); mPrepared = false; mPlayer.reset(); // mPlayer.setOnBufferingUpdateListener(this); mPlayer.setOnCompletionListener(this); mPlayer.setOnErrorListener(this); mPlayer.setOnPreparedListener(this); SongItem song = SongItem.getInstance(Songs.getSong(this, songId)); try { DownloadManager.Download download = acquireDownload(song); if (download == null) mPlayer.setDataSource(song.getCachePath()); else { /* * XXX: There is a bug here where if the server responds with a * different content length than was our initial guess (based on * the synced meta data), we'll end up waiting for the download * to complete forever. */ mPlayer.setDataSource(new TailStream(download.getDestination().getAbsolutePath(), song.getMimeType(), download.getExpectedContentLength())); } } catch (Exception e) { /* * This code looks suspicious to me. If this ever happens, I believe * we'll end up leaving the PlaylistService in a weird state where * it think its still playing but nothing is happening. */ Log.e(Constants.TAG, "Unable to start playback", e); mPlayer.reset(); } finally { song.close(); } notifySong(songId); mBufferListeners.broadcastOnBufferingUpdate(songId, 0); mPlayer.prepareAsync(); return true; } /** * Get or start a download for the request song id. * * @return The download instance (either recently started, or reacquired * from an existing download) if the song is not in cache; * otherwise, null. * * @throws IOException * When the download destination path cannot be opened for * writing. * @throws CacheAllocationException */ private DownloadManager.Download acquireDownload(SongItem song) throws IOException, CacheAllocationException { SourceItem source = SourceItem.getInstance(this, Sources.makeUri(song.getSourceId())); try { long songId = song.getId(); String url = source.getSongUrl(song.getSyncId()); long size = song.getSize(); String cachePath = song.getCachePath(); Log.v(TAG, "Preparing to download [url=" + url + "; size=" + size + "; cachePath=" + cachePath + "]"); long resumeFrom = 0; if (cachePath != null) { resumeFrom = (new File(cachePath)).length(); if (resumeFrom == size) { Log.i(TAG, "Cache hit, download of " + cachePath + " already complete!"); return null; } else { Log.i(TAG, "Partial cache hit, resuming from " + cachePath + " at " + resumeFrom); /* * XXX: We have a small race condition possibility here * since we aren't synchronizing anything. The download * might have just finished, in which case our lookup would * yield null, but we'll foolishly try a resumed download * for a very small section of the file. */ DownloadManager.Download download = mManager.lookupDownload(songId); if (download != null) return download; } } else { if (mManager.lookupDownload(songId) != null) throw new IllegalStateException("Download started, but did not register with the cache."); cachePath = mCacheMgr.requestStorage(this, song.getSourceId(), song.getSyncId()); } /* * We only allow 1 download at a time, so invoking this method * implicitly asks for all other downloads to be canceled. */ mManager.stopAllDownloads(); mManager.updateCredentials(source); try { return mManager.startDownload(songId, url, cachePath, size, resumeFrom); } catch (IOException e) { mManager.stopDownload(songId); throw e; } } finally { if (source != null) source.close(); } } /** * Check at key stages to make sure that the song to be played next * is preemptively downloading. */ private void prefetchCheck() throws RemoteException { long currentId; long nextId = -1; synchronized(mBinderLock) { if (mPlaying == false) return; currentId = getPlayingSong(); assert currentId >= 0; int next = mBinder.peekNext(); if (next < 0) return; nextId = mPlaylist.get(next); assert nextId >= 0; } if (mManager.lookupDownload(currentId) != null) { Log.i(TAG, "Prefetch miss due to active download."); return; } if (mManager.lookupDownload(nextId) != null) { Log.i(TAG, "Prefetch already in progress."); return; } SongItem song = SongItem.getInstance(Songs.getSong(this, nextId)); try { DownloadManager.Download download = acquireDownload(song); if (download == null) Log.i(TAG, "Prefetch not necessary, next track (nextId=" + nextId + ") already in cache"); else Log.i(TAG, "Prefetch started on next track (nextId=" + nextId + ")"); } catch (Exception e) { Log.e(TAG, "acquireDownload failed", e); } finally { song.close(); } } private class SongDownloadManager extends DownloadManager { private final Map<String, Long> mUrlToSongMap = Collections.synchronizedMap(new HashMap<String, Long>()); public SongDownloadManager(Context ctx) { super(ctx); } public synchronized void updateCredentials(SourceItem source) { AuthHelper.setCredentials(mClient, source); } @Override public void onStateChange(String url, int state, String message) { Log.i(TAG, "url=" + url + ", state=" + state + ", message=" + message); if (state == STATE_CONNECTED) { long songId = mUrlToSongMap.get(url); mDownloadListeners.broadcastOnDownloadBegin(songId); } } public boolean commitStorage(long songId) { Cursor c = getContentCursor(songId); long sourceId; long contentId; try { if (c.moveToFirst() == false) return false; sourceId = c.getLong(c.getColumnIndexOrThrow(Five.Music.Songs.SOURCE_ID)); contentId = c.getLong(c.getColumnIndexOrThrow(Five.Music.Songs._SYNC_ID)); } finally { c.close(); } mCacheMgr.commitStorage(sourceId, contentId); return true; } @Override public void onFinished(String url) { long songId = mUrlToSongMap.get(url); mDownloadListeners.broadcastOnDownloadFinish(songId); commitStorage(songId); final Download d = lookupDownload(url); mHandler.post(new Runnable() { public void run() { try { /* Should be on its way out, but until then * the manager thinks we're actively downloading. */ d.joinUninterruptibly(); prefetchCheck(); } catch (RemoteException e) {} } }); } @Override public void onAborted(String url) { long songId = mUrlToSongMap.get(url); mDownloadListeners.broadcastOnDownloadCancel(songId); //super.onAborted(url); } @Override public void onError(String url, int state, final String err) { long songId = mUrlToSongMap.get(url); mDownloadListeners.broadcastOnDownloadError(songId, err); } @Override public void onProgressUpdate(String url, int percent) { long songId = mUrlToSongMap.get(url); mDownloadListeners.broadcastOnDownloadProgressUpdate(songId, percent); } long getSongIdFromUrl(String url) { return mUrlToSongMap.get(url); } public Download lookupDownload(long songId) { /* Ouch. */ synchronized(mUrlToSongMap) { Set<Entry<String, Long>> set = mUrlToSongMap.entrySet(); for (Entry<String, Long> entry: set) { if (entry.getValue() == songId) return super.lookupDownload(entry.getKey()); } } return null; } public Download startDownload(long songId, String url, String path, long expectedContentLength, long resumeFrom) throws IOException { Download d = super.startDownload(url, path, expectedContentLength, resumeFrom); if (d != null) mUrlToSongMap.put(url, songId); return d; } public void stopDownload(long songId) { super.stopDownload(lookupDownload(songId)); } @Override public void removeDownload(String url) { super.removeDownload(url); mUrlToSongMap.remove(url); } }; /* This callback doesn't report useful information in 1.0r1, so we * ignore it. */ public void onBufferingUpdate(MediaPlayer mp, int percent) { // long songId = getPlayingSong(); // assert songId >= 0; // // mBufferListeners.broadcastOnBufferingUpdate(songId, percent); } private void tidyThenAdvance() { boolean playing; boolean paused; /* Cleanup the MediaPlayer object's state. */ synchronized(mBinderLock) { playing = mPlaying; paused = mPaused; if (mPlayer.isPlaying()) mPlayer.stop(); mPlayer.reset(); mPrepared = false; } /* If we were previously playing, advance to the next track. */ try { if (playing == true && paused == false) mBinder.next(); } catch (RemoteException e) {} } public boolean onError(MediaPlayer mp, int what, int extra) { Log.d(TAG, "Media playback error, what=" + what + ", extra=" + extra); assert mp == mPlayer; long songId = getPlayingSong(); if (songId >= 0) { /* If we have an error condition on this song's download, * trigger it as a download error. Normally, we hide network * errors and passively retry but if the MediaPlayer catches * up, we should report the last error we encountered. * XXX: This introduces a bug where onDownloadError could fire * twice consecutively for the same download. */ DownloadManager.Download dl = mManager.lookupDownload(songId); if (dl != null) { switch (dl.getDownloadState()) { case DownloadManager.STATE_HTTP_ERROR: case DownloadManager.STATE_FILE_ERROR: case DownloadManager.STATE_PAUSED_LOCAL_FAILURE: case DownloadManager.STATE_PAUSED_REMOTE_FAILURE: mDownloadListeners.broadcastOnDownloadError(songId, dl.getStateMessage()); break; } mManager.stopDownload(songId); } } tidyThenAdvance(); return true; } public void onCompletion(MediaPlayer mp) { Log.i(TAG, "Should be finished."); tidyThenAdvance(); } public void onPrepared(MediaPlayer mp) { assert mp == mPlayer; assert mPlaying == true; synchronized(mBinderLock) { if (mPaused == true) Log.i(TAG, "Ready to play, but paused."); else { Log.i(TAG, "Should be playing..."); mPlayer.start(); } mPrepared = true; } long songId = getPlayingSong(); assert songId >= 0; mBufferListeners.broadcastOnBufferingUpdate(songId, 100); } private final IPlaylistService.Stub mBinder = new IPlaylistService.Stub() { /*-********************************************************************/ public void registerOnMoveListener(IPlaylistMoveListener l) throws RemoteException { mMoveListeners.register(l); } public void unregisterOnMoveListener(IPlaylistMoveListener l) throws RemoteException { mMoveListeners.unregister(l); } public int next() throws RemoteException { int next = peekNext(); if (next >= 0) jump(next); else { stop(); jump(-1); } return next; } public int previous() throws RemoteException { int prev; synchronized(mBinderLock) { if (mPlaying == true) { if (mPaused == true) { play(); return mPosition; } else if (tell() > 10000) { seek(0); return mPosition; } } prev = mPosition - 1; if (prev < 0) { int n; if ((n = mPlaylist.size()) == 0) { stop(); jump(-1); return -1; } prev = n - 1; } } jump(prev); return prev; } public void jump(int pos) throws RemoteException { if (pos < -1 || pos >= mPlaylist.size()) return; synchronized(mBinderLock) { mPosition = pos; } if (pos >= 0) { mMoveListeners.broadcastOnJump(pos); if (mPlaying == true && mPaused == false) { synchronized(mBinderLock) { mPlayer.stop(); playInternal(mPlaylist.get(pos)); } } else play(); synchronized(mBinderLock) { prefetchCheck(); } } } public void play() throws RemoteException { long songId; synchronized(mBinderLock) { if (mPlaylist.isEmpty() == true) return; if (mPosition == -1) jump(0); songId = mPlaylist.get(mPosition); mPlaying = true; mPaused = false; /* TODO: How should we handle this? Gracefully destroying * the service may be a good idea. */ boolean ret = playInternal(songId); assert ret == true; } mMoveListeners.broadcastOnPlay(); } public void pause() throws RemoteException { if (isPlaying() == false) return; notifySong(-1); synchronized(mBinderLock) { if (mPlayer.isPlaying() == true) mPlayer.pause(); mPaused = true; } mMoveListeners.broadcastOnPause(); } public void unpause() throws RemoteException { if (isPaused() == false) return; synchronized(mBinderLock) { assert mPlaying == true; assert mPosition > 0; long songId = mPlaylist.get(mPosition); notifySong(songId); if (mPrepared == true) mPlayer.start(); else { boolean ret = playInternal(songId); assert ret == true; } mPaused = false; } mMoveListeners.broadcastOnUnpause(); } public void stop() throws RemoteException { if (mActive == false) mHandler.deferredStopSelf(); if (isPlaying() == false && isPaused() == false) return; notifySong(-1); synchronized(mBinderLock) { mPlayer.stop(); mPlayer.reset(); mPrepared = false; mPaused = false; mPlaying = false; } mMoveListeners.broadcastOnStop(); } public void seek(long pos) throws RemoteException { synchronized(mBinderLock) { mPlayer.seekTo((int)pos); } mMoveListeners.broadcastOnSeek(pos); } /*-********************************************************************/ public int getPosition() throws RemoteException { return mPosition; } public long tell() throws RemoteException { if (isPlaying() == false && isPaused() == false) return -1; int pos; synchronized(mBinderLock) { pos = mPlayer.getCurrentPosition(); } return (long)pos; } public long getSongDuration() throws RemoteException { int dur; synchronized(mBinderLock) { dur = mPlayer.getDuration(); } return (long)dur; } public boolean isPlaying() throws RemoteException { return mPlaying; } public boolean isStopped() throws RemoteException { return mPlaying == false; } public boolean isPaused() throws RemoteException { return mPaused; } public boolean isDownloading() throws RemoteException { int n = getPosition(); if (n == -1) return false; return mManager.lookupDownload(mPlaylist.get(n)) != null; } public boolean isOutputting() throws RemoteException { boolean playing; synchronized(mBinderLock) { playing = mPlayer.isPlaying(); } return playing; } /*-********************************************************************/ public List getPlaylist() throws RemoteException { return mPlaylist; } public List getPlaylistWindow(int from, int to) throws RemoteException { if (from >= to) return null; if (from < 0) return null; synchronized(mBinderLock) { if (mPlaylist.isEmpty() == true) return null; int n = mPlaylist.size(); assert n > 0; if (to > n) to = n; /* XXX: We assume that Android's serialization will * copy our list so as not to leak references. */ return mPlaylist.subList(from, to); } } public int getPlaylistLength() throws RemoteException { return mPlaylist.size(); } public long getSongAt(int pos) throws RemoteException { if (pos < 0) return -1; synchronized(mBinderLock) { if (pos >= mPlaylist.size()) return -1; return mPlaylist.get(pos); } } public int getPositionOf(long songId) throws RemoteException { return mPlaylist.lastIndexOf(songId); } public int peekNext() throws RemoteException { int next; synchronized(mBinderLock) { next = mPosition + 1; if (next >= mPlaylist.size()) return -1; } return next; } /*-********************************************************************/ public void shuffle() throws RemoteException { } public void setRepeat(int repeatMode) throws RemoteException { } public int getRepeat() throws RemoteException { return -1; } public void setRandom(boolean random) throws RemoteException { assert random == false; } public boolean getRandom() throws RemoteException { return false; } /*-********************************************************************/ public void registerOnChangeListener(IPlaylistChangeListener l) throws RemoteException { mChangeListeners.register(l); } public void unregisterOnChangeListener(IPlaylistChangeListener l) throws RemoteException { mChangeListeners.unregister(l); } public void loadPlaylistRef(long playlistId) throws RemoteException { } public long getPlaylistRef() throws RemoteException { return -1; } public boolean isPlaylistRefLiteral() throws RemoteException { return false; } public void clear() throws RemoteException { stop(); jump(-1); synchronized(mBinderLock) { mPlaylist.clear(); } mChangeListeners.broadcastOnClear(); } public void insert(long songId, int pos) throws RemoteException { synchronized(mBinderLock) { mPlaylist.add(pos, songId); if (mPosition >= 0) { if (pos <= mPosition) mPosition++; } if (peekNext() == pos) prefetchCheck(); } mChangeListeners.broadcastOnInsert(songId, pos); synchronized(mBinderLock) { if (isPlaying() == false && isPaused() == false) { mPosition = pos; play(); } } } public void insertNext(long songId) throws RemoteException { int n = getPosition(); if (n == -1) append(songId); else insert(songId, n + 1); } public void prepend(long songId) throws RemoteException { insert(songId, 0); } public void append(long songId) throws RemoteException { insert(songId, mPlaylist.size()); } public long remove(int pos) throws RemoteException { long songId; synchronized(mBinderLock) { if (mPosition == pos && isPlaying() == true) stop(); songId = mPlaylist.remove(pos); } mChangeListeners.broadcastOnRemove(pos); return songId; } public long move(int oldpos, int newpos) throws RemoteException { Log.d(TAG, "UNIMPLEMENTED: move(int,int)"); return -1; } /*-********************************************************************/ public void registerOnDownloadListener(IPlaylistDownloadListener l) throws RemoteException { /* Inform this new listener of the currently active * downloads. */ List<DownloadManager.Download> downloads = mManager.getDownloadsCopy(); for (DownloadManager.Download dl: downloads) { long songId = mManager.getSongIdFromUrl(dl.getUrl()); l.onDownloadBegin(songId); l.onDownloadProgressUpdate(songId, dl.getProgress()); } mDownloadListeners.register(l); } public void unregisterOnDownloadListener(IPlaylistDownloadListener l) throws RemoteException { mDownloadListeners.unregister(l); } public void registerOnBufferingListener(IPlaylistBufferListener l) throws RemoteException { if (mPlaying == true) { long songId = getPlayingSong(); l.onBufferingUpdate(songId, (mPrepared == true) ? 100 : 0); } mBufferListeners.register(l); } public void unregisterOnBufferingListener(IPlaylistBufferListener l) throws RemoteException { mBufferListeners.unregister(l); } }; }