Home | History | Annotate | Download | only in music
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.music;
     18 
     19 import android.app.Notification;
     20 import android.app.PendingIntent;
     21 import android.app.Service;
     22 import android.appwidget.AppWidgetManager;
     23 import android.content.ComponentName;
     24 import android.content.ContentResolver;
     25 import android.content.ContentUris;
     26 import android.content.ContentValues;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.IntentFilter;
     30 import android.content.BroadcastReceiver;
     31 import android.content.SharedPreferences;
     32 import android.content.SharedPreferences.Editor;
     33 import android.database.Cursor;
     34 import android.database.sqlite.SQLiteException;
     35 import android.media.audiofx.AudioEffect;
     36 import android.media.AudioManager;
     37 import android.media.AudioManager.OnAudioFocusChangeListener;
     38 import android.media.MediaPlayer;
     39 import android.net.Uri;
     40 import android.os.Handler;
     41 import android.os.IBinder;
     42 import android.os.Message;
     43 import android.os.PowerManager;
     44 import android.os.SystemClock;
     45 import android.os.PowerManager.WakeLock;
     46 import android.provider.MediaStore;
     47 import android.util.Log;
     48 import android.widget.RemoteViews;
     49 import android.widget.Toast;
     50 
     51 import java.io.FileDescriptor;
     52 import java.io.IOException;
     53 import java.io.PrintWriter;
     54 import java.lang.ref.WeakReference;
     55 import java.util.Random;
     56 import java.util.Vector;
     57 
     58 /**
     59  * Provides "background" audio playback capabilities, allowing the
     60  * user to switch between activities without stopping playback.
     61  */
     62 public class MediaPlaybackService extends Service {
     63     /** used to specify whether enqueue() should start playing
     64      * the new list of files right away, next or once all the currently
     65      * queued files have been played
     66      */
     67     public static final int NOW = 1;
     68     public static final int NEXT = 2;
     69     public static final int LAST = 3;
     70     public static final int PLAYBACKSERVICE_STATUS = 1;
     71 
     72     public static final int SHUFFLE_NONE = 0;
     73     public static final int SHUFFLE_NORMAL = 1;
     74     public static final int SHUFFLE_AUTO = 2;
     75 
     76     public static final int REPEAT_NONE = 0;
     77     public static final int REPEAT_CURRENT = 1;
     78     public static final int REPEAT_ALL = 2;
     79 
     80     public static final String PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
     81     public static final String META_CHANGED = "com.android.music.metachanged";
     82     public static final String QUEUE_CHANGED = "com.android.music.queuechanged";
     83 
     84     public static final String SERVICECMD = "com.android.music.musicservicecommand";
     85     public static final String CMDNAME = "command";
     86     public static final String CMDTOGGLEPAUSE = "togglepause";
     87     public static final String CMDSTOP = "stop";
     88     public static final String CMDPAUSE = "pause";
     89     public static final String CMDPREVIOUS = "previous";
     90     public static final String CMDNEXT = "next";
     91 
     92     public static final String TOGGLEPAUSE_ACTION = "com.android.music.musicservicecommand.togglepause";
     93     public static final String PAUSE_ACTION = "com.android.music.musicservicecommand.pause";
     94     public static final String PREVIOUS_ACTION = "com.android.music.musicservicecommand.previous";
     95     public static final String NEXT_ACTION = "com.android.music.musicservicecommand.next";
     96 
     97     private static final int TRACK_ENDED = 1;
     98     private static final int RELEASE_WAKELOCK = 2;
     99     private static final int SERVER_DIED = 3;
    100     private static final int FADEIN = 4;
    101     private static final int MAX_HISTORY_SIZE = 100;
    102 
    103     private MultiPlayer mPlayer;
    104     private String mFileToPlay;
    105     private int mShuffleMode = SHUFFLE_NONE;
    106     private int mRepeatMode = REPEAT_NONE;
    107     private int mMediaMountedCount = 0;
    108     private long [] mAutoShuffleList = null;
    109     private long [] mPlayList = null;
    110     private int mPlayListLen = 0;
    111     private Vector<Integer> mHistory = new Vector<Integer>(MAX_HISTORY_SIZE);
    112     private Cursor mCursor;
    113     private int mPlayPos = -1;
    114     private static final String LOGTAG = "MediaPlaybackService";
    115     private final Shuffler mRand = new Shuffler();
    116     private int mOpenFailedCounter = 0;
    117     String[] mCursorCols = new String[] {
    118             "audio._id AS _id",             // index must match IDCOLIDX below
    119             MediaStore.Audio.Media.ARTIST,
    120             MediaStore.Audio.Media.ALBUM,
    121             MediaStore.Audio.Media.TITLE,
    122             MediaStore.Audio.Media.DATA,
    123             MediaStore.Audio.Media.MIME_TYPE,
    124             MediaStore.Audio.Media.ALBUM_ID,
    125             MediaStore.Audio.Media.ARTIST_ID,
    126             MediaStore.Audio.Media.IS_PODCAST, // index must match PODCASTCOLIDX below
    127             MediaStore.Audio.Media.BOOKMARK    // index must match BOOKMARKCOLIDX below
    128     };
    129     private final static int IDCOLIDX = 0;
    130     private final static int PODCASTCOLIDX = 8;
    131     private final static int BOOKMARKCOLIDX = 9;
    132     private BroadcastReceiver mUnmountReceiver = null;
    133     private WakeLock mWakeLock;
    134     private int mServiceStartId = -1;
    135     private boolean mServiceInUse = false;
    136     private boolean mIsSupposedToBePlaying = false;
    137     private boolean mQuietMode = false;
    138     private AudioManager mAudioManager;
    139     private boolean mQueueIsSaveable = true;
    140     // used to track what type of audio focus loss caused the playback to pause
    141     private boolean mPausedByTransientLossOfFocus = false;
    142 
    143     private SharedPreferences mPreferences;
    144     // We use this to distinguish between different cards when saving/restoring playlists.
    145     // This will have to change if we want to support multiple simultaneous cards.
    146     private int mCardId;
    147 
    148     private MediaAppWidgetProvider mAppWidgetProvider = MediaAppWidgetProvider.getInstance();
    149 
    150     // interval after which we stop the service when idle
    151     private static final int IDLE_DELAY = 60000;
    152 
    153     private void startAndFadeIn() {
    154         mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
    155     }
    156 
    157     private Handler mMediaplayerHandler = new Handler() {
    158         float mCurrentVolume = 1.0f;
    159         @Override
    160         public void handleMessage(Message msg) {
    161             MusicUtils.debugLog("mMediaplayerHandler.handleMessage " + msg.what);
    162             switch (msg.what) {
    163                 case FADEIN:
    164                     if (!isPlaying()) {
    165                         mCurrentVolume = 0f;
    166                         mPlayer.setVolume(mCurrentVolume);
    167                         play();
    168                         mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
    169                     } else {
    170                         mCurrentVolume += 0.01f;
    171                         if (mCurrentVolume < 1.0f) {
    172                             mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
    173                         } else {
    174                             mCurrentVolume = 1.0f;
    175                         }
    176                         mPlayer.setVolume(mCurrentVolume);
    177                     }
    178                     break;
    179                 case SERVER_DIED:
    180                     if (mIsSupposedToBePlaying) {
    181                         next(true);
    182                     } else {
    183                         // the server died when we were idle, so just
    184                         // reopen the same song (it will start again
    185                         // from the beginning though when the user
    186                         // restarts)
    187                         openCurrent();
    188                     }
    189                     break;
    190                 case TRACK_ENDED:
    191                     if (mRepeatMode == REPEAT_CURRENT) {
    192                         seek(0);
    193                         play();
    194                     } else {
    195                         next(false);
    196                     }
    197                     break;
    198                 case RELEASE_WAKELOCK:
    199                     mWakeLock.release();
    200                     break;
    201                 default:
    202                     break;
    203             }
    204         }
    205     };
    206 
    207     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
    208         @Override
    209         public void onReceive(Context context, Intent intent) {
    210             String action = intent.getAction();
    211             String cmd = intent.getStringExtra("command");
    212             MusicUtils.debugLog("mIntentReceiver.onReceive " + action + " / " + cmd);
    213             if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
    214                 next(true);
    215             } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
    216                 prev();
    217             } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
    218                 if (isPlaying()) {
    219                     pause();
    220                     mPausedByTransientLossOfFocus = false;
    221                 } else {
    222                     play();
    223                 }
    224             } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
    225                 pause();
    226                 mPausedByTransientLossOfFocus = false;
    227             } else if (CMDSTOP.equals(cmd)) {
    228                 pause();
    229                 mPausedByTransientLossOfFocus = false;
    230                 seek(0);
    231             } else if (MediaAppWidgetProvider.CMDAPPWIDGETUPDATE.equals(cmd)) {
    232                 // Someone asked us to refresh a set of specific widgets, probably
    233                 // because they were just added.
    234                 int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
    235                 mAppWidgetProvider.performUpdate(MediaPlaybackService.this, appWidgetIds);
    236             }
    237         }
    238     };
    239 
    240     private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
    241         public void onAudioFocusChange(int focusChange) {
    242             // AudioFocus is a new feature: focus updates are made verbose on purpose
    243             switch (focusChange) {
    244                 case AudioManager.AUDIOFOCUS_LOSS:
    245                     Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS");
    246                     if(isPlaying()) {
    247                         mPausedByTransientLossOfFocus = false;
    248                         pause();
    249                     }
    250                     break;
    251                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
    252                 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
    253                     Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS_TRANSIENT");
    254                     if(isPlaying()) {
    255                         mPausedByTransientLossOfFocus = true;
    256                         pause();
    257                     }
    258                     break;
    259                 case AudioManager.AUDIOFOCUS_GAIN:
    260                     Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_GAIN");
    261                     if(!isPlaying() && mPausedByTransientLossOfFocus) {
    262                         mPausedByTransientLossOfFocus = false;
    263                         startAndFadeIn();
    264                     }
    265                     break;
    266                 default:
    267                     Log.e(LOGTAG, "Unknown audio focus change code");
    268             }
    269         }
    270     };
    271 
    272     public MediaPlaybackService() {
    273     }
    274 
    275     @Override
    276     public void onCreate() {
    277         super.onCreate();
    278 
    279         mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    280         mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(),
    281                 MediaButtonIntentReceiver.class.getName()));
    282 
    283         mPreferences = getSharedPreferences("Music", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
    284         mCardId = MusicUtils.getCardId(this);
    285 
    286         registerExternalStorageListener();
    287 
    288         // Needs to be done in this thread, since otherwise ApplicationContext.getPowerManager() crashes.
    289         mPlayer = new MultiPlayer();
    290         mPlayer.setHandler(mMediaplayerHandler);
    291 
    292         reloadQueue();
    293 
    294         IntentFilter commandFilter = new IntentFilter();
    295         commandFilter.addAction(SERVICECMD);
    296         commandFilter.addAction(TOGGLEPAUSE_ACTION);
    297         commandFilter.addAction(PAUSE_ACTION);
    298         commandFilter.addAction(NEXT_ACTION);
    299         commandFilter.addAction(PREVIOUS_ACTION);
    300         registerReceiver(mIntentReceiver, commandFilter);
    301 
    302         PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
    303         mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
    304         mWakeLock.setReferenceCounted(false);
    305 
    306         // If the service was idle, but got killed before it stopped itself, the
    307         // system will relaunch it. Make sure it gets stopped again in that case.
    308         Message msg = mDelayedStopHandler.obtainMessage();
    309         mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
    310     }
    311 
    312     @Override
    313     public void onDestroy() {
    314         // Check that we're not being destroyed while something is still playing.
    315         if (isPlaying()) {
    316             Log.e(LOGTAG, "Service being destroyed while still playing.");
    317         }
    318         // release all MediaPlayer resources, including the native player and wakelocks
    319         Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
    320         i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
    321         i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
    322         sendBroadcast(i);
    323         mPlayer.release();
    324         mPlayer = null;
    325 
    326         mAudioManager.abandonAudioFocus(mAudioFocusListener);
    327 
    328         // make sure there aren't any other messages coming
    329         mDelayedStopHandler.removeCallbacksAndMessages(null);
    330         mMediaplayerHandler.removeCallbacksAndMessages(null);
    331 
    332         if (mCursor != null) {
    333             mCursor.close();
    334             mCursor = null;
    335         }
    336 
    337         unregisterReceiver(mIntentReceiver);
    338         if (mUnmountReceiver != null) {
    339             unregisterReceiver(mUnmountReceiver);
    340             mUnmountReceiver = null;
    341         }
    342         mWakeLock.release();
    343         super.onDestroy();
    344     }
    345 
    346     private final char hexdigits [] = new char [] {
    347             '0', '1', '2', '3',
    348             '4', '5', '6', '7',
    349             '8', '9', 'a', 'b',
    350             'c', 'd', 'e', 'f'
    351     };
    352 
    353     private void saveQueue(boolean full) {
    354         if (!mQueueIsSaveable) {
    355             return;
    356         }
    357 
    358         Editor ed = mPreferences.edit();
    359         //long start = System.currentTimeMillis();
    360         if (full) {
    361             StringBuilder q = new StringBuilder();
    362 
    363             // The current playlist is saved as a list of "reverse hexadecimal"
    364             // numbers, which we can generate faster than normal decimal or
    365             // hexadecimal numbers, which in turn allows us to save the playlist
    366             // more often without worrying too much about performance.
    367             // (saving the full state takes about 40 ms under no-load conditions
    368             // on the phone)
    369             int len = mPlayListLen;
    370             for (int i = 0; i < len; i++) {
    371                 long n = mPlayList[i];
    372                 if (n < 0) {
    373                     continue;
    374                 } else if (n == 0) {
    375                     q.append("0;");
    376                 } else {
    377                     while (n != 0) {
    378                         int digit = (int)(n & 0xf);
    379                         n >>>= 4;
    380                         q.append(hexdigits[digit]);
    381                     }
    382                     q.append(";");
    383                 }
    384             }
    385             //Log.i("@@@@ service", "created queue string in " + (System.currentTimeMillis() - start) + " ms");
    386             ed.putString("queue", q.toString());
    387             ed.putInt("cardid", mCardId);
    388             if (mShuffleMode != SHUFFLE_NONE) {
    389                 // In shuffle mode we need to save the history too
    390                 len = mHistory.size();
    391                 q.setLength(0);
    392                 for (int i = 0; i < len; i++) {
    393                     int n = mHistory.get(i);
    394                     if (n == 0) {
    395                         q.append("0;");
    396                     } else {
    397                         while (n != 0) {
    398                             int digit = (n & 0xf);
    399                             n >>>= 4;
    400                             q.append(hexdigits[digit]);
    401                         }
    402                         q.append(";");
    403                     }
    404                 }
    405                 ed.putString("history", q.toString());
    406             }
    407         }
    408         ed.putInt("curpos", mPlayPos);
    409         if (mPlayer.isInitialized()) {
    410             ed.putLong("seekpos", mPlayer.position());
    411         }
    412         ed.putInt("repeatmode", mRepeatMode);
    413         ed.putInt("shufflemode", mShuffleMode);
    414         SharedPreferencesCompat.apply(ed);
    415 
    416         //Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() - start) + " ms");
    417     }
    418 
    419     private void reloadQueue() {
    420         String q = null;
    421 
    422         boolean newstyle = false;
    423         int id = mCardId;
    424         if (mPreferences.contains("cardid")) {
    425             newstyle = true;
    426             id = mPreferences.getInt("cardid", ~mCardId);
    427         }
    428         if (id == mCardId) {
    429             // Only restore the saved playlist if the card is still
    430             // the same one as when the playlist was saved
    431             q = mPreferences.getString("queue", "");
    432         }
    433         int qlen = q != null ? q.length() : 0;
    434         if (qlen > 1) {
    435             //Log.i("@@@@ service", "loaded queue: " + q);
    436             int plen = 0;
    437             int n = 0;
    438             int shift = 0;
    439             for (int i = 0; i < qlen; i++) {
    440                 char c = q.charAt(i);
    441                 if (c == ';') {
    442                     ensurePlayListCapacity(plen + 1);
    443                     mPlayList[plen] = n;
    444                     plen++;
    445                     n = 0;
    446                     shift = 0;
    447                 } else {
    448                     if (c >= '0' && c <= '9') {
    449                         n += ((c - '0') << shift);
    450                     } else if (c >= 'a' && c <= 'f') {
    451                         n += ((10 + c - 'a') << shift);
    452                     } else {
    453                         // bogus playlist data
    454                         plen = 0;
    455                         break;
    456                     }
    457                     shift += 4;
    458                 }
    459             }
    460             mPlayListLen = plen;
    461 
    462             int pos = mPreferences.getInt("curpos", 0);
    463             if (pos < 0 || pos >= mPlayListLen) {
    464                 // The saved playlist is bogus, discard it
    465                 mPlayListLen = 0;
    466                 return;
    467             }
    468             mPlayPos = pos;
    469 
    470             // When reloadQueue is called in response to a card-insertion,
    471             // we might not be able to query the media provider right away.
    472             // To deal with this, try querying for the current file, and if
    473             // that fails, wait a while and try again. If that too fails,
    474             // assume there is a problem and don't restore the state.
    475             Cursor crsr = MusicUtils.query(this,
    476                         MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
    477                         new String [] {"_id"}, "_id=" + mPlayList[mPlayPos] , null, null);
    478             if (crsr == null || crsr.getCount() == 0) {
    479                 // wait a bit and try again
    480                 SystemClock.sleep(3000);
    481                 crsr = getContentResolver().query(
    482                         MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
    483                         mCursorCols, "_id=" + mPlayList[mPlayPos] , null, null);
    484             }
    485             if (crsr != null) {
    486                 crsr.close();
    487             }
    488 
    489             // Make sure we don't auto-skip to the next song, since that
    490             // also starts playback. What could happen in that case is:
    491             // - music is paused
    492             // - go to UMS and delete some files, including the currently playing one
    493             // - come back from UMS
    494             // (time passes)
    495             // - music app is killed for some reason (out of memory)
    496             // - music service is restarted, service restores state, doesn't find
    497             //   the "current" file, goes to the next and: playback starts on its
    498             //   own, potentially at some random inconvenient time.
    499             mOpenFailedCounter = 20;
    500             mQuietMode = true;
    501             openCurrent();
    502             mQuietMode = false;
    503             if (!mPlayer.isInitialized()) {
    504                 // couldn't restore the saved state
    505                 mPlayListLen = 0;
    506                 return;
    507             }
    508 
    509             long seekpos = mPreferences.getLong("seekpos", 0);
    510             seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
    511             Log.d(LOGTAG, "restored queue, currently at position "
    512                     + position() + "/" + duration()
    513                     + " (requested " + seekpos + ")");
    514 
    515             int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
    516             if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
    517                 repmode = REPEAT_NONE;
    518             }
    519             mRepeatMode = repmode;
    520 
    521             int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
    522             if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
    523                 shufmode = SHUFFLE_NONE;
    524             }
    525             if (shufmode != SHUFFLE_NONE) {
    526                 // in shuffle mode we need to restore the history too
    527                 q = mPreferences.getString("history", "");
    528                 qlen = q != null ? q.length() : 0;
    529                 if (qlen > 1) {
    530                     plen = 0;
    531                     n = 0;
    532                     shift = 0;
    533                     mHistory.clear();
    534                     for (int i = 0; i < qlen; i++) {
    535                         char c = q.charAt(i);
    536                         if (c == ';') {
    537                             if (n >= mPlayListLen) {
    538                                 // bogus history data
    539                                 mHistory.clear();
    540                                 break;
    541                             }
    542                             mHistory.add(n);
    543                             n = 0;
    544                             shift = 0;
    545                         } else {
    546                             if (c >= '0' && c <= '9') {
    547                                 n += ((c - '0') << shift);
    548                             } else if (c >= 'a' && c <= 'f') {
    549                                 n += ((10 + c - 'a') << shift);
    550                             } else {
    551                                 // bogus history data
    552                                 mHistory.clear();
    553                                 break;
    554                             }
    555                             shift += 4;
    556                         }
    557                     }
    558                 }
    559             }
    560             if (shufmode == SHUFFLE_AUTO) {
    561                 if (! makeAutoShuffleList()) {
    562                     shufmode = SHUFFLE_NONE;
    563                 }
    564             }
    565             mShuffleMode = shufmode;
    566         }
    567     }
    568 
    569     @Override
    570     public IBinder onBind(Intent intent) {
    571         mDelayedStopHandler.removeCallbacksAndMessages(null);
    572         mServiceInUse = true;
    573         return mBinder;
    574     }
    575 
    576     @Override
    577     public void onRebind(Intent intent) {
    578         mDelayedStopHandler.removeCallbacksAndMessages(null);
    579         mServiceInUse = true;
    580     }
    581 
    582     @Override
    583     public int onStartCommand(Intent intent, int flags, int startId) {
    584         mServiceStartId = startId;
    585         mDelayedStopHandler.removeCallbacksAndMessages(null);
    586 
    587         if (intent != null) {
    588             String action = intent.getAction();
    589             String cmd = intent.getStringExtra("command");
    590             MusicUtils.debugLog("onStartCommand " + action + " / " + cmd);
    591 
    592             if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
    593                 next(true);
    594             } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
    595                 if (position() < 2000) {
    596                     prev();
    597                 } else {
    598                     seek(0);
    599                     play();
    600                 }
    601             } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
    602                 if (isPlaying()) {
    603                     pause();
    604                     mPausedByTransientLossOfFocus = false;
    605                 } else {
    606                     play();
    607                 }
    608             } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
    609                 pause();
    610                 mPausedByTransientLossOfFocus = false;
    611             } else if (CMDSTOP.equals(cmd)) {
    612                 pause();
    613                 mPausedByTransientLossOfFocus = false;
    614                 seek(0);
    615             }
    616         }
    617 
    618         // make sure the service will shut down on its own if it was
    619         // just started but not bound to and nothing is playing
    620         mDelayedStopHandler.removeCallbacksAndMessages(null);
    621         Message msg = mDelayedStopHandler.obtainMessage();
    622         mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
    623         return START_STICKY;
    624     }
    625 
    626     @Override
    627     public boolean onUnbind(Intent intent) {
    628         mServiceInUse = false;
    629 
    630         // Take a snapshot of the current playlist
    631         saveQueue(true);
    632 
    633         if (isPlaying() || mPausedByTransientLossOfFocus) {
    634             // something is currently playing, or will be playing once
    635             // an in-progress action requesting audio focus ends, so don't stop the service now.
    636             return true;
    637         }
    638 
    639         // If there is a playlist but playback is paused, then wait a while
    640         // before stopping the service, so that pause/resume isn't slow.
    641         // Also delay stopping the service if we're transitioning between tracks.
    642         if (mPlayListLen > 0  || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
    643             Message msg = mDelayedStopHandler.obtainMessage();
    644             mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
    645             return true;
    646         }
    647 
    648         // No active playlist, OK to stop the service right now
    649         stopSelf(mServiceStartId);
    650         return true;
    651     }
    652 
    653     private Handler mDelayedStopHandler = new Handler() {
    654         @Override
    655         public void handleMessage(Message msg) {
    656             // Check again to make sure nothing is playing right now
    657             if (isPlaying() || mPausedByTransientLossOfFocus || mServiceInUse
    658                     || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
    659                 return;
    660             }
    661             // save the queue again, because it might have changed
    662             // since the user exited the music app (because of
    663             // party-shuffle or because the play-position changed)
    664             saveQueue(true);
    665             stopSelf(mServiceStartId);
    666         }
    667     };
    668 
    669     /**
    670      * Called when we receive a ACTION_MEDIA_EJECT notification.
    671      *
    672      * @param storagePath path to mount point for the removed media
    673      */
    674     public void closeExternalStorageFiles(String storagePath) {
    675         // stop playback and clean up if the SD card is going to be unmounted.
    676         stop(true);
    677         notifyChange(QUEUE_CHANGED);
    678         notifyChange(META_CHANGED);
    679     }
    680 
    681     /**
    682      * Registers an intent to listen for ACTION_MEDIA_EJECT notifications.
    683      * The intent will call closeExternalStorageFiles() if the external media
    684      * is going to be ejected, so applications can clean up any files they have open.
    685      */
    686     public void registerExternalStorageListener() {
    687         if (mUnmountReceiver == null) {
    688             mUnmountReceiver = new BroadcastReceiver() {
    689                 @Override
    690                 public void onReceive(Context context, Intent intent) {
    691                     String action = intent.getAction();
    692                     if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
    693                         saveQueue(true);
    694                         mQueueIsSaveable = false;
    695                         closeExternalStorageFiles(intent.getData().getPath());
    696                     } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
    697                         mMediaMountedCount++;
    698                         mCardId = MusicUtils.getCardId(MediaPlaybackService.this);
    699                         reloadQueue();
    700                         mQueueIsSaveable = true;
    701                         notifyChange(QUEUE_CHANGED);
    702                         notifyChange(META_CHANGED);
    703                     }
    704                 }
    705             };
    706             IntentFilter iFilter = new IntentFilter();
    707             iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
    708             iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
    709             iFilter.addDataScheme("file");
    710             registerReceiver(mUnmountReceiver, iFilter);
    711         }
    712     }
    713 
    714     /**
    715      * Notify the change-receivers that something has changed.
    716      * The intent that is sent contains the following data
    717      * for the currently playing track:
    718      * "id" - Integer: the database row ID
    719      * "artist" - String: the name of the artist
    720      * "album" - String: the name of the album
    721      * "track" - String: the name of the track
    722      * The intent has an action that is one of
    723      * "com.android.music.metachanged"
    724      * "com.android.music.queuechanged",
    725      * "com.android.music.playbackcomplete"
    726      * "com.android.music.playstatechanged"
    727      * respectively indicating that a new track has
    728      * started playing, that the playback queue has
    729      * changed, that playback has stopped because
    730      * the last file in the list has been played,
    731      * or that the play-state changed (paused/resumed).
    732      */
    733     private void notifyChange(String what) {
    734 
    735         Intent i = new Intent(what);
    736         i.putExtra("id", Long.valueOf(getAudioId()));
    737         i.putExtra("artist", getArtistName());
    738         i.putExtra("album",getAlbumName());
    739         i.putExtra("track", getTrackName());
    740         i.putExtra("playing", isPlaying());
    741         sendStickyBroadcast(i);
    742 
    743         if (what.equals(QUEUE_CHANGED)) {
    744             saveQueue(true);
    745         } else {
    746             saveQueue(false);
    747         }
    748 
    749         // Share this notification directly with our widgets
    750         mAppWidgetProvider.notifyChange(this, what);
    751     }
    752 
    753     private void ensurePlayListCapacity(int size) {
    754         if (mPlayList == null || size > mPlayList.length) {
    755             // reallocate at 2x requested size so we don't
    756             // need to grow and copy the array for every
    757             // insert
    758             long [] newlist = new long[size * 2];
    759             int len = mPlayList != null ? mPlayList.length : mPlayListLen;
    760             for (int i = 0; i < len; i++) {
    761                 newlist[i] = mPlayList[i];
    762             }
    763             mPlayList = newlist;
    764         }
    765         // FIXME: shrink the array when the needed size is much smaller
    766         // than the allocated size
    767     }
    768 
    769     // insert the list of songs at the specified position in the playlist
    770     private void addToPlayList(long [] list, int position) {
    771         int addlen = list.length;
    772         if (position < 0) { // overwrite
    773             mPlayListLen = 0;
    774             position = 0;
    775         }
    776         ensurePlayListCapacity(mPlayListLen + addlen);
    777         if (position > mPlayListLen) {
    778             position = mPlayListLen;
    779         }
    780 
    781         // move part of list after insertion point
    782         int tailsize = mPlayListLen - position;
    783         for (int i = tailsize ; i > 0 ; i--) {
    784             mPlayList[position + i] = mPlayList[position + i - addlen];
    785         }
    786 
    787         // copy list into playlist
    788         for (int i = 0; i < addlen; i++) {
    789             mPlayList[position + i] = list[i];
    790         }
    791         mPlayListLen += addlen;
    792         if (mPlayListLen == 0) {
    793             mCursor.close();
    794             mCursor = null;
    795             notifyChange(META_CHANGED);
    796         }
    797     }
    798 
    799     /**
    800      * Appends a list of tracks to the current playlist.
    801      * If nothing is playing currently, playback will be started at
    802      * the first track.
    803      * If the action is NOW, playback will switch to the first of
    804      * the new tracks immediately.
    805      * @param list The list of tracks to append.
    806      * @param action NOW, NEXT or LAST
    807      */
    808     public void enqueue(long [] list, int action) {
    809         synchronized(this) {
    810             if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
    811                 addToPlayList(list, mPlayPos + 1);
    812                 notifyChange(QUEUE_CHANGED);
    813             } else {
    814                 // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen
    815                 addToPlayList(list, Integer.MAX_VALUE);
    816                 notifyChange(QUEUE_CHANGED);
    817                 if (action == NOW) {
    818                     mPlayPos = mPlayListLen - list.length;
    819                     openCurrent();
    820                     play();
    821                     notifyChange(META_CHANGED);
    822                     return;
    823                 }
    824             }
    825             if (mPlayPos < 0) {
    826                 mPlayPos = 0;
    827                 openCurrent();
    828                 play();
    829                 notifyChange(META_CHANGED);
    830             }
    831         }
    832     }
    833 
    834     /**
    835      * Replaces the current playlist with a new list,
    836      * and prepares for starting playback at the specified
    837      * position in the list, or a random position if the
    838      * specified position is 0.
    839      * @param list The new list of tracks.
    840      */
    841     public void open(long [] list, int position) {
    842         synchronized (this) {
    843             if (mShuffleMode == SHUFFLE_AUTO) {
    844                 mShuffleMode = SHUFFLE_NORMAL;
    845             }
    846             long oldId = getAudioId();
    847             int listlength = list.length;
    848             boolean newlist = true;
    849             if (mPlayListLen == listlength) {
    850                 // possible fast path: list might be the same
    851                 newlist = false;
    852                 for (int i = 0; i < listlength; i++) {
    853                     if (list[i] != mPlayList[i]) {
    854                         newlist = true;
    855                         break;
    856                     }
    857                 }
    858             }
    859             if (newlist) {
    860                 addToPlayList(list, -1);
    861                 notifyChange(QUEUE_CHANGED);
    862             }
    863             int oldpos = mPlayPos;
    864             if (position >= 0) {
    865                 mPlayPos = position;
    866             } else {
    867                 mPlayPos = mRand.nextInt(mPlayListLen);
    868             }
    869             mHistory.clear();
    870 
    871             saveBookmarkIfNeeded();
    872             openCurrent();
    873             if (oldId != getAudioId()) {
    874                 notifyChange(META_CHANGED);
    875             }
    876         }
    877     }
    878 
    879     /**
    880      * Moves the item at index1 to index2.
    881      * @param index1
    882      * @param index2
    883      */
    884     public void moveQueueItem(int index1, int index2) {
    885         synchronized (this) {
    886             if (index1 >= mPlayListLen) {
    887                 index1 = mPlayListLen - 1;
    888             }
    889             if (index2 >= mPlayListLen) {
    890                 index2 = mPlayListLen - 1;
    891             }
    892             if (index1 < index2) {
    893                 long tmp = mPlayList[index1];
    894                 for (int i = index1; i < index2; i++) {
    895                     mPlayList[i] = mPlayList[i+1];
    896                 }
    897                 mPlayList[index2] = tmp;
    898                 if (mPlayPos == index1) {
    899                     mPlayPos = index2;
    900                 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
    901                         mPlayPos--;
    902                 }
    903             } else if (index2 < index1) {
    904                 long tmp = mPlayList[index1];
    905                 for (int i = index1; i > index2; i--) {
    906                     mPlayList[i] = mPlayList[i-1];
    907                 }
    908                 mPlayList[index2] = tmp;
    909                 if (mPlayPos == index1) {
    910                     mPlayPos = index2;
    911                 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
    912                         mPlayPos++;
    913                 }
    914             }
    915             notifyChange(QUEUE_CHANGED);
    916         }
    917     }
    918 
    919     /**
    920      * Returns the current play list
    921      * @return An array of integers containing the IDs of the tracks in the play list
    922      */
    923     public long [] getQueue() {
    924         synchronized (this) {
    925             int len = mPlayListLen;
    926             long [] list = new long[len];
    927             for (int i = 0; i < len; i++) {
    928                 list[i] = mPlayList[i];
    929             }
    930             return list;
    931         }
    932     }
    933 
    934     private void openCurrent() {
    935         synchronized (this) {
    936             if (mCursor != null) {
    937                 mCursor.close();
    938                 mCursor = null;
    939             }
    940 
    941             if (mPlayListLen == 0) {
    942                 return;
    943             }
    944             stop(false);
    945 
    946             String id = String.valueOf(mPlayList[mPlayPos]);
    947 
    948             mCursor = getContentResolver().query(
    949                     MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
    950                     mCursorCols, "_id=" + id , null, null);
    951             if (mCursor != null) {
    952                 mCursor.moveToFirst();
    953                 open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
    954                 // go to bookmark if needed
    955                 if (isPodcast()) {
    956                     long bookmark = getBookmark();
    957                     // Start playing a little bit before the bookmark,
    958                     // so it's easier to get back in to the narrative.
    959                     seek(bookmark - 5000);
    960                 }
    961             }
    962         }
    963     }
    964 
    965     /**
    966      * Opens the specified file and readies it for playback.
    967      *
    968      * @param path The full path of the file to be opened.
    969      */
    970     public void open(String path) {
    971         synchronized (this) {
    972             if (path == null) {
    973                 return;
    974             }
    975 
    976             // if mCursor is null, try to associate path with a database cursor
    977             if (mCursor == null) {
    978 
    979                 ContentResolver resolver = getContentResolver();
    980                 Uri uri;
    981                 String where;
    982                 String selectionArgs[];
    983                 if (path.startsWith("content://media/")) {
    984                     uri = Uri.parse(path);
    985                     where = null;
    986                     selectionArgs = null;
    987                 } else {
    988                    uri = MediaStore.Audio.Media.getContentUriForPath(path);
    989                    where = MediaStore.Audio.Media.DATA + "=?";
    990                    selectionArgs = new String[] { path };
    991                 }
    992 
    993                 try {
    994                     mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);
    995                     if  (mCursor != null) {
    996                         if (mCursor.getCount() == 0) {
    997                             mCursor.close();
    998                             mCursor = null;
    999                         } else {
   1000                             mCursor.moveToNext();
   1001                             ensurePlayListCapacity(1);
   1002                             mPlayListLen = 1;
   1003                             mPlayList[0] = mCursor.getLong(IDCOLIDX);
   1004                             mPlayPos = 0;
   1005                         }
   1006                     }
   1007                 } catch (UnsupportedOperationException ex) {
   1008                 }
   1009             }
   1010             mFileToPlay = path;
   1011             mPlayer.setDataSource(mFileToPlay);
   1012             if (! mPlayer.isInitialized()) {
   1013                 stop(true);
   1014                 if (mOpenFailedCounter++ < 10 &&  mPlayListLen > 1) {
   1015                     // beware: this ends up being recursive because next() calls open() again.
   1016                     next(false);
   1017                 }
   1018                 if (! mPlayer.isInitialized() && mOpenFailedCounter != 0) {
   1019                     // need to make sure we only shows this once
   1020                     mOpenFailedCounter = 0;
   1021                     if (!mQuietMode) {
   1022                         Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
   1023                     }
   1024                     Log.d(LOGTAG, "Failed to open file for playback");
   1025                 }
   1026             } else {
   1027                 mOpenFailedCounter = 0;
   1028             }
   1029         }
   1030     }
   1031 
   1032     /**
   1033      * Starts playback of a previously opened file.
   1034      */
   1035     public void play() {
   1036         mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
   1037                 AudioManager.AUDIOFOCUS_GAIN);
   1038         mAudioManager.registerMediaButtonEventReceiver(new ComponentName(this.getPackageName(),
   1039                 MediaButtonIntentReceiver.class.getName()));
   1040 
   1041         if (mPlayer.isInitialized()) {
   1042             // if we are at the end of the song, go to the next song first
   1043             long duration = mPlayer.duration();
   1044             if (mRepeatMode != REPEAT_CURRENT && duration > 2000 &&
   1045                 mPlayer.position() >= duration - 2000) {
   1046                 next(true);
   1047             }
   1048 
   1049             mPlayer.start();
   1050 
   1051             RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar);
   1052             views.setImageViewResource(R.id.icon, R.drawable.stat_notify_musicplayer);
   1053             if (getAudioId() < 0) {
   1054                 // streaming
   1055                 views.setTextViewText(R.id.trackname, getPath());
   1056                 views.setTextViewText(R.id.artistalbum, null);
   1057             } else {
   1058                 String artist = getArtistName();
   1059                 views.setTextViewText(R.id.trackname, getTrackName());
   1060                 if (artist == null || artist.equals(MediaStore.UNKNOWN_STRING)) {
   1061                     artist = getString(R.string.unknown_artist_name);
   1062                 }
   1063                 String album = getAlbumName();
   1064                 if (album == null || album.equals(MediaStore.UNKNOWN_STRING)) {
   1065                     album = getString(R.string.unknown_album_name);
   1066                 }
   1067 
   1068                 views.setTextViewText(R.id.artistalbum,
   1069                         getString(R.string.notification_artist_album, artist, album)
   1070                         );
   1071             }
   1072 
   1073             Notification status = new Notification();
   1074             status.contentView = views;
   1075             status.flags |= Notification.FLAG_ONGOING_EVENT;
   1076             status.icon = R.drawable.stat_notify_musicplayer;
   1077             status.contentIntent = PendingIntent.getActivity(this, 0,
   1078                     new Intent("com.android.music.PLAYBACK_VIEWER")
   1079                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0);
   1080             startForeground(PLAYBACKSERVICE_STATUS, status);
   1081             if (!mIsSupposedToBePlaying) {
   1082                 mIsSupposedToBePlaying = true;
   1083                 notifyChange(PLAYSTATE_CHANGED);
   1084             }
   1085 
   1086         } else if (mPlayListLen <= 0) {
   1087             // This is mostly so that if you press 'play' on a bluetooth headset
   1088             // without every having played anything before, it will still play
   1089             // something.
   1090             setShuffleMode(SHUFFLE_AUTO);
   1091         }
   1092     }
   1093 
   1094     private void stop(boolean remove_status_icon) {
   1095         if (mPlayer.isInitialized()) {
   1096             mPlayer.stop();
   1097         }
   1098         mFileToPlay = null;
   1099         if (mCursor != null) {
   1100             mCursor.close();
   1101             mCursor = null;
   1102         }
   1103         if (remove_status_icon) {
   1104             gotoIdleState();
   1105         } else {
   1106             stopForeground(false);
   1107         }
   1108         if (remove_status_icon) {
   1109             mIsSupposedToBePlaying = false;
   1110         }
   1111     }
   1112 
   1113     /**
   1114      * Stops playback.
   1115      */
   1116     public void stop() {
   1117         stop(true);
   1118     }
   1119 
   1120     /**
   1121      * Pauses playback (call play() to resume)
   1122      */
   1123     public void pause() {
   1124         synchronized(this) {
   1125             if (isPlaying()) {
   1126                 mPlayer.pause();
   1127                 gotoIdleState();
   1128                 mIsSupposedToBePlaying = false;
   1129                 notifyChange(PLAYSTATE_CHANGED);
   1130                 saveBookmarkIfNeeded();
   1131             }
   1132         }
   1133     }
   1134 
   1135     /** Returns whether something is currently playing
   1136      *
   1137      * @return true if something is playing (or will be playing shortly, in case
   1138      * we're currently transitioning between tracks), false if not.
   1139      */
   1140     public boolean isPlaying() {
   1141         return mIsSupposedToBePlaying;
   1142     }
   1143 
   1144     /*
   1145       Desired behavior for prev/next/shuffle:
   1146 
   1147       - NEXT will move to the next track in the list when not shuffling, and to
   1148         a track randomly picked from the not-yet-played tracks when shuffling.
   1149         If all tracks have already been played, pick from the full set, but
   1150         avoid picking the previously played track if possible.
   1151       - when shuffling, PREV will go to the previously played track. Hitting PREV
   1152         again will go to the track played before that, etc. When the start of the
   1153         history has been reached, PREV is a no-op.
   1154         When not shuffling, PREV will go to the sequentially previous track (the
   1155         difference with the shuffle-case is mainly that when not shuffling, the
   1156         user can back up to tracks that are not in the history).
   1157 
   1158         Example:
   1159         When playing an album with 10 tracks from the start, and enabling shuffle
   1160         while playing track 5, the remaining tracks (6-10) will be shuffled, e.g.
   1161         the final play order might be 1-2-3-4-5-8-10-6-9-7.
   1162         When hitting 'prev' 8 times while playing track 7 in this example, the
   1163         user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next',
   1164         a random track will be picked again. If at any time user disables shuffling
   1165         the next/previous track will be picked in sequential order again.
   1166      */
   1167 
   1168     public void prev() {
   1169         synchronized (this) {
   1170             if (mShuffleMode == SHUFFLE_NORMAL) {
   1171                 // go to previously-played track and remove it from the history
   1172                 int histsize = mHistory.size();
   1173                 if (histsize == 0) {
   1174                     // prev is a no-op
   1175                     return;
   1176                 }
   1177                 Integer pos = mHistory.remove(histsize - 1);
   1178                 mPlayPos = pos.intValue();
   1179             } else {
   1180                 if (mPlayPos > 0) {
   1181                     mPlayPos--;
   1182                 } else {
   1183                     mPlayPos = mPlayListLen - 1;
   1184                 }
   1185             }
   1186             saveBookmarkIfNeeded();
   1187             stop(false);
   1188             openCurrent();
   1189             play();
   1190             notifyChange(META_CHANGED);
   1191         }
   1192     }
   1193 
   1194     public void next(boolean force) {
   1195         synchronized (this) {
   1196             if (mPlayListLen <= 0) {
   1197                 Log.d(LOGTAG, "No play queue");
   1198                 return;
   1199             }
   1200 
   1201             if (mShuffleMode == SHUFFLE_NORMAL) {
   1202                 // Pick random next track from the not-yet-played ones
   1203                 // TODO: make it work right after adding/removing items in the queue.
   1204 
   1205                 // Store the current file in the history, but keep the history at a
   1206                 // reasonable size
   1207                 if (mPlayPos >= 0) {
   1208                     mHistory.add(mPlayPos);
   1209                 }
   1210                 if (mHistory.size() > MAX_HISTORY_SIZE) {
   1211                     mHistory.removeElementAt(0);
   1212                 }
   1213 
   1214                 int numTracks = mPlayListLen;
   1215                 int[] tracks = new int[numTracks];
   1216                 for (int i=0;i < numTracks; i++) {
   1217                     tracks[i] = i;
   1218                 }
   1219 
   1220                 int numHistory = mHistory.size();
   1221                 int numUnplayed = numTracks;
   1222                 for (int i=0;i < numHistory; i++) {
   1223                     int idx = mHistory.get(i).intValue();
   1224                     if (idx < numTracks && tracks[idx] >= 0) {
   1225                         numUnplayed--;
   1226                         tracks[idx] = -1;
   1227                     }
   1228                 }
   1229 
   1230                 // 'numUnplayed' now indicates how many tracks have not yet
   1231                 // been played, and 'tracks' contains the indices of those
   1232                 // tracks.
   1233                 if (numUnplayed <=0) {
   1234                     // everything's already been played
   1235                     if (mRepeatMode == REPEAT_ALL || force) {
   1236                         //pick from full set
   1237                         numUnplayed = numTracks;
   1238                         for (int i=0;i < numTracks; i++) {
   1239                             tracks[i] = i;
   1240                         }
   1241                     } else {
   1242                         // all done
   1243                         gotoIdleState();
   1244                         if (mIsSupposedToBePlaying) {
   1245                             mIsSupposedToBePlaying = false;
   1246                             notifyChange(PLAYSTATE_CHANGED);
   1247                         }
   1248                         return;
   1249                     }
   1250                 }
   1251                 int skip = mRand.nextInt(numUnplayed);
   1252                 int cnt = -1;
   1253                 while (true) {
   1254                     while (tracks[++cnt] < 0)
   1255                         ;
   1256                     skip--;
   1257                     if (skip < 0) {
   1258                         break;
   1259                     }
   1260                 }
   1261                 mPlayPos = cnt;
   1262             } else if (mShuffleMode == SHUFFLE_AUTO) {
   1263                 doAutoShuffleUpdate();
   1264                 mPlayPos++;
   1265             } else {
   1266                 if (mPlayPos >= mPlayListLen - 1) {
   1267                     // we're at the end of the list
   1268                     if (mRepeatMode == REPEAT_NONE && !force) {
   1269                         // all done
   1270                         gotoIdleState();
   1271                         mIsSupposedToBePlaying = false;
   1272                         notifyChange(PLAYSTATE_CHANGED);
   1273                         return;
   1274                     } else if (mRepeatMode == REPEAT_ALL || force) {
   1275                         mPlayPos = 0;
   1276                     }
   1277                 } else {
   1278                     mPlayPos++;
   1279                 }
   1280             }
   1281             saveBookmarkIfNeeded();
   1282             stop(false);
   1283             openCurrent();
   1284             play();
   1285             notifyChange(META_CHANGED);
   1286         }
   1287     }
   1288 
   1289     private void gotoIdleState() {
   1290         mDelayedStopHandler.removeCallbacksAndMessages(null);
   1291         Message msg = mDelayedStopHandler.obtainMessage();
   1292         mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
   1293         stopForeground(true);
   1294     }
   1295 
   1296     private void saveBookmarkIfNeeded() {
   1297         try {
   1298             if (isPodcast()) {
   1299                 long pos = position();
   1300                 long bookmark = getBookmark();
   1301                 long duration = duration();
   1302                 if ((pos < bookmark && (pos + 10000) > bookmark) ||
   1303                         (pos > bookmark && (pos - 10000) < bookmark)) {
   1304                     // The existing bookmark is close to the current
   1305                     // position, so don't update it.
   1306                     return;
   1307                 }
   1308                 if (pos < 15000 || (pos + 10000) > duration) {
   1309                     // if we're near the start or end, clear the bookmark
   1310                     pos = 0;
   1311                 }
   1312 
   1313                 // write 'pos' to the bookmark field
   1314                 ContentValues values = new ContentValues();
   1315                 values.put(MediaStore.Audio.Media.BOOKMARK, pos);
   1316                 Uri uri = ContentUris.withAppendedId(
   1317                         MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX));
   1318                 getContentResolver().update(uri, values, null, null);
   1319             }
   1320         } catch (SQLiteException ex) {
   1321         }
   1322     }
   1323 
   1324     // Make sure there are at least 5 items after the currently playing item
   1325     // and no more than 10 items before.
   1326     private void doAutoShuffleUpdate() {
   1327         boolean notify = false;
   1328 
   1329         // remove old entries
   1330         if (mPlayPos > 10) {
   1331             removeTracks(0, mPlayPos - 9);
   1332             notify = true;
   1333         }
   1334         // add new entries if needed
   1335         int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
   1336         for (int i = 0; i < to_add; i++) {
   1337             // pick something at random from the list
   1338 
   1339             int lookback = mHistory.size();
   1340             int idx = -1;
   1341             while(true) {
   1342                 idx = mRand.nextInt(mAutoShuffleList.length);
   1343                 if (!wasRecentlyUsed(idx, lookback)) {
   1344                     break;
   1345                 }
   1346                 lookback /= 2;
   1347             }
   1348             mHistory.add(idx);
   1349             if (mHistory.size() > MAX_HISTORY_SIZE) {
   1350                 mHistory.remove(0);
   1351             }
   1352             ensurePlayListCapacity(mPlayListLen + 1);
   1353             mPlayList[mPlayListLen++] = mAutoShuffleList[idx];
   1354             notify = true;
   1355         }
   1356         if (notify) {
   1357             notifyChange(QUEUE_CHANGED);
   1358         }
   1359     }
   1360 
   1361     // check that the specified idx is not in the history (but only look at at
   1362     // most lookbacksize entries in the history)
   1363     private boolean wasRecentlyUsed(int idx, int lookbacksize) {
   1364 
   1365         // early exit to prevent infinite loops in case idx == mPlayPos
   1366         if (lookbacksize == 0) {
   1367             return false;
   1368         }
   1369 
   1370         int histsize = mHistory.size();
   1371         if (histsize < lookbacksize) {
   1372             Log.d(LOGTAG, "lookback too big");
   1373             lookbacksize = histsize;
   1374         }
   1375         int maxidx = histsize - 1;
   1376         for (int i = 0; i < lookbacksize; i++) {
   1377             long entry = mHistory.get(maxidx - i);
   1378             if (entry == idx) {
   1379                 return true;
   1380             }
   1381         }
   1382         return false;
   1383     }
   1384 
   1385     // A simple variation of Random that makes sure that the
   1386     // value it returns is not equal to the value it returned
   1387     // previously, unless the interval is 1.
   1388     private static class Shuffler {
   1389         private int mPrevious;
   1390         private Random mRandom = new Random();
   1391         public int nextInt(int interval) {
   1392             int ret;
   1393             do {
   1394                 ret = mRandom.nextInt(interval);
   1395             } while (ret == mPrevious && interval > 1);
   1396             mPrevious = ret;
   1397             return ret;
   1398         }
   1399     };
   1400 
   1401     private boolean makeAutoShuffleList() {
   1402         ContentResolver res = getContentResolver();
   1403         Cursor c = null;
   1404         try {
   1405             c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
   1406                     new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
   1407                     null, null);
   1408             if (c == null || c.getCount() == 0) {
   1409                 return false;
   1410             }
   1411             int len = c.getCount();
   1412             long [] list = new long[len];
   1413             for (int i = 0; i < len; i++) {
   1414                 c.moveToNext();
   1415                 list[i] = c.getLong(0);
   1416             }
   1417             mAutoShuffleList = list;
   1418             return true;
   1419         } catch (RuntimeException ex) {
   1420         } finally {
   1421             if (c != null) {
   1422                 c.close();
   1423             }
   1424         }
   1425         return false;
   1426     }
   1427 
   1428     /**
   1429      * Removes the range of tracks specified from the play list. If a file within the range is
   1430      * the file currently being played, playback will move to the next file after the
   1431      * range.
   1432      * @param first The first file to be removed
   1433      * @param last The last file to be removed
   1434      * @return the number of tracks deleted
   1435      */
   1436     public int removeTracks(int first, int last) {
   1437         int numremoved = removeTracksInternal(first, last);
   1438         if (numremoved > 0) {
   1439             notifyChange(QUEUE_CHANGED);
   1440         }
   1441         return numremoved;
   1442     }
   1443 
   1444     private int removeTracksInternal(int first, int last) {
   1445         synchronized (this) {
   1446             if (last < first) return 0;
   1447             if (first < 0) first = 0;
   1448             if (last >= mPlayListLen) last = mPlayListLen - 1;
   1449 
   1450             boolean gotonext = false;
   1451             if (first <= mPlayPos && mPlayPos <= last) {
   1452                 mPlayPos = first;
   1453                 gotonext = true;
   1454             } else if (mPlayPos > last) {
   1455                 mPlayPos -= (last - first + 1);
   1456             }
   1457             int num = mPlayListLen - last - 1;
   1458             for (int i = 0; i < num; i++) {
   1459                 mPlayList[first + i] = mPlayList[last + 1 + i];
   1460             }
   1461             mPlayListLen -= last - first + 1;
   1462 
   1463             if (gotonext) {
   1464                 if (mPlayListLen == 0) {
   1465                     stop(true);
   1466                     mPlayPos = -1;
   1467                     if (mCursor != null) {
   1468                         mCursor.close();
   1469                         mCursor = null;
   1470                     }
   1471                 } else {
   1472                     if (mPlayPos >= mPlayListLen) {
   1473                         mPlayPos = 0;
   1474                     }
   1475                     boolean wasPlaying = isPlaying();
   1476                     stop(false);
   1477                     openCurrent();
   1478                     if (wasPlaying) {
   1479                         play();
   1480                     }
   1481                 }
   1482                 notifyChange(META_CHANGED);
   1483             }
   1484             return last - first + 1;
   1485         }
   1486     }
   1487 
   1488     /**
   1489      * Removes all instances of the track with the given id
   1490      * from the playlist.
   1491      * @param id The id to be removed
   1492      * @return how many instances of the track were removed
   1493      */
   1494     public int removeTrack(long id) {
   1495         int numremoved = 0;
   1496         synchronized (this) {
   1497             for (int i = 0; i < mPlayListLen; i++) {
   1498                 if (mPlayList[i] == id) {
   1499                     numremoved += removeTracksInternal(i, i);
   1500                     i--;
   1501                 }
   1502             }
   1503         }
   1504         if (numremoved > 0) {
   1505             notifyChange(QUEUE_CHANGED);
   1506         }
   1507         return numremoved;
   1508     }
   1509 
   1510     public void setShuffleMode(int shufflemode) {
   1511         synchronized(this) {
   1512             if (mShuffleMode == shufflemode && mPlayListLen > 0) {
   1513                 return;
   1514             }
   1515             mShuffleMode = shufflemode;
   1516             if (mShuffleMode == SHUFFLE_AUTO) {
   1517                 if (makeAutoShuffleList()) {
   1518                     mPlayListLen = 0;
   1519                     doAutoShuffleUpdate();
   1520                     mPlayPos = 0;
   1521                     openCurrent();
   1522                     play();
   1523                     notifyChange(META_CHANGED);
   1524                     return;
   1525                 } else {
   1526                     // failed to build a list of files to shuffle
   1527                     mShuffleMode = SHUFFLE_NONE;
   1528                 }
   1529             }
   1530             saveQueue(false);
   1531         }
   1532     }
   1533     public int getShuffleMode() {
   1534         return mShuffleMode;
   1535     }
   1536 
   1537     public void setRepeatMode(int repeatmode) {
   1538         synchronized(this) {
   1539             mRepeatMode = repeatmode;
   1540             saveQueue(false);
   1541         }
   1542     }
   1543     public int getRepeatMode() {
   1544         return mRepeatMode;
   1545     }
   1546 
   1547     public int getMediaMountedCount() {
   1548         return mMediaMountedCount;
   1549     }
   1550 
   1551     /**
   1552      * Returns the path of the currently playing file, or null if
   1553      * no file is currently playing.
   1554      */
   1555     public String getPath() {
   1556         return mFileToPlay;
   1557     }
   1558 
   1559     /**
   1560      * Returns the rowid of the currently playing file, or -1 if
   1561      * no file is currently playing.
   1562      */
   1563     public long getAudioId() {
   1564         synchronized (this) {
   1565             if (mPlayPos >= 0 && mPlayer.isInitialized()) {
   1566                 return mPlayList[mPlayPos];
   1567             }
   1568         }
   1569         return -1;
   1570     }
   1571 
   1572     /**
   1573      * Returns the position in the queue
   1574      * @return the position in the queue
   1575      */
   1576     public int getQueuePosition() {
   1577         synchronized(this) {
   1578             return mPlayPos;
   1579         }
   1580     }
   1581 
   1582     /**
   1583      * Starts playing the track at the given position in the queue.
   1584      * @param pos The position in the queue of the track that will be played.
   1585      */
   1586     public void setQueuePosition(int pos) {
   1587         synchronized(this) {
   1588             stop(false);
   1589             mPlayPos = pos;
   1590             openCurrent();
   1591             play();
   1592             notifyChange(META_CHANGED);
   1593             if (mShuffleMode == SHUFFLE_AUTO) {
   1594                 doAutoShuffleUpdate();
   1595             }
   1596         }
   1597     }
   1598 
   1599     public String getArtistName() {
   1600         synchronized(this) {
   1601             if (mCursor == null) {
   1602                 return null;
   1603             }
   1604             return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
   1605         }
   1606     }
   1607 
   1608     public long getArtistId() {
   1609         synchronized (this) {
   1610             if (mCursor == null) {
   1611                 return -1;
   1612             }
   1613             return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID));
   1614         }
   1615     }
   1616 
   1617     public String getAlbumName() {
   1618         synchronized (this) {
   1619             if (mCursor == null) {
   1620                 return null;
   1621             }
   1622             return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
   1623         }
   1624     }
   1625 
   1626     public long getAlbumId() {
   1627         synchronized (this) {
   1628             if (mCursor == null) {
   1629                 return -1;
   1630             }
   1631             return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID));
   1632         }
   1633     }
   1634 
   1635     public String getTrackName() {
   1636         synchronized (this) {
   1637             if (mCursor == null) {
   1638                 return null;
   1639             }
   1640             return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
   1641         }
   1642     }
   1643 
   1644     private boolean isPodcast() {
   1645         synchronized (this) {
   1646             if (mCursor == null) {
   1647                 return false;
   1648             }
   1649             return (mCursor.getInt(PODCASTCOLIDX) > 0);
   1650         }
   1651     }
   1652 
   1653     private long getBookmark() {
   1654         synchronized (this) {
   1655             if (mCursor == null) {
   1656                 return 0;
   1657             }
   1658             return mCursor.getLong(BOOKMARKCOLIDX);
   1659         }
   1660     }
   1661 
   1662     /**
   1663      * Returns the duration of the file in milliseconds.
   1664      * Currently this method returns -1 for the duration of MIDI files.
   1665      */
   1666     public long duration() {
   1667         if (mPlayer.isInitialized()) {
   1668             return mPlayer.duration();
   1669         }
   1670         return -1;
   1671     }
   1672 
   1673     /**
   1674      * Returns the current playback position in milliseconds
   1675      */
   1676     public long position() {
   1677         if (mPlayer.isInitialized()) {
   1678             return mPlayer.position();
   1679         }
   1680         return -1;
   1681     }
   1682 
   1683     /**
   1684      * Seeks to the position specified.
   1685      *
   1686      * @param pos The position to seek to, in milliseconds
   1687      */
   1688     public long seek(long pos) {
   1689         if (mPlayer.isInitialized()) {
   1690             if (pos < 0) pos = 0;
   1691             if (pos > mPlayer.duration()) pos = mPlayer.duration();
   1692             return mPlayer.seek(pos);
   1693         }
   1694         return -1;
   1695     }
   1696 
   1697     /**
   1698      * Sets the audio session ID.
   1699      *
   1700      * @param sessionId: the audio session ID.
   1701      */
   1702     public void setAudioSessionId(int sessionId) {
   1703         synchronized (this) {
   1704             mPlayer.setAudioSessionId(sessionId);
   1705         }
   1706     }
   1707 
   1708     /**
   1709      * Returns the audio session ID.
   1710      */
   1711     public int getAudioSessionId() {
   1712         synchronized (this) {
   1713             return mPlayer.getAudioSessionId();
   1714         }
   1715     }
   1716 
   1717     /**
   1718      * Provides a unified interface for dealing with midi files and
   1719      * other media files.
   1720      */
   1721     private class MultiPlayer {
   1722         private MediaPlayer mMediaPlayer = new MediaPlayer();
   1723         private Handler mHandler;
   1724         private boolean mIsInitialized = false;
   1725 
   1726         public MultiPlayer() {
   1727             mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
   1728         }
   1729 
   1730         public void setDataSource(String path) {
   1731             try {
   1732                 mMediaPlayer.reset();
   1733                 mMediaPlayer.setOnPreparedListener(null);
   1734                 if (path.startsWith("content://")) {
   1735                     mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path));
   1736                 } else {
   1737                     mMediaPlayer.setDataSource(path);
   1738                 }
   1739                 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
   1740                 mMediaPlayer.prepare();
   1741             } catch (IOException ex) {
   1742                 // TODO: notify the user why the file couldn't be opened
   1743                 mIsInitialized = false;
   1744                 return;
   1745             } catch (IllegalArgumentException ex) {
   1746                 // TODO: notify the user why the file couldn't be opened
   1747                 mIsInitialized = false;
   1748                 return;
   1749             }
   1750             mMediaPlayer.setOnCompletionListener(listener);
   1751             mMediaPlayer.setOnErrorListener(errorListener);
   1752             Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
   1753             i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
   1754             i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
   1755             sendBroadcast(i);
   1756             mIsInitialized = true;
   1757         }
   1758 
   1759         public boolean isInitialized() {
   1760             return mIsInitialized;
   1761         }
   1762 
   1763         public void start() {
   1764             MusicUtils.debugLog(new Exception("MultiPlayer.start called"));
   1765             mMediaPlayer.start();
   1766         }
   1767 
   1768         public void stop() {
   1769             mMediaPlayer.reset();
   1770             mIsInitialized = false;
   1771         }
   1772 
   1773         /**
   1774          * You CANNOT use this player anymore after calling release()
   1775          */
   1776         public void release() {
   1777             stop();
   1778             mMediaPlayer.release();
   1779         }
   1780 
   1781         public void pause() {
   1782             mMediaPlayer.pause();
   1783         }
   1784 
   1785         public void setHandler(Handler handler) {
   1786             mHandler = handler;
   1787         }
   1788 
   1789         MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {
   1790             public void onCompletion(MediaPlayer mp) {
   1791                 // Acquire a temporary wakelock, since when we return from
   1792                 // this callback the MediaPlayer will release its wakelock
   1793                 // and allow the device to go to sleep.
   1794                 // This temporary wakelock is released when the RELEASE_WAKELOCK
   1795                 // message is processed, but just in case, put a timeout on it.
   1796                 mWakeLock.acquire(30000);
   1797                 mHandler.sendEmptyMessage(TRACK_ENDED);
   1798                 mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
   1799             }
   1800         };
   1801 
   1802         MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {
   1803             public boolean onError(MediaPlayer mp, int what, int extra) {
   1804                 switch (what) {
   1805                 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
   1806                     mIsInitialized = false;
   1807                     mMediaPlayer.release();
   1808                     // Creating a new MediaPlayer and settings its wakemode does not
   1809                     // require the media service, so it's OK to do this now, while the
   1810                     // service is still being restarted
   1811                     mMediaPlayer = new MediaPlayer();
   1812                     mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
   1813                     mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
   1814                     return true;
   1815                 default:
   1816                     Log.d("MultiPlayer", "Error: " + what + "," + extra);
   1817                     break;
   1818                 }
   1819                 return false;
   1820            }
   1821         };
   1822 
   1823         public long duration() {
   1824             return mMediaPlayer.getDuration();
   1825         }
   1826 
   1827         public long position() {
   1828             return mMediaPlayer.getCurrentPosition();
   1829         }
   1830 
   1831         public long seek(long whereto) {
   1832             mMediaPlayer.seekTo((int) whereto);
   1833             return whereto;
   1834         }
   1835 
   1836         public void setVolume(float vol) {
   1837             mMediaPlayer.setVolume(vol, vol);
   1838         }
   1839 
   1840         public void setAudioSessionId(int sessionId) {
   1841             mMediaPlayer.setAudioSessionId(sessionId);
   1842         }
   1843 
   1844         public int getAudioSessionId() {
   1845             return mMediaPlayer.getAudioSessionId();
   1846         }
   1847     }
   1848 
   1849     /*
   1850      * By making this a static class with a WeakReference to the Service, we
   1851      * ensure that the Service can be GCd even when the system process still
   1852      * has a remote reference to the stub.
   1853      */
   1854     static class ServiceStub extends IMediaPlaybackService.Stub {
   1855         WeakReference<MediaPlaybackService> mService;
   1856 
   1857         ServiceStub(MediaPlaybackService service) {
   1858             mService = new WeakReference<MediaPlaybackService>(service);
   1859         }
   1860 
   1861         public void openFile(String path)
   1862         {
   1863             mService.get().open(path);
   1864         }
   1865         public void open(long [] list, int position) {
   1866             mService.get().open(list, position);
   1867         }
   1868         public int getQueuePosition() {
   1869             return mService.get().getQueuePosition();
   1870         }
   1871         public void setQueuePosition(int index) {
   1872             mService.get().setQueuePosition(index);
   1873         }
   1874         public boolean isPlaying() {
   1875             return mService.get().isPlaying();
   1876         }
   1877         public void stop() {
   1878             mService.get().stop();
   1879         }
   1880         public void pause() {
   1881             mService.get().pause();
   1882         }
   1883         public void play() {
   1884             mService.get().play();
   1885         }
   1886         public void prev() {
   1887             mService.get().prev();
   1888         }
   1889         public void next() {
   1890             mService.get().next(true);
   1891         }
   1892         public String getTrackName() {
   1893             return mService.get().getTrackName();
   1894         }
   1895         public String getAlbumName() {
   1896             return mService.get().getAlbumName();
   1897         }
   1898         public long getAlbumId() {
   1899             return mService.get().getAlbumId();
   1900         }
   1901         public String getArtistName() {
   1902             return mService.get().getArtistName();
   1903         }
   1904         public long getArtistId() {
   1905             return mService.get().getArtistId();
   1906         }
   1907         public void enqueue(long [] list , int action) {
   1908             mService.get().enqueue(list, action);
   1909         }
   1910         public long [] getQueue() {
   1911             return mService.get().getQueue();
   1912         }
   1913         public void moveQueueItem(int from, int to) {
   1914             mService.get().moveQueueItem(from, to);
   1915         }
   1916         public String getPath() {
   1917             return mService.get().getPath();
   1918         }
   1919         public long getAudioId() {
   1920             return mService.get().getAudioId();
   1921         }
   1922         public long position() {
   1923             return mService.get().position();
   1924         }
   1925         public long duration() {
   1926             return mService.get().duration();
   1927         }
   1928         public long seek(long pos) {
   1929             return mService.get().seek(pos);
   1930         }
   1931         public void setShuffleMode(int shufflemode) {
   1932             mService.get().setShuffleMode(shufflemode);
   1933         }
   1934         public int getShuffleMode() {
   1935             return mService.get().getShuffleMode();
   1936         }
   1937         public int removeTracks(int first, int last) {
   1938             return mService.get().removeTracks(first, last);
   1939         }
   1940         public int removeTrack(long id) {
   1941             return mService.get().removeTrack(id);
   1942         }
   1943         public void setRepeatMode(int repeatmode) {
   1944             mService.get().setRepeatMode(repeatmode);
   1945         }
   1946         public int getRepeatMode() {
   1947             return mService.get().getRepeatMode();
   1948         }
   1949         public int getMediaMountedCount() {
   1950             return mService.get().getMediaMountedCount();
   1951         }
   1952         public int getAudioSessionId() {
   1953             return mService.get().getAudioSessionId();
   1954         }
   1955     }
   1956 
   1957     @Override
   1958     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
   1959         writer.println("" + mPlayListLen + " items in queue, currently at index " + mPlayPos);
   1960         writer.println("Currently loaded:");
   1961         writer.println(getArtistName());
   1962         writer.println(getAlbumName());
   1963         writer.println(getTrackName());
   1964         writer.println(getPath());
   1965         writer.println("playing: " + mIsSupposedToBePlaying);
   1966         writer.println("actual: " + mPlayer.mMediaPlayer.isPlaying());
   1967         writer.println("shuffle mode: " + mShuffleMode);
   1968         MusicUtils.debugDump(writer);
   1969     }
   1970 
   1971     private final IBinder mBinder = new ServiceStub(this);
   1972 }
   1973