com.jelly.music.player.Services.AudioPlaybackService.java Source code

Java tutorial

Introduction

Here is the source code for com.jelly.music.player.Services.AudioPlaybackService.java

Source

/*
 * Copyright (C) 2014 Saravan Pantham
 *
 * 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.jelly.music.player.Services;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnBufferingUpdateListener;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.RemoteControlClient;
import android.media.audiofx.PresetReverb;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.Toast;

import com.google.analytics.tracking.android.Fields;
import com.google.analytics.tracking.android.GoogleAnalytics;
import com.google.analytics.tracking.android.MapBuilder;
import com.google.analytics.tracking.android.Tracker;
import com.jelly.music.player.AsyncTasks.AsyncGetSongStreamURLTask;
import com.jelly.music.player.BroadcastReceivers.HeadsetButtonsReceiver;
import com.jelly.music.player.BroadcastReceivers.HeadsetPlugBroadcastReceiver;
import com.jelly.music.player.DBHelpers.DBAccessHelper;
import com.jelly.music.player.Helpers.AudioManagerHelper;
import com.jelly.music.player.Helpers.EqualizerHelper;
import com.jelly.music.player.Helpers.SongHelper;
import com.jelly.music.player.R;
import com.jelly.music.player.RemoteControlClient.RemoteControlClientCompat;
import com.jelly.music.player.RemoteControlClient.RemoteControlHelper;
import com.jelly.music.player.Scrobbling.ScrobbleDroidHelper;
import com.jelly.music.player.Scrobbling.SimpleLastFMHelper;
import com.jelly.music.player.Utils.Common;
import com.jelly.music.player.WidgetProviders.AlbumArtWidgetProvider;
import com.jelly.music.player.WidgetProviders.BlurredWidgetProvider;
import com.jelly.music.player.WidgetProviders.LargeWidgetProvider;
import com.jelly.music.player.WidgetProviders.SmallWidgetProvider;
import com.jelly.music.player.PlaybackKickstarter.PlaybackKickstarter;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;

/**
 * The meat and potatoes of the entire app. Manages 
 * playback, equalizer effects, and all other audio 
 * related operations.
 * 
 * @author Saravan Pantham
 */
public class AudioPlaybackService extends Service {

    //Context and Intent.
    private Context mContext;
    private Service mService;

    //Global Objects Provider.
    private Common mApp;

    //PrepareServiceListener instance.
    private PrepareServiceListener mPrepareServiceListener;

    //MediaPlayer objects and flags.
    private MediaPlayer mMediaPlayer;
    private MediaPlayer mMediaPlayer2;
    private int mCurrentMediaPlayer = 1;
    private boolean mFirstRun = true;

    //AudioManager.
    private AudioManager mAudioManager;
    private AudioManagerHelper mAudioManagerHelper;

    //Flags that indicate whether the mediaPlayers have been initialized.
    private boolean mMediaPlayerPrepared = false;
    private boolean mMediaPlayer2Prepared = false;

    //Cursor object(s) that will guide the rest of this queue.
    private Cursor mCursor;
    private MergeCursor mMergeCursor;

    //Holds the indeces of the current cursor, in the order that they'll be played.
    private ArrayList<Integer> mPlaybackIndecesList = new ArrayList<Integer>();

    //Holds the indeces of songs that were unplayable.
    private ArrayList<Integer> mFailedIndecesList = new ArrayList<Integer>();

    //Song data helpers for each MediaPlayer object.
    private SongHelper mMediaPlayerSongHelper;
    private SongHelper mMediaPlayer2SongHelper;

    //Pointer variable.
    private int mCurrentSongIndex;

    //Equalizer/Audio FX helpers.
    private EqualizerHelper mEqualizerHelper;

    //Notification elements.
    private NotificationCompat.Builder mNotificationBuilder;
    public static final int mNotificationId = 1080; //NOTE: Using 0 as a notification ID causes Android to ignore the notification call.

    //Custom actions for media player controls via the notification bar.
    public static final String LAUNCH_NOW_PLAYING_ACTION = "com.jelly.music.player.LAUNCH_NOW_PLAYING_ACTION";
    public static final String PREVIOUS_ACTION = "com.jelly.music.player.PREVIOUS_ACTION";
    public static final String PLAY_PAUSE_ACTION = "com.jelly.music.player.PLAY_PAUSE_ACTION";
    public static final String NEXT_ACTION = "com.jelly.music.player.NEXT_ACTION";
    public static final String STOP_SERVICE = "com.jelly.music.player.STOP_SERVICE";

    //Indicates if an enqueue/queue reordering operation was performed on the original queue.
    private boolean mEnqueuePerformed = false;

    //Handler object.
    private Handler mHandler;

    //Volume variables that handle the crossfade effect.
    private float mFadeOutVolume = 1.0f;
    private float mFadeInVolume = 0.0f;

    //Headset plug receiver.
    private HeadsetPlugBroadcastReceiver mHeadsetPlugReceiver;

    //Crossfade.
    private int mCrossfadeDuration;

    //A-B Repeat variables.
    private int mRepeatSongRangePointA = 0;
    private int mRepeatSongRangePointB = 0;

    //Indicates if the user changed the track manually.
    private boolean mTrackChangedByUser = false;

    //RemoteControlClient for use with remote controls and ICS+ lockscreen controls.
    private RemoteControlClientCompat mRemoteControlClientCompat;
    private ComponentName mMediaButtonReceiverComponent;

    //Enqueue reorder scalar.
    private int mEnqueueReorderScalar = 0;

    //Temp placeholder for GMusic Uri.
    public static final Uri URI_BEING_LOADED = Uri.parse("uri_being_loaded");

    //Google Analytics.
    private GoogleAnalytics mGAInstance;
    private Tracker mTracker;
    private long mServiceStartTime;

    /**
     * Constructor that should be used whenever this 
     * service is being explictly created.
     * 
     * @param context The context being passed in.
     */
    public AudioPlaybackService(Context context) {
        mContext = context;
    }

    /**
     * Empty constructor. Required if a custom constructor 
     * was explicitly declared (see above).
     */
    public AudioPlaybackService() {
        super();
    }

    /**
     * Prepares the MediaPlayer objects for first use 
     * and starts the service. The workflow of the entire 
     * service starts here.
     * 
     * @param intent Calling intent.
     * @param flags Service flags.
     * @param startId Service start ID.
     */
    @SuppressLint("NewApi")
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);

        //Context.
        mContext = getApplicationContext();
        mService = this;
        mHandler = new Handler();

        mApp = (Common) getApplicationContext();
        mApp.setService((AudioPlaybackService) this);
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);

        //Initialize Google Analytics.
        //initGoogleAnalytics();

        //Initialize the MediaPlayer objects.
        initMediaPlayers();

        //Time to play nice with other music players (and audio apps) and request audio focus.
        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        mAudioManagerHelper = new AudioManagerHelper();

        // Request audio focus for playback
        mAudioManagerHelper.setHasAudioFocus(requestAudioFocus());

        //Grab the crossfade duration for this session.
        mCrossfadeDuration = mApp.getCrossfadeDuration();

        //Initialize audio effects (equalizer, virtualizer, bass boost) for this session.
        initAudioFX();

        mMediaButtonReceiverComponent = new ComponentName(this.getPackageName(),
                HeadsetButtonsReceiver.class.getName());
        mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);

        if (mApp.getSharedPreferences().getBoolean(Common.SHOW_LOCKSCREEN_CONTROLS, true) == true) {
            initRemoteControlClient();
        }

        mApp.getPlaybackKickstarter().setBuildCursorListener(buildCursorListener);

        //The service has been successfully started.
        setPrepareServiceListener(mApp.getPlaybackKickstarter());
        getPrepareServiceListener().onServiceRunning(this);

        return START_STICKY;
    }

    /**
     * Public interface that provides access to 
     * major events during the service startup 
     * process.
     * 
     * @author Saravan Pantham
     */
    public interface PrepareServiceListener {

        /**
         * Called when the service is up and running.
         */
        public void onServiceRunning(AudioPlaybackService service);

        /**
         * Called when the service failed to start. 
         * Also returns the failure reason via the exception 
         * parameter.
         */
        public void onServiceFailed(Exception exception);

    }

    /**
     * Initializes Google Analytics.
     */
    private void initGoogleAnalytics() {
        try {
            if (mApp.isGoogleAnalyticsEnabled()) {
                String gaTrackingId = getResources().getString(R.string.ga_trackingId);
                mServiceStartTime = System.currentTimeMillis();

                mGAInstance = GoogleAnalytics.getInstance(getApplicationContext());
                mTracker = mGAInstance.getTracker(gaTrackingId);

                mTracker.set(Fields.SESSION_CONTROL, "start");
                mTracker.send(MapBuilder
                        .createEvent("Jelly Service", "Service started!", "User is playing music.", null).build());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * Initializes remote control clients for this service session. 
     * Currently used for lockscreen controls.
     */
    public void initRemoteControlClient() {
        if (mRemoteControlClientCompat == null) {
            Intent remoteControlIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
            remoteControlIntent.setComponent(mMediaButtonReceiverComponent);

            mRemoteControlClientCompat = new RemoteControlClientCompat(
                    PendingIntent.getBroadcast(mContext, 0, remoteControlIntent, 0));
            RemoteControlHelper.registerRemoteControlClient(mAudioManager, mRemoteControlClientCompat);

        }

        mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
        mRemoteControlClientCompat.setTransportControlFlags(RemoteControlClient.FLAG_KEY_MEDIA_PLAY
                | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
                | RemoteControlClient.FLAG_KEY_MEDIA_STOP | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);

    }

    /**
     * Initializes the MediaPlayer objects for this service session.
     */
    private void initMediaPlayers() {

        /*
         * Release the MediaPlayer objects if they are still valid.
         */
        if (mMediaPlayer != null) {
            mMediaPlayer.release();
            mMediaPlayer = null;
        }

        if (mMediaPlayer2 != null) {
            getMediaPlayer2().release();
            mMediaPlayer2 = null;
        }

        mMediaPlayer = new MediaPlayer();
        mMediaPlayer2 = new MediaPlayer();
        setCurrentMediaPlayer(1);

        getMediaPlayer().reset();
        getMediaPlayer2().reset();

        //Loop the players if the repeat mode is set to repeat the current song.
        if (getRepeatMode() == Common.REPEAT_SONG) {
            getMediaPlayer().setLooping(true);
            getMediaPlayer2().setLooping(true);
        }

        try {
            mMediaPlayer.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
            getMediaPlayer2().setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
        } catch (Exception e) {
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer2 = new MediaPlayer();
            setCurrentMediaPlayer(1);
        }

        //Set the mediaPlayers' stream sources.
        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        getMediaPlayer2().setAudioStreamType(AudioManager.STREAM_MUSIC);

    }

    /**
     * Initializes the list of pointers to each cursor row.
     */
    private void initPlaybackIndecesList(boolean playAll) {
        if (getCursor() != null && getPlaybackIndecesList() != null) {
            getPlaybackIndecesList().clear();
            for (int i = 0; i < getCursor().getCount(); i++) {
                getPlaybackIndecesList().add(i);
            }

            if (isShuffleOn() && !playAll) {
                //Build a new list that doesn't include the current song index.
                ArrayList<Integer> newList = new ArrayList<Integer>(getPlaybackIndecesList());
                newList.remove(getCurrentSongIndex());

                //Shuffle the new list.
                Collections.shuffle(newList, new Random(System.nanoTime()));

                //Plug in the current song index back into the new list.
                newList.add(getCurrentSongIndex(), getCurrentSongIndex());
                mPlaybackIndecesList = newList;

            } else if (isShuffleOn() && playAll) {
                //Shuffle all elements.
                Collections.shuffle(getPlaybackIndecesList(), new Random(System.nanoTime()));
            }

        } else {
            stopSelf();
        }

    }

    /**
     * Requests AudioFocus from the OS.
     * 
     * @return True if AudioFocus was gained. False, otherwise.
     */
    private boolean requestAudioFocus() {
        int result = mAudioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN);

        if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            //Stop the service.
            mService.stopSelf();
            Toast.makeText(mContext, R.string.close_other_audio_apps, Toast.LENGTH_LONG).show();
            return false;
        } else {
            return true;
        }

    }

    /**
     * Initializes the equalizer and audio effects for this service session.
     */
    public void initAudioFX() {

        try {
            //Instatiate the equalizer helper object.
            mEqualizerHelper = new EqualizerHelper(mContext, mMediaPlayer.getAudioSessionId(),
                    getMediaPlayer2().getAudioSessionId(), mApp.isEqualizerEnabled());

        } catch (UnsupportedOperationException e) {
            e.printStackTrace();
            mEqualizerHelper.setIsEqualizerSupported(false);
        } catch (Exception e) {
            e.printStackTrace();
            mEqualizerHelper.setIsEqualizerSupported(false);
        }

    }

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

    /**
     * Retrieves the EQ values for mMediaPlayer's current song and 
     * applies them to the EQ engine.
     * 
     * @param songId The id of the song that mMediaPlayer is current handling.
     */
    private void applyMediaPlayerEQ(String songId) {

        if (mEqualizerHelper == null)
            return;

        short fiftyHertzBand = mEqualizerHelper.getEqualizer().getBand(50000);
        short oneThirtyHertzBand = mEqualizerHelper.getEqualizer().getBand(130000);
        short threeTwentyHertzBand = mEqualizerHelper.getEqualizer().getBand(320000);
        short eightHundredHertzBand = mEqualizerHelper.getEqualizer().getBand(800000);
        short twoKilohertzBand = mEqualizerHelper.getEqualizer().getBand(2000000);
        short fiveKilohertzBand = mEqualizerHelper.getEqualizer().getBand(5000000);
        short twelvePointFiveKilohertzBand = mEqualizerHelper.getEqualizer().getBand(9000000);

        //Get the equalizer/audioFX settings for this specific song.
        int[] eqValues = mApp.getDBAccessHelper().getSongEQValues(songId);

        //50Hz Band.
        if (eqValues[0] == 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(fiftyHertzBand, (short) 0);
        } else if (eqValues[0] < 16) {

            if (eqValues[0] == 0) {
                mEqualizerHelper.getEqualizer().setBandLevel(fiftyHertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer().setBandLevel(fiftyHertzBand, (short) (-(16 - eqValues[0]) * 100));
            }

        } else if (eqValues[0] > 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(fiftyHertzBand, (short) ((eqValues[0] - 16) * 100));
        }

        //130Hz Band.
        if (eqValues[1] == 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(oneThirtyHertzBand, (short) 0);
        } else if (eqValues[1] < 16) {

            if (eqValues[1] == 0) {
                mEqualizerHelper.getEqualizer().setBandLevel(oneThirtyHertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer().setBandLevel(oneThirtyHertzBand,
                        (short) (-(16 - eqValues[1]) * 100));
            }

        } else if (eqValues[1] > 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(oneThirtyHertzBand, (short) ((eqValues[1] - 16) * 100));
        }

        //320Hz Band.
        if (eqValues[2] == 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(threeTwentyHertzBand, (short) 0);
        } else if (eqValues[2] < 16) {

            if (eqValues[2] == 0) {
                mEqualizerHelper.getEqualizer().setBandLevel(threeTwentyHertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer().setBandLevel(threeTwentyHertzBand,
                        (short) (-(16 - eqValues[2]) * 100));
            }

        } else if (eqValues[2] > 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(threeTwentyHertzBand, (short) ((eqValues[2] - 16) * 100));
        }

        //800Hz Band.
        if (eqValues[3] == 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(eightHundredHertzBand, (short) 0);
        } else if (eqValues[3] < 16) {

            if (eqValues[3] == 0) {
                mEqualizerHelper.getEqualizer().setBandLevel(eightHundredHertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer().setBandLevel(eightHundredHertzBand,
                        (short) (-(16 - eqValues[3]) * 100));
            }

        } else if (eqValues[3] > 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(eightHundredHertzBand, (short) ((eqValues[3] - 16) * 100));
        }

        //2kHz Band.
        if (eqValues[4] == 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(twoKilohertzBand, (short) 0);
        } else if (eqValues[4] < 16) {

            if (eqValues[4] == 0) {
                mEqualizerHelper.getEqualizer().setBandLevel(twoKilohertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer().setBandLevel(twoKilohertzBand, (short) (-(16 - eqValues[4]) * 100));
            }

        } else if (eqValues[4] > 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(twoKilohertzBand, (short) ((eqValues[4] - 16) * 100));
        }

        //5kHz Band.
        if (eqValues[5] == 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(fiveKilohertzBand, (short) 0);
        } else if (eqValues[5] < 16) {

            if (eqValues[5] == 0) {
                mEqualizerHelper.getEqualizer().setBandLevel(fiveKilohertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer().setBandLevel(fiveKilohertzBand,
                        (short) (-(16 - eqValues[5]) * 100));
            }

        } else if (eqValues[5] > 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(fiveKilohertzBand, (short) ((eqValues[5] - 16) * 100));
        }

        //12.5kHz Band.
        if (eqValues[6] == 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(twelvePointFiveKilohertzBand, (short) 0);
        } else if (eqValues[6] < 16) {

            if (eqValues[6] == 0) {
                mEqualizerHelper.getEqualizer().setBandLevel(twelvePointFiveKilohertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer().setBandLevel(twelvePointFiveKilohertzBand,
                        (short) (-(16 - eqValues[6]) * 100));
            }

        } else if (eqValues[6] > 16) {
            mEqualizerHelper.getEqualizer().setBandLevel(twelvePointFiveKilohertzBand,
                    (short) ((eqValues[6] - 16) * 100));
        }

        //Set the audioFX values.
        mEqualizerHelper.getVirtualizer().setStrength((short) eqValues[7]);
        mEqualizerHelper.getBassBoost().setStrength((short) eqValues[8]);

        if (eqValues[9] == 0) {
            mEqualizerHelper.getReverb().setPreset(PresetReverb.PRESET_NONE);
        } else if (eqValues[9] == 1) {
            mEqualizerHelper.getReverb().setPreset(PresetReverb.PRESET_LARGEHALL);
        } else if (eqValues[9] == 2) {
            mEqualizerHelper.getReverb().setPreset(PresetReverb.PRESET_LARGEROOM);
        } else if (eqValues[9] == 3) {
            mEqualizerHelper.getReverb().setPreset(PresetReverb.PRESET_MEDIUMHALL);
        } else if (eqValues[9] == 4) {
            mEqualizerHelper.getReverb().setPreset(PresetReverb.PRESET_MEDIUMROOM);
        } else if (eqValues[9] == 5) {
            mEqualizerHelper.getReverb().setPreset(PresetReverb.PRESET_SMALLROOM);
        } else if (eqValues[9] == 6) {
            mEqualizerHelper.getReverb().setPreset(PresetReverb.PRESET_PLATE);
        }

    }

    /**
     * Retrieves the EQ values for mMediaPlayer2's current song and 
     * applies them to the EQ engine.
     * 
     * @param songId The id of the song that mMediaPlayer is current handling.
     */
    private void applyMediaPlayer2EQ(String songId) {

        if (mEqualizerHelper == null)
            return;

        short fiftyHertzBand = mEqualizerHelper.getEqualizer2().getBand(50000);
        short oneThirtyHertzBand = mEqualizerHelper.getEqualizer2().getBand(130000);
        short threeTwentyHertzBand = mEqualizerHelper.getEqualizer2().getBand(320000);
        short eightHundredHertzBand = mEqualizerHelper.getEqualizer2().getBand(800000);
        short twoKilohertzBand = mEqualizerHelper.getEqualizer2().getBand(2000000);
        short fiveKilohertzBand = mEqualizerHelper.getEqualizer2().getBand(5000000);
        short twelvePointFiveKilohertzBand = mEqualizerHelper.getEqualizer2().getBand(9000000);

        //Get the mEqualizerHelper.getEqualizer()/audioFX settings for this specific song.
        int[] eqValues = mApp.getDBAccessHelper().getSongEQValues(songId);

        //50Hz Band.
        if (eqValues[0] == 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(fiftyHertzBand, (short) 0);
        } else if (eqValues[0] < 16) {

            if (eqValues[0] == 0) {
                mEqualizerHelper.getEqualizer2().setBandLevel(fiftyHertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer2().setBandLevel(fiftyHertzBand, (short) (-(16 - eqValues[0]) * 100));
            }

        } else if (eqValues[0] > 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(fiftyHertzBand, (short) ((eqValues[0] - 16) * 100));
        }

        //130Hz Band.
        if (eqValues[1] == 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(oneThirtyHertzBand, (short) 0);
        } else if (eqValues[1] < 16) {

            if (eqValues[1] == 0) {
                mEqualizerHelper.getEqualizer2().setBandLevel(oneThirtyHertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer2().setBandLevel(oneThirtyHertzBand,
                        (short) (-(16 - eqValues[1]) * 100));
            }

        } else if (eqValues[1] > 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(oneThirtyHertzBand, (short) ((eqValues[1] - 16) * 100));
        }

        //320Hz Band.
        if (eqValues[2] == 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(threeTwentyHertzBand, (short) 0);
        } else if (eqValues[2] < 16) {

            if (eqValues[2] == 0) {
                mEqualizerHelper.getEqualizer2().setBandLevel(threeTwentyHertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer2().setBandLevel(threeTwentyHertzBand,
                        (short) (-(16 - eqValues[2]) * 100));
            }

        } else if (eqValues[2] > 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(threeTwentyHertzBand, (short) ((eqValues[2] - 16) * 100));
        }

        //800Hz Band.
        if (eqValues[3] == 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(eightHundredHertzBand, (short) 0);
        } else if (eqValues[3] < 16) {

            if (eqValues[3] == 0) {
                mEqualizerHelper.getEqualizer2().setBandLevel(eightHundredHertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer2().setBandLevel(eightHundredHertzBand,
                        (short) (-(16 - eqValues[3]) * 100));
            }

        } else if (eqValues[3] > 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(eightHundredHertzBand,
                    (short) ((eqValues[3] - 16) * 100));
        }

        //2kHz Band.
        if (eqValues[4] == 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(twoKilohertzBand, (short) 0);
        } else if (eqValues[4] < 16) {

            if (eqValues[4] == 0) {
                mEqualizerHelper.getEqualizer2().setBandLevel(twoKilohertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer2().setBandLevel(twoKilohertzBand,
                        (short) (-(16 - eqValues[4]) * 100));
            }

        } else if (eqValues[4] > 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(twoKilohertzBand, (short) ((eqValues[4] - 16) * 100));
        }

        //5kHz Band.
        if (eqValues[5] == 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(fiveKilohertzBand, (short) 0);
        } else if (eqValues[5] < 16) {

            if (eqValues[5] == 0) {
                mEqualizerHelper.getEqualizer2().setBandLevel(fiveKilohertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer2().setBandLevel(fiveKilohertzBand,
                        (short) (-(16 - eqValues[5]) * 100));
            }

        } else if (eqValues[5] > 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(fiveKilohertzBand, (short) ((eqValues[5] - 16) * 100));
        }

        //12.5kHz Band.
        if (eqValues[6] == 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(twelvePointFiveKilohertzBand, (short) 0);
        } else if (eqValues[6] < 16) {

            if (eqValues[6] == 0) {
                mEqualizerHelper.getEqualizer2().setBandLevel(twelvePointFiveKilohertzBand, (short) -1500);
            } else {
                mEqualizerHelper.getEqualizer2().setBandLevel(twelvePointFiveKilohertzBand,
                        (short) (-(16 - eqValues[6]) * 100));
            }

        } else if (eqValues[6] > 16) {
            mEqualizerHelper.getEqualizer2().setBandLevel(twelvePointFiveKilohertzBand,
                    (short) ((eqValues[6] - 16) * 100));
        }

        //Set the audioFX values.
        mEqualizerHelper.getVirtualizer2().setStrength((short) eqValues[7]);
        mEqualizerHelper.getBassBoost2().setStrength((short) eqValues[8]);

        if (eqValues[9] == 0) {
            mEqualizerHelper.getReverb2().setPreset(PresetReverb.PRESET_NONE);
        } else if (eqValues[9] == 1) {
            mEqualizerHelper.getReverb2().setPreset(PresetReverb.PRESET_LARGEHALL);
        } else if (eqValues[9] == 2) {
            mEqualizerHelper.getReverb2().setPreset(PresetReverb.PRESET_LARGEROOM);
        } else if (eqValues[9] == 3) {
            mEqualizerHelper.getReverb2().setPreset(PresetReverb.PRESET_MEDIUMHALL);
        } else if (eqValues[9] == 4) {
            mEqualizerHelper.getReverb2().setPreset(PresetReverb.PRESET_MEDIUMROOM);
        } else if (eqValues[9] == 5) {
            mEqualizerHelper.getReverb2().setPreset(PresetReverb.PRESET_SMALLROOM);
        } else if (eqValues[9] == 6) {
            mEqualizerHelper.getReverb2().setPreset(PresetReverb.PRESET_PLATE);
        }

    }

    /**
     * Builds and returns a fully constructed Notification for devices 
     * on Jelly Bean and above (API 16+).
     */
    @SuppressLint("NewApi")
    private Notification buildJBNotification(SongHelper songHelper) {
        mNotificationBuilder = new NotificationCompat.Builder(mContext);
        mNotificationBuilder.setOngoing(true);
        mNotificationBuilder.setAutoCancel(false);
        mNotificationBuilder.setSmallIcon(R.drawable.notif_icon);

        //Open up the player screen when the user taps on the notification.
        Intent launchNowPlayingIntent = new Intent();
        launchNowPlayingIntent.setAction(AudioPlaybackService.LAUNCH_NOW_PLAYING_ACTION);
        PendingIntent launchNowPlayingPendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(),
                0, launchNowPlayingIntent, 0);
        mNotificationBuilder.setContentIntent(launchNowPlayingPendingIntent);

        //Grab the notification layouts.
        RemoteViews notificationView = new RemoteViews(mContext.getPackageName(),
                R.layout.notification_custom_layout);
        RemoteViews expNotificationView = new RemoteViews(mContext.getPackageName(),
                R.layout.notification_custom_expanded_layout);

        //Initialize the notification layout buttons.
        Intent previousTrackIntent = new Intent();
        previousTrackIntent.setAction(AudioPlaybackService.PREVIOUS_ACTION);
        PendingIntent previousTrackPendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(), 0,
                previousTrackIntent, 0);

        Intent playPauseTrackIntent = new Intent();
        playPauseTrackIntent.setAction(AudioPlaybackService.PLAY_PAUSE_ACTION);
        PendingIntent playPauseTrackPendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(), 0,
                playPauseTrackIntent, 0);

        Intent nextTrackIntent = new Intent();
        nextTrackIntent.setAction(AudioPlaybackService.NEXT_ACTION);
        PendingIntent nextTrackPendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(), 0,
                nextTrackIntent, 0);

        Intent stopServiceIntent = new Intent();
        stopServiceIntent.setAction(AudioPlaybackService.STOP_SERVICE);
        PendingIntent stopServicePendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(), 0,
                stopServiceIntent, 0);

        //Check if audio is playing and set the appropriate play/pause button.
        if (mApp.getService().isPlayingMusic()) {
            notificationView.setImageViewResource(R.id.notification_base_play, R.drawable.btn_playback_pause_light);
            expNotificationView.setImageViewResource(R.id.notification_expanded_base_play,
                    R.drawable.btn_playback_pause_light);
        } else {
            notificationView.setImageViewResource(R.id.notification_base_play, R.drawable.btn_playback_play_light);
            expNotificationView.setImageViewResource(R.id.notification_expanded_base_play,
                    R.drawable.btn_playback_play_light);
        }

        //Set the notification content.
        expNotificationView.setTextViewText(R.id.notification_expanded_base_line_one, songHelper.getTitle());
        expNotificationView.setTextViewText(R.id.notification_expanded_base_line_two, songHelper.getArtist());
        expNotificationView.setTextViewText(R.id.notification_expanded_base_line_three, songHelper.getAlbum());

        notificationView.setTextViewText(R.id.notification_base_line_one, songHelper.getTitle());
        notificationView.setTextViewText(R.id.notification_base_line_two, songHelper.getArtist());

        //Set the states of the next/previous buttons and their pending intents.
        if (mApp.getService().isOnlySongInQueue()) {
            //This is the only song in the queue, so disable the previous/next buttons.
            expNotificationView.setViewVisibility(R.id.notification_expanded_base_next, View.INVISIBLE);
            expNotificationView.setViewVisibility(R.id.notification_expanded_base_previous, View.INVISIBLE);
            expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_play,
                    playPauseTrackPendingIntent);

            notificationView.setViewVisibility(R.id.notification_base_next, View.INVISIBLE);
            notificationView.setViewVisibility(R.id.notification_base_previous, View.INVISIBLE);
            notificationView.setOnClickPendingIntent(R.id.notification_base_play, playPauseTrackPendingIntent);

        } else if (mApp.getService().isFirstSongInQueue()) {
            //This is the the first song in the queue, so disable the previous button.
            expNotificationView.setViewVisibility(R.id.notification_expanded_base_previous, View.INVISIBLE);
            expNotificationView.setViewVisibility(R.id.notification_expanded_base_next, View.VISIBLE);
            expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_play,
                    playPauseTrackPendingIntent);
            expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_next,
                    nextTrackPendingIntent);

            notificationView.setViewVisibility(R.id.notification_base_previous, View.INVISIBLE);
            notificationView.setViewVisibility(R.id.notification_base_next, View.VISIBLE);
            notificationView.setOnClickPendingIntent(R.id.notification_base_play, playPauseTrackPendingIntent);
            notificationView.setOnClickPendingIntent(R.id.notification_base_next, nextTrackPendingIntent);

        } else if (mApp.getService().isLastSongInQueue()) {
            //This is the last song in the cursor, so disable the next button.
            expNotificationView.setViewVisibility(R.id.notification_expanded_base_previous, View.VISIBLE);
            expNotificationView.setViewVisibility(R.id.notification_expanded_base_next, View.INVISIBLE);
            expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_play,
                    playPauseTrackPendingIntent);
            expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_next,
                    nextTrackPendingIntent);

            notificationView.setViewVisibility(R.id.notification_base_previous, View.VISIBLE);
            notificationView.setViewVisibility(R.id.notification_base_next, View.INVISIBLE);
            notificationView.setOnClickPendingIntent(R.id.notification_base_play, playPauseTrackPendingIntent);
            notificationView.setOnClickPendingIntent(R.id.notification_base_next, nextTrackPendingIntent);

        } else {
            //We're smack dab in the middle of the queue, so keep the previous and next buttons enabled.
            expNotificationView.setViewVisibility(R.id.notification_expanded_base_previous, View.VISIBLE);
            expNotificationView.setViewVisibility(R.id.notification_expanded_base_next, View.VISIBLE);
            expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_play,
                    playPauseTrackPendingIntent);
            expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_next,
                    nextTrackPendingIntent);
            expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_previous,
                    previousTrackPendingIntent);

            notificationView.setViewVisibility(R.id.notification_base_previous, View.VISIBLE);
            notificationView.setViewVisibility(R.id.notification_base_next, View.VISIBLE);
            notificationView.setOnClickPendingIntent(R.id.notification_base_play, playPauseTrackPendingIntent);
            notificationView.setOnClickPendingIntent(R.id.notification_base_next, nextTrackPendingIntent);
            notificationView.setOnClickPendingIntent(R.id.notification_base_previous, previousTrackPendingIntent);

        }

        //Set the "Stop Service" pending intents.
        expNotificationView.setOnClickPendingIntent(R.id.notification_expanded_base_collapse,
                stopServicePendingIntent);
        notificationView.setOnClickPendingIntent(R.id.notification_base_collapse, stopServicePendingIntent);

        //Set the album art.
        expNotificationView.setImageViewBitmap(R.id.notification_expanded_base_image, songHelper.getAlbumArt());
        notificationView.setImageViewBitmap(R.id.notification_base_image, songHelper.getAlbumArt());

        //Attach the shrunken layout to the notification.
        mNotificationBuilder.setContent(notificationView);

        //Build the notification object.
        Notification notification = mNotificationBuilder.build();

        //Attach the expanded layout to the notification and set its flags.
        notification.bigContentView = expNotificationView;
        notification.flags = Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_NO_CLEAR
                | Notification.FLAG_ONGOING_EVENT;

        return notification;
    }

    /**
     * Builds and returns a fully constructed Notification for devices 
     * on Ice Cream Sandwich (APIs 14 & 15).
     */
    private Notification buildICSNotification(SongHelper songHelper) {
        mNotificationBuilder = new NotificationCompat.Builder(mContext);
        mNotificationBuilder.setOngoing(true);
        mNotificationBuilder.setAutoCancel(false);
        mNotificationBuilder.setSmallIcon(R.drawable.notif_icon);

        //Open up the player screen when the user taps on the notification.
        Intent launchNowPlayingIntent = new Intent();
        launchNowPlayingIntent.setAction(AudioPlaybackService.LAUNCH_NOW_PLAYING_ACTION);
        PendingIntent launchNowPlayingPendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(),
                0, launchNowPlayingIntent, 0);
        mNotificationBuilder.setContentIntent(launchNowPlayingPendingIntent);

        //Grab the notification layout.
        RemoteViews notificationView = new RemoteViews(mContext.getPackageName(),
                R.layout.notification_custom_layout);

        //Initialize the notification layout buttons.
        Intent previousTrackIntent = new Intent();
        previousTrackIntent.setAction(AudioPlaybackService.PREVIOUS_ACTION);
        PendingIntent previousTrackPendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(), 0,
                previousTrackIntent, 0);

        Intent playPauseTrackIntent = new Intent();
        playPauseTrackIntent.setAction(AudioPlaybackService.PLAY_PAUSE_ACTION);
        PendingIntent playPauseTrackPendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(), 0,
                playPauseTrackIntent, 0);

        Intent nextTrackIntent = new Intent();
        nextTrackIntent.setAction(AudioPlaybackService.NEXT_ACTION);
        PendingIntent nextTrackPendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(), 0,
                nextTrackIntent, 0);

        Intent stopServiceIntent = new Intent();
        stopServiceIntent.setAction(AudioPlaybackService.STOP_SERVICE);
        PendingIntent stopServicePendingIntent = PendingIntent.getBroadcast(mContext.getApplicationContext(), 0,
                stopServiceIntent, 0);

        //Check if audio is playing and set the appropriate play/pause button.
        if (mApp.getService().isPlayingMusic()) {
            notificationView.setImageViewResource(R.id.notification_base_play, R.drawable.btn_playback_pause_light);
        } else {
            notificationView.setImageViewResource(R.id.notification_base_play, R.drawable.btn_playback_play_light);
        }

        //Set the notification content.    
        notificationView.setTextViewText(R.id.notification_base_line_one, songHelper.getTitle());
        notificationView.setTextViewText(R.id.notification_base_line_two, songHelper.getArtist());

        //Set the states of the next/previous buttons and their pending intents.
        if (mApp.getService().isOnlySongInQueue()) {
            //This is the only song in the queue, so disable the previous/next buttons.
            notificationView.setViewVisibility(R.id.notification_base_next, View.INVISIBLE);
            notificationView.setViewVisibility(R.id.notification_base_previous, View.INVISIBLE);
            notificationView.setOnClickPendingIntent(R.id.notification_base_play, playPauseTrackPendingIntent);

        } else if (mApp.getService().isFirstSongInQueue()) {
            //This is the the first song in the queue, so disable the previous button. 
            notificationView.setViewVisibility(R.id.notification_base_previous, View.INVISIBLE);
            notificationView.setViewVisibility(R.id.notification_base_next, View.VISIBLE);
            notificationView.setOnClickPendingIntent(R.id.notification_base_play, playPauseTrackPendingIntent);
            notificationView.setOnClickPendingIntent(R.id.notification_base_next, nextTrackPendingIntent);

        } else if (mApp.getService().isLastSongInQueue()) {
            //This is the last song in the cursor, so disable the next button.    
            notificationView.setViewVisibility(R.id.notification_base_previous, View.VISIBLE);
            notificationView.setViewVisibility(R.id.notification_base_next, View.INVISIBLE);
            notificationView.setOnClickPendingIntent(R.id.notification_base_play, playPauseTrackPendingIntent);
            notificationView.setOnClickPendingIntent(R.id.notification_base_next, nextTrackPendingIntent);

        } else {
            //We're smack dab in the middle of the queue, so keep the previous and next buttons enabled.       
            notificationView.setViewVisibility(R.id.notification_base_previous, View.VISIBLE);
            notificationView.setViewVisibility(R.id.notification_base_next, View.VISIBLE);
            notificationView.setOnClickPendingIntent(R.id.notification_base_play, playPauseTrackPendingIntent);
            notificationView.setOnClickPendingIntent(R.id.notification_base_next, nextTrackPendingIntent);
            notificationView.setOnClickPendingIntent(R.id.notification_base_previous, previousTrackPendingIntent);

        }

        //Set the "Stop Service" pending intent.
        notificationView.setOnClickPendingIntent(R.id.notification_base_collapse, stopServicePendingIntent);

        //Set the album art.
        notificationView.setImageViewBitmap(R.id.notification_base_image, songHelper.getAlbumArt());

        //Attach the shrunken layout to the notification.
        mNotificationBuilder.setContent(notificationView);

        //Build the notification object and set its flags.
        Notification notification = mNotificationBuilder.build();
        notification.flags = Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_NO_CLEAR
                | Notification.FLAG_ONGOING_EVENT;

        return notification;
    }

    /**
     * Returns the appropriate notification based on the device's 
     * API level.
     */
    private Notification buildNotification(SongHelper songHelper) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
            return buildJBNotification(songHelper);
        else
            return buildICSNotification(songHelper);
    }

    /**
     * Updates the current notification with info from the specified 
     * SongHelper object.
     */
    public void updateNotification(SongHelper songHelper) {
        Notification notification = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
            notification = buildJBNotification(songHelper);
        else
            notification = buildICSNotification(songHelper);

        //Update the current notification.
        NotificationManager notifManager = (NotificationManager) mApp
                .getSystemService(Context.NOTIFICATION_SERVICE);
        notifManager.notify(mNotificationId, notification);

    }

    /**
     * Updates all remote control clients (including the lockscreen controls).
     */
    public void updateRemoteControlClients(SongHelper songHelper) {
        try {
            //Update the remote controls
            mRemoteControlClientCompat.editMetadata(true)
                    .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, getCurrentSong().getArtist())
                    .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, getCurrentSong().getTitle())
                    .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, getCurrentSong().getAlbum())
                    .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, getCurrentMediaPlayer().getDuration())
                    .putBitmap(RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK,
                            getCurrentSong().getAlbumArt())
                    .apply();

            if (mRemoteControlClientCompat != null) {

                if (getCurrentMediaPlayer().isPlaying())
                    mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
                else
                    mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);

            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * This method combines mCursor with the specified newCursor.
     * 
     * @param newCursor The new cursor to append to mCursor.
     * @param playNext Pass true if newCursor should be appeneded after the current song.
     */
    public void enqueueCursor(Cursor newCursor, boolean playNext) {

        Cursor[] cursorArray = { getCursor(), newCursor };
        mMergeCursor = new MergeCursor(cursorArray);
        setCursor(mMergeCursor);
        getCursor().moveToPosition(mPlaybackIndecesList.get(mCurrentSongIndex));
        mEnqueuePerformed = true;

        if (playNext) {
            //Check which mMediaPlayer is currently playing, and prepare the other mediaPlayer.
            prepareAlternateMediaPlayer();

        }

    }

    /**
     * This method combines the current cursor with the specified playlist cursor.
     * @param newCursor
     */
    public void enqueuePlaylistCursor(Cursor newCursor) {

        String[] matrixCursorColumns = { DBAccessHelper.SONG_ARTIST, DBAccessHelper.SONG_ALBUM,
                DBAccessHelper.SONG_TITLE, DBAccessHelper.SONG_FILE_PATH, DBAccessHelper.SONG_DURATION,
                DBAccessHelper.SONG_GENRE, DBAccessHelper.SONG_ID, DBAccessHelper.SONG_ALBUM_ART_PATH,
                DBAccessHelper.SONG_SOURCE };

        //Create an empty matrix getCursor() with the specified columns.
        MatrixCursor mMatrixCursor = new MatrixCursor(matrixCursorColumns);

        //Make a copy of the old getCursor() and copy it's contents over to the matrix getCursor().
        Cursor tempCursor = getCursor();

        tempCursor.moveToFirst();
        MediaMetadataRetriever mMMDR = new MediaMetadataRetriever();
        for (int i = 0; i < tempCursor.getCount(); i++) {
            tempCursor.moveToPosition(i);

            //Check which type of getCursor() the service currently has.
            if (getCursor().getColumnIndex(DBAccessHelper.SONG_FILE_PATH) == -1) {

                //We'll have to manually extract the info from the audio file.
                /*            String songFilePath = tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.PLAYLIST_SONG_FILE_PATH));
                                
                            try {
                               mMMDR.setDataSource(songFilePath);
                            } catch (Exception e) {
                               //Skip the song if there's a problem with reading it.
                               continue;
                            }*/

                String songArtist = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
                String songAlbum = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
                String songTitle = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
                String songDuration = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
                String songGenre = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE);

                mMatrixCursor
                        .addRow(new Object[] { songArtist, songAlbum, songTitle, "", songDuration, songGenre });

            } else {

                mMatrixCursor.addRow(
                        new Object[] { tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_ARTIST)),
                                tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_ALBUM)),
                                tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_TITLE)),
                                tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_FILE_PATH)),
                                tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_DURATION)),
                                tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_GENRE)),
                                tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_ID)),
                                tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_ALBUM_ART_PATH)),
                                tempCursor.getString(tempCursor.getColumnIndex(DBAccessHelper.SONG_SOURCE)) });

            }

        }

        tempCursor.close();

        //Copy the contents of the new getCursor() over to the MatrixCursor.
        if (newCursor.getCount() > 0) {

            String songArtist = "";
            String songAlbum = "";
            String songTitle = "";
            String filePath = "";
            String songDuration = "";
            for (int j = 0; j < newCursor.getCount(); j++) {
                /*            newCursor.moveToPosition(j);
                            filePath = newCursor.getString(newCursor.getColumnIndex(DBAccessHelper.PLAYLIST_SONG_FILE_PATH));
                                
                            try {
                               mMMDR.setDataSource(filePath);
                            } catch (Exception e) {
                               continue;
                            }*/

                //Get the metadata from the song file.
                songArtist = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
                songAlbum = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
                songTitle = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
                songDuration = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
                String songGenre = mMMDR.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE);

                mMatrixCursor.addRow(
                        new Object[] { songArtist, songAlbum, songTitle, filePath, songDuration, songGenre });

            }

        }

        mEnqueuePerformed = true;
        newCursor.close();
        mCursor = (Cursor) mMatrixCursor;
        mMatrixCursor.close();

    }

    /**
     * Listens for audio focus changes and reacts accordingly.
     */
    private OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() {

        @Override
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
                //We've temporarily lost focus, so pause the mMediaPlayer, wherever it's at.
                try {
                    getCurrentMediaPlayer().pause();
                    updateNotification(mApp.getService().getCurrentSong());
                    updateWidgets();
                    scrobbleTrack(SimpleLastFMHelper.PAUSE);
                    mAudioManagerHelper.setHasAudioFocus(false);
                } catch (Exception e) {
                    e.printStackTrace();
                }

            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
                //Lower the current mMediaPlayer volume.
                mAudioManagerHelper.setAudioDucked(true);
                mAudioManagerHelper.setTargetVolume(5);
                mAudioManagerHelper.setStepDownIncrement(1);
                mAudioManagerHelper.setCurrentVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
                mAudioManagerHelper.setOriginalVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
                mHandler.post(duckDownVolumeRunnable);

            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {

                if (mAudioManagerHelper.isAudioDucked()) {
                    //Crank the volume back up again.
                    mAudioManagerHelper.setTargetVolume(mAudioManagerHelper.getOriginalVolume());
                    mAudioManagerHelper.setStepUpIncrement(1);
                    mAudioManagerHelper.setCurrentVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));

                    mHandler.post(duckUpVolumeRunnable);
                    mAudioManagerHelper.setAudioDucked(false);
                } else {
                    //We've regained focus. Update the audioFocus tag, but don't start the mMediaPlayer.
                    mAudioManagerHelper.setHasAudioFocus(true);

                }

            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                //We've lost focus permanently so pause the service. We'll have to request focus again later.
                getCurrentMediaPlayer().pause();
                updateNotification(mApp.getService().getCurrentSong());
                updateWidgets();
                scrobbleTrack(SimpleLastFMHelper.PAUSE);
                mAudioManagerHelper.setHasAudioFocus(false);

            }

        }

    };

    /**
     * Fades out volume before a duck operation.
     */
    private Runnable duckDownVolumeRunnable = new Runnable() {

        @Override
        public void run() {
            if (mAudioManagerHelper.getCurrentVolume() > mAudioManagerHelper.getTargetVolume()) {
                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
                        (mAudioManagerHelper.getCurrentVolume() - mAudioManagerHelper.getStepDownIncrement()), 0);

                mAudioManagerHelper.setCurrentVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
                mHandler.postDelayed(this, 50);
            }

        }

    };

    /**
     * Fades in volume after a duck operation.
     */
    private Runnable duckUpVolumeRunnable = new Runnable() {

        @Override
        public void run() {
            if (mAudioManagerHelper.getCurrentVolume() < mAudioManagerHelper.getTargetVolume()) {
                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
                        (mAudioManagerHelper.getCurrentVolume() + mAudioManagerHelper.getStepUpIncrement()), 0);

                mAudioManagerHelper.setCurrentVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
                mHandler.postDelayed(this, 50);
            }

        }

    };

    /**
     * Called once mMediaPlayer is prepared.
     */
    public OnPreparedListener mediaPlayerPrepared = new OnPreparedListener() {

        @Override
        public void onPrepared(MediaPlayer mediaPlayer) {

            //Update the prepared flag.
            setIsMediaPlayerPrepared(true);

            //Set the completion listener for mMediaPlayer.
            getMediaPlayer().setOnCompletionListener(onMediaPlayerCompleted);

            //Check to make sure we have AudioFocus.
            if (checkAndRequestAudioFocus() == true) {

                //Check if the the user saved the track's last playback position.
                if (getMediaPlayerSongHelper().getSavedPosition() != -1) {
                    //Seek to the saved track position.
                    mMediaPlayer.seekTo((int) getMediaPlayerSongHelper().getSavedPosition());
                    mApp.broadcastUpdateUICommand(new String[] { Common.SHOW_AUDIOBOOK_TOAST },
                            new String[] { "" + getMediaPlayerSongHelper().getSavedPosition() });

                }

                //This is the first time mMediaPlayer has been prepared, so start it immediately.
                if (mFirstRun) {
                    startMediaPlayer();
                    mFirstRun = false;
                }

            } else {
                return;
            }

        }

    };

    /**
     * Called once mMediaPlayer2 is prepared.
     */
    public OnPreparedListener mediaPlayer2Prepared = new OnPreparedListener() {

        @Override
        public void onPrepared(MediaPlayer mediaPlayer) {

            //Update the prepared flag.
            setIsMediaPlayer2Prepared(true);

            //Set the completion listener for mMediaPlayer2.
            getMediaPlayer2().setOnCompletionListener(onMediaPlayer2Completed);

            //Check to make sure we have AudioFocus.
            if (checkAndRequestAudioFocus() == true) {

                //Check if the the user saved the track's last playback position.
                if (getMediaPlayer2SongHelper().getSavedPosition() != -1) {
                    //Seek to the saved track position.
                    mMediaPlayer2.seekTo((int) getMediaPlayer2SongHelper().getSavedPosition());
                    mApp.broadcastUpdateUICommand(new String[] { Common.SHOW_AUDIOBOOK_TOAST },
                            new String[] { "" + getMediaPlayer2SongHelper().getSavedPosition() });

                }

            } else {
                return;
            }

        }

    };

    /**
     * Completion listener for mMediaPlayer.
     */
    private OnCompletionListener onMediaPlayerCompleted = new OnCompletionListener() {

        @Override
        public void onCompletion(MediaPlayer mp) {

            //Remove the crossfade playback.
            mHandler.removeCallbacks(startCrossFadeRunnable);
            mHandler.removeCallbacks(crossFadeRunnable);

            //Set the track position handler (notifies the handler when the track should start being faded).
            if (mHandler != null && mApp.isCrossfadeEnabled()) {
                mHandler.post(startCrossFadeRunnable);
            }

            //Reset the fadeVolume variables.
            mFadeInVolume = 0.0f;
            mFadeOutVolume = 1.0f;

            //Reset the volumes for both mediaPlayers.
            getMediaPlayer().setVolume(1.0f, 1.0f);
            getMediaPlayer2().setVolume(1.0f, 1.0f);

            try {
                if (isAtEndOfQueue() && getRepeatMode() != Common.REPEAT_PLAYLIST) {
                    stopSelf();
                } else if (isMediaPlayer2Prepared()) {
                    startMediaPlayer2();
                } else {
                    //Check every 100ms if mMediaPlayer2 is prepared.
                    mHandler.post(startMediaPlayer2IfPrepared);
                }

            } catch (IllegalStateException e) {
                //mMediaPlayer2 isn't prepared yet.
                mHandler.post(startMediaPlayer2IfPrepared);
            }

        }

    };

    /**
     * Completion listener for mMediaPlayer2.
     */
    private OnCompletionListener onMediaPlayer2Completed = new OnCompletionListener() {

        @Override
        public void onCompletion(MediaPlayer mp) {

            //Remove the crossfade playback.
            mHandler.removeCallbacks(startCrossFadeRunnable);
            mHandler.removeCallbacks(crossFadeRunnable);

            //Set the track position handler (notifies the handler when the track should start being faded).
            if (mHandler != null && mApp.isCrossfadeEnabled()) {
                mHandler.post(startCrossFadeRunnable);
            }

            //Reset the fadeVolume variables.
            mFadeInVolume = 0.0f;
            mFadeOutVolume = 1.0f;

            //Reset the volumes for both mediaPlayers.
            getMediaPlayer().setVolume(1.0f, 1.0f);
            getMediaPlayer2().setVolume(1.0f, 1.0f);

            try {
                if (isAtEndOfQueue() && getRepeatMode() != Common.REPEAT_PLAYLIST) {
                    stopSelf();
                } else if (isMediaPlayerPrepared()) {
                    startMediaPlayer();
                } else {
                    //Check every 100ms if mMediaPlayer is prepared.
                    mHandler.post(startMediaPlayerIfPrepared);
                }

            } catch (IllegalStateException e) {
                //mMediaPlayer isn't prepared yet.
                mHandler.post(startMediaPlayerIfPrepared);
            }

        }

    };

    /**
     * Buffering listener.
     */
    public OnBufferingUpdateListener bufferingListener = new OnBufferingUpdateListener() {

        @Override
        public void onBufferingUpdate(MediaPlayer mp, int percent) {

            if (mApp.getSharedPreferences().getBoolean("NOW_PLAYING_ACTIVE", false) == true) {

                if (mp == getCurrentMediaPlayer()) {
                    float max = mp.getDuration() / 1000;
                    float maxDividedByHundred = max / 100;
                    mApp.broadcastUpdateUICommand(new String[] { Common.UPDATE_BUFFERING_PROGRESS },
                            new String[] { "" + (int) (percent * maxDividedByHundred) });
                }

            }

        }

    };

    /**
     * Error listener for mMediaPlayer.
     */
    public OnErrorListener onErrorListener = new OnErrorListener() {

        @Override
        public boolean onError(MediaPlayer mMediaPlayer, int what, int extra) {
            /* This error listener might seem like it's not doing anything. 
             * However, removing this will cause the mMediaPlayer object to go crazy 
             * and skip around. The key here is to make this method return true. This 
             * notifies the mMediaPlayer object that we've handled all errors and that 
             * it shouldn't do anything else to try and remedy the situation. 
             * 
             * TL;DR: Don't touch this interface. Ever.
             */
            return true;
        }

    };

    /**
     * Starts mMediaPlayer if it is prepared and ready for playback. 
     * Otherwise, continues checking every 100ms if mMediaPlayer is prepared.
     */
    private Runnable startMediaPlayerIfPrepared = new Runnable() {

        @Override
        public void run() {
            if (isMediaPlayerPrepared())
                startMediaPlayer();
            else
                mHandler.postDelayed(this, 100);

        }

    };

    /**
     * Starts mMediaPlayer if it is prepared and ready for playback.
     * Otherwise, continues checking every 100ms if mMediaPlayer2 is prepared.
     */
    private Runnable startMediaPlayer2IfPrepared = new Runnable() {

        @Override
        public void run() {
            if (isMediaPlayer2Prepared())
                startMediaPlayer2();
            else
                mHandler.postDelayed(this, 100);

        }

    };

    /**
     * First runnable that handles the cross fade operation between two tracks.
     */
    public Runnable startCrossFadeRunnable = new Runnable() {

        @Override
        public void run() {

            //Check if we're in the last part of the current song.
            try {
                if (getCurrentMediaPlayer().isPlaying()) {

                    int currentTrackDuration = getCurrentMediaPlayer().getDuration();
                    int currentTrackFadePosition = currentTrackDuration - (mCrossfadeDuration * 1000);
                    if (getCurrentMediaPlayer().getCurrentPosition() >= currentTrackFadePosition) {
                        //Launch the next runnable that will handle the cross fade effect.
                        mHandler.postDelayed(crossFadeRunnable, 100);

                    } else {
                        mHandler.postDelayed(startCrossFadeRunnable, 1000);
                    }

                } else {
                    mHandler.postDelayed(startCrossFadeRunnable, 1000);
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        }

    };

    /**
     * Crossfade runnable.
     */
    public Runnable crossFadeRunnable = new Runnable() {

        @Override
        public void run() {
            try {

                //Do not crossfade if the current song is set to repeat itself.
                if (getRepeatMode() != Common.REPEAT_SONG) {

                    //Do not crossfade if this is the last track in the queue.
                    if (getCursor().getCount() > (mCurrentSongIndex + 1)) {

                        //Set the next mMediaPlayer's volume and raise it incrementally.
                        if (getCurrentMediaPlayer() == getMediaPlayer()) {

                            getMediaPlayer2().setVolume(mFadeInVolume, mFadeInVolume);
                            getMediaPlayer().setVolume(mFadeOutVolume, mFadeOutVolume);

                            //If the mMediaPlayer is already playing or it hasn't been prepared yet, we can't use crossfade.
                            if (!getMediaPlayer2().isPlaying()) {

                                if (mMediaPlayer2Prepared == true) {

                                    if (checkAndRequestAudioFocus() == true) {

                                        //Check if the the user requested to save the track's last playback position.
                                        if (getMediaPlayer2SongHelper().getSavedPosition() != -1) {
                                            //Seek to the saved track position.
                                            getMediaPlayer2()
                                                    .seekTo((int) getMediaPlayer2SongHelper().getSavedPosition());
                                            mApp.broadcastUpdateUICommand(
                                                    new String[] { Common.SHOW_AUDIOBOOK_TOAST }, new String[] {
                                                            "" + getMediaPlayer2SongHelper().getSavedPosition() });

                                        }

                                        getMediaPlayer2().start();
                                    } else {
                                        return;
                                    }

                                }

                            }

                        } else {

                            getMediaPlayer().setVolume(mFadeInVolume, mFadeInVolume);
                            getMediaPlayer2().setVolume(mFadeOutVolume, mFadeOutVolume);

                            //If the mMediaPlayer is already playing or it hasn't been prepared yet, we can't use crossfade.
                            if (!getMediaPlayer().isPlaying()) {

                                if (mMediaPlayerPrepared == true) {

                                    if (checkAndRequestAudioFocus() == true) {

                                        //Check if the the user requested to save the track's last playback position.
                                        if (getMediaPlayerSongHelper().getSavedPosition() != -1) {
                                            //Seek to the saved track position.
                                            getMediaPlayer()
                                                    .seekTo((int) getMediaPlayerSongHelper().getSavedPosition());
                                            mApp.broadcastUpdateUICommand(
                                                    new String[] { Common.SHOW_AUDIOBOOK_TOAST }, new String[] {
                                                            "" + getMediaPlayerSongHelper().getSavedPosition() });

                                        }

                                        getMediaPlayer().start();
                                    } else {
                                        return;
                                    }

                                }

                            }

                        }

                        mFadeInVolume = mFadeInVolume + (float) (1.0f / (((float) mCrossfadeDuration) * 10.0f));
                        mFadeOutVolume = mFadeOutVolume - (float) (1.0f / (((float) mCrossfadeDuration) * 10.0f));

                        mHandler.postDelayed(crossFadeRunnable, 100);
                    }

                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        }

    };

    /**
     * Grabs the song parameters at the specified index, retrieves its 
     * data source, and beings to asynchronously prepare mMediaPlayer. 
     * Once mMediaPlayer is prepared, mediaPlayerPrepared is called.
     * 
     * @return True if the method completed with no exceptions. False, otherwise.
     */
    public boolean prepareMediaPlayer(int songIndex) {

        try {

            //Stop here if we're at the end of the queue.
            if (songIndex == -1)
                return true;

            //Reset mMediaPlayer to it's uninitialized state.
            getMediaPlayer().reset();

            //Loop the player if the repeat mode is set to repeat the current song.
            if (getRepeatMode() == Common.REPEAT_SONG) {
                getMediaPlayer().setLooping(true);
            }

            //Set mMediaPlayer's song data.
            SongHelper songHelper = new SongHelper();
            if (mFirstRun) {
                /*
                 * We're not preloading the next song (mMediaPlayer2 is not 
                 * playing right now). mMediaPlayer's song is pointed at 
                 * by mCurrentSongIndex.
                 */
                songHelper.populateSongData(mContext, songIndex);
                setMediaPlayerSongHelper(songHelper);

                //Set this service as a foreground service.
                startForeground(mNotificationId, buildNotification(songHelper));

            } else {
                songHelper.populateSongData(mContext, songIndex);
                setMediaPlayerSongHelper(songHelper);
            }

            /*
             * Set the data source for mMediaPlayer and start preparing it 
             * asynchronously.
             */
            getMediaPlayer().setDataSource(mContext, getSongDataSource(getMediaPlayerSongHelper()));
            getMediaPlayer().setOnPreparedListener(mediaPlayerPrepared);
            getMediaPlayer().setOnErrorListener(onErrorListener);
            getMediaPlayer().prepareAsync();

        } catch (Exception e) {
            Log.e("DEBUG", "MESSAGE", e);
            e.printStackTrace();

            //Display an error toast to the user.
            showErrorToast();

            //Add the current song index to the list of failed indeces.
            getFailedIndecesList().add(songIndex);

            //Start preparing the next song.
            if (!isAtEndOfQueue() || mFirstRun)
                prepareMediaPlayer(songIndex + 1);
            else
                return false;

            return false;
        }

        return true;
    }

    /**
     * Grabs the song parameters at the specified index, retrieves its 
     * data source, and beings to asynchronously prepare mMediaPlayer2. 
     * Once mMediaPlayer2 is prepared, mediaPlayer2Prepared is called.
     * 
     * @return True if the method completed with no exceptions. False, otherwise.
     */
    public boolean prepareMediaPlayer2(int songIndex) {

        try {

            //Stop here if we're at the end of the queue.
            if (songIndex == -1)
                return true;

            //Reset mMediaPlayer2 to its uninitialized state.
            getMediaPlayer2().reset();

            //Loop the player if the repeat mode is set to repeat the current song.
            if (getRepeatMode() == Common.REPEAT_SONG) {
                getMediaPlayer2().setLooping(true);
            }

            //Set mMediaPlayer2's song data.
            SongHelper songHelper = new SongHelper();
            songHelper.populateSongData(mContext, songIndex);
            setMediaPlayer2SongHelper(songHelper);

            /*
             * Set the data source for mMediaPlayer and start preparing it 
             * asynchronously.
             */
            getMediaPlayer2().setDataSource(mContext, getSongDataSource(getMediaPlayer2SongHelper()));
            getMediaPlayer2().setOnPreparedListener(mediaPlayer2Prepared);
            getMediaPlayer2().setOnErrorListener(onErrorListener);
            getMediaPlayer2().prepareAsync();

        } catch (Exception e) {
            e.printStackTrace();

            //Display an error toast to the user.
            showErrorToast();

            //Add the current song index to the list of failed indeces.
            getFailedIndecesList().add(songIndex);

            //Start preparing the next song.
            if (!isAtEndOfQueue())
                prepareMediaPlayer2(songIndex + 1);
            else
                return false;

            return false;
        }

        return true;
    }

    /**
     * Returns the Uri of a song's data source. 
     * If the song is a local file, its file path is 
     * returned. If the song is from GMusic, its local 
     * copy path is returned (if it exists). If no local 
     * copy exists, the song's remote URL is requested 
     * from Google's servers and a temporary placeholder 
     * (URI_BEING_LOADED) is returned.
     */
    private Uri getSongDataSource(SongHelper songHelper) {

        if (songHelper.getSource().equals(DBAccessHelper.GMUSIC)) {

            //Check if a local copy of the song exists.
            if (songHelper.getLocalCopyPath() != null && songHelper.getLocalCopyPath().length() > 2) {

                //Double check to make sure that the local copy file exists.
                if (new File(songHelper.getLocalCopyPath()).exists()) {
                    //The local copy exists. Return its path.
                    return Uri.parse(songHelper.getLocalCopyPath());
                } else {
                    //The local copy doesn't exist. Request the remote URL and return a placeholder Uri.
                    AsyncGetSongStreamURLTask task = new AsyncGetSongStreamURLTask(mContext, songHelper.getId());
                    task.execute();

                    return URI_BEING_LOADED;
                }

            } else {
                //Request the remote URL and return a placeholder Uri.
                AsyncGetSongStreamURLTask task = new AsyncGetSongStreamURLTask(mContext, songHelper.getId());
                task.execute();

                return URI_BEING_LOADED;
            }

        } else {
            //Return the song's file path.
            return Uri.parse(songHelper.getFilePath());
        }

    }

    /**
     * Updates all open homescreen/lockscreen widgets.
     */
    public void updateWidgets() {
        try {
            //Fire a broadcast message to the widget(s) to update them.
            Intent smallWidgetIntent = new Intent(mContext, SmallWidgetProvider.class);
            smallWidgetIntent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
            int smallWidgetIds[] = AppWidgetManager.getInstance(mContext)
                    .getAppWidgetIds(new ComponentName(mContext, SmallWidgetProvider.class));
            smallWidgetIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, smallWidgetIds);
            mContext.sendBroadcast(smallWidgetIntent);

            Intent largeWidgetIntent = new Intent(mContext, LargeWidgetProvider.class);
            largeWidgetIntent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
            int largeWidgetIds[] = AppWidgetManager.getInstance(mContext)
                    .getAppWidgetIds(new ComponentName(mContext, LargeWidgetProvider.class));
            largeWidgetIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, largeWidgetIds);
            mContext.sendBroadcast(largeWidgetIntent);

            Intent blurredWidgetIntent = new Intent(mContext, BlurredWidgetProvider.class);
            blurredWidgetIntent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
            int blurredWidgetIds[] = AppWidgetManager.getInstance(mContext)
                    .getAppWidgetIds(new ComponentName(mContext, BlurredWidgetProvider.class));
            blurredWidgetIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, blurredWidgetIds);
            mContext.sendBroadcast(blurredWidgetIntent);

            Intent albumArtWidgetIntent = new Intent(mContext, AlbumArtWidgetProvider.class);
            albumArtWidgetIntent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
            int albumArtWidgetIds[] = AppWidgetManager.getInstance(mContext)
                    .getAppWidgetIds(new ComponentName(mContext, AlbumArtWidgetProvider.class));
            albumArtWidgetIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, albumArtWidgetIds);
            mContext.sendBroadcast(albumArtWidgetIntent);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * Sets the A-B Repeat song markers.
     * 
     * @param pointA The duration to repeat from (in millis).
     * @param pointB The duration to repeat until (in millis).
     */
    public void setRepeatSongRange(int pointA, int pointB) {
        mRepeatSongRangePointA = pointA;
        mRepeatSongRangePointB = pointB;
        getCurrentMediaPlayer().seekTo(pointA);
        mHandler.postDelayed(checkABRepeatRange, 100);
    }

    /**
     * Clears the A-B Repeat song markers.
     */
    public void clearABRepeatRange() {
        mHandler.removeCallbacks(checkABRepeatRange);
        mRepeatSongRangePointA = 0;
        mRepeatSongRangePointB = 0;
        mApp.getSharedPreferences().edit().putInt(Common.REPEAT_MODE, Common.REPEAT_OFF);
    }

    /**
     * Called repetitively to check for A-B repeat markers.
     */
    private Runnable checkABRepeatRange = new Runnable() {

        @Override
        public void run() {
            try {
                if (getCurrentMediaPlayer().isPlaying()) {

                    if (getCurrentMediaPlayer().getCurrentPosition() >= (mRepeatSongRangePointB)) {
                        getCurrentMediaPlayer().seekTo(mRepeatSongRangePointA);
                    }

                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            if (mApp.getSharedPreferences().getInt(Common.REPEAT_MODE, Common.REPEAT_OFF) == Common.A_B_REPEAT) {
                mHandler.postDelayed(checkABRepeatRange, 100);
            }

        }

    };

    /**
     * Fix for KitKat error where the service is killed as soon 
     * as the app is swiped away from the Recents menu.
     */
    @Override
    public void onTaskRemoved(Intent rootIntent) {
        Intent intent = new Intent(this, KitKatFixActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(intent);

    }

    /**
     * Displays an error toast.
     */
    private void showErrorToast() {
        Toast.makeText(mContext, R.string.song_failed_to_load, Toast.LENGTH_SHORT).show();
    }

    /**
     * Deploys the current track's data to the specified 
     * scrobbler.
     * 
     * @param state The scrobble state.
     */
    public void scrobbleTrack(int state) {

        //If scrobbling is enabled, send out the appropriate action events.
        if (mApp.getSharedPreferences().getInt("SCROBBLING", 0) == 0) {
            //Scrobbling is disabled.
            return;
        }

        //Get the metadata of the track.
        getCursor().moveToPosition(mPlaybackIndecesList.get(mCurrentSongIndex));
        String songTitle = getCursor().getString(getCursor().getColumnIndex(DBAccessHelper.SONG_TITLE));
        String songArtist = getCursor().getString(getCursor().getColumnIndex(DBAccessHelper.SONG_ARTIST));
        String songAlbum = getCursor().getString(getCursor().getColumnIndex(DBAccessHelper.SONG_ALBUM));

        int songDurationInSecs;
        try {
            songDurationInSecs = getCursor().getInt(getCursor().getColumnIndex(DBAccessHelper.SONG_DURATION))
                    / 1000;
        } catch (Exception e) {
            songDurationInSecs = 0;
        }

        if (mApp.getSharedPreferences().getInt("SCROBBLING", 0) == 1) {
            //Simple LastFM Helper.
            SimpleLastFMHelper.initializeActionIntent();
            SimpleLastFMHelper.attachMetadata(state, songArtist, songAlbum, songTitle, songDurationInSecs);
            SimpleLastFMHelper.sendBroadcast(mContext);

        } else if (mApp.getSharedPreferences().getInt("SCROBBLING", 0) == 2) {
            //Scrobble Droid.
            ScrobbleDroidHelper.initializeActionIntent();
            if (state == SimpleLastFMHelper.START || state == SimpleLastFMHelper.RESUME) {
                ScrobbleDroidHelper.attachMetadata(true, songArtist, songAlbum, songTitle, songDurationInSecs);
            } else if (state == SimpleLastFMHelper.PAUSE) {
                ScrobbleDroidHelper.attachMetadata(false, songArtist, songAlbum, songTitle, songDurationInSecs);
            }

            ScrobbleDroidHelper.sendBroadcast(mContext);
        }

    }

    /**
     * Checks if we have AudioFocus. If not, it explicitly requests it.
     * 
     * @return True if we have AudioFocus. False, otherwise.
     */
    private boolean checkAndRequestAudioFocus() {
        if (mAudioManagerHelper.hasAudioFocus() == false) {
            if (requestAudioFocus() == true) {
                return true;
            } else {
                //Unable to get focus. Notify the user.
                Toast.makeText(mContext, R.string.unable_to_get_audio_focus, Toast.LENGTH_LONG).show();
                return false;
            }

        } else {
            return true;
        }

    }

    /**
     * Registers the headset plug receiver.
     */
    public void registerHeadsetPlugReceiver() {
        //Register the headset plug receiver.
        if (mApp.getSharedPreferences().getString("UNPLUG_ACTION", "DO_NOTHING").equals("PAUSE_MUSIC_PLAYBACK")) {
            IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
            mHeadsetPlugReceiver = new HeadsetPlugBroadcastReceiver();
            mService.registerReceiver(mHeadsetPlugReceiver, filter);
        }

    }

    /**
     * Increments mCurrentSongIndex based on mErrorCount. 
     * Returns the new value of mCurrentSongIndex.
     */
    public int incrementCurrentSongIndex() {
        if ((getCurrentSongIndex() + 1) < getCursor().getCount())
            mCurrentSongIndex++;

        return mCurrentSongIndex;
    }

    /**
     * Decrements mCurrentSongIndex by one. Returns the new value 
     * of mCurrentSongIndex.
     */
    public int decrementCurrentSongIndex() {
        if ((getCurrentSongIndex() - 1) > -1)
            mCurrentSongIndex--;

        return mCurrentSongIndex;
    }

    /**
     * Increments mEnqueueReorderScalar. Returns the new value 
     * of mEnqueueReorderScalar.
     */
    public int incrementEnqueueReorderScalar() {
        mEnqueueReorderScalar++;
        return mCurrentSongIndex;
    }

    /**
     * Decrements mEnqueueReorderScalar. Returns the new value 
     * of mEnqueueReorderScalar.
     */
    public int decrementEnqueueReorderScalar() {
        mEnqueueReorderScalar--;
        return mCurrentSongIndex;
    }

    /**
     * Starts playing mMediaPlayer and sends out the update UI broadcast, 
     * and updates the notification and any open widgets.
     *  
     * Do NOT call this method before mMediaPlayer has been prepared.
     */
    private void startMediaPlayer() throws IllegalStateException {

        //Set the media player's equalizer/audio fx.
        applyMediaPlayerEQ(getMediaPlayerSongHelper().getId());

        //Aaaaand let the show begin!
        setCurrentMediaPlayer(1);
        getMediaPlayer().start();

        //Set the new value for mCurrentSongIndex.
        if (mFirstRun == false) {
            do {
                setCurrentSongIndex(determineNextSongIndex());
            } while (getFailedIndecesList().contains(getCurrentSongIndex()));

            getFailedIndecesList().clear();

        } else {
            while (getFailedIndecesList().contains(getCurrentSongIndex())) {
                setCurrentSongIndex(determineNextSongIndex());
            }

            //Initialize the crossfade runnable.
            if (mHandler != null && mApp.isCrossfadeEnabled()) {
                mHandler.post(startCrossFadeRunnable);
            }

        }

        //Update the UI.
        String[] updateFlags = new String[] { Common.UPDATE_PAGER_POSTIION, Common.UPDATE_PLAYBACK_CONTROLS,
                Common.HIDE_STREAMING_BAR, Common.UPDATE_SEEKBAR_DURATION, Common.UPDATE_EQ_FRAGMENT };

        String[] flagValues = new String[] { getCurrentSongIndex() + "", "", "",
                getMediaPlayer().getDuration() + "", "" };

        mApp.broadcastUpdateUICommand(updateFlags, flagValues);
        setCurrentSong(getCurrentSong());

        //Start preparing the next song.
        prepareMediaPlayer2(determineNextSongIndex());
    }

    /**
     * Starts playing mMediaPlayer2, sends out the update UI broadcast,
     * and updates the notification and any open widgets.
     *
     * Do NOT call this method before mMediaPlayer2 has been prepared.
     */
    private void startMediaPlayer2() throws IllegalStateException {

        //Set the media player's equalizer/audio fx.
        applyMediaPlayer2EQ(getMediaPlayer2SongHelper().getId());

        //Aaaaaand let the show begin!
        setCurrentMediaPlayer(2);
        getMediaPlayer2().start();

        //Set the new value for mCurrentSongIndex.
        do {
            setCurrentSongIndex(determineNextSongIndex());
        } while (getFailedIndecesList().contains(getCurrentSongIndex()));

        getFailedIndecesList().clear();

        //Update the UI.
        String[] updateFlags = new String[] { Common.UPDATE_PAGER_POSTIION, Common.UPDATE_PLAYBACK_CONTROLS,
                Common.HIDE_STREAMING_BAR, Common.UPDATE_SEEKBAR_DURATION, Common.UPDATE_EQ_FRAGMENT };

        String[] flagValues = new String[] { getCurrentSongIndex() + "", "", "",
                getMediaPlayer2().getDuration() + "", "" };

        mApp.broadcastUpdateUICommand(updateFlags, flagValues);
        setCurrentSong(getCurrentSong());

        //Start preparing the next song.
        prepareMediaPlayer(determineNextSongIndex());
    }

    /**
     * Starts/resumes the current media player. Returns true if 
     * the operation succeeded. False, otherwise.
     */
    public boolean startPlayback() {

        try {
            //Check to make sure we have audio focus.
            if (checkAndRequestAudioFocus()) {
                getCurrentMediaPlayer().start();

                //Update the UI and scrobbler.
                String[] updateFlags = new String[] { Common.UPDATE_PLAYBACK_CONTROLS };
                String[] flagValues = new String[] { "" };

                mApp.broadcastUpdateUICommand(updateFlags, flagValues);
                updateNotification(mApp.getService().getCurrentSong());
                updateWidgets();
                scrobbleTrack(SimpleLastFMHelper.START);

            } else {
                return false;
            }

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    /**
     * Pauses the current media player. Returns true if 
     * the operation succeeded. False, otherwise.
     */
    public boolean pausePlayback() {

        try {
            getCurrentMediaPlayer().pause();

            //Update the UI and scrobbler.
            String[] updateFlags = new String[] { Common.UPDATE_PLAYBACK_CONTROLS };
            String[] flagValues = new String[] { "" };

            mApp.broadcastUpdateUICommand(updateFlags, flagValues);
            updateNotification(mApp.getService().getCurrentSong());
            updateWidgets();
            scrobbleTrack(SimpleLastFMHelper.PAUSE);

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    /**
     * Stops the current media player. Returns true if 
     * the operation succeeded. False, otherwise.
     */
    public boolean stopPlayback() {

        try {
            getCurrentMediaPlayer().stop();

            //Update the UI and scrobbler.
            String[] updateFlags = new String[] { Common.UPDATE_PLAYBACK_CONTROLS };
            String[] flagValues = new String[] { "" };

            mApp.broadcastUpdateUICommand(updateFlags, flagValues);
            updateNotification(mApp.getService().getCurrentSong());
            updateWidgets();
            scrobbleTrack(SimpleLastFMHelper.PAUSE);

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    /**
     * Skips to the next track (if there is one) and starts 
     * playing it. Returns true if the operation succeeded. 
     * False, otherwise.
     */
    public boolean skipToNextTrack() {
        try {
            //Reset both MediaPlayer objects.
            getMediaPlayer().reset();
            getMediaPlayer2().reset();
            clearCrossfadeCallbacks();

            //Loop the players if the repeat mode is set to repeat the current song.
            if (getRepeatMode() == Common.REPEAT_SONG) {
                getMediaPlayer().setLooping(true);
                getMediaPlayer2().setLooping(true);
            }

            //Remove crossfade runnables and reset all volume levels.
            getHandler().removeCallbacks(crossFadeRunnable);
            getMediaPlayer().setVolume(1.0f, 1.0f);
            getMediaPlayer2().setVolume(1.0f, 1.0f);

            //Increment the song index.
            incrementCurrentSongIndex();

            //Update the UI.
            String[] updateFlags = new String[] { Common.UPDATE_PAGER_POSTIION };
            String[] flagValues = new String[] { getCurrentSongIndex() + "" };
            mApp.broadcastUpdateUICommand(updateFlags, flagValues);

            //Start the playback process.
            mFirstRun = true;
            prepareMediaPlayer(getCurrentSongIndex());

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    /**
     * Skips to the previous track (if there is one) and starts 
     * playing it. Returns true if the operation succeeded. 
     * False, otherwise.
     */
    public boolean skipToPreviousTrack() {

        /*
         * If the current track is not within the first three seconds,
         * reset it. If it IS within the first three seconds, skip to the
         * previous track.
         */
        try {
            if (getCurrentMediaPlayer().getCurrentPosition() > 3000) {
                getCurrentMediaPlayer().seekTo(0);
                return true;
            }

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        try {
            //Reset both MediaPlayer objects.
            getMediaPlayer().reset();
            getMediaPlayer2().reset();
            clearCrossfadeCallbacks();

            //Loop the players if the repeat mode is set to repeat the current song.
            if (getRepeatMode() == Common.REPEAT_SONG) {
                getMediaPlayer().setLooping(true);
                getMediaPlayer2().setLooping(true);
            }

            //Remove crossfade runnables and reset all volume levels.
            getHandler().removeCallbacks(crossFadeRunnable);
            getMediaPlayer().setVolume(1.0f, 1.0f);
            getMediaPlayer2().setVolume(1.0f, 1.0f);

            //Decrement the song index.
            decrementCurrentSongIndex();

            //Update the UI.
            String[] updateFlags = new String[] { Common.UPDATE_PAGER_POSTIION };
            String[] flagValues = new String[] { getCurrentSongIndex() + "" };
            mApp.broadcastUpdateUICommand(updateFlags, flagValues);

            //Start the playback process.
            mFirstRun = true;
            prepareMediaPlayer(getCurrentSongIndex());

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    /**
     * Skips to the specified track index (if there is one) and starts 
     * playing it. Returns true if the operation succeeded. 
     * False, otherwise.
     */
    public boolean skipToTrack(int trackIndex) {
        try {
            //Reset both MediaPlayer objects.
            getMediaPlayer().reset();
            getMediaPlayer2().reset();
            clearCrossfadeCallbacks();

            //Loop the players if the repeat mode is set to repeat the current song.
            if (getRepeatMode() == Common.REPEAT_SONG) {
                getMediaPlayer().setLooping(true);
                getMediaPlayer2().setLooping(true);
            }

            //Remove crossfade runnables and reset all volume levels.
            getHandler().removeCallbacks(crossFadeRunnable);
            getMediaPlayer().setVolume(1.0f, 1.0f);
            getMediaPlayer2().setVolume(1.0f, 1.0f);

            //Update the song index.
            setCurrentSongIndex(trackIndex);

            //Update the UI.
            String[] updateFlags = new String[] { Common.UPDATE_PAGER_POSTIION };
            String[] flagValues = new String[] { getCurrentSongIndex() + "" };
            mApp.broadcastUpdateUICommand(updateFlags, flagValues);

            //Start the playback process.
            mFirstRun = true;
            prepareMediaPlayer(trackIndex);

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    /**
     * Toggles the playback state between playing and paused and 
     * returns whether the current media player is now playing 
     * music or not.
     */
    public boolean togglePlaybackState() {
        if (isPlayingMusic())
            pausePlayback();
        else
            startPlayback();

        return isPlayingMusic();
    }

    /**
     * Determines the next song's index based on the repeat 
     * mode and current song index. Returns -1 if we're at 
     * the end of the queue.
     */
    private int determineNextSongIndex() {
        if (isAtEndOfQueue() && getRepeatMode() == Common.REPEAT_PLAYLIST)
            return 0;
        else if (!isAtEndOfQueue() && getRepeatMode() == Common.REPEAT_SONG)
            return getCurrentSongIndex();
        else if (isAtEndOfQueue())
            return -1;
        else
            return (getCurrentSongIndex() + 1);

    }

    /**
     * Checks which MediaPlayer object is currently in use, and 
     * starts preparing the other one.
     */
    public void prepareAlternateMediaPlayer() {
        if (mCurrentMediaPlayer == 1)
            prepareMediaPlayer2(determineNextSongIndex());
        else
            prepareMediaPlayer(determineNextSongIndex());

    }

    /**
     * Toggles shuffle mode and returns whether shuffle is now on or off.
    */
    public boolean toggleShuffleMode() {
        if (isShuffleOn()) {
            //Set shuffle off.
            mApp.getSharedPreferences().edit().putBoolean(Common.SHUFFLE_ON, false).commit();

            //Save the element at the current index.
            int currentElement = getPlaybackIndecesList().get(getCurrentSongIndex());

            //Reset the cursor pointers list.
            Collections.sort(getPlaybackIndecesList());

            //Reset the current index to the index of the old element.
            setCurrentSongIndex(getPlaybackIndecesList().indexOf(currentElement));

        } else {
            //Set shuffle on.
            mApp.getSharedPreferences().edit().putBoolean(Common.SHUFFLE_ON, true).commit();

            //Build a new list that doesn't include the current song index.
            ArrayList<Integer> newList = new ArrayList<Integer>(getPlaybackIndecesList());
            newList.remove(getCurrentSongIndex());

            //Shuffle the new list.
            Collections.shuffle(newList, new Random(System.nanoTime()));

            //Plug in the current song index back into the new list.
            newList.add(getCurrentSongIndex(), getCurrentSongIndex());
            mPlaybackIndecesList = newList;

            //Collections.shuffle(getPlaybackIndecesList().subList(0, getCurrentSongIndex()));
            //Collections.shuffle(getPlaybackIndecesList().subList(getCurrentSongIndex()+1, getPlaybackIndecesList().size()));

        }

        /* Since the queue changed, we're gonna have to update the 
         * next MediaPlayer object with the new song info.
         */
        prepareAlternateMediaPlayer();

        //Update all UI elements with the new queue order.
        mApp.broadcastUpdateUICommand(new String[] { Common.NEW_QUEUE_ORDER }, new String[] { "" });
        return isShuffleOn();
    }

    /**
     * Applies the specified repeat mode.
     */
    public void setRepeatMode(int repeatMode) {
        if (repeatMode == Common.REPEAT_OFF || repeatMode == Common.REPEAT_PLAYLIST
                || repeatMode == Common.REPEAT_SONG || repeatMode == Common.A_B_REPEAT) {
            //Save the repeat mode.
            mApp.getSharedPreferences().edit().putInt(Common.REPEAT_MODE, repeatMode).commit();
        } else {
            //Just in case a bogus value is passed in.
            mApp.getSharedPreferences().edit().putInt(Common.REPEAT_MODE, Common.REPEAT_OFF).commit();
        }

        /* 
         * Set the both MediaPlayer objects to loop if the repeat mode 
         * is Common.REPEAT_SONG.
         */
        try {
            if (repeatMode == Common.REPEAT_SONG) {
                getMediaPlayer().setLooping(true);
                getMediaPlayer2().setLooping(true);
            } else {
                getMediaPlayer().setLooping(false);
                getMediaPlayer2().setLooping(false);
            }

            //Prepare the appropriate next song.
            prepareAlternateMediaPlayer();

        } catch (Exception e) {
            e.printStackTrace();
        }

        /*
         * Remove the crossfade callbacks and reinitalize them
         * only if the user didn't select A-B repeat.
         */
        clearCrossfadeCallbacks();

        if (repeatMode != Common.A_B_REPEAT)
            if (mHandler != null && mApp.isCrossfadeEnabled())
                mHandler.post(startCrossFadeRunnable);

    }

    /**
     * Returns the current active MediaPlayer object.
     */
    public MediaPlayer getCurrentMediaPlayer() {
        if (mCurrentMediaPlayer == 1)
            return mMediaPlayer;
        else
            return mMediaPlayer2;
    }

    /**
     * Returns the primary MediaPlayer object. Don't 
     * use this method directly unless you have a good 
     * reason to explicitly call mMediaPlayer. Use 
     * getCurrentMediaPlayer() whenever possible.
     */
    public MediaPlayer getMediaPlayer() {
        return mMediaPlayer;
    }

    /**
     * Returns the secondary MediaPlayer object. Don't 
     * use this method directly unless you have a good 
     * reason to explicitly call mMediaPlayer2. Use 
     * getCurrentMediaPlayer() whenever possible.
     */
    public MediaPlayer getMediaPlayer2() {
        return mMediaPlayer2;
    }

    /**
     * Indicates if mMediaPlayer is prepared and 
     * ready for playback.
     */
    public boolean isMediaPlayerPrepared() {
        return mMediaPlayerPrepared;
    }

    /**
     * Indicates if mMediaPlayer2 is prepared and 
     * ready for playback.
     */
    public boolean isMediaPlayer2Prepared() {
        return mMediaPlayer2Prepared;
    }

    /**
     * Indicates if music is currently playing.
     */
    public boolean isPlayingMusic() {
        try {
            if (getCurrentMediaPlayer().isPlaying())
                return true;
            else
                return false;

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * Returns an instance of SongHelper. This 
     * object can be used to pull details about 
     * the current song.
     */
    public SongHelper getCurrentSong() {
        if (getCurrentMediaPlayer() == mMediaPlayer) {
            return mMediaPlayerSongHelper;
        } else {
            return mMediaPlayer2SongHelper;
        }

    }

    /**
     * Removes all crossfade callbacks on the current
     * Handler object. Also resets the volumes of the
     * MediaPlayer objects to 1.0f.
     */
    private void clearCrossfadeCallbacks() {
        if (mHandler == null)
            return;

        mHandler.removeCallbacks(startCrossFadeRunnable);
        mHandler.removeCallbacks(crossFadeRunnable);

        try {
            getMediaPlayer().setVolume(1.0f, 1.0f);
            getMediaPlayer2().setVolume(1.0f, 1.0f);
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }

    }

    /**
     * Returns mMediaPlayer's SongHelper instance.
     */
    public SongHelper getMediaPlayerSongHelper() {
        return mMediaPlayerSongHelper;
    }

    /**
     * Returns mMediaPlayer2's SongHelper instance.
     */
    public SongHelper getMediaPlayer2SongHelper() {
        return mMediaPlayer2SongHelper;
    }

    /**
     * Returns the service's cursor object.
     */
    public Cursor getCursor() {
        return mCursor;
    }

    /**
     * Returns the list of playback indeces that are used 
     * to traverse the cursor object.
     */
    public ArrayList<Integer> getPlaybackIndecesList() {
        return mPlaybackIndecesList;
    }

    /**
     * Returns the list of playback indeces that could 
     * not be played.
     */
    public ArrayList<Integer> getFailedIndecesList() {
        return mFailedIndecesList;
    }

    /**
     * Returns the current value of mCurrentSongIndex.
     */
    public int getCurrentSongIndex() {
        return mCurrentSongIndex;
    }

    /**
     * Indicates if the track was changed by the user.
     */
    public boolean getTrackChangedByUser() {
        return mTrackChangedByUser;
    }

    /**
     * Indicates if an enqueue operation was performed.
     */
    public boolean getEnqueuePerformed() {
        return mEnqueuePerformed;
    }

    /**
     * Returns the EqualizerHelper instance. This 
     * can be used to modify equalizer settings and 
     * toggle them on/off.
     */
    public EqualizerHelper getEqualizerHelper() {
        return mEqualizerHelper;
    }

    /**
     * Returns the mAudioManagerHelper instance. This 
     * can be used to modify AudioFocus states.
     */
    public AudioManagerHelper getAudioManagerHelper() {
        return mAudioManagerHelper;
    }

    /**
     * Returns the mHandler object.
     */
    public Handler getHandler() {
        return mHandler;
    }

    /**
     * Returns the headset plug receiver object.
     */
    public HeadsetPlugBroadcastReceiver getHeadsetPlugReceiver() {
        return mHeadsetPlugReceiver;
    }

    /**
     * Returns the current enqueue reorder scalar.
     */
    public int getEnqueueReorderScalar() {
        return mEnqueueReorderScalar;
    }

    /**
     * Returns point A in milliseconds for A-B repeat.
     */
    public int getRepeatSongRangePointA() {
        return mRepeatSongRangePointA;
    }

    /**
     * Returns point B in milliseconds for A-B repeat.
     */
    public int getRepeatSongRangePointB() {
        return mRepeatSongRangePointB;
    }

    /**
     * Returns the current repeat mode. The repeat mode 
     * is determined based on the value that is saved in 
     * SharedPreferences.
     */
    public int getRepeatMode() {
        return mApp.getSharedPreferences().getInt(Common.REPEAT_MODE, Common.REPEAT_OFF);
    }

    /**
     * Indicates if shuffle mode is turned on or off.
     */
    public boolean isShuffleOn() {
        return mApp.getSharedPreferences().getBoolean(Common.SHUFFLE_ON, false);
    }

    /**
     * Indicates if mCurrentSongIndex points to the last 
     * song in the current queue.
     */
    public boolean isAtEndOfQueue() {
        return (getCurrentSongIndex() == (getPlaybackIndecesList().size() - 1));
    }

    /**
     * Indicates if mCurrentSongIndex points to the first 
     * song in the current queue.
     */
    public boolean isAtStartOfQueue() {
        return getCurrentSongIndex() == 0;
    }

    /**
     * Sets the current active media player. Note that this 
     * method does not modify the MediaPlayer objects in any 
     * way. It simply changes the int variable that points to 
     * the new current MediaPlayer object.
     */
    public void setCurrentMediaPlayer(int currentMediaPlayer) {
        mCurrentMediaPlayer = currentMediaPlayer;
    }

    /**
     * Sets the prepared flag for mMediaPlayer.
     */
    public void setIsMediaPlayerPrepared(boolean prepared) {
        mMediaPlayerPrepared = prepared;
    }

    /**
     * Sets the prepared flag for mMediaPlayer2.
     */
    public void setIsMediaPlayer2Prepared(boolean prepared) {
        mMediaPlayer2Prepared = prepared;
    }

    /**
     * Changes the value of mCurrentSongIndex.
     */
    public void setCurrentSongIndex(int currentSongIndex) {
        mCurrentSongIndex = currentSongIndex;
    }

    /**
     * Sets whether the track was changed by the user or not.
     */
    public void setTrackChangedByUser(boolean trackChangedByUser) {
        mTrackChangedByUser = trackChangedByUser;
    }

    /**
     * Sets whether an enqueue operation was performed or not.
     */
    public void setEnqueuePerformed(boolean enqueuePerformed) {
        mEnqueuePerformed = enqueuePerformed;
    }

    /**
     * Sets the new enqueue reorder scalar value.
     */
    public void setEnqueueReorderScalar(int scalar) {
        mEnqueueReorderScalar = scalar;
    }

    /**
     * Sets point A in milliseconds for A-B repeat.
     */
    public void setRepeatSongRangePointA(int value) {
        mRepeatSongRangePointA = value;
    }

    /**
     * Returns point B in milliseconds for A-B repeat.
     */
    public void getRepeatSongRangePointB(int value) {
        mRepeatSongRangePointB = value;
    }

    /**
     * Replaces the current cursor object with the new one.
     */
    public void setCursor(Cursor cursor) {
        mCursor = cursor;
    }

    /**
     * Moves the cursor back to the first song in the queue. 
     * This does not necessarily move the cursor to index 0. 
     * It moves it to the element at index 0 in 
     * mPlaybackIndecesList.
     */
    public void moveCursorToQueueStart() {
        getCursor().moveToPosition(getPlaybackIndecesList().get(0));
    }

    /**
     * Moves the cursor forward to the last song in the queue. 
     * This does not necessarily move the cursor to index 
     * {cursorSize-1}. It moves it to the element at index 
     * {cursorSize-1} in mPlaybackIndecesList.
     */
    public void moveCursorToQueueEnd() {
        getCursor().moveToPosition(getPlaybackIndecesList().get(getPlaybackIndecesList().size() - 1));
    }

    /**
     * Moves the cursor to the specified index in the queue.
     * Returns true if the index was valid and the cursor 
     * position was moved successfully. False, otherwise.
     */
    public boolean moveCursorToIndex(int index) {
        if (index < getCursor().getCount() && index > -1) {
            getCursor().moveToPosition(getPlaybackIndecesList().get(index));
            return true;
        } else {
            return false;
        }

    }

    /**
     * Returns true if there's only one song in the current queue.
     * False, otherwise.
     */
    public boolean isOnlySongInQueue() {
        if (getCurrentSongIndex() == 0 && getCursor().getCount() == 1)
            return true;
        else
            return false;

    }

    /**
     * Returns true if mCurrentSongIndex is pointing at the first 
     * song in the queue and there is more than one song in the 
     * queue. False, otherwise.
     */
    public boolean isFirstSongInQueue() {
        if (getCurrentSongIndex() == 0 && getCursor().getCount() > 1)
            return true;
        else
            return false;

    }

    /**
     * Returns true if mCurrentSongIndex is pointing at the last 
     * song in the queue. False, otherwise.
     */
    public boolean isLastSongInQueue() {
        if (getCurrentSongIndex() == (getCursor().getCount() - 1))
            return true;
        else
            return false;

    }

    /**
     * Returns an instance of the PrepareServiceListener.
     */
    public PrepareServiceListener getPrepareServiceListener() {
        return mPrepareServiceListener;
    }

    /**
     * Sets the mPrepareServiceListener object.
     */
    public void setPrepareServiceListener(PrepareServiceListener listener) {
        mPrepareServiceListener = listener;
    }

    /**
     * Sets mMediaPlayerSongHelper.
     */
    public void setMediaPlayerSongHelper(SongHelper songHelper) {
        mMediaPlayerSongHelper = songHelper;
    }

    /**
     * Sets mMediaPlayer2SongHelper.
     */
    public void setMediaPlayer2SongHelper(SongHelper songHelper) {
        mMediaPlayer2SongHelper = songHelper;
    }

    /**
     * Sets the current MediaPlayer's SongHelper object. Also 
     * indirectly calls the updateNotification() and updateWidgets() 
     * methods via the [CURRENT SONG HELPER].setIsCurrentSong() method.
     */
    private void setCurrentSong(SongHelper songHelper) {
        if (getCurrentMediaPlayer() == mMediaPlayer) {
            mMediaPlayerSongHelper = songHelper;
            mMediaPlayerSongHelper.setIsCurrentSong();
        } else {
            mMediaPlayer2SongHelper = songHelper;
            mMediaPlayer2SongHelper.setIsCurrentSong();
        }

    }

    /**
     * (non-Javadoc)
     * @see android.app.Service#onDestroy()
     */
    @Override
    public void onDestroy() {

        //Notify the UI that the service is about to stop.
        mApp.broadcastUpdateUICommand(new String[] { Common.SERVICE_STOPPING }, new String[] { "" });

        //Fire a broadcast message to the widget(s) to update them.
        updateWidgets();

        //Send service stop event to GAnalytics.
        try {
            if (mApp.isGoogleAnalyticsEnabled()) {
                mTracker.set(Fields.SESSION_CONTROL, "end");
                mTracker.send(
                        MapBuilder.createTiming("Jelly Service", System.currentTimeMillis() - mServiceStartTime,
                                "Service duration.", "User stopped music playback.").build());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        //Save the last track's info within the current queue.
        try {
            mApp.getSharedPreferences().edit().putLong("LAST_SONG_TRACK_POSITION",
                    getCurrentMediaPlayer().getCurrentPosition());
        } catch (Exception e) {
            e.printStackTrace();
            mApp.getSharedPreferences().edit().putLong("LAST_SONG_TRACK_POSITION", 0);
        }

        //If the current song is repeating a specific range, reset the repeat option.
        if (getRepeatMode() == Common.REPEAT_SONG) {
            setRepeatMode(Common.REPEAT_OFF);
        }

        mFadeInVolume = 0.0f;
        mFadeOutVolume = 1.0f;

        //Unregister the headset plug receiver and RemoteControlClient.
        try {
            RemoteControlHelper.unregisterRemoteControlClient(mAudioManager, mRemoteControlClientCompat);
            unregisterReceiver(mHeadsetPlugReceiver);
        } catch (Exception e) {
            //Just null out the receiver if it hasn't been registered yet.
            mHeadsetPlugReceiver = null;
        }

        //Remove the notification.
        NotificationManager notificationManager = (NotificationManager) this.getSystemService(NOTIFICATION_SERVICE);
        notificationManager.cancel(mNotificationId);

        try {
            mEqualizerHelper.releaseEQObjects();
            mEqualizerHelper = null;
        } catch (Exception e1) {
            e1.printStackTrace();
            mEqualizerHelper = null;
        }

        if (mMediaPlayer != null)
            mMediaPlayer.release();

        if (mMediaPlayer2 != null)
            getMediaPlayer2().release();

        mMediaPlayer = null;
        mMediaPlayer2 = null;

        //Close the cursor(s).
        try {
            getCursor().close();
            setCursor(null);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //Final scrobbling.
        scrobbleTrack(SimpleLastFMHelper.PAUSE);

        /*
         * If A-B repeat is enabled, disable it to prevent the
         * next service instance from repeating the same section
         * over and over on the new track.
         */
        if (getRepeatMode() == Common.A_B_REPEAT)
            setRepeatMode(Common.REPEAT_OFF);

        //Remove audio focus and unregister the audio buttons receiver.
        mAudioManagerHelper.setHasAudioFocus(false);
        mAudioManager.abandonAudioFocus(audioFocusChangeListener);
        mAudioManager.unregisterMediaButtonEventReceiver(
                new ComponentName(getPackageName(), HeadsetButtonsReceiver.class.getName()));
        mAudioManager = null;
        mMediaButtonReceiverComponent = null;
        mRemoteControlClientCompat = null;

        //Nullify the service object.
        mApp.setService(null);
        mApp.setIsServiceRunning(false);
        mApp = null;

    }

    /**
     * Interface implementation to listen for service cursor events.
     */
    public PlaybackKickstarter.BuildCursorListener buildCursorListener = new PlaybackKickstarter.BuildCursorListener() {

        @Override
        public void onServiceCursorReady(Cursor cursor, int currentSongIndex, boolean playAll) {

            if (cursor.getCount() == 0) {
                Toast.makeText(mContext, R.string.no_audio_files_found, Toast.LENGTH_SHORT).show();
                if (mApp.getNowPlayingActivity() != null)
                    mApp.getNowPlayingActivity().finish();

                return;
            }

            setCursor(cursor);
            setCurrentSongIndex(currentSongIndex);
            getFailedIndecesList().clear();
            initPlaybackIndecesList(playAll);
            mFirstRun = true;
            prepareMediaPlayer(currentSongIndex);

            //Notify NowPlayingActivity to initialize its ViewPager.
            mApp.broadcastUpdateUICommand(new String[] { Common.INIT_PAGER }, new String[] { "" });

        }

        @Override
        public void onServiceCursorFailed(String exceptionMessage) {
            //We don't have a valid cursor, so stop the service.
            Log.e("SERVICE CURSOR EXCEPTION", "onServiceCursorFailed(): " + exceptionMessage);
            Toast.makeText(mContext, R.string.unable_to_start_playback, Toast.LENGTH_SHORT).show();
            stopSelf();

        }

        @Override
        public void onServiceCursorUpdated(Cursor cursor) {
            //Make sure the new cursor and the old cursor are the same size.
            if (getCursor().getCount() == cursor.getCount()) {
                setCursor(cursor);
            }

        }

    };

}