de.dcja.prettygreatmusicplayer.MusicPlaybackService.java Source code

Java tutorial

Introduction

Here is the source code for de.dcja.prettygreatmusicplayer.MusicPlaybackService.java

Source

/**
   The Pretty Great Music Player
   Copyright (C) 2014  Tyler Smith
    
   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 3 of the License, 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.
    
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package de.dcja.prettygreatmusicplayer;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import ch.blinkenlights.android.vanilla.ReadaheadThread;

public class MusicPlaybackService extends Service {
    static final int MSG_REGISTER_CLIENT = 1;
    static final int MSG_UNREGISTER_CLIENT = 2;

    // Playback control
    static final int MSG_PLAYPAUSE = 3;
    static final int MSG_NEXT = 4;
    static final int MSG_PREVIOUS = 5;
    static final int MSG_SET_PLAYLIST = 6;
    static final int MSG_PAUSE = 7;
    static final int MSG_PAUSE_IN_ONE_SEC = 8;
    static final int MSG_CANCEL_PAUSE_IN_ONE_SEC = 9;
    static final int MSG_TOGGLE_SHUFFLE = 10;
    static final int MSG_SEEK_TO = 11;
    static final int MSG_JUMPBACK = 12;
    static final int MSG_PLAY = 13;

    // State management
    static final int MSG_REQUEST_STATE = 17;
    static final int MSG_SERVICE_STATUS = 18;
    static final int MSG_STOP_SERVICE = 19;

    public enum PlaybackState {
        PLAYING, PAUSED, UNKNOWN
    }

    static final String PRETTY_SONG_NAME = "PRETTY_SONG_NAME";
    static final String PRETTY_ARTIST_NAME = "PRETTY_ARTIST_NAME";
    static final String PRETTY_ALBUM_NAME = "PRETTY_ALBUM_NAME";
    static final String ALBUM_NAME = "ALBUM_NAME";
    static final String PLAYBACK_STATE = "PLAYBACK_STATE";
    static final String TRACK_DURATION = "TRACK_DURATION";
    static final String TRACK_POSITION = "TRACK_POSITION";
    static final String IS_SHUFFLING = "IS_SHUFFLING";

    private static final ComponentName cn = new ComponentName(MusicBroadcastReceiver.class.getPackage().getName(),
            MusicBroadcastReceiver.class.getName());

    private FileInputStream fis;
    private File songFile;

    private Timer timer;

    private AudioManager am;
    private Looper mServiceLooper;
    private ServiceHandler mServiceHandler;
    private MediaPlayer mp;
    private OnDemandMediaMetadataRetriever onDemandMediaMetadataRetriever;
    private static final String TAG = "MusicPlaybackService";
    private static boolean isRunning = false;

    private static int uniqueid = new String("Music Playback Service").hashCode();

    private OnAudioFocusChangeListener audioFocusListener = new PrettyGoodAudioFocusChangeListener();

    private static IntentFilter filter = new IntentFilter();
    static {
        filter.addAction("android.intent.action.HEADSET_PLUG");
        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED);
        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
    }
    private static MusicBroadcastReceiver receiver = new MusicBroadcastReceiver();

    /**
     * Keeps track of all current registered clients.
     */
    List<Messenger> mClients = new ArrayList<Messenger>();

    final Messenger mMessenger = new Messenger(new IncomingHandler(this));

    public AudioManager mAudioManager;

    // These are used to report song progress when the song isn't started yet.
    private int lastDuration = 0;
    private int lastPosition = 0;
    public long audioFocusLossTime = 0;
    private long pauseTime = Long.MAX_VALUE;
    private boolean _shuffle = false;
    private List<Integer> shuffleFrontList = new ArrayList<Integer>();
    private Random random;
    private List<Integer> shuffleBackList = new ArrayList<Integer>();
    private String artist;
    private String artistAbsPath;
    private String album;
    private long lastResumeUpdateTime;
    private SharedPreferences sharedPref;
    private HeadphoneBroadcastReceiver headphoneReceiver;
    private PowerManager powerManager;
    WakeLock wakeLock;
    private ReadaheadThread mReadaheadThread;

    // Handler that receives messages from the thread
    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            Log.i(TAG, "ServiceHandler got a message!" + msg);
        }
    }

    @Override
    public synchronized void onCreate() {
        Log.i(TAG, "Music Playback Service Created!");
        isRunning = true;
        sharedPref = PreferenceManager.getDefaultSharedPreferences(this);

        powerManager = (PowerManager) getSystemService(POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PGMPWakeLock");

        random = new Random();

        mp = new MediaPlayer();
        mReadaheadThread = new ReadaheadThread();

        mp.setOnCompletionListener(new OnCompletionListener() {

            @Override
            public void onCompletion(MediaPlayer mp) {
                Log.i(TAG, "Song complete");
                next();
            }

        });

        onDemandMediaMetadataRetriever = new OnDemandMediaMetadataRetriever();

        // https://developer.android.com/training/managing-audio/audio-focus.html
        audioFocusListener = new PrettyGoodAudioFocusChangeListener();

        // Get permission to play audio
        am = (AudioManager) getBaseContext().getSystemService(Context.AUDIO_SERVICE);

        HandlerThread thread = new HandlerThread("ServiceStartArguments");
        thread.start();

        // Get the HandlerThread's Looper and use it for our Handler
        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);

        // https://stackoverflow.com/questions/19474116/the-constructor-notification-is-deprecated
        // https://stackoverflow.com/questions/6406730/updating-an-ongoing-notification-quietly/15538209#15538209
        Intent resultIntent = new Intent(this, NowPlaying.class);
        resultIntent.putExtra("From_Notification", true);

        // Use the FLAG_ACTIVITY_CLEAR_TOP to prevent launching a second
        // NowPlaying if one already exists.
        resultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, resultIntent, 0);

        Builder builder = new NotificationCompat.Builder(this.getApplicationContext());

        String contentText = getResources().getString(R.string.ticker_text);
        if (songFile != null) {
            contentText = Utils.getPrettySongName(songFile);
        }

        Notification notification = builder.setContentText(contentText).setSmallIcon(R.drawable.ic_pgmp_launcher)
                .setWhen(System.currentTimeMillis()).setContentIntent(pendingIntent)
                .setContentTitle(getResources().getString(R.string.notification_title)).build();

        startForeground(uniqueid, notification);

        timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            public void run() {
                onTimerTick();
            }
        }, 0, 500L);

        Log.i(TAG, "Registering event receiver");
        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        // Apparently audio registration is persistent across lots of things...
        // restarts, installs, etc.
        mAudioManager.registerMediaButtonEventReceiver(cn);
        // I tried to register this in the manifest, but it doesn't seen to
        // accept it, so I'll do it this way.
        getApplicationContext().registerReceiver(receiver, filter);

        headphoneReceiver = new HeadphoneBroadcastReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("android.intent.action.HEADSET_PLUG");
        registerReceiver(headphoneReceiver, filter);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("MyService", "Received start id " + startId + ": " + intent);
        if (intent == null) {
            // intent can be null if this is called by the OS due to
            // "START STICKY"
            // Start, but don't do anything until we get a message from the
            // user.
            return START_STICKY;
        }
        int command = intent.getIntExtra("Message", -1);
        if (command != -1) {
            Log.i(TAG, "I got a message! " + command);
            if (command == MSG_PLAYPAUSE) {
                Log.i(TAG, "I got a playpause message");
                playPause();
            } else if (command == MSG_PAUSE) {
                Log.i(TAG, "I got a pause message");
                pause();
            } else if (command == MSG_PLAY) {
                Log.i(TAG, "I got a play message");
                play();
            } else if (command == MSG_NEXT) {
                Log.i(TAG, "I got a next message");
                next();
            } else if (command == MSG_PREVIOUS) {
                Log.i(TAG, "I got a previous message");
                previous();
            } else if (command == MSG_JUMPBACK) {
                Log.i(TAG, "I got a jumpback message");
                jumpback();
            } else if (command == MSG_STOP_SERVICE) {
                Log.i(TAG, "I got a stop message");
                headphoneReceiver.active = false;
                timer.cancel();
                stopForeground(true);
                stopSelf();
            } else if (command == MSG_PAUSE_IN_ONE_SEC) {
                pauseTime = System.currentTimeMillis() + 1000;
            } else if (command == MSG_CANCEL_PAUSE_IN_ONE_SEC) {
                pauseTime = Long.MAX_VALUE;
            }
            return START_STICKY;
        }

        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        mServiceHandler.sendMessage(msg);
        // If we get killed, after returning from here, restart
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }

    // Receives messages from activities which want to control the jams
    private static class IncomingHandler extends Handler {
        MusicPlaybackService _service;

        private IncomingHandler(MusicPlaybackService service) {
            _service = service;
        }

        @Override
        public void handleMessage(Message msg) {
            Log.i(TAG, "Music Playback service got a message!");
            switch (msg.what) {
            case MSG_REGISTER_CLIENT:
                Log.i(TAG, "Got MSG_REGISTER_CLIENT");
                synchronized (_service.mClients) {
                    _service.mClients.add(msg.replyTo);
                }
                break;
            case MSG_UNREGISTER_CLIENT:
                Log.i(TAG, "Got MSG_UNREGISTER_CLIENT");
                synchronized (_service.mClients) {
                    _service.mClients.remove(msg.replyTo);
                }
                break;
            case MSG_PLAYPAUSE:
                // if we got a playpause message, assume that the user can hear
                // what's happening and wants to switch it.
                Log.i(TAG, "Got a playpause message!");
                // Assume that we're not changing songs
                _service.playPause();
                break;
            case MSG_NEXT:
                Log.i(TAG, "Got a next message!");
                _service.next();
                break;
            case MSG_PREVIOUS:
                Log.i(TAG, "Got a previous message!");
                _service.previous();
                break;
            case MSG_JUMPBACK:
                Log.i(TAG, "Got a jump back message!");
                _service.jumpback();
                break;
            case MSG_TOGGLE_SHUFFLE:
                Log.i(TAG, "Got a toggle shuffle message!");
                _service.toggleShuffle();
                break;
            case MSG_SET_PLAYLIST:
                Log.i(TAG, "Got a set playlist message!");
                Playlist playlist = Playlist.getPlaylistForFilePaths(_service,
                        msg.getData().getStringArray(SongList.SONG_ABS_FILE_NAME_LIST));
                PlaybackInfoHolder.getInstance().setActivePlaylist(playlist);
                playlist.selectSong(msg.getData().getInt(SongList.SONG_ABS_FILE_NAME_LIST_POSITION));
                _service.songFile = new File(playlist.getPlaying().getFileName());
                int songPosition = msg.getData().getInt(TRACK_POSITION, 0);
                _service.startPlayingFile(songPosition);
                _service.updateNotification();
                break;
            case MSG_REQUEST_STATE:
                Log.i(TAG, "Got a state request message!");
                break;
            case MSG_SEEK_TO:
                Log.i(TAG, "Got a seek request message!");
                int progress = msg.getData().getInt(TRACK_POSITION);
                _service.jumpTo(progress);
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }

    private void onTimerTick() {
        long currentTime = System.currentTimeMillis();
        if (pauseTime < currentTime) {
            pause();
        }
        updatePlaybackInfo();
        updateResumePosition();
        sendUpdateToClients();
        sendUpdateToWidgets();
    }

    private void updatePlaybackInfo() {
        PlaybackInfoHolder pih = PlaybackInfoHolder.getInstance();
        synchronized (pih) {
            pih.setPlaybackState(mp.isPlaying() ? PlaybackState.PLAYING : PlaybackState.PAUSED);
            if (mp.isPlaying()) {
                pih.setTrackDuration(mp.getDuration());
                pih.setTrackPosition(mp.getCurrentPosition());
            }
        }
    }

    private void updateResumePosition() {
        long currentTime = System.currentTimeMillis();
        if (currentTime - 10000 > lastResumeUpdateTime) {
            if (mp != null && songFile != null && mp.isPlaying()) {
                int pos = mp.getCurrentPosition();
                SharedPreferences prefs = getSharedPreferences("PrettyGoodMusicPlayer", MODE_PRIVATE);
                Log.i(TAG, "Preferences update success: " + prefs.edit()
                        .putString(songFile.getParentFile().getAbsolutePath(), songFile.getName() + "~" + pos)
                        .commit());
            }
            lastResumeUpdateTime = currentTime;
        }
    }

    private void sendUpdateToClients() {
        List<Messenger> toRemove = new ArrayList<Messenger>();
        synchronized (mClients) {
            for (Messenger client : mClients) {
                Message msg = Message.obtain(null, MSG_SERVICE_STATUS);
                try {
                    client.send(msg);
                } catch (RemoteException e) {
                    e.printStackTrace();
                    toRemove.add(client);
                }
            }

            for (Messenger remove : toRemove) {
                mClients.remove(remove);
            }
        }
    }

    private void sendUpdateToWidgets() {
        Intent intent = new Intent(PlayerControlWidgetProvider.ACTION_PLAYBACK_STATUS_CHANGED);
        getApplicationContext().sendBroadcast(intent);
    }

    public static boolean isRunning() {
        return isRunning;
    }

    @Override
    public synchronized void onDestroy() {
        super.onDestroy();
        unregisterReceiver(headphoneReceiver);
        am.abandonAudioFocus(MusicPlaybackService.this.audioFocusListener);
        mAudioManager.unregisterMediaButtonEventReceiver(cn);
        getApplicationContext().unregisterReceiver(receiver);
        mp.stop();
        mp.reset();
        mp.release();
        if (wakeLock.isHeld()) {
            wakeLock.release();
        }
        Log.i("MyService", "Service Stopped.");
        isRunning = false;
    }

    private synchronized void jumpback() {
        if (mp.isPlaying()) {
            int progressMillis = mp.getCurrentPosition();
            if (progressMillis <= 20000) {
                mp.seekTo(0);
            } else {
                mp.seekTo(progressMillis - 20000);
            }
            lastPosition = mp.getCurrentPosition();
        } else {
            // if we're paused but initialized, try to seek
            try {
                int progressMillis = mp.getCurrentPosition();
                if (progressMillis <= 20000) {
                    mp.seekTo(0);
                } else {
                    mp.seekTo(progressMillis - 20000);
                }
                lastPosition = mp.getCurrentPosition();
            } catch (Exception e) {
                Log.w(TAG, "Unable to seek to position, file may not have been loaded");
            }
        }

    }

    private synchronized void previous() {
        // if we're playing, and we're more than 3 seconds into the file, then
        // just
        // start the song over
        if (mp.isPlaying()) {
            int progressMillis = mp.getCurrentPosition();
            if (progressMillis > 3000) {
                mp.seekTo(0);
                return;
            }
        }

        mp.stop();
        mp.reset();
        try {
            fis.close();
        } catch (IOException e) {
            Log.w(TAG, "Failed to close the file");
            e.printStackTrace();
        }
        PlaybackInfoHolder pih = PlaybackInfoHolder.getInstance();
        String next;
        synchronized (pih) {
            Playlist playlist = pih.getActivePlaylist();
            playlist.previous();
            next = playlist.getPlaying().getFileName();
        }
        try {
            songFile = new File(next);
            fis = new FileInputStream(songFile);
            mp.setDataSource(fis.getFD());
            mReadaheadThread.setSource(songFile.getAbsolutePath());
            mp.prepare();
            mp.start();
        } catch (IOException e) {
            Log.w(TAG, "Failed to open " + next);
            e.printStackTrace();
            // Just go to the next song back
            previous();
        }
        updateNotification();
    }

    private synchronized void startPlayingFile(int songProgress) {
        // Have we loaded a file yet?
        if (mp.getDuration() > 0) {
            pause();
            mp.stop();
            mp.reset();
        }

        // open the file, pass it into the mp
        try {
            fis = new FileInputStream(songFile);
            mp.setDataSource(fis.getFD());
            mReadaheadThread.setSource(songFile.getAbsolutePath());
            mp.prepare();
            if (songProgress > 0) {
                mp.seekTo(songProgress);
            }
            wakeLock.acquire();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private synchronized void jumpTo(int position) {
        if (mp.isPlaying()) {
            mp.seekTo(position);
        } else {
            // if we're paused but initialized, try to seek
            try {
                mp.seekTo(position);
                lastPosition = mp.getCurrentPosition();
            } catch (Exception e) {
                Log.w(TAG, "Unable to seek to position, file may not have been loaded");
            }
        }
    }

    private synchronized void playPause() {
        if (mp.isPlaying()) {
            pause();
        } else {
            play();
        }
    }

    private synchronized void play() {
        pauseTime = Long.MAX_VALUE;
        if (mp.isPlaying()) {
            // do nothing
        } else {
            // Request audio focus for playback
            int result = am.requestAudioFocus(MusicPlaybackService.this.audioFocusListener,
                    // Use the music stream.
                    AudioManager.STREAM_MUSIC,
                    // Request permanent focus.
                    AudioManager.AUDIOFOCUS_GAIN);
            Log.d(TAG, "requestAudioFocus result = " + result);
            Log.i(TAG, "About to play " + songFile);

            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                Log.d(TAG, "We got audio focus!");
                mp.start();
                updateNotification();
                wakeLock.acquire();
            } else {
                Log.e(TAG, "Unable to get audio focus");
            }
        }
    }

    /**
     * Pause the currently playing song.
     */
    private synchronized void pause() {
        // Sometimes the call to isPlaying can throw an error "internal/external state mismatch corrected"
        // When this happens, I think the player moves itself to "paused" even though it's still playing.
        //if (mp.isPlaying()) {
        //   mp.pause();
        //} else {
        //   Log.w(TAG, "Odd condition - pause was called but the media player reported that it is already paused");
        try {
            // this is a hack, but it seems to be the most consistent way to address the problem
            // this forces the media player to check its current state before trying to pause.
            int position = mp.getCurrentPosition();
            mp.stop();
            mp.prepare();
            mp.seekTo(position);
            wakeLock.release();
        } catch (Exception e) {
            Log.w(TAG, "Caught exception while trying to pause ", e);
        }
        //}
        updateNotification();
    }

    private synchronized void next() {
        mp.stop();
        mp.reset();
        try {
            fis.close();
        } catch (Exception e) {
            Log.w(TAG, "Failed to close the file");
            e.printStackTrace();
        }
        PlaybackInfoHolder pih = PlaybackInfoHolder.getInstance();
        String next;
        synchronized (pih) {
            if (pih.getActivePlaylist() == null) {
                return;
            }
            Playlist playlist = pih.getActivePlaylist();
            playlist.next();
            next = playlist.getPlaying().getFileName();
        }
        try {
            songFile = new File(next);
            fis = new FileInputStream(songFile);
            mp.setDataSource(fis.getFD());
            mReadaheadThread.setSource(songFile.getAbsolutePath());
            mp.prepare();
            mp.start();
        } catch (IOException e) {
            Log.w(TAG, "Failed to open " + next);
            e.printStackTrace();
            // I think our best chance is to go to the next song
            next();
        }
        updateNotification();
    }

    public void toggleShuffle() {
        PlaybackInfoHolder pih = PlaybackInfoHolder.getInstance();
        synchronized (pih) {
            pih.getActivePlaylist().toggleShuffling();
        }
    }

    private void updateNotification() {
        boolean audiobookMode = sharedPref.getBoolean("pref_audiobook_mode", false);

        // https://stackoverflow.com/questions/5528288/how-do-i-update-the-notification-text-for-a-foreground-service-in-android
        Intent resultIntent = new Intent(this, NowPlaying.class);
        // Use the FLAG_ACTIVITY_CLEAR_TOP to prevent launching a second
        // NowPlaying if one already exists.
        resultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        resultIntent.putExtra("From_Notification", true);

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, resultIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        Builder builder = new NotificationCompat.Builder(this.getApplicationContext());
        int icon = R.drawable.ic_pgmp_launcher;
        String contentText = getResources().getString(R.string.ticker_text);
        if (songFile != null) {
            PlaybackInfoHolder pih = PlaybackInfoHolder.getInstance();
            String artist;
            String songName;
            synchronized (pih) {
                Playlist.Song song = pih.getActivePlaylist().getPlaying();
                artist = song.getArtist();
                songName = song.getSong();
            }
            contentText = artist + ": " + songName;
            if (mp != null) {
                if (mp.isPlaying()) {
                    icon = R.drawable.ic_pgmp_launcher;
                }
            }
        }

        Intent previousIntent = new Intent("Previous", null, this, MusicPlaybackService.class);
        previousIntent.putExtra("Message", MSG_PREVIOUS);
        PendingIntent previousPendingIntent = PendingIntent.getService(this, 0, previousIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        Intent jumpBackIntent = new Intent("JumpBack", null, this, MusicPlaybackService.class);
        jumpBackIntent.putExtra("Message", MSG_JUMPBACK);
        PendingIntent jumpBackPendingIntent = PendingIntent.getService(this, 0, jumpBackIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        Intent nextIntent = new Intent("Next", null, this, MusicPlaybackService.class);
        nextIntent.putExtra("Message", MSG_NEXT);
        PendingIntent nextPendingIntent = PendingIntent.getService(this, 0, nextIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        PendingIntent playPausePendingIntent;
        Intent playPauseIntent = new Intent("PlayPause", null, this, MusicPlaybackService.class);
        playPauseIntent.putExtra("Message", MSG_PLAYPAUSE);
        playPausePendingIntent = PendingIntent.getService(this, 0, playPauseIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        int playPauseIcon;
        if (mp != null && mp.isPlaying()) {
            playPauseIcon = R.drawable.ic_action_pause;
        } else {
            playPauseIcon = R.drawable.ic_action_play;
        }

        Notification notification;
        if (audiobookMode) {
            notification = builder.setContentText(contentText).setSmallIcon(icon)
                    .setWhen(System.currentTimeMillis()).setContentIntent(pendingIntent)
                    .setContentTitle(getResources().getString(R.string.notification_title))
                    .addAction(R.drawable.ic_action_rewind20, "", jumpBackPendingIntent)
                    .addAction(playPauseIcon, "", playPausePendingIntent)
                    .addAction(R.drawable.ic_action_next, "", nextPendingIntent).build();
        } else {
            notification = builder.setContentText(contentText).setSmallIcon(icon)
                    .setWhen(System.currentTimeMillis()).setContentIntent(pendingIntent)
                    .setContentTitle(getResources().getString(R.string.notification_title))
                    .addAction(R.drawable.ic_action_previous, "", previousPendingIntent)
                    .addAction(playPauseIcon, "", playPausePendingIntent)
                    .addAction(R.drawable.ic_action_next, "", nextPendingIntent).build();
        }

        NotificationManager mNotificationManager = (NotificationManager) getSystemService(
                Context.NOTIFICATION_SERVICE);
        mNotificationManager.notify(uniqueid, notification);
    }

    private class PrettyGoodAudioFocusChangeListener implements AudioManager.OnAudioFocusChangeListener {

        private PlaybackState stateOnFocusLoss = PlaybackState.UNKNOWN;

        public void onAudioFocusChange(int focusChange) {
            Log.w(TAG, "Focus change received " + focusChange);
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
                Log.i(TAG, "AUDIOFOCUS_LOSS_TRANSIENT");
                if (mp.isPlaying()) {
                    stateOnFocusLoss = PlaybackState.PLAYING;
                } else {
                    stateOnFocusLoss = PlaybackState.PAUSED;
                }
                pause();
                MusicPlaybackService.this.audioFocusLossTime = System.currentTimeMillis();
                // Pause playback
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                Log.i(TAG, "AUDIOFOCUS_GAIN");
                // If it's been less than 20 seconds, resume playback
                long curr = System.currentTimeMillis();
                if (((curr - MusicPlaybackService.this.audioFocusLossTime) < 30000)
                        && stateOnFocusLoss == PlaybackState.PLAYING) {
                    play();
                } else {
                    Log.i(TAG, "It's been more than 30 seconds or we were paused, don't auto-play");
                }
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                Log.i(TAG, "AUDIOFOCUS_LOSS");
                if (mp.isPlaying()) {
                    stateOnFocusLoss = PlaybackState.PLAYING;
                } else {
                    stateOnFocusLoss = PlaybackState.PAUSED;
                }
                pause();
                MusicPlaybackService.this.audioFocusLossTime = System.currentTimeMillis();
                // Stop playback
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
                Log.i(TAG, "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
                MusicPlaybackService.this.audioFocusLossTime = System.currentTimeMillis();
                if (mp.isPlaying()) {
                    stateOnFocusLoss = PlaybackState.PLAYING;
                } else {
                    stateOnFocusLoss = PlaybackState.PAUSED;
                }
                pause();
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) {
                Log.i(TAG, "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK");
                long curr = System.currentTimeMillis();
                if (((curr - MusicPlaybackService.this.audioFocusLossTime) < 30000)
                        && stateOnFocusLoss == PlaybackState.PLAYING) {
                    play();
                } else {
                    Log.i(TAG, "It's been more than 30 seconds or we were paused, don't auto-play");
                }
            }
        }
    }

    private static class OnDemandMediaMetadataRetriever {
        /**
         * Since it's only necessary to fetch metadata once the media file has changed, this class
         * will only look up new metadata if the file is different from the last time metadata has
         * been looked up.
         */

        private MediaMetadataRetriever mediaMetadataRetriever;

        private String latestFileName;

        private HashMap<Integer, String> retrievedMetadata;

        public OnDemandMediaMetadataRetriever() {
            mediaMetadataRetriever = new MediaMetadataRetriever();
            retrievedMetadata = new HashMap<>();
        }

        public void setDataSource(String fileName) {
            if (!fileName.equals(latestFileName)) {
                retrievedMetadata.clear();
                latestFileName = fileName;
                mediaMetadataRetriever.setDataSource(fileName);
            }
        }

        public String retrieveMetadata(int keyCode) {
            if (!retrievedMetadata.containsKey(keyCode))
                retrievedMetadata.put(keyCode, mediaMetadataRetriever.extractMetadata(keyCode));
            return retrievedMetadata.get(keyCode);
        }
    }

    private static class HeadphoneBroadcastReceiver extends BroadcastReceiver {

        /**
         * If the option to automatically resume on headphone re-connect is selected, 
         * keep track of the time they were unplugged.
         */
        private long resumeOnQuickReconnectDisconnectTime = 0;

        /**
         * There seems to be a race condition that causes headphone events to get fired while the service is shutting down
         * use this flag to indicate that events should be ignored.
         */
        private boolean active = true;

        @Override
        public void onReceive(Context context, Intent intent) {
            if (!active) {
                return;
            }
            SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
            if (Intent.ACTION_HEADSET_PLUG.equals(intent.getAction())) {
                Log.i(TAG, "Got headset plug action");
                String disconnectBehavior = sharedPref.getString("pref_disconnect_behavior",
                        context.getString(R.string.pause_after_one_sec));
                /*
                 * state - 0 for unplugged, 1 for plugged. name - Headset type,
                 * human readable string microphone - 1 if headset has a microphone,
                 * 0 otherwise
                 */
                if (disconnectBehavior.equals(context.getString(R.string.resume_on_quick_reconnect))) {
                    if (intent.getIntExtra("state", -1) == 0) {
                        Log.i(TAG, "headphones disconnected, pausing");
                        Intent msgIntent = new Intent(context, MusicPlaybackService.class);
                        msgIntent.putExtra("Message", MusicPlaybackService.MSG_PAUSE);
                        context.startService(msgIntent);
                        resumeOnQuickReconnectDisconnectTime = System.currentTimeMillis();
                    } else if (intent.getIntExtra("state", -1) == 1) {
                        long currentTime = System.currentTimeMillis();
                        if (currentTime - resumeOnQuickReconnectDisconnectTime < 1000) {
                            // Resume
                            Log.i(TAG, "headphones plugged back in within 1000ms, resuming");
                            Intent msgIntent = new Intent(context, MusicPlaybackService.class);
                            msgIntent.putExtra("Message", MusicPlaybackService.MSG_PLAY);
                            context.startService(msgIntent);
                        }
                    }
                } else if (disconnectBehavior.equals(context.getString(R.string.resume_on_reconnect))) {
                    if (intent.getIntExtra("state", -1) == 0) {
                        Log.i(TAG, "headphones disconnected, pausing");
                        Intent msgIntent = new Intent(context, MusicPlaybackService.class);
                        msgIntent.putExtra("Message", MusicPlaybackService.MSG_PAUSE);
                        context.startService(msgIntent);
                        resumeOnQuickReconnectDisconnectTime = System.currentTimeMillis();
                    } else if (intent.getIntExtra("state", -1) == 1) {
                        // check to make sure we were playing at one point.
                        if (resumeOnQuickReconnectDisconnectTime > 0) {
                            // Resume
                            Log.i(TAG, "headphones plugged back in, resuming");
                            Intent msgIntent = new Intent(context, MusicPlaybackService.class);
                            msgIntent.putExtra("Message", MusicPlaybackService.MSG_PLAY);
                            context.startService(msgIntent);
                        }
                    }
                } else if (disconnectBehavior.equals(context.getString(R.string.pause_after_one_sec))) {
                    if (intent.getIntExtra("state", -1) == 0) {
                        Log.i(TAG, "headphones disconnected, pausing in 1 seconds");
                        Intent msgIntent = new Intent(context, MusicPlaybackService.class);
                        msgIntent.putExtra("Message", MusicPlaybackService.MSG_PAUSE_IN_ONE_SEC);
                        context.startService(msgIntent);
                        // If the headphone is plugged back in quickly after being
                        // unplugged, keep playing
                    } else if (intent.getIntExtra("state", -1) == 1) {
                        Log.i(TAG, "headphones plugged back in, cancelling disconnect");
                        Intent msgIntent = new Intent(context, MusicPlaybackService.class);
                        msgIntent.putExtra("Message", MusicPlaybackService.MSG_CANCEL_PAUSE_IN_ONE_SEC);
                        context.startService(msgIntent);
                    }
                } else {
                    // Pause immediately
                    Log.i(TAG, "headphones disconnected, pausing");
                    Intent msgIntent = new Intent(context, MusicPlaybackService.class);
                    msgIntent.putExtra("Message", MusicPlaybackService.MSG_PAUSE);
                    context.startService(msgIntent);
                }
            }
        }
    }

}