com.namelessdev.mpdroid.NotificationService.java Source code

Java tutorial

Introduction

Here is the source code for com.namelessdev.mpdroid.NotificationService.java

Source

/*
 * Copyright (C) 2010-2014 The MPDroid 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.namelessdev.mpdroid;

import com.namelessdev.mpdroid.cover.CachedCover;
import com.namelessdev.mpdroid.cover.ICoverRetriever;
import com.namelessdev.mpdroid.helpers.CoverManager;
import com.namelessdev.mpdroid.helpers.MPDControl;
import com.namelessdev.mpdroid.tools.Tools;

import org.a0z.mpd.MPDStatus;
import org.a0z.mpd.Music;
import org.a0z.mpd.event.StatusChangeListener;
import org.a0z.mpd.exception.MPDServerException;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.MediaMetadataRetriever;
import android.media.RemoteControlClient;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;

import java.util.Date;

/**
 * A service that handles the Notification, RemoteControlClient, MediaButtonReceiver and
 * incoming MPD command intents.
 */
public final class NotificationService extends Service implements StatusChangeListener {

    private static final String TAG = "NotificationService";

    private static final String FULLY_QUALIFIED_NAME = "com.namelessdev.mpdroid." + TAG + '.';

    /**
     * This will close the notification, no matter the notification state.
     */
    public static final String ACTION_CLOSE_NOTIFICATION = FULLY_QUALIFIED_NAME + "CLOSE_NOTIFICATION";

    /**
     * This readies the notification in accordance with the current state.
     */
    public static final String ACTION_OPEN_NOTIFICATION = FULLY_QUALIFIED_NAME + "NOTIFICATION_OPEN";

    /** Pre-built PendingIntent actions */
    private static final PendingIntent notificationClose = buildStaticPendingIntent(ACTION_CLOSE_NOTIFICATION);

    private static final PendingIntent notificationNext = buildStaticPendingIntent(MPDControl.ACTION_NEXT);

    private static final PendingIntent notificationPause = buildStaticPendingIntent(MPDControl.ACTION_PAUSE);

    private static final PendingIntent notificationPlay = buildStaticPendingIntent(MPDControl.ACTION_PLAY);

    private static final PendingIntent notificationPrevious = buildStaticPendingIntent(MPDControl.ACTION_PREVIOUS);

    // 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).
    private static final int NOTIFICATION_ID = 1;

    /** Set up the message handler. */
    private final Handler delayedPauseHandler = new Handler() {
        @Override
        public void handleMessage(final Message msg) {
            super.handleMessage(msg);
            shutdownNotification();
        }
    };

    private final Handler delayedDisconnectionHandler = new Handler() {
        @Override
        public void handleMessage(final Message msg) {
            super.handleMessage(msg);
            if (!app.oMPDAsyncHelper.oMPD.isConnected()) {
                shutdownNotification();
            }
        }
    };

    private RemoteControlClient mRemoteControlClient = null;

    // The component name of MusicIntentReceiver, for use with media button and remote control APIs
    private ComponentName mMediaButtonReceiverComponent = null;

    private AudioManager mAudioManager = null;

    private boolean isAudioFocusedOnThis = false;

    private NotificationManager mNotificationManager = null;

    private Notification mNotification = null;

    private final MPDApplication app = MPDApplication.getInstance();

    private Music mCurrentMusic = null;

    private boolean mediaPlayerServiceIsBuffering = false;

    private boolean serviceHandlerActive = false;

    /**
     * Last time the status was refreshed
     */
    private long lastStatusRefresh = 0L;

    /**
     * What was the elapsed time (in ms) when the last status refresh happened?
     * Use this for guessing the elapsed time for the lock screen.
     */
    private long lastKnownElapsed = 0L;

    /**
     * Service: Don't rely on intents for important status updating.
     * If something started the notification by another class and
     * not user input, store it here.
     */
    private boolean notificationAutomaticallyGenerated = false;

    private Bitmap mAlbumCover = null;

    private String mAlbumCoverPath = null;

    /**
     * Build a static pending intent for use with the notification button controls.
     *
     * @param action  The ACTION intent string.
     * @return The pending intent.
     */
    private static PendingIntent buildStaticPendingIntent(final String action) {
        final Intent intent = new Intent(MPDApplication.getInstance(), RemoteControlReceiver.class);
        intent.setAction(action);
        return PendingIntent.getBroadcast(MPDApplication.getInstance(), 0, intent, 0);
    }

    /**
     * This builds the static bits of a new collapsed notification
     *
     * @return Returns a notification builder object.
     */
    private static NotificationCompat.Builder buildStaticCollapsedNotification() {
        /** Build the click PendingIntent */
        final Intent musicPlayerActivity = new Intent(MPDApplication.getInstance(), MainMenuActivity.class);
        final TaskStackBuilder stackBuilder = TaskStackBuilder.create(MPDApplication.getInstance());
        stackBuilder.addParentStack(MainMenuActivity.class);
        stackBuilder.addNextIntent(musicPlayerActivity);
        final PendingIntent notificationClick = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

        final NotificationCompat.Builder builder = new NotificationCompat.Builder(MPDApplication.getInstance());
        builder.setSmallIcon(R.drawable.icon_bw);
        builder.setContentIntent(notificationClick);
        builder.setStyle(new NotificationCompat.BigTextStyle());

        return builder;
    }

    /**
     * This registers some media buttons via the RemoteControlReceiver.class which will take
     * action by intent to this onStartCommand().
     */
    private void registerMediaButtons() {
        mMediaButtonReceiverComponent = new ComponentName(this, RemoteControlReceiver.class);
        mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);

        final Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        intent.setComponent(mMediaButtonReceiverComponent);
        mRemoteControlClient = new RemoteControlClient(PendingIntent.getBroadcast(this /*context*/,
                0 /*requestCode, ignored*/, intent /*intent*/, 0 /*flags*/));

        final int controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
                | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
                | RemoteControlClient.FLAG_KEY_MEDIA_STOP;

        mRemoteControlClient.setTransportControlFlags(controlFlags);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            enableSeeking(controlFlags);
        }

        mAudioManager.registerRemoteControlClient(mRemoteControlClient);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "Creating service");

        //TODO: Acquire a network wake lock here if the user wants us to !
        //Otherwise we'll just shut down on screen off and reconnect on screen on
        //Tons of work ahead
        app.addConnectionLock(this);
        app.oMPDAsyncHelper.addStatusChangeListener(this);

        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);

        registerMediaButtons();
        tryToGetAudioFocus();
    }

    /**
     * Called when we receive an Intent. When we receive an intent sent to us via startService(),
     * this is the method that gets called. So here we react appropriately depending on the
     * Intent's action, which specifies what is being requested of us.
     */
    @Override
    public int onStartCommand(final Intent intent, final int flags, final int startId) {
        super.onStartCommand(intent, flags, startId);
        final String action = intent.getAction();

        Log.d(TAG, "received command, action=" + action + " from intent: " + intent);

        /** An action must be submitted to start this service. */
        if (action == null) {
            Log.e(TAG, "NotificationService started without action, stopping...");
            stopSelf();
        }

        /**
         * This translates StreamingService requests into NotificationService requests,
         * sometimes, depending on the current NotificationService state.
         */
        mediaPlayerServiceIsBuffering = false;
        switch (action) {
        case StreamingService.ACTION_BUFFERING_BEGIN:
            /** If the notification was requested by StreamingService, set it here. */
            if (!app.getApplicationState().notificationMode && app.getApplicationState().streamingMode) {
                notificationAutomaticallyGenerated = true;
                app.getApplicationState().notificationMode = true;
            }
            mediaPlayerServiceIsBuffering = true;
            /** Fall Through */
        case StreamingService.ACTION_BUFFERING_END:
        case StreamingService.ACTION_STREAMING_STOP:
            stateChanged(getStatus(), null);
            break;
        case StreamingService.ACTION_BUFFERING_ERROR:
            updatePlayingInfo(getStatus());
            break;
        case StreamingService.ACTION_NOTIFICATION_STOP: /** StreamingService _requests_ stop */
            if (notificationAutomaticallyGenerated && !app.getApplicationState().persistentNotification) {
                app.getApplicationState().persistentNotification = false;
                app.getApplicationState().notificationMode = false;
                stopSelf();

                notificationAutomaticallyGenerated = false;
            } else {
                tryToGetAudioFocus();
                stateChanged(getStatus(), null);
            }
            break;
        case ACTION_OPEN_NOTIFICATION:
            stateChanged(getStatus(), null);
            break;
        default:
            if (!app.getApplicationState().notificationMode) {
                Log.e(TAG, "Please report this: NotificationService opened by something when it "
                        + "shouldn't be and taking no action: " + action);
                stopSelf();
            }
            break;
        }

        if (!app.getApplicationState().notificationMode) {
            Log.w(TAG, "NotificationService opened by something, action was taken: " + action);
            stopSelf();
        }

        /**
         * Means we started the service, but don't want
         * it to restart in case it's killed.
         */
        return START_NOT_STICKY;
    }

    /**
     * A simple method to enable lock screen seeking on 4.3 and higher.
     *
     * @param controlFlags The control flags you set beforehand, so that we can add our
     *                     required flag
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void enableSeeking(final int controlFlags) {
        mRemoteControlClient
                .setTransportControlFlags(controlFlags | RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE);

        /* Allows Android to show the song position */
        mRemoteControlClient
                .setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() {
                    /**
                     * Android's callback that queries us for the elapsed time
                     * Here, we are guessing the elapsed time using the last time we
                     * updated the elapsed time and its value at the time.
                     *
                     * @return The guessed song position
                     */
                    @Override
                    public long onGetPlaybackPosition() {
                        /**
                         * If we don't know the position, return
                         * a negative value as per the API spec.
                         */
                        long result = -1L;

                        if (lastStatusRefresh > 0L) {
                            result = lastKnownElapsed + new Date().getTime() - lastStatusRefresh;
                        }
                        return result;
                    }
                });

        /* Allows Android to seek */
        mRemoteControlClient
                .setPlaybackPositionUpdateListener(new RemoteControlClient.OnPlaybackPositionUpdateListener() {
                    /**
                     * Android's callback for when the user seeks using the remote
                     * control.
                     *
                     * @param newPositionMs The position in MS where we should seek
                     */
                    @Override
                    public void onPlaybackPositionUpdate(final long newPositionMs) {
                        try {
                            app.oMPDAsyncHelper.oMPD.seek(newPositionMs / DateUtils.SECOND_IN_MILLIS);
                            mRemoteControlClient.setPlaybackState(getRemoteState(getStatus()), newPositionMs, 1.0f);
                        } catch (final MPDServerException e) {
                            Log.e(TAG, "Could not seek", e);
                        }

                    }
                });
    }

    /**
     * We try to get audio focus, but don't really try too hard.
     * We just want the lock screen cover art.
     */
    private void tryToGetAudioFocus() {
        if ((!app.getApplicationState().streamingMode || StreamingService.isWoundDown()) && !isAudioFocusedOnThis) {
            Log.d(TAG, "requesting audio focus");
            final int result = mAudioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC,
                    AudioManager.AUDIOFOCUS_GAIN);

            isAudioFocusedOnThis = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
        }
    }

    /**
     * A simple method to return a status with error logging.
     *
     * @return An MPDStatus object.
     */
    private MPDStatus getStatus() {
        MPDStatus mpdStatus = null;
        try {
            mpdStatus = app.oMPDAsyncHelper.oMPD.getStatus();
        } catch (final MPDServerException e) {
            Log.d(TAG, "Couldn't get the status to updatePlayingInfo()", e);
        }

        if (mpdStatus == null) {
            Log.d(TAG, "mpdStatus was null, could not updatePlayingInfo().");
        }

        return mpdStatus;
    }

    /**
     * Get the RemoteControlClient status for the corresponding MPDStatus
     *
     * @param mpdStatus MPDStatus to parse
     * @return state to give to RemoteControlClient
     */
    private int getRemoteState(final MPDStatus mpdStatus) {
        final int state;

        if (mpdStatus == null) {
            state = RemoteControlClient.PLAYSTATE_ERROR;
        } else if (mediaPlayerServiceIsBuffering) {
            state = RemoteControlClient.PLAYSTATE_BUFFERING;
        } else {
            switch (mpdStatus.getState()) {
            case MPDStatus.MPD_STATE_PLAYING:
                state = RemoteControlClient.PLAYSTATE_PLAYING;
                break;
            case MPDStatus.MPD_STATE_STOPPED:
                state = RemoteControlClient.PLAYSTATE_STOPPED;
                break;
            default:
                state = RemoteControlClient.PLAYSTATE_PAUSED;
                break;
            }
        }
        return state;
    }

    /**
     * This method will update the current playing track, notification views,
     * the RemoteControlClient & the cover art.
     */
    private void updatePlayingInfo(final MPDStatus status) {
        Log.d(TAG, "updatePlayingInfo(int,MPDStatus)");

        final MPDStatus mpdStatus = status == null ? getStatus() : status;

        if (lastStatusRefresh <= 0L && mpdStatus != null) {
            /**
             * Only update the refresh date and elapsed time if it is the first start to
             * make sure we have initial data, but updateStatus and trackChanged will take care
             * of that afterwards.
             */
            lastStatusRefresh = new Date().getTime();
            lastKnownElapsed = mpdStatus.getElapsedTime() * DateUtils.SECOND_IN_MILLIS;
        }

        if (mpdStatus != null) {
            final int songPos = mpdStatus.getSongPos();
            mCurrentMusic = app.oMPDAsyncHelper.oMPD.getPlaylist().getByIndex(songPos);
        }

        if (mCurrentMusic != null) {
            final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(app);

            if (settings.getBoolean(CoverManager.PREFERENCE_CACHE, true)) {
                updateAlbumCoverWithCached();
            } /** TODO: Add no cache option */
            setupNotification();
            updateRemoteControlClient(mpdStatus);
        }
    }

    /**
     * This method retrieves a path to an album cover bitmap from the cache.
     *
     * @return String A path to a cached album cover bitmap.
     */
    private String retrieveCoverArtPath() {
        final ICoverRetriever cache = new CachedCover();
        String coverArtPath = null;
        String[] coverArtPaths = null;

        try {
            coverArtPaths = cache.getCoverUrl(mCurrentMusic.getAlbumInfo());
        } catch (final Exception e) {
            Log.d(TAG, "Failed to get the cover URL from the cache.", e);
        }

        if (coverArtPaths != null && coverArtPaths.length > 0) {
            coverArtPath = coverArtPaths[0];
        }
        return coverArtPath;
    }

    /**
     * This method updates mAlbumCover if it is different than currently playing, if cache is
     * enabled.
     */
    private void updateAlbumCoverWithCached() {
        final String coverArtPath = retrieveCoverArtPath();

        if (coverArtPath != null && !coverArtPath.equals(mAlbumCoverPath)) {
            if (mAlbumCover != null && !mAlbumCover.isRecycled()) {
                mAlbumCover.recycle();
            }

            mAlbumCoverPath = coverArtPath;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                /**
                 * Don't resize; it WOULD be nice to use the standard 64x64 large notification
                 * size here, but KitKat and MPDroid allow fullscreen lock screen AlbumArt and
                 * 64x64 looks pretty bad on a higher DPI device.
                 */
                /** TODO: Maybe inBitmap stuff here? */
                mAlbumCover = BitmapFactory.decodeFile(coverArtPath);
            } else {
                mAlbumCover = Tools.decodeSampledBitmapFromPath(coverArtPath,
                        getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
                        getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height),
                        false);
            }
        }
    }

    /**
     * This method builds the base, otherwise known as the collapsed notification. The expanded
     * notification method builds upon this method.
     *
     * @param resultView The RemoteView to begin with, be it new or from the current notification.
     * @return The base, otherwise known as, collapsed notification resources for RemoteViews.
     */
    private RemoteViews buildBaseNotification(final RemoteViews resultView) {
        final String title = mCurrentMusic.getTitle();

        /** If in streaming, the notification should be persistent. */
        if (app.getApplicationState().streamingMode && !StreamingService.isWoundDown()) {
            resultView.setViewVisibility(R.id.notificationClose, View.GONE);
        } else {
            resultView.setViewVisibility(R.id.notificationClose, View.VISIBLE);
            resultView.setOnClickPendingIntent(R.id.notificationClose, notificationClose);
        }

        if (MPDStatus.MPD_STATE_PLAYING.equals(getStatus().getState())) {
            resultView.setOnClickPendingIntent(R.id.notificationPlayPause, notificationPause);
            resultView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_media_pause);
        } else {
            resultView.setOnClickPendingIntent(R.id.notificationPlayPause, notificationPlay);
            resultView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_media_play);
        }

        /** When streaming, move things down (hopefully, very) temporarily. */
        if (mediaPlayerServiceIsBuffering) {
            resultView.setTextViewText(R.id.notificationTitle, getString(R.string.buffering));
            resultView.setTextViewText(R.id.notificationArtist, title);
        } else {
            resultView.setTextViewText(R.id.notificationTitle, title);
            resultView.setTextViewText(R.id.notificationArtist, mCurrentMusic.getArtist());
        }

        resultView.setOnClickPendingIntent(R.id.notificationNext, notificationNext);

        if (mAlbumCover != null) {
            resultView.setImageViewUri(R.id.notificationIcon, Uri.parse(mAlbumCoverPath));
        }

        return resultView;
    }

    /**
     * This generates the collapsed notification from base.
     *
     * @return The collapsed notification resources for RemoteViews.
     */
    private NotificationCompat.Builder buildNewCollapsedNotification() {
        final RemoteViews resultView = buildBaseNotification(
                new RemoteViews(getPackageName(), R.layout.notification));
        return buildStaticCollapsedNotification().setContent(resultView);
    }

    /**
     * buildExpandedNotification builds upon the collapsed notification resources to create
     * the resources necessary for the expanded notification RemoteViews.
     *
     * @return The expanded notification RemoteViews.
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private RemoteViews buildExpandedNotification() {
        final RemoteViews resultView;

        if (mNotification == null || mNotification.bigContentView == null) {
            resultView = new RemoteViews(getPackageName(), R.layout.notification_big);
        } else {
            resultView = mNotification.bigContentView;
        }

        buildBaseNotification(resultView);

        /** When streaming, move things down (hopefully, very) temporarily. */
        if (mediaPlayerServiceIsBuffering) {
            resultView.setTextViewText(R.id.notificationAlbum, mCurrentMusic.getArtist());
        } else {
            resultView.setTextViewText(R.id.notificationAlbum, mCurrentMusic.getAlbum());
        }

        resultView.setOnClickPendingIntent(R.id.notificationPrev, notificationPrevious);

        return resultView;
    }

    /**
     * Build a new notification or perform an update on an existing notification.
     */
    private void setupNotification() {
        Log.d(TAG, "update notification: " + mCurrentMusic.getArtist() + " - " + mCurrentMusic.getTitle());

        NotificationCompat.Builder builder = null;
        RemoteViews expandedNotification = null;

        /** These have a very specific order. */
        if (mNotification == null || mNotification.contentView == null) {
            builder = buildNewCollapsedNotification();
        } else {
            buildBaseNotification(mNotification.contentView);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            expandedNotification = buildExpandedNotification();
        }

        if (builder != null) {
            mNotification = builder.build();
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            mNotification.bigContentView = expandedNotification;
        }

        mNotificationManager.notify(NOTIFICATION_ID, mNotification);
        startForeground(NOTIFICATION_ID, mNotification);
    }

    private void shutdownNotification() {
        if (app.getApplicationState().persistentNotification) {
            windDownResources();
        } else {
            stopSelf();
        }
    }

    /**
     * Update the remote controls.
     *
     * @param mpdStatus The current server status object.
     */
    private void updateRemoteControlClient(final MPDStatus mpdStatus) {
        final int state = getRemoteState(mpdStatus);

        mRemoteControlClient.editMetadata(true)
                .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, mCurrentMusic.getAlbum())
                .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, mCurrentMusic.getAlbumArtist())
                .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, mCurrentMusic.getArtist())
                .putLong(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, (long) mCurrentMusic.getTrack())
                .putLong(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, (long) mCurrentMusic.getDisc())
                .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION,
                        mCurrentMusic.getTime() * DateUtils.SECOND_IN_MILLIS)
                .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, mCurrentMusic.getTitle())
                .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, mAlbumCover).apply();

        /** Notify of the elapsed time if on 4.3 or higher */
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mRemoteControlClient.setPlaybackState(state, lastKnownElapsed, 1.0f);
        } else {
            mRemoteControlClient.setPlaybackState(state);
        }
        Log.d(TAG, "Updated remote client with state " + state + " for music " + mCurrentMusic);
    }

    /**
     * Used when persistent notification is enabled in
     * leu of selfStop() and used during selfStop().
     */
    private void windDownResources() {
        Log.d(TAG, "windDownResources()");
        stopForeground(true);

        if (mAudioManager != null) {
            mAudioManager.abandonAudioFocus(null);
        }

        if (mNotificationManager != null) {
            mNotificationManager.cancel(NOTIFICATION_ID);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "Removing connection lock");
        app.removeConnectionLock(this);
        app.oMPDAsyncHelper.removeStatusChangeListener(this);
        delayedPauseHandler.removeCallbacksAndMessages(null);

        app.getApplicationState().notificationMode = false;

        windDownResources();

        if (mAlbumCover != null && !mAlbumCover.isRecycled()) {
            mAlbumCover.recycle();
        }

        if (mAudioManager != null) {
            if (mRemoteControlClient != null) {
                mRemoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
                mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
            }

            if (mMediaButtonReceiverComponent != null) {
                mAudioManager.unregisterMediaButtonEventReceiver(mMediaButtonReceiverComponent);
            }

            mAudioManager.abandonAudioFocus(null);
        }
    }

    @Override
    public IBinder onBind(final Intent intent) {
        return null;
    }

    @Override
    public void connectionStateChanged(final boolean connected, final boolean connectionLost) {
        if (connected) {
            if (app.oMPDAsyncHelper.getConnectionSettings().persistentNotification) {
                stateChanged(getStatus(), null);
            }
        } else {
            final long idleDelay = 10000L;
            /** Give 10 Seconds for Network Problems */

            final Message msg = delayedDisconnectionHandler.obtainMessage();
            delayedDisconnectionHandler.sendMessageDelayed(msg, idleDelay);
        }
    }

    /**
     * A JMPDComm callback to be invoked during library state changes.
     *
     * @param updating true when updating, false when not updating.
     * @param dbChanged true when the server database has been updated, false otherwise.
     */
    @Override
    public void libraryStateChanged(final boolean updating, final boolean dbChanged) {
    }

    @Override
    public void playlistChanged(final MPDStatus mpdStatus, final int oldPlaylistVersion) {
        /**
         * This is required because streams will emit a playlist (current queue) event as the
         * metadata will change while the same audio file is playing (no track change).
         */
        if (mCurrentMusic != null && mCurrentMusic.isStream()) {
            updatePlayingInfo(mpdStatus);
        }
    }

    @Override
    public void randomChanged(final boolean random) {
    }

    @Override
    public void repeatChanged(final boolean repeating) {
    }

    /**
     * This is the idle delay for shutting down this service after inactivity
     * (in milliseconds). This idle is also longer than StreamingService to
     * avoid being unnecessarily brought up to shut right back down.
     */
    private void setupServiceHandler() {
        final long idleDelay = 630000L;
        /** 10 Minutes 30 Seconds */
        final Message msg = delayedPauseHandler.obtainMessage();
        serviceHandlerActive = true;
        delayedPauseHandler.sendMessageDelayed(msg, idleDelay);
    }

    /** Kills any active service handlers. */
    private void stopServiceHandler() {
        /** If we have a message in the queue, remove it. */
        delayedPauseHandler.removeCallbacksAndMessages(null);
        serviceHandlerActive = false; /** No notification if stopped or paused. */
    }

    @Override
    public void stateChanged(final MPDStatus mpdStatus, final String oldState) {
        if (mpdStatus == null) {
            Log.w(TAG, "Null mpdStatus received in stateChanged");
        } else {
            lastStatusRefresh = new Date().getTime();
            // MPDs elapsed time is in seconds, convert to milliseconds
            lastKnownElapsed = mpdStatus.getElapsedTime() * DateUtils.SECOND_IN_MILLIS;
            switch (mpdStatus.getState()) {
            case MPDStatus.MPD_STATE_PLAYING:
                stopServiceHandler();
                tryToGetAudioFocus();
                updatePlayingInfo(mpdStatus);
                break;
            case MPDStatus.MPD_STATE_STOPPED:
                if (app.getApplicationState().persistentNotification) {
                    windDownResources(); /** Hide immediately, requires user intervention */
                } else {
                    stopSelf();
                }
                break;
            case MPDStatus.MPD_STATE_PAUSED:
                if (!serviceHandlerActive) {
                    setupServiceHandler();
                    updatePlayingInfo(mpdStatus);
                }
                break;
            default:
                break;
            }
        }
    }

    @Override
    public void trackChanged(final MPDStatus mpdStatus, final int oldTrack) {
        lastStatusRefresh = new Date().getTime();
        lastKnownElapsed = 0L;

        updatePlayingInfo(mpdStatus);
    }

    @Override
    public void volumeChanged(final MPDStatus mpdStatus, final int oldVolume) {
    }
}