// // CustomMediaPlayer adds looping, shuffling, and playlist handling // package com.ideabag.playtunes.media; import java.util.Random; import com.ideabag.playtunes.database.MediaQuery; import android.content.Context; import android.database.Cursor; import android.media.AudioManager; import android.media.MediaPlayer; import android.os.PowerManager; import android.provider.MediaStore; public class PlaylistMediaPlayer { public static final String TAG = "PlaylistMediaPlayer"; protected boolean isShuffling, isPlaying; protected Cursor mPlaylistCursor; // Local copy of the playlist Cursor that we keep in case the cursor is closed or changed protected MediaQuery mMediaQuery = null; protected int mPlaylistPosition = -1; protected int mPlaylistSize = -1; protected boolean isPrepared = false; private int[] mShuffledPlaylist; protected int mLoopState; protected MediaPlayer mMediaPlayer; private boolean isDucking = false; private PlaybackListener PlaybackChanged = null; protected Context mContext; private long shuffleRandomNumberSeed = -1; //private PowerManager pm; //private PowerManager.WakeLock wl; // // // public interface PlaybackListener { void onTrackChanged( String media_id ); void onPlay(); void onPause(); void onPlaylistDone(); void onLoopingChanged( int loop ); void onShuffleChanged( boolean isShuffling ); void onDurationChanged( int position, int duration ); } // Loop states public static final int LOOP_NO = 0; public static final int LOOP_ALL = 1; public static final int LOOP_ONE = 2; public PlaylistMediaPlayer( Context context ) { super(); //( ( TelephonyManager ) context.getSystemService( Context.TELEPHONY_SERVICE ) ).listen( phoneListener, PhoneStateListener.LISTEN_CALL_STATE ); mContext = context; mMediaPlayer = new MediaPlayer(); mMediaPlayer.setAudioStreamType( AudioManager.STREAM_MUSIC ); mMediaPlayer.setWakeMode( mContext, PowerManager.PARTIAL_WAKE_LOCK ); mMediaPlayer.setOnCompletionListener( loopOnCompletionListener ); mMediaPlayer.setOnPreparedListener( onPreparedListener ); mLoopState = LOOP_NO; // // Intents for loss of connection to media // //hardwareStopIntents.addAction( Intent.ACTION_MEDIA_EJECT ); //hardwareStopIntents.addAction( Intent.ACTION_MEDIA_UNMOUNTED ); //hardwareStopIntents.addAction( Intent.ACTION_MEDIA_REMOVED ); //mContext.registerReceiver(HardwareStopReceiver, hardwareStopIntents); //pm = ( PowerManager ) mContext.getSystemService( Context.POWER_SERVICE ); //wl = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, TAG ); } public void setPlaybackListener( PlaybackListener pbl ) { this.PlaybackChanged = pbl; } MediaPlayer.OnCompletionListener loopOnCompletionListener = new MediaPlayer.OnCompletionListener() { public void onCompletion( MediaPlayer mp ) { if ( null != mPlaylistCursor ) { if ( mLoopState == LOOP_ALL ) { //android.util.Log.i( TAG, "LOOP_ALL"); setPlaylistPosition( ( mPlaylistPosition + 1 ) % mPlaylistSize ); // Will always loop the whole thing } else if ( mLoopState == LOOP_NO ) { //android.util.Log.i( TAG, "LOOP_NO"); setPlaylistPosition( mPlaylistPosition + 1 ); } else if ( mLoopState == LOOP_ONE ) { // We don't need to change the media on LOOP_ONE, but we need to alert the client //android.util.Log.i( TAG, "LOOP_ONE"); setPlaylistPosition( mPlaylistPosition ); } } } }; MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() { @Override public void onPrepared( MediaPlayer mp ) { isPrepared = true; if ( PlaybackChanged != null ) { PlaybackChanged.onDurationChanged( mp.getCurrentPosition(), mp.getDuration() ); } if ( isPlaying ) { if ( null != PlaybackChanged ) { PlaybackChanged.onPlay(); } mp.start(); } else { if ( null != PlaybackChanged ) { PlaybackChanged.onPause(); } } } }; // // Public Methods // // // // Clean up the media player and associated acts when you're done // public void destroy() { mMediaPlayer.reset(); mMediaPlayer.release(); /* if ( wl.isHeld() ) { wl.release(); } */ //mContext.unregisterReceiver( HardwareStopReceiver ); } public void setPlaylistQuery( MediaQuery query ) { if ( null == mMediaQuery || !mMediaQuery.equals( query ) ) { mMediaQuery = query; if ( mPlaylistCursor != null && !mPlaylistCursor.isClosed() ) { mPlaylistCursor.close(); } mPlaylistCursor = MediaQuery.execute( mContext, mMediaQuery ); //mPlaylistCursor.registerDataSetObserver( mPlaylistCursorObserver ); if ( mPlaylistCursor != null ) { mPlaylistSize = mPlaylistCursor.getCount(); mPlaylistPosition = 0; /* if ( isShuffling ) { generateShuffledPlaylist(); } */ setShuffle( false ); } else { mMediaQuery = null; } } } public MediaQuery getPlaylistQuery() { return mMediaQuery; } public boolean isPlaying() { return isPlaying; } public void play() { if ( null != mPlaylistCursor && !mPlaylistCursor.isBeforeFirst() && !mPlaylistCursor.isAfterLast() ) { isPlaying = true; if ( null != PlaybackChanged ) { PlaybackChanged.onPlay(); } if ( isPrepared ) { mMediaPlayer.start(); } /* if ( !wl.isHeld() ) { wl.acquire(); // Wake lock acquired } */ } } public void pause() { if ( mMediaPlayer.isPlaying() ) { isPlaying = false; mMediaPlayer.pause(); if ( null != PlaybackChanged ) { PlaybackChanged.onPause(); } } } public void back() { if ( null != mPlaylistCursor && null != mMediaPlayer ) { int position = mMediaPlayer.getCurrentPosition(); int duration = mMediaPlayer.getDuration(); if ( position >= ( 0.08 * duration ) ) { setSeekPosition( 0 ); } else { previousTrack(); } } } public void previousTrack() { if ( null != mPlaylistCursor ) { setPlaylistPosition( mPlaylistPosition - 1 ); } } public void nextTrack() { if ( null != mPlaylistCursor ) { setPlaylistPosition( mPlaylistPosition + 1 ); } } public boolean hasNextTrack() { return ( mLoopState != LOOP_NO || mPlaylistPosition < mPlaylistCursor.getCount() - 1 ); } public boolean hasPreviousTrack() { return ( mLoopState != LOOP_NO || mPlaylistPosition > 0 ); } // // Abstracting out collecting information on the current song // // NOTE: PlaylistMediaPlayer expects column 0 of the Cursor to be the ID of the media // public boolean isPlaylistReady() { return !( mPlaylistCursor == null || mPlaylistPosition < 0 || mPlaylistPosition >= mPlaylistSize //|| mPlaylistCursor.isAfterLast() //|| mPlaylistCursor.isBeforeFirst() || mPlaylistCursor.isClosed() ); } public String getCurrentMediaID() { String media_id = null; if ( isPlaylistReady() ) { if ( isShuffling ) { int shufflePosition = mShuffledPlaylist[ mPlaylistPosition ]; mPlaylistCursor.moveToPosition( shufflePosition ); media_id = mPlaylistCursor.getString( 0 ); } else { mPlaylistCursor.moveToPosition( mPlaylistPosition ); media_id = mPlaylistCursor.getString( 0 ); } } return media_id; } public int getTrackPosition() { return mMediaPlayer.getCurrentPosition(); } public int getTrackDuration() { return mMediaPlayer.getDuration(); } public void setPlaylistPosition( int position ) { try { mPlaylistPosition = position; // Is the new position out of bounds? if ( mPlaylistPosition < 0 || mPlaylistPosition >= mPlaylistSize) { // Out of bounds, but looping, so we bring the position back if ( mLoopState == LOOP_ALL ) { mPlaylistPosition = mPlaylistPosition % mPlaylistSize; } else { // Not looping and out of bounds pause(); // Send stopped event if ( null != PlaybackChanged ) { PlaybackChanged.onPlaylistDone(); } return; } } // // NOTE: Calling reset clears the looping, this only affects the LOOP_ONE state. // Nonetheless, we need to set the loop state again after calling reset() // mMediaPlayer.reset(); isPrepared = false; //setLooping( mLoopState ); if ( !isShuffling ) { mPlaylistCursor.moveToPosition( mPlaylistPosition ); mMediaPlayer.setDataSource( mPlaylistCursor.getString( mPlaylistCursor.getColumnIndexOrThrow( MediaStore.Audio.Media.DATA ) ) ); if ( null != PlaybackChanged ) { PlaybackChanged.onTrackChanged( getCurrentMediaID() ); } } else { // Shuffle! int nextInt = mShuffledPlaylist[ mPlaylistPosition ]; mPlaylistCursor.moveToPosition( nextInt ); mMediaPlayer.setDataSource( mPlaylistCursor.getString( mPlaylistCursor.getColumnIndexOrThrow( MediaStore.Audio.Media.DATA ) ) ); if ( null != PlaybackChanged ) { PlaybackChanged.onTrackChanged( getCurrentMediaID() ); } } mMediaPlayer.prepareAsync(); } catch ( Exception e ) { e.printStackTrace(); } } public int getPlaylistPosition() { return mPlaylistPosition; } public void setSeekPosition( int msecs ) { if ( this.isPlaying && mMediaPlayer.isPlaying() ) { mMediaPlayer.pause(); } mMediaPlayer.seekTo( msecs ); if ( isPlaying ) { play(); } } // // Looping / Repeating // // The standard media player has a built-in setLooping function that sets whether the current media will loop or not. // We want to be able to loop the entire playlist. To represent this third state of looping, the LoopState enum // is used. MediaPlayer's setLooping() method still accepts a boolean and PlaylistMediaPlayer's mLoopState is update // with the appropriate LoopState.LOOP_NO or LoopState.LOOP_ONE value. The setLooping() method is overloaded with a // version that accepts a LoopState enum and through this you can set any of the three loop states. // public void setLooping( int state ) { mLoopState = state; if ( null != PlaybackChanged ) { PlaybackChanged.onLoopingChanged( mLoopState ); } } public int getLoopState() { return mLoopState; } // // Shuffling // // To avoid altering the structure of the playlist Cursor, we generate // a list of numbers where the numbers represent the indices of songs // in the cursor. // // For shuffling, we generate a seed number whenever shuffle is turned on // That seed number is used by the algorithm to // // // // Updated: Should not change the play position when turning shuffle on or off // public void setShuffle( boolean shouldShuffle ) { isShuffling = shouldShuffle; if ( isShuffling ) { generateShuffledPlaylist(); } else { mShuffledPlaylist = null; } if ( null != PlaybackChanged ) { PlaybackChanged.onShuffleChanged( shouldShuffle );//( mMediaPlayer.getCurrentPosition() ); } } public boolean isShuffling() { return isShuffling; } private void generateShuffledPlaylist() { int size = mPlaylistSize; mShuffledPlaylist = new int[ mPlaylistSize ]; for ( int i = 0; i < size; i++ ) { mShuffledPlaylist[ i ] = i; } shuffleArray( mShuffledPlaylist ); } // // Implementing Fisher�Yates shuffle void shuffleArray( int[] ar ) { Random rnd = new Random(); for ( int i = ar.length - 1; i > 0; i-- ) { if ( i == mPlaylistPosition) { continue; } int index = rnd.nextInt( i + 1 ); while ( index == mPlaylistPosition) { index = rnd.nextInt( i + 1 ); } // Simple swap int a = ar[ index ]; ar[ index ] = ar[ i ]; ar[ i ] = a; } } // Volume Ducking public void startVolumeDucking() { if ( !isDucking ) { mMediaPlayer.setVolume( 0.4f, 0.4f ); isDucking = true; } } public void stopVolumeDucking() { if ( isDucking ) { mMediaPlayer.setVolume( 1.0f, 1.0f ); } } }