org.mariotaku.harmony.MusicPlaybackService.java Source code

Java tutorial

Introduction

Here is the source code for org.mariotaku.harmony.MusicPlaybackService.java

Source

/*
 *  YAMMP - Yet Another Multi Media Player for android
 *  Copyright (C) 2011-2012  Mariotaku Lee <mariotaku.lee@gmail.com>
 *
 *  This file is part of YAMMP.
 *
 *  YAMMP is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  YAMMP is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with YAMMP.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.mariotaku.harmony;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaPlayer;
import android.media.audiofx.AudioEffect;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.support.v4.app.NotificationCompat;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.format.DateUtils;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Calendar;
import java.util.Random;
import java.util.Vector;
import org.mariotaku.harmony.model.TrackInfo;
import org.mariotaku.harmony.util.MusicUtils;
import org.mariotaku.harmony.util.PreferencesEditor;
import android.content.res.Resources;
import org.mariotaku.harmony.model.AlbumInfo;

/**
 * Provides "background" audio playback capabilities, allowing the user to
 * switch between activities without stopping playback.
 */
public class MusicPlaybackService extends Service implements Constants {

    /**
     * used to specify whether enqueue() should start playing the new list of
     * files right away, next or once all the currently queued files have been
     * played
     */

    private static final int TRACK_ENDED = 1;
    private static final int RELEASE_WAKELOCK = 2;
    private static final int SERVER_DIED = 3;
    private static final int FOCUSCHANGE = 4;
    private static final int FADEDOWN = 5;
    private static final int FADEUP = 6;

    private static final int START_SLEEP_TIMER = 1;
    private static final int STOP_SLEEP_TIMER = 2;

    private MultiPlayer mPlayer;
    private ContentResolver mResolver;
    private TelephonyManager mTelephonyManager;

    private TrackInfo mTrackInfo;
    private NotificationManager mNotificationManager;
    private int mShuffleMode = SHUFFLE_MODE_NONE;

    private int mRepeatMode = REPEAT_MODE_NONE;
    private long[] mPlayList = null;
    private int mPlayListLen = 0;
    private Vector<Integer> mHistory = new Vector<Integer>();

    //private Cursor mCursor;
    private int mPlayPos = -1;

    private final Shuffler mShuffler = new Shuffler();
    private int mOpenFailedCounter = 0;
    private static final String[] CURSOR_COLUMNS = new String[] { MediaStore.Audio.Media._ID,
            MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.TITLE,
            MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ARTIST_ID,
            MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.IS_PODCAST, MediaStore.Audio.Media.BOOKMARK };
    private final static int IDCOLIDX = 0;
    private final static int PODCASTCOLIDX = 8;
    private final static int BOOKMARKCOLIDX = 9;
    private BroadcastReceiver mUnmountReceiver = null;
    private BroadcastReceiver mA2dpReceiver = null;
    private WakeLock mWakeLock;
    private int mServiceStartId = -1;
    private boolean mServiceInUse = false;
    private boolean mIsSupposedToBePlaying = false;
    private boolean mQuietMode = false;
    private AudioManager mAudioManager;
    private boolean mQueueIsSaveable = true;
    // used to track what type of audio focus loss caused the playback to pause
    private boolean mPausedByTransientLossOfFocus = false;
    // used to track current volume
    private float mCurrentVolume = 1.0f;
    private PreferencesEditor mPreferences;
    // We use this to distinguish between different cards when saving/restoring
    // playlists.
    // This will have to change if we want to support multiple simultaneous
    // cards.
    private int mCardId;
    // interval after which we stop the service when idle
    private static final int IDLE_DELAY = 60000;
    private boolean mGentleSleepTimer, mSleepTimerTimedUp;
    private long mCurrentTimestamp, mStopTimestamp;

    private boolean mExternalAudioDeviceConnected = false;

    private Handler mMediaplayerHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            MusicUtils.debugLog("mMediaplayerHandler.handleMessage " + msg.what);
            switch (msg.what) {
            case FADEDOWN:
                mCurrentVolume -= 0.05f;
                if (mCurrentVolume > 0.2f) {
                    mMediaplayerHandler.sendEmptyMessageDelayed(FADEDOWN, 10);
                } else {
                    mCurrentVolume = 0.2f;
                }
                mPlayer.setVolume(mCurrentVolume);
                break;
            case FADEUP:
                mCurrentVolume += 0.01f;
                if (mCurrentVolume < 1.0f) {
                    mMediaplayerHandler.sendEmptyMessageDelayed(FADEUP, 10);
                } else {
                    mCurrentVolume = 1.0f;
                }
                mPlayer.setVolume(mCurrentVolume);
                break;
            case SERVER_DIED:
                if (mIsSupposedToBePlaying) {
                    next(true);
                } else {
                    // the server died when we were idle, so just
                    // reopen the same song (it will start again
                    // from the beginning though when the user
                    // restarts)
                    openCurrent();
                }
                break;
            case TRACK_ENDED:
                if (mRepeatMode == REPEAT_MODE_CURRENT) {
                    seek(0);
                    play();
                } else {
                    next(false);
                }
                break;
            case RELEASE_WAKELOCK:
                mWakeLock.release();
                break;
            case FOCUSCHANGE:
                // This code is here so we can better synchronize it with
                // the code that handles fade-in
                switch (msg.arg1) {
                case AudioManager.AUDIOFOCUS_LOSS:
                    Log.v(LOGTAG_SERVICE, "AudioFocus: received AUDIOFOCUS_LOSS");
                    if (isPlaying()) {
                        mPausedByTransientLossOfFocus = false;
                    }
                    pause();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    mMediaplayerHandler.removeMessages(FADEUP);
                    mMediaplayerHandler.sendEmptyMessage(FADEDOWN);
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    Log.v(LOGTAG_SERVICE, "AudioFocus: received AUDIOFOCUS_LOSS_TRANSIENT");
                    if (isPlaying()) {
                        mPlayer.setVolume((float) Math.pow(10.0, -8 / 20.0));
                    }
                    break;
                case AudioManager.AUDIOFOCUS_GAIN:
                    Log.v(LOGTAG_SERVICE, "AudioFocus: received AUDIOFOCUS_GAIN");
                    if (isPlaying() || mPausedByTransientLossOfFocus) {
                        mPausedByTransientLossOfFocus = false;
                        mCurrentVolume = 0f;
                        mPlayer.setVolume(mCurrentVolume);
                        play(); // also queues a fade-in
                    } else {
                        mMediaplayerHandler.removeMessages(FADEDOWN);
                        mMediaplayerHandler.sendEmptyMessage(FADEUP);
                    }
                    break;
                default:
                    Log.e(LOGTAG_SERVICE, "Unknown audio focus change code");
                }
                break;

            default:
                break;
            }
        }
    };

    private BroadcastReceiver mExternalAudioDeviceStatusReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (Intent.ACTION_HEADSET_PLUG.equals(action)) {
                int state = intent.getIntExtra("state", 0);
                mExternalAudioDeviceConnected = (state == 1);
            }
        }
    };

    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {

            String action = intent.getAction();
            String cmd = intent.getStringExtra(CMDNAME);
            MusicUtils.debugLog("mIntentReceiver.onReceive " + action + " / " + cmd);
            if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
                next(true);
            } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
                prev();
            } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
                if (isPlaying()) {
                    pause();
                    mPausedByTransientLossOfFocus = false;
                } else {
                    play();
                }
            } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
                pause();
                mPausedByTransientLossOfFocus = false;
            } else if (CMDSTOP.equals(cmd)) {
                pause();
                mPausedByTransientLossOfFocus = false;
                seek(0);
            } else if (CMDTOGGLEFAVORITE.equals(cmd)) {
                if (!isFavorite()) {
                    addToFavorites();
                } else {
                    removeFromFavorites();
                }
            }
        }
    };
    private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {

        @Override
        public void onAudioFocusChange(int focusChange) {

            mMediaplayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
        }
    };
    private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {

        @Override
        public void onCallStateChanged(int state, String incomingNumber) {

            switch (state) {
            case TelephonyManager.CALL_STATE_RINGING:
                Log.v(LOGTAG_SERVICE, "PhoneState: received CALL_STATE_RINGING");
                if (isPlaying()) {
                    mPausedByTransientLossOfFocus = true;
                    pause();
                }
                break;

            case TelephonyManager.CALL_STATE_OFFHOOK:
                Log.v(LOGTAG_SERVICE, "PhoneState: received CALL_STATE_OFFHOOK");
                mPausedByTransientLossOfFocus = false;
                if (isPlaying()) {
                    pause();
                }
                break;
            }
        }
    };

    private static final char hexdigits[] = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
            'c', 'd', 'e', 'f' };
    private Handler mDelayedStopHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            // Check again to make sure nothing is playing right now
            if (isPlaying() || mPausedByTransientLossOfFocus || mServiceInUse
                    || mMediaplayerHandler.hasMessages(TRACK_ENDED))
                return;
            // save the queue again, because it might have changed
            // since the user exited the music app (because of
            // party-shuffle or because the play-position changed)
            saveQueue(true);
            stopSelf(mServiceStartId);
        }
    };
    private Handler mSleepTimerHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            switch (msg.what) {
            case START_SLEEP_TIMER:
                mSleepTimerHandler.removeMessages(START_SLEEP_TIMER, null);
                if (mGentleSleepTimer) {
                    if (isPlaying()) {
                        mSleepTimerTimedUp = true;
                    } else {
                        pause();
                        mNotificationManager.cancel(ID_NOTIFICATION_SLEEPTIMER);
                    }
                } else {
                    pause();
                    mNotificationManager.cancel(ID_NOTIFICATION_SLEEPTIMER);
                }
                mStopTimestamp = 0;
                break;
            case STOP_SLEEP_TIMER:
                mStopTimestamp = 0;
                mSleepTimerHandler.removeMessages(START_SLEEP_TIMER, null);
                mNotificationManager.cancel(ID_NOTIFICATION_SLEEPTIMER);
                break;
            }
        }
    };
    private final IBinder mBinder = new ServiceStub(this);

    private Resources mResources;

    public MusicPlaybackService() {

    }

    public TrackInfo getTrackInfo() {
        return mTrackInfo;
    }

    public void addToFavorites() {
    }

    public void addToFavorites(long id) {
        MusicUtils.addToFavorites(this, id);
        notifyChange(BROADCAST_FAVORITESTATE_CHANGED);
    }

    /**
     * Called when we receive a ACTION_MEDIA_EJECT notification.
     * 
     * @param storagePath
     *            path to mount point for the removed media
     */
    public void closeExternalStorageFiles(String storagePath) {

        stop(true);
        notifyChange(BROADCAST_QUEUE_CHANGED);
        notifyChange(BROADCAST_MEDIA_CHANGED);
    }

    /**
     * Returns the duration of the file in milliseconds. Currently this method
     * returns -1 for the duration of MIDI files.
     */
    public long getDuration() {
        if (mPlayer.isInitialized())
            return mPlayer.getDuration();
        return -1;
    }

    /**
     * Appends a list of tracks to the current playlist. If nothing is playing
     * currently, playback will be started at the first track. If the action is
     * NOW, playback will switch to the first of the new tracks immediately.
     * 
     * @param list
     *            The list of tracks to append.
     * @param action
     *            NOW, NEXT or LAST
     */
    public void enqueue(long[] list, int action) {

        synchronized (this) {
            if (action == ACTION_NEXT && mPlayPos + 1 < mPlayListLen) {
                addToPlayList(list, mPlayPos + 1);
                notifyChange(BROADCAST_QUEUE_CHANGED);
            } else {
                // action == LAST || action == NOW || mPlayPos + 1 ==
                // mPlayListLen
                addToPlayList(list, Integer.MAX_VALUE);
                notifyChange(BROADCAST_QUEUE_CHANGED);
                if (action == ACTION_NOW) {
                    mPlayPos = mPlayListLen - list.length;
                    openCurrent();
                    play();
                    notifyChange(BROADCAST_MEDIA_CHANGED);
                    return;
                }
            }
            if (mPlayPos < 0) {
                mPlayPos = 0;
                openCurrent();
                play();
                notifyChange(BROADCAST_MEDIA_CHANGED);
            }
        }
    }

    /**
     * Returns the audio session ID.
     */
    public int getAudioSessionId() {

        synchronized (this) {
            return mPlayer.getAudioSessionId();
        }
    }

    /**
     * Returns the current play list
     * 
     * @return An array of integers containing the IDs of the tracks in the play
     *         list
     */
    //FIXME
    public long[] getQueue() {
        synchronized (this) {
            final int len = mPlayListLen;
            final long[] list = new long[len];
            for (int i = 0; i < len; i++) {
                list[i] = mPlayList[i];
            }
            return list;
        }
    }

    /**
     * Returns the position in the queue
     * 
     * @return the position in the queue
     */
    public int getQueuePosition() {
        synchronized (this) {
            return mPlayPos;
        }
    }

    public int getRepeatMode() {
        return mRepeatMode;
    }

    public int getShuffleMode() {
        return mShuffleMode;
    }

    public boolean isFavorite() {
        return false;
    }

    public boolean isFavorite(long id) {
        return MusicUtils.isFavorite(this, id);
    }

    /**
     * Returns whether something is currently playing
     * 
     * @return true if something is playing (or will be playing shortly, in case
     *         we're currently transitioning between tracks), false if not.
     */
    public boolean isPlaying() {
        return mIsSupposedToBePlaying;
    }

    /**
     * Moves the item at index1 to index2.
     * 
     * @param from
     * @param to
     */
    public synchronized void moveQueueItem(int from, int to) {
        if (from >= mPlayListLen) {
            from = mPlayListLen - 1;
        }
        if (to >= mPlayListLen) {
            to = mPlayListLen - 1;
        }
        if (from < to) {
            final long tmp = mPlayList[from];
            for (int i = from; i < to; i++) {
                mPlayList[i] = mPlayList[i + 1];
            }
            mPlayList[to] = tmp;
            if (mPlayPos == from) {
                mPlayPos = to;
            } else if (mPlayPos >= from && mPlayPos <= to) {
                mPlayPos--;
            }
        } else if (to < from) {
            long tmp = mPlayList[from];
            for (int i = from; i > to; i--) {
                mPlayList[i] = mPlayList[i - 1];
            }
            mPlayList[to] = tmp;
            if (mPlayPos == from) {
                mPlayPos = to;
            } else if (mPlayPos >= to && mPlayPos <= from) {
                mPlayPos++;
            }
        }
        notifyChange(BROADCAST_QUEUE_CHANGED);
    }

    public void next(boolean force) {

        synchronized (this) {
            if (mSleepTimerTimedUp) {
                pause();
                mNotificationManager.cancel(ID_NOTIFICATION_SLEEPTIMER);
                mSleepTimerTimedUp = false;
                return;
            }

            if (mPlayListLen <= 0) {
                Log.d(LOGTAG_SERVICE, "No play queue");
                return;
            }

            if (mShuffleMode == SHUFFLE_MODE_ALL) {
                if (mPlayPos >= 0) {
                    if (!mHistory.contains(mPlayPos)) {
                        mHistory.add(mPlayPos);
                    }
                }

                int numTracks = mPlayListLen;
                int[] tracks = new int[numTracks];
                for (int i = 0; i < numTracks; i++) {
                    tracks[i] = i;
                }

                int numHistory = mHistory.size();
                int numUnplayed = numTracks;
                for (int i = 0; i < numHistory; i++) {
                    int idx = mHistory.get(i).intValue();
                    if (idx < numTracks && tracks[idx] >= 0) {
                        numUnplayed--;
                        tracks[idx] = -1;
                    }
                }

                // 'numUnplayed' now indicates how many tracks have not yet
                // been played, and 'tracks' contains the indices of those
                // tracks.
                if (numUnplayed <= 0) {
                    // everything's already been played
                    if (mRepeatMode == REPEAT_MODE_ALL || force) {
                        // pick from full set
                        numUnplayed = numTracks;
                        for (int i = 0; i < numTracks; i++) {
                            tracks[i] = i;
                        }
                    } else {
                        // all done
                        gotoIdleState();
                        if (mIsSupposedToBePlaying) {
                            mIsSupposedToBePlaying = false;
                            notifyChange(BROADCAST_PLAY_STATE_CHANGED);
                        }
                        return;
                    }
                }
                int skip = mShuffler.shuffle(numUnplayed);
                int cnt = -1;
                while (true) {
                    while (tracks[++cnt] < 0) {
                        ;
                    }
                    skip--;
                    if (skip < 0) {
                        break;
                    }
                }
                mPlayPos = cnt;
            } else {
                if (mPlayPos >= mPlayListLen - 1) {
                    // we're at the end of the list
                    if (mRepeatMode == REPEAT_MODE_NONE && !force) {
                        // all done
                        gotoIdleState();
                        mIsSupposedToBePlaying = false;
                        notifyChange(BROADCAST_PLAY_STATE_CHANGED);
                        return;
                    } else if (mRepeatMode == REPEAT_MODE_ALL || force) {
                        mPlayPos = 0;
                    }
                } else {
                    mPlayPos++;
                }
            }
            saveBookmarkIfNeeded();
            stop(false);
            openCurrent();
            play();
            notifyChange(BROADCAST_MEDIA_CHANGED);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {

        mDelayedStopHandler.removeCallbacksAndMessages(null);
        mServiceInUse = true;
        return mBinder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // Needs to be done in this thread, since otherwise
        // ApplicationContext.getPowerManager() crashes.
        mResources = getResources();
        mPlayer = new MultiPlayer(this);
        mResolver = getContentResolver();
        mPlayer.setHandler(mMediaplayerHandler);

        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        mAudioManager.registerMediaButtonEventReceiver(
                new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()));

        mPreferences = new PreferencesEditor(this);

        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        mCardId = MusicUtils.getCardId(this);

        registerExternalStorageListener();
        registerA2dpServiceListener();

        reloadQueue();

        IntentFilter commandFilter = new IntentFilter();
        commandFilter.addAction(SERVICECMD);
        commandFilter.addAction(TOGGLEPAUSE_ACTION);
        commandFilter.addAction(PAUSE_ACTION);
        commandFilter.addAction(NEXT_ACTION);
        commandFilter.addAction(PREVIOUS_ACTION);
        commandFilter.addAction(CYCLEREPEAT_ACTION);
        commandFilter.addAction(TOGGLESHUFFLE_ACTION);
        commandFilter.addAction(BROADCAST_PLAYSTATUS_REQUEST);
        registerReceiver(mIntentReceiver, commandFilter);

        registerReceiver(mExternalAudioDeviceStatusReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));

        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
        mWakeLock.setReferenceCounted(false);

        // If the service was idle, but got killed before it stopped itself, the
        // system will relaunch it. Make sure it gets stopped again in that
        // case.
        Message msg = mDelayedStopHandler.obtainMessage();
        mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
    }

    @Override
    public void onDestroy() {

        // Check that we're not being destroyed while something is still
        // playing.
        if (isPlaying()) {
            Log.e(LOGTAG_SERVICE, "Service being destroyed while still playing.");
        }

        mPlayer.release();
        mPlayer = null;

        mAudioManager.abandonAudioFocus(mAudioFocusListener);

        final TelephonyManager mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);

        // make sure there aren't any other messages coming
        mDelayedStopHandler.removeCallbacksAndMessages(null);
        mMediaplayerHandler.removeCallbacksAndMessages(null);

        mTrackInfo = null;

        unregisterReceiver(mIntentReceiver);
        unregisterReceiver(mA2dpReceiver);
        unregisterReceiver(mExternalAudioDeviceStatusReceiver);
        if (mUnmountReceiver != null) {
            unregisterReceiver(mUnmountReceiver);
            mUnmountReceiver = null;
        }
        mWakeLock.release();
        mNotificationManager.cancelAll();
        super.onDestroy();
    }

    @Override
    public void onRebind(Intent intent) {

        mDelayedStopHandler.removeCallbacksAndMessages(null);
        mServiceInUse = true;
    }

    /*
     * Desired behavior for prev/next/shuffle:
     * 
     * - NEXT will move to the next track in the list when not shuffling, and to
     * a track randomly picked from the not-yet-played tracks when shuffling. If
     * all tracks have already been played, pick from the full set, but avoid
     * picking the previously played track if possible. - when shuffling, PREV
     * will go to the previously played track. Hitting PREV again will go to the
     * track played before that, etc. When the start of the history has been
     * reached, PREV is a no-op. When not shuffling, PREV will go to the
     * sequentially previous track (the difference with the shuffle-case is
     * mainly that when not shuffling, the user can back up to tracks that are
     * not in the history).
     * 
     * Example: When playing an album with 10 tracks from the start, and
     * enabling shuffle while playing track 5, the remaining tracks (6-10) will
     * be shuffled, e.g. the final play order might be 1-2-3-4-5-8-10-6-9-7.
     * When hitting 'prev' 8 times while playing track 7 in this example, the
     * user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next', a
     * random track will be picked again. If at any time user disables shuffling
     * the next/previous track will be picked in sequential order again.
     */

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        mServiceStartId = startId;
        mDelayedStopHandler.removeCallbacksAndMessages(null);

        if (intent != null) {
            String action = intent.getAction();
            String cmd = intent.getStringExtra("command");
            MusicUtils.debugLog("onStartCommand " + action + " / " + cmd);

            if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
                next(true);
            } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
                prev();
            } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
                if (isPlaying()) {
                    pause();
                    mPausedByTransientLossOfFocus = false;
                } else {
                    play();
                }
            } else if (CMDTOGGLEFAVORITE.equals(cmd)) {
                if (!isFavorite()) {
                    addToFavorites();
                } else {
                    removeFromFavorites();
                }
            } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
                pause();
                mPausedByTransientLossOfFocus = false;
            } else if (CMDSTOP.equals(cmd)) {
                pause();
                mPausedByTransientLossOfFocus = false;
                seek(0);
            } else if (BROADCAST_PLAYSTATUS_REQUEST.equals(action)) {
                notifyChange(BROADCAST_PLAYSTATUS_RESPONSE);
            }
        }

        // make sure the service will shut down on its own if it was
        // just started but not bound to and nothing is playing
        mDelayedStopHandler.removeCallbacksAndMessages(null);
        Message msg = mDelayedStopHandler.obtainMessage();
        mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
        return START_STICKY;
    }

    @Override
    public boolean onUnbind(Intent intent) {

        mServiceInUse = false;

        // Take a snapshot of the current playlist
        saveQueue(true);

        if (isPlaying() || mPausedByTransientLossOfFocus) // something is
            // currently
            // playing, or will
            // be playing once
            // an in-progress action requesting audio focus ends, so don't stop
            // the service now.
            return true;

        // If there is a playlist but playback is paused, then wait a while
        // before stopping the service, so that pause/resume isn't slow.
        // Also delay stopping the service if we're transitioning between
        // tracks.
        if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
            Message msg = mDelayedStopHandler.obtainMessage();
            mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
            return true;
        }

        // No active playlist, OK to stop the service right now
        stopSelf(mServiceStartId);
        return true;
    }

    /**
     * Replaces the current playlist with a new list, and prepares for starting
     * playback at the specified position in the list, or a random position if
     * the specified position is 0.
     * 
     * @param list
     *            The new list of tracks.
     */
    public void open(long[] list, int position) {

        synchronized (this) {
            final long oldId = mTrackInfo != null ? mTrackInfo.id : -1;
            final int listlength = list.length;
            boolean newlist = true;
            if (mPlayListLen == listlength) {
                // possible fast path: list might be the same
                newlist = false;
                for (int i = 0; i < listlength; i++) {
                    if (list[i] != mPlayList[i]) {
                        newlist = true;
                        break;
                    }
                }
            }
            if (newlist) {
                addToPlayList(list, -1);
                notifyChange(BROADCAST_QUEUE_CHANGED);
            }
            if (position >= 0) {
                mPlayPos = position;
            } else {
                mPlayPos = mShuffler.shuffle(mPlayListLen);
            }
            mHistory.clear();
            saveBookmarkIfNeeded();
            openCurrent();
            final long currentId = mTrackInfo != null ? mTrackInfo.id : -1;
            if (oldId != currentId) {
                notifyChange(BROADCAST_MEDIA_CHANGED);
            }
        }
    }

    /**
     * Opens the specified file and readies it for playback.
     * 
     * @param path
     *            The full path of the file to be opened.
     */
    public synchronized void openUri(final Uri uri) {
        mTrackInfo = null;
        if (uri == null)
            return;
        final Cursor cur = mResolver.query(uri, CURSOR_COLUMNS, null, null, null);
        if (cur == null)
            return;
        if (cur.getCount() > 0) {
            cur.moveToFirst();
            mTrackInfo = new TrackInfo(cur);
        }
        cur.close();
        if (mTrackInfo == null)
            return;
        mPlayer.setDataSource(mTrackInfo.data);
        if (!mPlayer.isInitialized()) {
            stop(true);
            if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
                // beware: this ends up being recursive because next() calls
                // open() again.
                next(false);
            }
            if (!mPlayer.isInitialized() && mOpenFailedCounter != 0) {
                // need to make sure we only shows this once
                mOpenFailedCounter = 0;
                if (!mQuietMode) {
                    Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
                }
                Log.d(LOGTAG_SERVICE, "Failed to open file for playback");
            }
        } else {
            mOpenFailedCounter = 0;
        }
    }

    /**
     * Pauses playback (call play() to resume)
     */
    public void pause() {

        synchronized (this) {
            mMediaplayerHandler.removeMessages(FADEUP);
            if (isPlaying()) {

                mPlayer.pause();
                gotoIdleState();
                mIsSupposedToBePlaying = false;
                notifyChange(BROADCAST_PLAY_STATE_CHANGED);
                saveBookmarkIfNeeded();
            }
        }
    }

    /**
     * Starts playback of a previously opened file.
     */
    public void play() {
        final TrackInfo track = getTrackInfo();
        if (track == null)
            return;

        if (mTelephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK)
            return;

        mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN);
        mAudioManager.registerMediaButtonEventReceiver(
                new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()));

        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);

        if (mPlayer.isInitialized()) {
            // if we are at the end of the song, go to the next song first
            long duration = mPlayer.getDuration();
            if (mRepeatMode != REPEAT_MODE_CURRENT && duration > 2000 && mPlayer.getPosition() >= duration - 2000) {
                next(true);
            }

            mPlayer.start();

            // make sure we fade in, in case a previous fadein was stopped
            // because
            // of another focus loss
            mMediaplayerHandler.removeMessages(FADEDOWN);
            mMediaplayerHandler.sendEmptyMessage(FADEUP);

            final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
            final NotificationCompat.Style style = new NotificationCompat.BigPictureStyle();
            builder.setSmallIcon(R.drawable.ic_stat_playback);
            builder.setContentTitle(track.title);
            if (!TrackInfo.isUnknownArtist(track)) {
                builder.setContentText(track.artist);
            } else if (!TrackInfo.isUnknownAlbum(track)) {
                builder.setContentText(track.album);
            } else {
                builder.setContentText(getString(R.string.unknown_artist));
            }
            final AlbumInfo album = AlbumInfo.getAlbumInfo(this, track);
            builder.setLargeIcon(getAlbumArtForNotification(album != null ? album.album_art : null));
            builder.setStyle(style);
            builder.setOngoing(true);
            builder.setOnlyAlertOnce(true);
            builder.setWhen(0);
            builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(INTENT_PLAYBACK_VIEWER), 0));

            startForeground(ID_NOTIFICATION_PLAYBACK, builder.getNotification());

            if (!mIsSupposedToBePlaying) {
                mIsSupposedToBePlaying = true;
                notifyChange(BROADCAST_PLAY_STATE_CHANGED);
            }

        } else if (mPlayListLen <= 0) {
            // This is mostly so that if you press 'play' on a bluetooth headset
            // without every having played anything before, it will still play
            // something.
            setShuffleMode(SHUFFLE_MODE_ALL);
        }
    }

    private Bitmap getAlbumArtForNotification(final String path) {
        if (path == null)
            return null;
        final BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, opts);
        final float bmp_size = Math.max(opts.outWidth, opts.outHeight);
        if (bmp_size == 0)
            return null;
        final int width = mResources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
        final int height = mResources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
        opts.inSampleSize = (int) Math.floor(bmp_size / Math.max(width, height));
        opts.inJustDecodeBounds = false;
        final Bitmap bitmap = BitmapFactory.decodeFile(path, opts);
        if (bitmap == null)
            return null;
        final Bitmap scaled;
        if (bitmap.getWidth() > bitmap.getHeight()) {
            scaled = Bitmap.createScaledBitmap(bitmap, height * bitmap.getWidth() / bitmap.getHeight(), height,
                    true);
        } else {
            scaled = Bitmap.createScaledBitmap(bitmap, width, width * bitmap.getHeight() / bitmap.getWidth(), true);
        }
        bitmap.recycle();
        final int x = Math.max(0, (scaled.getWidth() - width) / 2),
                y = Math.max(0, (scaled.getHeight() - height) / 2);
        final Bitmap cropped = Bitmap.createBitmap(scaled, x, y, Math.min(width, scaled.getWidth()),
                Math.min(height, scaled.getHeight()));
        scaled.recycle();
        return cropped;
    }

    /**
     * Returns the current playback position in milliseconds
     */
    public long getPosition() {
        if (mPlayer.isInitialized())
            return mPlayer.getPosition();
        return -1;
    }

    public void prev() {

        synchronized (this) {
            if (mShuffleMode == SHUFFLE_MODE_ALL) {
                // go to previously-played track and remove it from the history
                int histsize = mHistory.size();
                if (histsize == 0) // prev is a no-op
                    return;
                Integer pos = mHistory.remove(histsize - 1);
                mPlayPos = pos.intValue();
            } else {
                if (mPlayPos > 0) {
                    mPlayPos--;
                } else {
                    mPlayPos = mPlayListLen - 1;
                }
            }
            saveBookmarkIfNeeded();
            stop(false);
            openCurrent();
            play();
            notifyChange(BROADCAST_MEDIA_CHANGED);
        }
    }

    public void registerA2dpServiceListener() {

        mA2dpReceiver = new BroadcastReceiver() {

            @Override
            public void onReceive(Context context, Intent intent) {

                String action = intent.getAction();
                if (BROADCAST_PLAYSTATUS_REQUEST.equals(action)) {
                    notifyChange(BROADCAST_PLAYSTATUS_RESPONSE);
                }
            }
        };
        final IntentFilter filter = new IntentFilter();
        filter.addAction(BROADCAST_PLAYSTATUS_REQUEST);
        registerReceiver(mA2dpReceiver, filter);
    }

    /**
     * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
     * intent will call closeExternalStorageFiles() if the external media is
     * going to be ejected, so applications can clean up any files they have
     * open.
     */
    public void registerExternalStorageListener() {
        if (mUnmountReceiver == null) {
            mUnmountReceiver = new BroadcastReceiver() {

                @Override
                public void onReceive(Context context, Intent intent) {

                    String action = intent.getAction();
                    if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
                        saveQueue(true);
                        mQueueIsSaveable = false;
                        closeExternalStorageFiles(intent.getData().getPath());
                    } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
                        mCardId = MusicUtils.getCardId(context);
                        reloadQueue();
                        mQueueIsSaveable = true;
                        notifyChange(BROADCAST_QUEUE_CHANGED);
                        notifyChange(BROADCAST_MEDIA_CHANGED);
                    }
                }
            };
            final IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_MEDIA_EJECT);
            filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
            filter.addDataScheme(ContentResolver.SCHEME_FILE);
            registerReceiver(mUnmountReceiver, filter);
        }
    }

    public void removeFromFavorites() {
    }

    public void removeFromFavorites(long id) {
        MusicUtils.removeFromFavorites(this, id);
        notifyChange(BROADCAST_FAVORITESTATE_CHANGED);
    }

    /**
     * Removes all instances of the track with the given id from the playlist.
     * 
     * @param id
     *            The id to be removed
     * @return how many instances of the track were removed
     */
    public int removeTrack(long id) {

        int numremoved = 0;
        synchronized (this) {
            for (int i = 0; i < mPlayListLen; i++) {
                if (mPlayList[i] == id) {
                    numremoved += removeTracksInternal(i, i);
                    i--;
                }
            }
        }
        if (numremoved > 0) {
            notifyChange(BROADCAST_QUEUE_CHANGED);
        }
        return numremoved;
    }

    /**
     * Removes the range of tracks specified from the play list. If a file
     * within the range is the file currently being played, playback will move
     * to the next file after the range.
     * 
     * @param first
     *            The first file to be removed
     * @param last
     *            The last file to be removed
     * @return the number of tracks deleted
     */
    public int removeTracks(int first, int last) {

        int numremoved = removeTracksInternal(first, last);
        if (numremoved > 0) {
            notifyChange(BROADCAST_QUEUE_CHANGED);
        }
        return numremoved;
    }

    /**
     * Seeks to the position specified.
     * 
     * @param pos
     *            The position to seek to, in milliseconds
     */
    public long seek(long pos) {

        if (mPlayer.isInitialized()) {
            if (pos < 0) {
                pos = 0;
            }
            if (pos > mPlayer.getDuration()) {
                pos = mPlayer.getDuration();
            }
            long result = mPlayer.seek(pos);
            notifyChange(BROADCAST_SEEK_CHANGED);
            return result;
        }
        return 0;
    }

    /**
     * Starts playing the track at the given id in the queue.
     * 
     * @param id
     *            The id in the queue of the track that will be played.
     */
    public void setQueueId(long id) {
        int pos = -1;

        for (int i = 0; i < mPlayList.length; i++) {
            if (id == mPlayList[i]) {
                pos = i;
            }
        }
        if (pos < 0)
            return;

        setQueuePosition(pos);
    }

    /**
     * Starts playing the track at the given position in the queue.
     * 
     * @param pos
     *            The position in the queue of the track that will be played.
     */
    public synchronized void setQueuePosition(final int pos) {
        stop(false);
        mPlayPos = pos;
        openCurrent();
        play();
        notifyChange(BROADCAST_MEDIA_CHANGED);
    }

    public synchronized void setRepeatMode(int mode) {
        if (mRepeatMode == mode)
            return;
        if (mShuffleMode == SHUFFLE_MODE_ALL && mode == REPEAT_MODE_CURRENT) {
            setShuffleMode(SHUFFLE_MODE_NONE);
        }
        mRepeatMode = mode;
        notifyChange(BROADCAST_REPEAT_MODE_CHANGED);
        saveQueue(false);
        mPreferences.setIntPref(PREFERENCE_KEY_REPEAT_MODE, mRepeatMode);
    }

    public synchronized void setShuffleMode(int shufflemode) {
        if (mShuffleMode == shufflemode || mPlayListLen < 1)
            return;
        if (mRepeatMode == REPEAT_MODE_CURRENT) {
            mRepeatMode = REPEAT_MODE_NONE;
        }
        mShuffleMode = shufflemode;
        notifyChange(BROADCAST_SHUFFLE_MODE_CHANGED);
        saveQueue(false);
        mPreferences.setIntPref(PREFERENCE_KEY_SHUFFLE_MODE, mShuffleMode);
    }

    /**
     * Stops playback.
     */
    public void stop() {
        stop(true);
    }

    public void toggleFavorite() {
        if (!isFavorite()) {
            addToFavorites();
        } else {
            removeFromFavorites();
        }
    }

    public boolean togglePause() {
        if (isPlaying()) {
            pause();
        } else {
            play();
        }
        return isPlaying();
    }

    // insert the list of songs at the specified position in the playlist
    private void addToPlayList(long[] list, int position) {
        final int addlen = list.length;
        if (position < 0) { // overwrite
            mPlayListLen = 0;
            position = 0;
        }
        ensurePlayListCapacity(mPlayListLen + addlen);
        if (position > mPlayListLen) {
            position = mPlayListLen;
        }

        // move part of list after insertion point
        final int tailsize = mPlayListLen - position;
        for (int i = tailsize; i > 0; i--) {
            mPlayList[position + i] = mPlayList[position + i - addlen];
        }

        // copy list into playlist
        for (int i = 0; i < addlen; i++) {
            mPlayList[position + i] = list[i];
        }
        mPlayListLen += addlen;
        if (mPlayListLen == 0) {
            mTrackInfo = null;
            notifyChange(BROADCAST_MEDIA_CHANGED);
        }
    }

    private void ensurePlayListCapacity(int size) {

        if (mPlayList == null || size > mPlayList.length) {
            // reallocate at 2x requested size so we don't
            // need to grow and copy the array for every
            // insert
            long[] newlist = new long[size * 2];
            int len = mPlayList != null ? mPlayList.length : mPlayListLen;
            for (int i = 0; i < len; i++) {
                newlist[i] = mPlayList[i];
            }
            mPlayList = newlist;
        }
    }

    private long getBookmark() {
        return 0;
    }

    private long getSleepTimerRemained() {

        Calendar now = Calendar.getInstance();
        long mCurrentTimestamp = now.getTimeInMillis();
        if (mStopTimestamp != 0)
            return mStopTimestamp - mCurrentTimestamp;
        else
            return 0;
    }

    private void gotoIdleState() {

        mDelayedStopHandler.removeCallbacksAndMessages(null);
        Message msg = mDelayedStopHandler.obtainMessage();
        mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
        mNotificationManager.cancel(ID_NOTIFICATION_PLAYBACK);
    }

    private boolean isPodcast() {
        return false;
    }

    /**
     * Notify the change-receivers that something has changed. The intent that
     * is sent contains the following data for the currently playing track: "id"
     * - Integer: the database row ID "artist" - String: the name of the artist
     * "album_artist" - String: the name of the album artist "album" - String:
     * the name of the album "track" - String: the name of the track The intent
     * has an action that is one of "org.mariotaku.harmony.metachanged"
     * "org.mariotaku.harmony.queuechanged", "org.mariotaku.harmony.playbackcomplete"
     * "org.mariotaku.harmony.playstatechanged" respectively indicating that a new track has
     * started playing, that the playback queue has changed, that playback has
     * stopped because the last file in the list has been played, or that the
     * play-state changed (paused/resumed).
     */
    private void notifyChange(String action) {
        final Intent intent = new Intent(action);
        final TrackInfo track = getTrackInfo();
        if (track != null) {
            intent.putExtra(BROADCAST_KEY_ID, track.id);
            intent.putExtra(BROADCAST_KEY_ARTIST, track.artist);
            intent.putExtra(BROADCAST_KEY_ALBUM, track.album);
            intent.putExtra(BROADCAST_KEY_TRACK, track.title);
            intent.putExtra(BROADCAST_KEY_SONGID, track.id);
            intent.putExtra(BROADCAST_KEY_ALBUMID, track.album_id);
            intent.putExtra(BROADCAST_KEY_PLAYING, isPlaying());
            intent.putExtra(BROADCAST_KEY_DURATION, getDuration());
            intent.putExtra(BROADCAST_KEY_POSITION, getPosition());
            intent.putExtra(BROADCAST_KEY_SHUFFLEMODE, getShuffleMode());
            intent.putExtra(BROADCAST_KEY_REPEATMODE, getRepeatMode());
        }
        if (mPlayList != null) {
            intent.putExtra(BROADCAST_KEY_LISTSIZE, Long.valueOf(mPlayList.length));
        } else {
            intent.putExtra(BROADCAST_KEY_LISTSIZE, Long.valueOf(mPlayListLen));
        }
        sendBroadcast(intent);

        if (BROADCAST_MEDIA_CHANGED.equals(action)) {
            if (isPlaying()) {
                sendScrobbleBroadcast(SCROBBLE_PLAYSTATE_START);
            } else {
                sendScrobbleBroadcast(SCROBBLE_PLAYSTATE_COMPLETE);
            }
        } else if (BROADCAST_PLAY_STATE_CHANGED.equals(action)) {
            if (isPlaying()) {
                sendScrobbleBroadcast(SCROBBLE_PLAYSTATE_RESUME);
            } else {
                sendScrobbleBroadcast(SCROBBLE_PLAYSTATE_PAUSE);
            }
        }
        if (BROADCAST_QUEUE_CHANGED.equals(action)) {
            saveQueue(true);
        } else {
            saveQueue(false);
        }
    }

    private synchronized void openCurrent() {
        mTrackInfo = null;
        stop(false);
        if (mPlayListLen == 0)
            return;
        final long id = mPlayList[mPlayPos];
        openUri(ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id));
        // go to bookmark if needed
        if (isPodcast()) {
            final long bookmark = getBookmark();
            // Start playing a little bit before the bookmark,
            // so it's easier to get back in to the narrative.
            seek(bookmark - 5000);
        }
    }

    private void reloadQueue() {
        String q = null;

        final int id = mPreferences.getIntState(STATE_KEY_CARDID, mCardId);
        if (id == mCardId) {
            // Only restore the saved playlist if the card is still
            // the same one as when the playlist was saved
            q = mPreferences.getStringState(STATE_KEY_QUEUE, "");
        }
        int qlen = q != null ? q.length() : 0;
        if (qlen > 1) {
            // Log.i("@@@@ service", "loaded queue: " + q);
            int plen = 0;
            int n = 0;
            int shift = 0;
            for (int i = 0; i < qlen; i++) {
                char c = q.charAt(i);
                if (c == ';') {
                    ensurePlayListCapacity(plen + 1);
                    mPlayList[plen] = n;
                    plen++;
                    n = 0;
                    shift = 0;
                } else {
                    if (c >= '0' && c <= '9') {
                        n += c - '0' << shift;
                    } else if (c >= 'a' && c <= 'f') {
                        n += 10 + c - 'a' << shift;
                    } else {
                        // bogus playlist data
                        plen = 0;
                        break;
                    }
                    shift += 4;
                }
            }
            mPlayListLen = plen;

            int pos = mPreferences.getIntState(STATE_KEY_CURRPOS, 0);
            if (pos < 0 || pos >= mPlayListLen) {
                // The saved playlist is bogus, discard it
                mPlayListLen = 0;
                return;
            }
            mPlayPos = pos;

            // When reloadQueue is called in response to a card-insertion,
            // we might not be able to query the media provider right away.
            // To deal with this, try querying for the current file, and if
            // that fails, wait a while and try again. If that too fails,
            // assume there is a problem and don't restore the state.
            Cursor crsr = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                    new String[] { "_id" }, "_id=" + mPlayList[mPlayPos], null, null);
            if (crsr == null || crsr.getCount() == 0) {
                // wait a bit and try again
                SystemClock.sleep(3000);
                crsr = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, CURSOR_COLUMNS,
                        "_id=" + mPlayList[mPlayPos], null, null);
            }
            if (crsr != null) {
                crsr.close();
            }

            mOpenFailedCounter = 20;
            mQuietMode = true;
            openCurrent();
            mQuietMode = false;
            if (!mPlayer.isInitialized()) {
                // couldn't restore the saved state
                mPlayListLen = 0;
                return;
            }

            long seekpos = mPreferences.getLongState(STATE_KEY_SEEKPOS, 0);
            seek(seekpos >= 0 && seekpos < getDuration() ? seekpos : 0);
            Log.d(LOGTAG_SERVICE, "restored queue, currently at position " + getPosition() + "/" + getDuration()
                    + " (requested " + seekpos + ")");

            setRepeatMode(mPreferences.getIntPref(PREFERENCE_KEY_REPEAT_MODE, REPEAT_MODE_NONE));
            setShuffleMode(mPreferences.getIntPref(PREFERENCE_KEY_SHUFFLE_MODE, SHUFFLE_MODE_NONE));
            if (mShuffleMode != SHUFFLE_MODE_NONE) {
                // in shuffle mode we need to restore the history too
                q = mPreferences.getStringState(STATE_KEY_HISTORY, "");
                qlen = q != null ? q.length() : 0;
                if (qlen > 1) {
                    plen = 0;
                    n = 0;
                    shift = 0;
                    mHistory.clear();
                    for (int i = 0; i < qlen; i++) {
                        char c = q.charAt(i);
                        if (c == ';') {
                            if (n >= mPlayListLen) {
                                // bogus history data
                                mHistory.clear();
                                break;
                            }
                            if (!mHistory.contains(mPlayPos)) {
                                mHistory.add(mPlayPos);
                            }
                            n = 0;
                            shift = 0;
                        } else {
                            if (c >= '0' && c <= '9') {
                                n += c - '0' << shift;
                            } else if (c >= 'a' && c <= 'f') {
                                n += 10 + c - 'a' << shift;
                            } else {
                                // bogus history data
                                mHistory.clear();
                                break;
                            }
                            shift += 4;
                        }
                    }
                }
            }
        }
    }

    private int removeTracksInternal(int first, int last) {

        synchronized (this) {
            if (last < first)
                return 0;
            if (first < 0) {
                first = 0;
            }
            if (last >= mPlayListLen) {
                last = mPlayListLen - 1;
            }

            boolean gotonext = false;
            if (first <= mPlayPos && mPlayPos <= last) {
                mPlayPos = first;
                gotonext = true;
            } else if (mPlayPos > last) {
                mPlayPos -= last - first + 1;
            }
            int num = mPlayListLen - last - 1;
            for (int i = 0; i < num; i++) {
                mPlayList[first + i] = mPlayList[last + 1 + i];
            }
            mPlayListLen -= last - first + 1;

            if (gotonext) {
                if (mPlayListLen == 0) {
                    stop(true);
                    mPlayPos = -1;
                } else {
                    if (mPlayPos >= mPlayListLen) {
                        mPlayPos = 0;
                    }
                    final boolean wasPlaying = isPlaying();
                    stop(false);
                    openCurrent();
                    if (wasPlaying) {
                        play();
                    }
                }
                notifyChange(BROADCAST_MEDIA_CHANGED);
            }
            return last - first + 1;
        }
    }

    private void saveBookmarkIfNeeded() {

        try {
            if (isPodcast()) {
                long pos = getPosition();
                long bookmark = getBookmark();
                long duration = getDuration();
                if (pos < bookmark && pos + 10000 > bookmark || pos > bookmark && pos - 10000 < bookmark) // The existing bookmark is
                    // close to the current
                    // position, so don't update it.
                    return;
                if (pos < 15000 || pos + 10000 > duration) {
                    // if we're near the start or end, clear the bookmark
                    pos = 0;
                }

                // write 'pos' to the bookmark field
                ContentValues values = new ContentValues();
                values.put(MediaStore.Audio.Media.BOOKMARK, pos);
                Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mTrackInfo.id);
                mResolver.update(uri, values, null, null);
            }
        } catch (SQLiteException ex) {
        }
    }

    private void saveQueue(boolean full) {

        if (!mQueueIsSaveable)
            return;

        // long start = System.currentTimeMillis();
        if (full) {
            StringBuilder q = new StringBuilder();

            // The current playlist is saved as a list of "reverse hexadecimal"
            // numbers, which we can generate faster than normal decimal or
            // hexadecimal numbers, which in turn allows us to save the playlist
            // more often without worrying too much about performance.
            // (saving the full state takes about 40 ms under no-load conditions
            // on the phone)
            int len = mPlayListLen;
            for (int i = 0; i < len; i++) {
                long n = mPlayList[i];
                if (n < 0) {
                    continue;
                } else if (n == 0) {
                    q.append("0;");
                } else {
                    while (n != 0) {
                        int digit = (int) (n & 0xf);
                        n >>>= 4;
                        q.append(hexdigits[digit]);
                    }
                    q.append(";");
                }
            }
            // Log.i("@@@@ service", "created queue string in " +
            // (System.currentTimeMillis() - start) + " ms");
            mPreferences.setStringState(STATE_KEY_QUEUE, q.toString());
            mPreferences.setIntState(STATE_KEY_CARDID, mCardId);
            if (mShuffleMode != SHUFFLE_MODE_NONE) {
                // In shuffle mode we need to save the history too
                len = mHistory.size();
                q.setLength(0);
                for (int i = 0; i < len; i++) {
                    int n = mHistory.get(i);
                    if (n == 0) {
                        q.append("0;");
                    } else {
                        while (n != 0) {
                            int digit = n & 0xf;
                            n >>>= 4;
                            q.append(hexdigits[digit]);
                        }
                        q.append(";");
                    }
                }
                mPreferences.setStringState(STATE_KEY_HISTORY, q.toString());
            }
        }
        mPreferences.setIntState(STATE_KEY_CURRPOS, mPlayPos);
        if (mPlayer.isInitialized()) {
            mPreferences.setLongState(STATE_KEY_SEEKPOS, mPlayer.getPosition());
        }
        // Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis()
        // - start) + " ms");
    }

    private void sendScrobbleBroadcast(int state) {
        final boolean enabled = mPreferences.getBooleanPref(KEY_ENABLE_SCROBBLING, false);
        final TrackInfo track = getTrackInfo();
        if (!enabled || track == null)
            return;
        final Intent i = new Intent(SCROBBLE_SLS_API);
        i.putExtra(BROADCAST_KEY_APP_NAME, getString(R.string.app_name));
        i.putExtra(BROADCAST_KEY_APP_PACKAGE, getPackageName());
        i.putExtra(BROADCAST_KEY_STATE, state);
        i.putExtra(BROADCAST_KEY_ARTIST, track.artist);
        i.putExtra(BROADCAST_KEY_ALBUM, track.album);
        i.putExtra(BROADCAST_KEY_TRACK, track.title);
        i.putExtra(BROADCAST_KEY_DURATION, (int) (getDuration() / 1000));
        sendBroadcast(i);
    }

    private void startSleepTimer(long milliseconds, boolean gentle) {

        Calendar now = Calendar.getInstance();
        mCurrentTimestamp = now.getTimeInMillis();
        mStopTimestamp = mCurrentTimestamp + milliseconds;

        int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT | DateUtils.FORMAT_ABBREV_ALL
                | DateUtils.FORMAT_CAP_AMPM;

        format_flags |= DateUtils.FORMAT_SHOW_TIME;
        String time = DateUtils.formatDateTime(this, mStopTimestamp, format_flags);

        CharSequence contentTitle = getString(R.string.sleep_timer_enabled);
        CharSequence contentText = getString(R.string.notification_sleep_timer, time);
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(), 0);
        Notification notification = new Notification(R.drawable.ic_stat_playback, null, 0);
        notification.flags = Notification.FLAG_ONGOING_EVENT;
        notification.icon = R.drawable.ic_stat_sleeptimer;
        notification.setLatestEventInfo(this, contentTitle, contentText, contentIntent);

        mGentleSleepTimer = gentle;
        mNotificationManager.notify(ID_NOTIFICATION_SLEEPTIMER, notification);
        mSleepTimerHandler.sendEmptyMessageDelayed(START_SLEEP_TIMER, milliseconds);
        final int nmin = (int) milliseconds / 60 / 1000;
        Toast.makeText(this, getResources().getQuantityString(R.plurals.NNNminutes_notif, nmin, nmin),
                Toast.LENGTH_SHORT).show();
    }

    private void stop(boolean remove_status_icon) {

        if (mPlayer.isInitialized()) {
            mPlayer.stop();
        }
        mTrackInfo = null;
        if (remove_status_icon) {
            gotoIdleState();
        } else {
            mNotificationManager.cancel(ID_NOTIFICATION_PLAYBACK);
        }
        if (remove_status_icon) {
            mIsSupposedToBePlaying = false;
        }
    }

    private void stopSleepTimer() {

        mSleepTimerHandler.sendEmptyMessage(STOP_SLEEP_TIMER);
        Toast.makeText(this, R.string.sleep_timer_disabled, Toast.LENGTH_SHORT).show();
    }

    /**
     * Provides a unified interface for dealing with midi files and other media
     * files.
     */
    private class MultiPlayer {

        private final Context mContext;
        private MediaPlayer mMediaPlayer;
        private Handler mHandler;
        private boolean mIsInitialized = false;

        private MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {

            @Override
            public void onCompletion(MediaPlayer mp) {

                // Acquire a temporary wakelock, since when we return from
                // this callback the MediaPlayer will release its wakelock
                // and allow the device to go to sleep.
                // This temporary wakelock is released when the RELEASE_WAKELOCK
                // message is processed, but just in case, put a timeout on it.
                mWakeLock.acquire(30000);
                mHandler.sendEmptyMessage(TRACK_ENDED);
                mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
            }
        };

        private MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {

            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {

                switch (what) {
                case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
                    mIsInitialized = false;
                    release();
                    // Creating a new MediaPlayer and settings its wakemode
                    // does not require the media service, so it's OK to do
                    // this now, while the service is still being restarted
                    initMediaPlayer();
                    mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
                    return true;
                default:
                    Log.d("MultiPlayer", "Error: " + what + "," + extra);
                    break;
                }
                return false;
            }
        };

        public MultiPlayer(Context context) {
            mContext = context;
            initMediaPlayer();
        }

        void initMediaPlayer() {
            mIsInitialized = false;
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
            final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
            intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
            intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
            intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC);
            mContext.sendBroadcast(intent);
        }

        public long getDuration() {
            return mMediaPlayer.getDuration();
        }

        public int getAudioSessionId() {
            return mMediaPlayer.getAudioSessionId();
        }

        public boolean isInitialized() {
            return mIsInitialized;
        }

        public boolean isPlaying() {
            return mMediaPlayer.isPlaying();
        }

        public void pause() {
            mMediaPlayer.pause();
        }

        public long getPosition() {
            return mMediaPlayer.getCurrentPosition();
        }

        /**
         * You CANNOT use this player anymore after calling release()
         */
        public void release() {
            final Intent intent = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
            intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
            intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
            intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC);
            mContext.sendBroadcast(intent);
            mIsInitialized = false;
            if (isInitialized() || isPlaying()) {
                stop();
            }
            mMediaPlayer.release();
        }

        public long seek(long whereto) {
            mMediaPlayer.seekTo((int) whereto);
            return whereto;
        }

        public void setDataSource(String path) {
            try {
                mMediaPlayer.reset();
                mMediaPlayer.setOnPreparedListener(null);
                if (path.startsWith("content://")) {
                    mMediaPlayer.setDataSource(mContext, Uri.parse(path));
                } else {
                    mMediaPlayer.setDataSource(path);
                }
                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
                mMediaPlayer.prepare();
            } catch (IOException ex) {
                mIsInitialized = false;
                return;
            } catch (IllegalArgumentException ex) {
                mIsInitialized = false;
                return;
            }
            mMediaPlayer.setOnCompletionListener(listener);
            mMediaPlayer.setOnErrorListener(errorListener);
            mIsInitialized = true;
        }

        public void setHandler(Handler handler) {
            mHandler = handler;
        }

        public void setVolume(float vol) {
            mMediaPlayer.setVolume(vol, vol);
            mCurrentVolume = vol;
        }

        public void start() {
            MusicUtils.debugLog(new Exception("MultiPlayer.start called"));
            mMediaPlayer.start();
        }

        public void stop() {
            mMediaPlayer.reset();
            mIsInitialized = false;
        }

    }

    // A simple variation of Random that makes sure that the
    // value it returns is not equal to the value it returned
    // previously, unless the interval is 1.
    private class Shuffler {

        private int mPrevious;
        private Random mRandom = new Random();

        public int shuffle(int interval) {

            int ret;
            long ret_id;
            do {
                ret = mRandom.nextInt(interval);
                ret_id = mPlayList[ret];
            } while (ret == mPrevious && interval > 1 || !isFavorite(ret_id) && mHistory.contains(ret_id));

            mPrevious = ret;
            return ret;
        }
    }

    /*
     * By making this a static class with a WeakReference to the Service, we
     * ensure that the Service can be GCd even when the system process still has
     * a remote reference to the stub.
     */
    private static class ServiceStub extends IMusicPlaybackService.Stub {

        private final WeakReference<MusicPlaybackService> mService;

        private ServiceStub(final MusicPlaybackService service) {
            mService = new WeakReference<MusicPlaybackService>(service);
        }

        @Override
        public long getDuration() {
            return mService.get().getDuration();
        }

        @Override
        public void enqueue(long[] list, int action) {
            mService.get().enqueue(list, action);
        }

        @Override
        public int getAudioSessionId() {
            return mService.get().getAudioSessionId();
        }

        @Override
        public TrackInfo getTrackInfo() {
            return mService.get().getTrackInfo();
        }

        @Override
        public long[] getQueue() {
            return mService.get().getQueue();
        }

        @Override
        public int getQueuePosition() {
            return mService.get().getQueuePosition();
        }

        @Override
        public int getRepeatMode() {
            return mService.get().getRepeatMode();
        }

        @Override
        public int getShuffleMode() {
            return mService.get().getShuffleMode();
        }

        @Override
        public long getSleepTimerRemained() {
            return mService.get().getSleepTimerRemained();
        }

        @Override
        public boolean isPlaying() {
            return mService.get().isPlaying();
        }

        @Override
        public void moveQueueItem(int from, int to) {
            mService.get().moveQueueItem(from, to);
        }

        @Override
        public void next() {
            mService.get().next(true);
        }

        @Override
        public void open(long[] list, int position) {
            mService.get().open(list, position);
        }

        @Override
        public void pause() {
            mService.get().pause();
        }

        @Override
        public void play() {
            mService.get().play();
        }

        @Override
        public long getPosition() {
            return mService.get().getPosition();
        }

        @Override
        public void prev() {
            mService.get().prev();
        }

        @Override
        public int removeTrack(long id) {
            return mService.get().removeTrack(id);
        }

        @Override
        public int removeTracks(int first, int last) {
            return mService.get().removeTracks(first, last);
        }

        @Override
        public long seek(long pos) {
            return mService.get().seek(pos);
        }

        @Override
        public void setQueueId(long id) {
            mService.get().setQueueId(id);
        }

        @Override
        public void setQueuePosition(int pos) {
            mService.get().setQueuePosition(pos);
        }

        @Override
        public void setRepeatMode(final int mode) {
            mService.get().setRepeatMode(mode);
        }

        @Override
        public void setShuffleMode(final int mode) {
            mService.get().setShuffleMode(mode);
        }

        @Override
        public void startSleepTimer(long milliseconds, boolean gentle) {
            mService.get().startSleepTimer(milliseconds, gentle);
        }

        @Override
        public void stop() {
            mService.get().stop();
        }

        @Override
        public void stopSleepTimer() {
            mService.get().stopSleepTimer();
        }

        @Override
        public boolean togglePause() {
            return mService.get().togglePause();
        }

    }
}