/* * Copyright (c) 2013 Allogy Interactive. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.allogy.app.media; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.os.Binder; import android.os.IBinder; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import com.allogy.app.R; import com.allogy.app.adapter.AudioPlaylistArrayAdapter; import com.allogy.app.provider.Academic; import com.allogy.app.util.Util; /** * <p> * Service that enables the play back of audio files. * <ul> * <li> * Enables preparing an audio for play back.</li> * <li> * Enables play back.</li> * <li> * Enables pausing.</li> * <li> * Enables seeking.</li> * <li> * Enables enables navigating files in a play list.<i>NOTE: Currently the play * list is hard coded and will not be saved when the service is terminated.</i></li> * </ul> * </p> * * @author Diego Nunez */ public final class AudioPlayerService extends Service implements PlaybackTimer { // TODO: Move any hard coded string references into the string.xml resource // and reference them from there. // / // / CONSTANTS // / public static final String LOG_TAG = "AudioPlayerService"; public static final boolean DBG_LOG_ENABLE = false; // public static final int ERROR = -1; public static final String INTENT_EXTRA_LESSON_FILE_ID = "audioplayerservice.lessonfileid"; // / // / PROPERTIES // / /** * <p> * 1 second. * </p> */ public static final int TIMER_UPDATE_INTERVAL = 1000; private MediaPlayer mMediaPlayer = new MediaPlayer(); private AudioPlayerBinder mBinder = new AudioPlayerBinder(); private OnUpdateListener mUpdateListener = null; private List<AudioItem> mPlayList = new ArrayList<AudioItem>(); private AudioItem currentAudio = null; /** * <p> * Keeps track of the playback state. * </p> */ private boolean playbackIsPaused = false, playbackHasStopped = true, playbackPrepared = false; private NotificationManager mNotificationManager; // / // / SERVICE EVENTS // / @Override public IBinder onBind(Intent intent) { mNotificationManager .cancel(R.string.audioplayerservice_notification_id); return mBinder; } @Override public void onCreate() { mMediaPlayer.setOnCompletionListener(mPlaybackCompletionListener); mNotificationManager = (NotificationManager) this .getSystemService(Context.NOTIFICATION_SERVICE); // Register the phone state listener. TelephonyManager tm = (TelephonyManager) this .getSystemService(Context.TELEPHONY_SERVICE); tm.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } @Override public void onStart(Intent intent, int startId) { FetchStartIntentExtra(intent); } @Override public int onStartCommand(Intent intent, int flags, int startId) { FetchStartIntentExtra(intent); return Service.START_STICKY; } @Override public void onRebind(Intent intent) { super.onRebind(intent); } @Override public boolean onUnbind(Intent intent) { mBinder.SetUpdateListener(null); if (playbackHasStopped || playbackIsPaused) { StopPlaybackTimer(); this.stopSelf(); } else { showNotification(currentAudio.getDisplayName(), true, R.drawable.production); } return true; } @Override public void onDestroy() { super.onDestroy(); // perform clean up. StopPlaybackTimer(); if (null != mMediaPlayer) { mMediaPlayer.release(); } mNotificationManager .cancel(R.string.audioplayerservice_notification_id); // Unregister the phone state listener. TelephonyManager tm = (TelephonyManager) this .getSystemService(Context.TELEPHONY_SERVICE); tm.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); } // / // / METHODS // / /** * Adds an audio item to the play list if it is contained in the * <b>Intent</b>. * * @param intent * The Intent that called the <b>AudioPlayerService</b>. */ private void FetchStartIntentExtra(Intent intent) { int id = Util.OUT_OF_BOUNDS; if (null != intent && intent.hasExtra(INTENT_EXTRA_LESSON_FILE_ID) && (id = intent.getIntExtra(INTENT_EXTRA_LESSON_FILE_ID, Util.OUT_OF_BOUNDS)) != Util.OUT_OF_BOUNDS) { Cursor cursor = this.getContentResolver().query( Academic.LessonFiles.CONTENT_URI, new String[] { Academic.LessonFiles.URI }, String.format("%s = ?", Academic.LessonFiles._ID), new String[] { Integer.toString(id) }, null); if (cursor.moveToFirst()) { mBinder .AddToPlayList((new AudioItem( id, cursor .getString(cursor .getColumnIndexOrThrow(Academic.LessonFiles.URI))))); } cursor.close(); } } /** * Posts a <b>Notification</b> to the <b>NotificationManager</b>. * * @param message * The message to show on the <b>Notification</b>. * @param autocancel * If the <b>Notification</b> is canceled when clicked by the * user. * @param icon * The image to display for the <b>Notification</b>. */ private void showNotification(String message, boolean autocancel, int icon) { Notification notification = new Notification(icon, message, System .currentTimeMillis()); // We do not want a new Activity to show, so we provide an empty // intent. PendingIntent pintent = PendingIntent.getActivity(this, 0, new Intent( this, AudioPlayerActivity.class), PendingIntent.FLAG_ONE_SHOT); if (autocancel) { notification.flags = Notification.FLAG_AUTO_CANCEL; } notification.setLatestEventInfo(this, this .getString(R.string.audioplayerservice_notification_id), message, pintent); mNotificationManager.notify( R.string.audioplayerservice_notification_id, notification); } // / // / EVENT LISTENERS // / /** * Event handler for <b>MediaPlayer</b> completion event. We make sure the * play back has stopped. */ private OnCompletionListener mPlaybackCompletionListener = new OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mBinder.Stop(); } }; /** * Event handler for the state of the phone. When a call is incoming, be * stop play back. */ private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { switch (state) { case TelephonyManager.CALL_STATE_IDLE: // TODO: We might be able to restart play back automatically in // this state. break; case TelephonyManager.CALL_STATE_OFFHOOK: // Not needed. break; case TelephonyManager.CALL_STATE_RINGING: if (!playbackHasStopped && !playbackIsPaused) { mBinder.Stop(); } break; default: break; } super.onCallStateChanged(state, incomingNumber); } }; // / // / THREADS // / /* Update Timer */ private Timer mPlaybackTimer = null; /** * */ public void StartPlaybackTimer() { StopPlaybackTimer(); if (null != mUpdateListener) { mPlaybackTimer = new Timer(); mPlaybackTimer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { UpdatePlaybackProgress(); } }, 0, TIMER_UPDATE_INTERVAL); } } /** * */ public void UpdatePlaybackProgress() { if (!playbackIsPaused && null != mUpdateListener) { mUpdateListener.onUpdate(mMediaPlayer.getCurrentPosition()); } } /** * */ public void StopPlaybackTimer() { if (null != mPlaybackTimer) { mPlaybackTimer.cancel(); mPlaybackTimer = null; } } /* End Update Timer */ // / // / INTERNAL CLASSES // / /** * The interface for which an <b>Activity</b> can interact with the * <b>AudioPlayerService</b>. */ public class AudioPlayerBinder extends Binder { /** * Setter for specifying an update event handler. * * @param listener * The update event handler. */ public void SetUpdateListener(OnUpdateListener listener) { mUpdateListener = listener; } /** * Getter for the <b>Service</b>. * * @return <p> * The current <b>AudioPlayerService</b> binded by this instance * of the <b>Binder</b> object. * </p> */ public AudioPlayerService GetService() { return AudioPlayerService.this; } /** * Getter for the <b>MediaPlayer</b> prepared state. * * @return True if the <b>MediaPlayer</b> is prepared, false otherwise. */ public boolean isPlaybackPrepared() { return playbackPrepared; } /** * <p> * Prepares the audio to be played and gives the opportunity to set up a * progress tracking. <i>Note: This method must always be called prior * to calling play such that the new desired song is played.</i> * </p> * * @param audio * <p> * The AudioItem to play. * </p> * * @return <p> * The length of the audio's play back. * </p> */ public int PreparePlayback(AudioItem audio) { int result = 0; if (null == audio) { return result; } currentAudio = audio; File audioFile = new File(currentAudio.getUri()); if(DBG_LOG_ENABLE) { Log.i(LOG_TAG, "playbackHasStopped : " + playbackHasStopped + " Audio File Present : " + audioFile.exists()); } if (playbackHasStopped && audioFile.exists()) { try { mMediaPlayer.setDataSource(currentAudio.getUri()); mMediaPlayer.prepare(); result = mMediaPlayer.getDuration(); if(DBG_LOG_ENABLE) { Log.i(LOG_TAG, " Media Player Prepared, Duration : " + result); } playbackPrepared = true; } catch (IllegalStateException ise) { playbackPrepared = false; if(DBG_LOG_ENABLE) { ise.printStackTrace(); } } catch (IOException ioe) { playbackPrepared = false; if(DBG_LOG_ENABLE) { ioe.printStackTrace(); } } } currentAudio.Tag = result; return result; } /** * <p> * Begins playback of an audio file. <i>Note: This method should not be * called to resume playback if it has been paused.</i> * </p> * * @return True if successful, false otherwise. */ public boolean Play() { if (!playbackPrepared && null != currentAudio) { if(DBG_LOG_ENABLE) { Log.i(LOG_TAG, " Playback not Prepared and currentAudio file is : " + currentAudio.getUri()); } PreparePlayback(currentAudio); } if(DBG_LOG_ENABLE) { Log.i(LOG_TAG, "playbackPrepared : " + playbackPrepared + "playbackIsPaused : " + playbackIsPaused); } if (playbackPrepared && !playbackIsPaused) { mMediaPlayer.start(); StartPlaybackTimer(); playbackHasStopped = false; return true; } return false; } /** * <p> * Pauses and Unpauses the playback of an audio file after it has * already been started. * </p> * * @return True if the play back of audio is paused, false if it is not * paused. */ public boolean Pause() { boolean result = false; if (!playbackHasStopped && !playbackIsPaused) { try { mMediaPlayer.pause(); result = playbackIsPaused = true; } catch (IllegalStateException ise) { // false will be returned. } } else if (!playbackHasStopped && playbackIsPaused) { try { mMediaPlayer.start(); playbackIsPaused = false; } catch (IllegalStateException ise) { // false will be returned. } } return result; } /** * Getter for the <b>MediaPlayer</b> pause state. * * @return True if paused, false otherwise. */ public boolean isPaused() { return !playbackHasStopped & playbackIsPaused; } /** * <p> * Completely stops the playback of an audio file an resets the * <b>MediaPlayer</b> to an uninitialized state. <i>NOTE: To replay the * audio file, it must be prepared again and then it can be played.</i> * </p> */ public void Stop() { if (playbackPrepared) { if (!playbackHasStopped) { try { mMediaPlayer.stop(); playbackHasStopped = true; if (null != mUpdateListener) { mUpdateListener.onUpdate(0); } } catch (IllegalStateException ise) { // We still want to reset even if an error occurred. } } mMediaPlayer.reset(); playbackHasStopped = true; playbackPrepared = false; playbackIsPaused = false; StopPlaybackTimer(); } } /** * Getter for the <b>MediaPlayer</b> stopped state. * * @return True if stopped, false otherwise. */ public boolean isStopped() { return playbackHasStopped; } /** * Enables seeking. * * @param progress * The location to seek to in milliseconds. */ public void SeekTo(int progress) { if (!playbackHasStopped) { mMediaPlayer.seekTo(progress); } } /** * Getter for the <b>MediaPlayer</b>'s current play back duration. * * @return The current duration of audio playback. */ public int GetPlaybackDuration() { return mMediaPlayer.getDuration(); } /** * Getter for the <b>MediaPlayer</b>'s current play back progress. * * @return The current progress of audio playback. */ public int GetPlaybackProgress() { return mMediaPlayer.getCurrentPosition(); } /** * Getter for the current <b>AudioItem</b>. * * @return The currently playing <b>AudioItem</b>. */ public AudioItem GetCurrentAudio() { return currentAudio; } /** * Enables skipping to the next <b>AudioItem</b> in the play list. * * @return <p> * The play time of the audio if the skip was successful, * <b>AudioPlayerService.ERROR</b> if the play list is empty. * </p> */ public AudioItem SkipForward() { Stop(); if (mPlayList.size() == 0) { return null; } else if (null == currentAudio) { AudioItem item = mPlayList.get(0); return item; } else { int next = (mPlayList.indexOf(currentAudio) + 1) % mPlayList.size(); AudioItem item = mPlayList.get(next); return item; } } /** * Enables skipping to the previous <b>AudioItem</b> in the play list. * * @return <p> * The play time of the audio if the skip was successful, * <b>AudioPlayerService.ERROR</b> if the play list is empty. * </p> */ public AudioItem SkipBackwards() { Stop(); if (mPlayList.size() == 0) { return null; } else if (null == currentAudio) { AudioItem item = mPlayList.get(0); return item; } else { int index = mPlayList.indexOf(currentAudio); int size = mPlayList.size(); int prev = (index - 1 + size) % size; AudioItem item = mPlayList.get(prev); return item; } } /** * Adds and <b>AudioItem</b> to the plays list. * * @return <p> * True if the audio file was successfully added to the play * list, false otherwise. * </p> */ public boolean AddToPlayList(AudioItem item) { if (!mPlayList.contains(item)) { return mPlayList.add(item); } return false; } /** * Removes an <b>AudioItem</b> from the play list. * * @return <p> * True if the audio file was successfully removed from the play * list, false otherwise. * </p> */ public boolean RemoveFromPlayList(String item) { if (mPlayList.contains(item)) { return mPlayList.remove(item); } return false; } /** * Clears all <b>AudioItem</b>'s from the play list. * * @return <p> * True if the play list was successfully cleared, false if the * play list was already empty. * </p> */ public boolean ClearPlayList() { if (mPlayList.size() == 0) { return false; } mPlayList.clear(); return true; } /** * Retrieves an adapter for displaying the current play list onto a * <b>ListView</b>. * * @param context * The <b>Context</b> that holds the <b>ListView</b>. * @return A new instance of <b>AudioPlaylistArrayAdapter</b>. */ public AudioPlaylistArrayAdapter ShowPlayList(Context context) { return new AudioPlaylistArrayAdapter(context, mPlayList); } } }