/* * This code is based on the RandomMusicPlayer example from * the Android Open Source Project samples. It has been modified * for use in Quran Android. * * Copyright (C) 2011 The Android Open Source Project * * 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.quran.labs.androidquran.service; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.drawable.ColorDrawable; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnPreparedListener; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaButtonReceiver; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.support.v7.app.NotificationCompat; import android.util.SparseIntArray; import com.crashlytics.android.Crashlytics; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.data.QuranInfo; import com.quran.labs.androidquran.data.SuraAyah; import com.quran.labs.androidquran.database.DatabaseUtils; import com.quran.labs.androidquran.database.SuraTimingDatabaseHandler; import com.quran.labs.androidquran.service.util.AudioFocusHelper; import com.quran.labs.androidquran.service.util.AudioFocusable; import com.quran.labs.androidquran.service.util.AudioRequest; import com.quran.labs.androidquran.service.util.QuranDownloadNotifier; import com.quran.labs.androidquran.service.util.RepeatInfo; import com.quran.labs.androidquran.ui.PagerActivity; import com.quran.labs.androidquran.util.AudioUtils; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import timber.log.Timber; /** * Service that handles media playback. This is the Service through which we * perform all the media handling in our application. It waits for Intents * (which come from our main activity, {@link PagerActivity}, which signal * the service to perform specific operations: Play, Pause, Rewind, Skip, etc. */ public class AudioService extends Service implements OnCompletionListener, OnPreparedListener, OnErrorListener, AudioFocusable, MediaPlayer.OnSeekCompleteListener { // These are the Intent actions that we are prepared to handle. Notice that // the fact these constants exist in our class is a mere convenience: what // really defines the actions our service can handle are the <action> tags // in the <intent-filters> tag for our service in AndroidManifest.xml. public static final String ACTION_PLAYBACK = "com.quran.labs.androidquran.action.PLAYBACK"; public static final String ACTION_PLAY = "com.quran.labs.androidquran.action.PLAY"; public static final String ACTION_PAUSE = "com.quran.labs.androidquran.action.PAUSE"; public static final String ACTION_STOP = "com.quran.labs.androidquran.action.STOP"; public static final String ACTION_SKIP = "com.quran.labs.androidquran.action.SKIP"; public static final String ACTION_REWIND = "com.quran.labs.androidquran.action.REWIND"; public static final String ACTION_CONNECT = "com.quran.labs.androidquran.action.CONNECT"; public static final String ACTION_UPDATE_REPEAT = "com.quran.labs.androidquran.action.UPDATE_REPEAT"; // pending notification request codes private static final int REQUEST_CODE_MAIN = 0; private static final int REQUEST_CODE_PREVIOUS = 1; private static final int REQUEST_CODE_PAUSE = 2; private static final int REQUEST_CODE_SKIP = 3; private static final int REQUEST_CODE_STOP = 4; private static final int REQUEST_CODE_RESUME = 5; public static class AudioUpdateIntent { public static final String INTENT_NAME = "com.quran.labs.androidquran.audio.AudioUpdate"; public static final String STATUS = "status"; public static final String SURA = "sura"; public static final String AYAH = "ayah"; public static final String REPEAT_COUNT = "repeat_count"; public static final String REQUEST = "request"; public static final int STOPPED = 0; public static final int PLAYING = 1; public static final int PAUSED = 2; } // The volume we set the media player to when we lose audio focus, but are // allowed to reduce the volume instead of stopping playback. public static final float DUCK_VOLUME = 0.1f; // our media player private MediaPlayer mPlayer = null; // are we playing an override file (basmalah/isti3atha) private boolean mPlayerOverride; // our AudioFocusHelper object, if it's available (it's available on SDK // level >= 8). If not available, this will be null. Always check for null // before using! private AudioFocusHelper mAudioFocusHelper = null; // object representing the current playing request private AudioRequest mAudioRequest = null; // so user can pass in a serializable AudioRequest to the intent public static final String EXTRA_PLAY_INFO = "com.quran.labs.androidquran.PLAY_INFO"; // ignore the passed in play info if we're already playing public static final String EXTRA_IGNORE_IF_PLAYING = "com.quran.labs.androidquran.IGNORE_IF_PLAYING"; // used to override what is playing now (stop then play) public static final String EXTRA_STOP_IF_PLAYING = "com.quran.labs.androidquran.STOP_IF_PLAYING"; // repeat info public static final String EXTRA_VERSE_REPEAT_COUNT = "com.quran.labs.androidquran.VERSE_REPEAT_COUNT"; public static final String EXTRA_RANGE_REPEAT_COUNT = "com.quran.labs.androidquran.RANGE_REPEAT_COUNT"; public static final String EXTRA_RANGE_RESTRICT = "com.quran.labs.androidquran.RANGE_RESTRICT"; // indicates the state our service: private enum State { Stopped, // media player is stopped and not prepared to play Preparing, // media player is preparing... Playing, // playback active (media player ready!). (but the media // player may actually be paused in this state if we don't have audio // focus. But we stay in this state so that we know we have to resume // playback once we get focus back) Paused // playback paused (media player ready!) } private State mState = State.Stopped; // do we have audio focus? private enum AudioFocus { NoFocusNoDuck, // we don't have audio focus, and can't duck NoFocusCanDuck, // we don't have focus, but can play at a low volume Focused // we have full audio focus } private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck; // are we already in the foreground private boolean mIsSetupAsForeground = false; // should we stop (after preparing is done) or not private boolean mShouldStop = false; // Wifi lock that we hold when streaming files from the internet, // in order to prevent the device from shutting off the Wifi radio private WifiLock mWifiLock; // The ID we use for the notification (the onscreen alert that appears // at the notification area at the top of the screen as an icon -- and // as text as well if the user expands the notification area). final int NOTIFICATION_ID = 4; private NotificationManager mNotificationManager; // TODO: Merge these builders into one private NotificationCompat.Builder mNotificationBuilder; private NotificationCompat.Builder mPausedNotificationBuilder; private LocalBroadcastManager mBroadcastManager = null; private BroadcastReceiver noisyAudioStreamReceiver; private MediaSessionCompat mMediaSession; private int mGaplessSura = 0; private int mNotificationColor; private Bitmap mNotificationIcon; private Bitmap mDisplayIcon; private SparseIntArray mGaplessSuraData = null; private AsyncTask<Integer, Void, SparseIntArray> mTimingTask = null; public static final int MSG_START_AUDIO = 1; public static final int MSG_UPDATE_AUDIO_POS = 2; private static class ServiceHandler extends Handler { private WeakReference<AudioService> mServiceRef; public ServiceHandler(AudioService service) { mServiceRef = new WeakReference<>(service); } @Override public void handleMessage(Message msg) { final AudioService service = mServiceRef.get(); if (service == null || msg == null) { return; } if (msg.what == MSG_START_AUDIO) { service.configAndStartMediaPlayer(); } else if (msg.what == MSG_UPDATE_AUDIO_POS) { service.updateAudioPlayPosition(); } } } private Handler mHandler; /** * Makes sure the media player exists and has been reset. This will create * the media player if needed, or reset the existing media player if one * already exists. */ private void createMediaPlayerIfNeeded() { if (mPlayer == null) { mPlayer = new MediaPlayer(); // Make sure the media player will acquire a wake-lock while playing. // If we don't do that, the CPU might go to sleep while the song is // playing, causing playback to stop. // // Remember that to use this, we have to declare the // android.permission.WAKE_LOCK permission in AndroidManifest.xml. mPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); // we want the media player to notify us when it's ready preparing, // and when it's done playing: mPlayer.setOnPreparedListener(this); mPlayer.setOnCompletionListener(this); mPlayer.setOnErrorListener(this); mPlayer.setOnSeekCompleteListener(this); mMediaSession.setActive(true); } else { Crashlytics.log("resetting mPlayer..."); mPlayer.reset(); } } @Override public void onCreate() { Timber.i("debug: Creating service"); mHandler = new ServiceHandler(this); final Context appContext = getApplicationContext(); mWifiLock = ((WifiManager) appContext.getSystemService(Context.WIFI_SERVICE)) .createWifiLock(WifiManager.WIFI_MODE_FULL, "QuranAudioLock"); mNotificationManager = (NotificationManager) appContext.getSystemService(NOTIFICATION_SERVICE); // create the Audio Focus Helper, if the Audio Focus feature is available mAudioFocusHelper = new AudioFocusHelper(appContext, this); mBroadcastManager = LocalBroadcastManager.getInstance(appContext); noisyAudioStreamReceiver = new NoisyAudioStreamReceiver(); registerReceiver( noisyAudioStreamReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); ComponentName receiver = new ComponentName(this, MediaButtonReceiver.class); mMediaSession = new MediaSessionCompat(appContext, "QuranMediaSession", receiver, null); mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); mMediaSession.setCallback(new MediaSessionCallback()); mNotificationColor = ContextCompat.getColor(this, R.color.audio_notification_color); try { // for Android Wear, use a 1x1 Bitmap with the notification color mDisplayIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mDisplayIcon); canvas.drawColor(mNotificationColor); } catch (OutOfMemoryError oom) { Crashlytics.logException(oom); } } private class MediaSessionCallback extends MediaSessionCompat.Callback { @Override public void onPlay() { processPlayRequest(); } @Override public void onSkipToNext() { processSkipRequest(); } @Override public void onSkipToPrevious() { processRewindRequest(); } @Override public void onPause() { processPauseRequest(); } @Override public void onStop() { processStopRequest(); } } public int onStartCommand(Intent intent, int flags, int startId) { if (intent == null) { // handle a crash that occurs where intent comes in as null if (State.Stopped == mState) { mHandler.removeCallbacksAndMessages(null); stopSelf(); } return START_NOT_STICKY; } final String action = intent.getAction(); if (ACTION_CONNECT.equals(action)) { if (State.Stopped == mState) { processStopRequest(true); } else { int sura = -1; int ayah = -1; int repeatCount = -200; int state = AudioUpdateIntent.PLAYING; if (State.Paused == mState) { state = AudioUpdateIntent.PAUSED; } if (mAudioRequest != null) { sura = mAudioRequest.getCurrentSura(); ayah = mAudioRequest.getCurrentAyah(); final RepeatInfo repeatInfo = mAudioRequest.getRepeatInfo(); if (repeatInfo != null) { repeatCount = repeatInfo.getRepeatCount(); } } Intent updateIntent = new Intent(AudioUpdateIntent.INTENT_NAME); updateIntent.putExtra(AudioUpdateIntent.STATUS, state); updateIntent.putExtra(AudioUpdateIntent.SURA, sura); updateIntent.putExtra(AudioUpdateIntent.AYAH, ayah); updateIntent.putExtra(AudioUpdateIntent.REPEAT_COUNT, repeatCount); updateIntent.putExtra(AudioUpdateIntent.REQUEST, mAudioRequest); mBroadcastManager.sendBroadcast(updateIntent); } } else if (ACTION_PLAYBACK.equals(action)) { AudioRequest playInfo = intent.getParcelableExtra(EXTRA_PLAY_INFO); if (playInfo != null) { if (State.Stopped == mState || !intent.getBooleanExtra(EXTRA_IGNORE_IF_PLAYING, false)) { mAudioRequest = playInfo; Crashlytics.log("audio request has changed..."); } } if (intent.getBooleanExtra(EXTRA_STOP_IF_PLAYING, false)) { if (mPlayer != null) { mPlayer.stop(); } mState = State.Stopped; Crashlytics.log("stop if playing..."); } processTogglePlaybackRequest(); } else if (ACTION_PLAY.equals(action)) { processPlayRequest(); } else if (ACTION_PAUSE.equals(action)) { processPauseRequest(); } else if (ACTION_SKIP.equals(action)) { processSkipRequest(); } else if (ACTION_STOP.equals(action)) { processStopRequest(); } else if (ACTION_REWIND.equals(action)) { processRewindRequest(); } else if (ACTION_UPDATE_REPEAT.equals(action)) { if (mAudioRequest != null) { // set the repeat info if applicable final int verseRepeatCount = intent .getIntExtra(EXTRA_VERSE_REPEAT_COUNT, mAudioRequest.getVerseRepeatCount()); mAudioRequest.setVerseRepeatCount(verseRepeatCount); // set the range repeat count final int rangeRepeatCount = intent .getIntExtra(EXTRA_RANGE_REPEAT_COUNT, mAudioRequest.getRangeRepeatCount()); mAudioRequest.setRangeRepeatCount(rangeRepeatCount); // set the enforce range flag if (intent.hasExtra(EXTRA_RANGE_RESTRICT)) { final boolean enforceRange = intent.getBooleanExtra(EXTRA_RANGE_RESTRICT, false); mAudioRequest.setEnforceBounds(enforceRange); } } } else { MediaButtonReceiver.handleIntent(mMediaSession, intent); } // we don't want the service to restart if killed return START_NOT_STICKY; } private class ReadGaplessDataTask extends AsyncTask<Integer, Void, SparseIntArray> { private int mSura = 0; private String mDatabasePath = null; public ReadGaplessDataTask(String database) { mDatabasePath = database; } @Override protected SparseIntArray doInBackground(Integer... params) { int sura = params[0]; mSura = sura; SuraTimingDatabaseHandler db = SuraTimingDatabaseHandler.getDatabaseHandler(mDatabasePath); SparseIntArray map = null; Cursor cursor = null; try { cursor = db.getAyahTimings(sura); Timber.d("got cursor of data"); if (cursor != null && cursor.moveToFirst()) { map = new SparseIntArray(); do { int ayah = cursor.getInt(1); int time = cursor.getInt(2); map.put(ayah, time); } while (cursor.moveToNext()); } } catch (SQLException se) { // don't crash the app if the database is corrupt Crashlytics.logException(se); } finally { DatabaseUtils.closeCursor(cursor); } return map; } @Override protected void onPostExecute(SparseIntArray map) { mGaplessSura = mSura; mGaplessSuraData = map; mTimingTask = null; } } private int getSeekPosition(boolean isRepeating) { if (mAudioRequest == null) { return -1; } if (mGaplessSura == mAudioRequest.getCurrentSura()) { if (mGaplessSuraData != null) { int ayah = mAudioRequest.getCurrentAyah(); Integer time = mGaplessSuraData.get(ayah); if (ayah == 1 && !isRepeating) { return mGaplessSuraData.get(0); } return time; } } return -1; } private void updateAudioPlayPosition() { Timber.d("updateAudioPlayPosition"); if (mAudioRequest == null) { return; } if (mPlayer != null || mGaplessSuraData == null) { int sura = mAudioRequest.getCurrentSura(); int ayah = mAudioRequest.getCurrentAyah(); int updatedAyah = ayah; int maxAyahs = QuranInfo.getNumAyahs(sura); if (sura != mGaplessSura) { return; } setState(PlaybackStateCompat.STATE_PLAYING); int pos = mPlayer.getCurrentPosition(); Integer ayahTime = mGaplessSuraData.get(ayah); Timber.d("updateAudioPlayPosition: %d:%d, currently at %d vs expected at %d", sura, ayah, pos, ayahTime); if (ayahTime > pos) { int iterAyah = ayah; while (--iterAyah > 0) { ayahTime = mGaplessSuraData.get(iterAyah); if (ayahTime <= pos) { updatedAyah = iterAyah; break; } else { updatedAyah--; } } } else { int iterAyah = ayah; while (++iterAyah <= maxAyahs) { ayahTime = mGaplessSuraData.get(iterAyah); if (ayahTime > pos) { updatedAyah = iterAyah - 1; break; } else { updatedAyah++; } } } Timber.d("updateAudioPlayPosition: %d:%d, decided ayah should be: %d", sura, ayah, updatedAyah); if (updatedAyah != ayah) { ayahTime = mGaplessSuraData.get(ayah); if (Math.abs(pos - ayahTime) < 150) { // shouldn't change ayahs if the delta is just 150ms... mHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 150); return; } SuraAyah nextAyah = mAudioRequest.setCurrentAyah(sura, updatedAyah); if (nextAyah == null) { processStopRequest(); return; } else if (nextAyah.sura != sura || nextAyah.ayah != updatedAyah) { // remove any messages currently in the queue mHandler.removeCallbacksAndMessages(null); // if the ayah hasn't changed, we're repeating the ayah, // otherwise, we're repeating a range. this variable is // what determines whether or not we replay the basmallah. final boolean ayahRepeat = (ayah == nextAyah.ayah && sura == nextAyah.sura); if (ayahRepeat) { // jump back to the ayah we should repeat and play it pos = getSeekPosition(true); mPlayer.seekTo(pos); } else { // we're repeating into a different sura final boolean flag = sura != mAudioRequest.getCurrentSura(); playAudio(flag); } return; } // moved on to next ayah updateNotification(); } else { // if we have end of sura info and we bypassed end of sura // line, switch the sura. ayahTime = mGaplessSuraData.get(999); if (ayahTime > 0 && pos >= ayahTime) { SuraAyah repeat = mAudioRequest.setCurrentAyah(sura + 1, 1); if (repeat != null && repeat.sura == sura) { // remove any messages currently in the queue mHandler.removeCallbacksAndMessages(null); // jump back to the ayah we should repeat and play it pos = getSeekPosition(false); mPlayer.seekTo(pos); } else { playAudio(true); } return; } } notifyAyahChanged(); if (maxAyahs >= (updatedAyah + 1)) { Integer t = mGaplessSuraData.get(updatedAyah + 1); t = t - mPlayer.getCurrentPosition(); Timber.d("updateAudioPlayPosition postingDelayed after: %d", t); if (t < 100) { t = 100; } else if (t > 10000) { t = 10000; } mHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, t); } // if we're on the last ayah, don't do anything - let the file // complete on its own to avoid getCurrentPosition() bugs. } } private void processTogglePlaybackRequest() { if (State.Paused == mState || State.Stopped == mState) { processPlayRequest(); } else { processPauseRequest(); } } private void processPlayRequest() { if (mAudioRequest == null) { return; } tryToGetAudioFocus(); // actually play the file if (State.Stopped == mState) { if (mAudioRequest.isGapless()) { if (mTimingTask != null) { mTimingTask.cancel(true); } String dbPath = mAudioRequest.getGaplessDatabaseFilePath(); mTimingTask = new ReadGaplessDataTask(dbPath); mTimingTask.execute(mAudioRequest.getCurrentSura()); } // If we're stopped, just go ahead to the next file and start playing playAudio(mAudioRequest.getCurrentSura() == 9 && mAudioRequest.getCurrentAyah() == 1); } else if (State.Paused == mState) { // If we're paused, just continue playback and restore the // 'foreground service' state. mState = State.Playing; setUpAsForeground(); configAndStartMediaPlayer(false); notifyAudioStatus(AudioUpdateIntent.PLAYING); } } private void processPauseRequest() { if (State.Playing == mState) { // Pause media player and cancel the 'foreground service' state. mState = State.Paused; mHandler.removeCallbacksAndMessages(null); mPlayer.pause(); setState(PlaybackStateCompat.STATE_PAUSED); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { // while paused, we always retain the MediaPlayer relaxResources(false, true); } else { // on jellybean and above, stay in the foreground and // update the notification. relaxResources(false, false); pauseNotification(); notifyAudioStatus(AudioUpdateIntent.PAUSED); } } else if (State.Stopped == mState) { // if we get a pause while we're already stopped, it means we likely woke up because // of AudioIntentReceiver, so just stop in this case. setState(PlaybackStateCompat.STATE_STOPPED); stopSelf(); } } private void processRewindRequest() { if (State.Playing == mState || State.Paused == mState) { setState(PlaybackStateCompat.STATE_REWINDING); int seekTo = 0; int pos = mPlayer.getCurrentPosition(); if (mAudioRequest.isGapless()) { seekTo = getSeekPosition(true); pos = pos - seekTo; } if (pos > 1500 && !mPlayerOverride) { mPlayer.seekTo(seekTo); mState = State.Playing; // in case we were paused } else { tryToGetAudioFocus(); int sura = mAudioRequest.getCurrentSura(); mAudioRequest.gotoPreviousAyah(); if (mAudioRequest.isGapless() && sura == mAudioRequest.getCurrentSura()) { int timing = getSeekPosition(true); if (timing > -1) { mPlayer.seekTo(timing); } updateNotification(); mState = State.Playing; // in case we were paused return; } playAudio(); } } } private void processSkipRequest() { if (mAudioRequest == null) { return; } if (State.Playing == mState || State.Paused == mState) { setState(PlaybackStateCompat.STATE_SKIPPING_TO_NEXT); if (mPlayerOverride) { playAudio(false); } else { final int sura = mAudioRequest.getCurrentSura(); tryToGetAudioFocus(); mAudioRequest.gotoNextAyah(true); if (mAudioRequest.isGapless() && sura == mAudioRequest.getCurrentSura()) { int timing = getSeekPosition(false); if (timing > -1) { mPlayer.seekTo(timing); mState = State.Playing; // in case we were paused } updateNotification(); return; } playAudio(); } } } private void processStopRequest() { processStopRequest(false); } private void processStopRequest(boolean force) { setState(PlaybackStateCompat.STATE_STOPPED); mHandler.removeCallbacksAndMessages(null); if (State.Preparing == mState) { mShouldStop = true; relaxResources(false, true); } if (force || State.Playing == mState || State.Paused == mState) { mState = State.Stopped; // let go of all resources... relaxResources(true, true); giveUpAudioFocus(); // service is no longer necessary. Will be started again if needed. mHandler.removeCallbacksAndMessages(null); stopSelf(); // stop async task if it's running if (mTimingTask != null) { mTimingTask.cancel(true); } // tell the ui we've stopped notifyAudioStatus(AudioUpdateIntent.STOPPED); } } private void notifyAyahChanged() { if (mAudioRequest != null) { Intent updateIntent = new Intent(AudioUpdateIntent.INTENT_NAME); updateIntent.putExtra(AudioUpdateIntent.STATUS, AudioUpdateIntent.PLAYING); updateIntent.putExtra(AudioUpdateIntent.SURA, mAudioRequest.getCurrentSura()); updateIntent.putExtra(AudioUpdateIntent.AYAH, mAudioRequest.getCurrentAyah()); mBroadcastManager.sendBroadcast(updateIntent); MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, mAudioRequest.getTitle(this)); if (mPlayer.isPlaying()) { metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mPlayer.getDuration()); } if (mDisplayIcon != null) { metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, mDisplayIcon); } mMediaSession.setMetadata(metadataBuilder.build()); } } private void notifyAudioStatus(int status) { Intent updateIntent = new Intent(AudioUpdateIntent.INTENT_NAME); updateIntent.putExtra(AudioUpdateIntent.STATUS, status); mBroadcastManager.sendBroadcast(updateIntent); } /** * Releases resources used by the service for playback. This includes the * "foreground service" status and notification, the wake locks and * possibly the MediaPlayer. * * @param releaseMediaPlayer Indicates whether the Media Player should also * be released or not */ private void relaxResources(boolean releaseMediaPlayer, boolean stopForeground) { if (stopForeground) { // stop being a foreground service stopForeground(true); mIsSetupAsForeground = false; } // stop and release the Media Player, if it's available if (releaseMediaPlayer && mPlayer != null) { mPlayer.reset(); mPlayer.release(); mPlayer = null; mMediaSession.setActive(false); } // we can also release the Wifi lock, if we're holding it if (mWifiLock.isHeld()) { mWifiLock.release(); } } private void giveUpAudioFocus() { if (mAudioFocus == AudioFocus.Focused && mAudioFocusHelper != null && mAudioFocusHelper.abandonFocus()) { mAudioFocus = AudioFocus.NoFocusNoDuck; } } /** * Reconfigures MediaPlayer according to audio focus settings and * starts/restarts it. This method starts/restarts the MediaPlayer * respecting the current audio focus state. So if we have focus, * it will play normally; if we don't have focus, it will either * leave the MediaPlayer paused or set it to a low volume, depending * on what is allowed by the current focus settings. This method assumes * mPlayer != null, so if you are calling it, you have to do so from a * context where you are sure this is the case. */ private void configAndStartMediaPlayer() { configAndStartMediaPlayer(true); } private void configAndStartMediaPlayer(boolean canSeek) { Timber.d("configAndStartMediaPlayer()"); if (mAudioFocus == AudioFocus.NoFocusNoDuck) { // If we don't have audio focus and can't duck, we have to pause, // even if mState is State.Playing. But we stay in the Playing state // so that we know we have to resume playback once we get focus back. if (mPlayer.isPlaying()) { mPlayer.pause(); } return; } else if (mAudioFocus == AudioFocus.NoFocusCanDuck) { // we'll be relatively quiet mPlayer.setVolume(DUCK_VOLUME, DUCK_VOLUME); } else { mPlayer.setVolume(1.0f, 1.0f); } // we can be loud if (mShouldStop) { processStopRequest(); mShouldStop = false; return; } if (mPlayerOverride) { if (!mPlayer.isPlaying()) { mPlayer.start(); } return; } Timber.d("checking if playing..."); if (!mPlayer.isPlaying()) { if (canSeek && mAudioRequest.isGapless()) { int timing = getSeekPosition(false); if (timing != -1) { Timber.d("got timing: %d, seeking and updating later...", timing); mPlayer.seekTo(timing); return; } else { Timber.d("no timing data yet, will try again..."); // try to play again after 200 ms mHandler.sendEmptyMessageDelayed(MSG_START_AUDIO, 200); return; } } else if (mAudioRequest.isGapless()) { mHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200); } mPlayer.start(); } } private void tryToGetAudioFocus() { if (mAudioFocus != AudioFocus.Focused && mAudioFocusHelper != null && mAudioFocusHelper.requestFocus()) { mAudioFocus = AudioFocus.Focused; } } /** * Starts playing the next file. */ private void playAudio() { playAudio(false); } private void playAudio(boolean playRepeatSeparator) { mState = State.Stopped; relaxResources(false, false); // release everything except MediaPlayer mPlayerOverride = false; try { String url = mAudioRequest == null ? null : mAudioRequest.getUrl(); if (mAudioRequest == null || url == null) { Intent updateIntent = new Intent(AudioUpdateIntent.INTENT_NAME); updateIntent.putExtra(AudioUpdateIntent.STATUS, AudioUpdateIntent.STOPPED); mBroadcastManager.sendBroadcast(updateIntent); processStopRequest(true); // stop everything! return; } final boolean isStreaming = url.startsWith("http:") || url.startsWith("https:"); if (!isStreaming) { File f = new File(url); if (!f.exists()) { Intent updateIntent = new Intent(AudioUpdateIntent.INTENT_NAME); updateIntent.putExtra(AudioUpdateIntent.STATUS, AudioUpdateIntent.STOPPED); updateIntent.putExtra(EXTRA_PLAY_INFO, mAudioRequest); mBroadcastManager.sendBroadcast(updateIntent); processStopRequest(true); return; } } int overrideResource = 0; if (playRepeatSeparator) { final int sura = mAudioRequest.getCurrentSura(); final int ayah = mAudioRequest.getCurrentAyah(); if (sura != 9 && ayah > 1) { overrideResource = R.raw.bismillah; } else if (sura == 9 && (ayah > 1 || mAudioRequest.needsIsti3athaAudio())) { overrideResource = R.raw.isti3atha; } // otherwise, ayah of 1 will automatically play the file's basmala } Timber.d("okay, we are preparing to play - streaming is: %b", isStreaming); createMediaPlayerIfNeeded(); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); setState(PlaybackStateCompat.STATE_CONNECTING); try { boolean playUrl = true; if (overrideResource != 0) { AssetFileDescriptor afd = getResources().openRawResourceFd(overrideResource); if (afd != null) { mPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); afd.close(); mPlayerOverride = true; playUrl = false; } } if (playUrl) { overrideResource = 0; mPlayer.setDataSource(url); } } catch (IllegalStateException ie) { Crashlytics.log("IllegalStateException() while " + "setting data source, trying to reset..."); if (overrideResource != 0) { playAudio(false); return; } mPlayer.reset(); mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mPlayer.setDataSource(url); } mState = State.Preparing; if (!mIsSetupAsForeground) { setUpAsForeground(); } // starts preparing the media player in the background. When it's // done, it will call our OnPreparedListener (that is, the // onPrepared() method on this class, since we set the listener // to 'this'). // // Until the media player is prepared, we *cannot* call start() on it! Timber.d("preparingAsync()..."); Crashlytics.log("prepareAsync: " + overrideResource + ", " + url); mPlayer.prepareAsync(); // If we are streaming from the internet, we want to hold a Wifi lock, // which prevents the Wifi radio from going to sleep while the song is // playing. If, on the other hand, we are *not* streaming, we want to // release the lock if we were holding it before. if (isStreaming) { mWifiLock.acquire(); } else if (mWifiLock.isHeld()) { mWifiLock.release(); } } catch (IOException ex) { Timber.e("IOException playing file: %s", ex.getMessage()); ex.printStackTrace(); } } private void setState(int state) { long position = 0; if (mPlayer != null && mPlayer.isPlaying()) { position = mPlayer.getCurrentPosition(); } PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); builder.setState(state, position, 1.0f); builder.setActions( PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT); mMediaSession.setPlaybackState(builder.build()); } @Override public void onSeekComplete(MediaPlayer mediaPlayer) { Timber.d("seek complete! %d vs %d", mediaPlayer.getCurrentPosition(), mPlayer.getCurrentPosition()); mPlayer.start(); mHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200); } /** Called when media player is done playing current file. */ @Override public void onCompletion(MediaPlayer player) { // The media player finished playing the current file, so // we go ahead and start the next. if (mPlayerOverride) { playAudio(false); } else { boolean flag = false; final int beforeSura = mAudioRequest.getCurrentSura(); if (mAudioRequest.gotoNextAyah(false)) { // we actually switched to a different ayah - so if the // sura changed, then play the basmala if the ayah is // not the first one (or if we're in sura tawba). flag = beforeSura != mAudioRequest.getCurrentSura(); } playAudio(flag); } } /** Called when media player is done preparing. */ @Override public void onPrepared(MediaPlayer player) { Timber.d("okay, prepared!"); // The media player is done preparing. That means we can start playing! mState = State.Playing; if (mShouldStop) { processStopRequest(); mShouldStop = false; return; } // if gapless and sura changed, get the new data if (mAudioRequest.isGapless()) { if (mGaplessSura != mAudioRequest.getCurrentSura()) { if (mTimingTask != null) { mTimingTask.cancel(true); } String dbPath = mAudioRequest.getGaplessDatabaseFilePath(); mTimingTask = new ReadGaplessDataTask(dbPath); mTimingTask.execute(mAudioRequest.getCurrentSura()); } } if (mPlayerOverride || !mAudioRequest.isGapless()) { notifyAyahChanged(); } updateNotification(); configAndStartMediaPlayer(); } /** Updates the notification. */ void updateNotification() { mNotificationBuilder.setContentText(mAudioRequest.getTitle(getApplicationContext())); mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); } void pauseNotification() { mPausedNotificationBuilder.setContentText(mAudioRequest.getTitle(getApplicationContext())); mNotificationManager.notify(NOTIFICATION_ID, mPausedNotificationBuilder.build()); } /** * Configures service as a foreground service. A foreground service * is a service that's doing something the user is actively aware of * (such as playing music), and must appear to the user as a notification. * That's why we create the notification here. */ private void setUpAsForeground() { // clear the "downloading complete" notification (if it exists) mNotificationManager.cancel(QuranDownloadNotifier.DOWNLOADING_COMPLETE_NOTIFICATION); final Context appContext = getApplicationContext(); final PendingIntent pi = PendingIntent.getActivity( appContext, REQUEST_CODE_MAIN, new Intent(appContext, PagerActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); final PendingIntent previousIntent = PendingIntent.getService( appContext, REQUEST_CODE_PREVIOUS, AudioUtils.getAudioIntent(this, ACTION_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); final PendingIntent nextIntent = PendingIntent.getService( appContext, REQUEST_CODE_SKIP, AudioUtils.getAudioIntent(this, ACTION_SKIP), PendingIntent.FLAG_UPDATE_CURRENT); final PendingIntent pauseIntent = PendingIntent.getService( appContext, REQUEST_CODE_PAUSE, AudioUtils.getAudioIntent(this, ACTION_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT); final PendingIntent resumeIntent = PendingIntent.getService( appContext, REQUEST_CODE_RESUME, AudioUtils.getAudioIntent(this, ACTION_PLAYBACK), PendingIntent.FLAG_UPDATE_CURRENT); final PendingIntent stopIntent = PendingIntent.getService( appContext, REQUEST_CODE_STOP, AudioUtils.getAudioIntent(this, ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT); // if the notification icon is null, let's try to build it if (mNotificationIcon == null) { try { Resources resources = appContext.getResources(); Bitmap logo = BitmapFactory.decodeResource(resources, R.drawable.icon); int iconWidth = logo.getWidth(); int iconHeight = logo.getHeight(); ColorDrawable cd = new ColorDrawable(ContextCompat.getColor(appContext, R.color.audio_notification_background_color)); Bitmap bitmap = Bitmap.createBitmap(iconWidth * 2, iconHeight * 2, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); cd.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); cd.draw(canvas); canvas.drawBitmap(logo, iconWidth / 2, iconHeight / 2, null); mNotificationIcon = bitmap; } catch (OutOfMemoryError oomError) { // if this happens, we need to handle it gracefully, since it's not crash worthy. Crashlytics.logException(oomError); } } String audioTitle = mAudioRequest.getTitle(getApplicationContext()); if (mNotificationBuilder == null) { mNotificationBuilder = new NotificationCompat.Builder(appContext); mNotificationBuilder .setSmallIcon(R.drawable.ic_notification) .setColor(mNotificationColor) .setOngoing(true) .setContentTitle(getString(R.string.app_name)) .setContentIntent(pi) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .addAction(R.drawable.ic_previous, getString(R.string.previous), previousIntent) .addAction(R.drawable.ic_pause, getString(R.string.pause), pauseIntent) .addAction(R.drawable.ic_next, getString(R.string.next), nextIntent) .setShowWhen(false) .setWhen(0) // older platforms seem to ignore setShowWhen(false) .setLargeIcon(mNotificationIcon) .setStyle( new NotificationCompat.MediaStyle() .setShowActionsInCompactView(0, 1, 2) .setMediaSession(mMediaSession.getSessionToken())); } mNotificationBuilder.setTicker(audioTitle); mNotificationBuilder.setContentText(audioTitle); if (mPausedNotificationBuilder == null) { mPausedNotificationBuilder = new NotificationCompat.Builder(appContext); mPausedNotificationBuilder .setSmallIcon(R.drawable.ic_notification) .setColor(mNotificationColor) .setOngoing(true) .setContentTitle(getString(R.string.app_name)) .setContentIntent(pi) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .addAction(R.drawable.ic_play, getString(R.string.play), resumeIntent) .addAction(R.drawable.ic_stop, getString(R.string.stop), stopIntent) .setShowWhen(false) .setWhen(0) .setLargeIcon(mNotificationIcon) .setStyle( new NotificationCompat.MediaStyle() .setShowActionsInCompactView(0, 1) .setMediaSession(mMediaSession.getSessionToken())); } mPausedNotificationBuilder.setContentText(audioTitle); startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); mIsSetupAsForeground = true; } /** * Called when there's an error playing media. When this happens, the media * player goes to the Error state. We warn the user about the error and * reset the media player. */ public boolean onError(MediaPlayer mp, int what, int extra) { Timber.e("Error: what=%s, extra=%s", String.valueOf(what), String.valueOf(extra)); mState = State.Stopped; relaxResources(true, true); giveUpAudioFocus(); return true; // true indicates we handled the error } public void onGainedAudioFocus() { mAudioFocus = AudioFocus.Focused; // restart media player with new focus settings if (State.Playing == mState) { configAndStartMediaPlayer(false); } } public void onLostAudioFocus(boolean canDuck) { mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck; // start/restart/pause media player with new focus settings if (mPlayer != null && mPlayer.isPlaying()) { configAndStartMediaPlayer(false); } } @Override public void onDestroy() { // Service is being killed, so make sure we release our resources mHandler.removeCallbacksAndMessages(null); unregisterReceiver(noisyAudioStreamReceiver); mState = State.Stopped; relaxResources(true, true); giveUpAudioFocus(); mMediaSession.release(); super.onDestroy(); } @Override public IBinder onBind(Intent arg0) { return null; } private class NoisyAudioStreamReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { // pause audio when headphones are unplugged processPauseRequest(); } } } }