/Examples/AppFundamentalsVideoSourceExamples/src/MediaPlaybackService.java
Java | 1270 lines | 954 code | 99 blank | 217 comment | 254 complexity | 554707df6239a8638eb8f5b9657a4cca MD5 | raw file
- /*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.android.music;
- import android.app.Notification;
- import android.app.PendingIntent;
- import android.app.Service;
- import android.appwidget.AppWidgetManager;
- 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.content.BroadcastReceiver;
- import android.content.SharedPreferences;
- import android.content.SharedPreferences.Editor;
- import android.database.Cursor;
- import android.database.sqlite.SQLiteException;
- import android.graphics.Bitmap;
- import android.media.audiofx.AudioEffect;
- import android.media.AudioManager;
- import android.media.AudioManager.OnAudioFocusChangeListener;
- import android.media.MediaMetadataRetriever;
- import android.media.MediaPlayer;
- import android.media.MediaPlayer.OnCompletionListener;
- import android.media.RemoteControlClient;
- import android.media.RemoteControlClient.MetadataEditor;
- import android.net.Uri;
- import android.os.Handler;
- import android.os.IBinder;
- import android.os.Message;
- import android.os.PowerManager;
- import android.os.SystemClock;
- import android.os.PowerManager.WakeLock;
- import android.provider.MediaStore;
- import android.util.Log;
- import android.widget.RemoteViews;
- import android.widget.Toast;
- import java.io.FileDescriptor;
- import java.io.IOException;
- import java.io.PrintWriter;
- import java.lang.ref.WeakReference;
- import java.util.Random;
- import java.util.Vector;
- /**
- * Provides "background" audio playback capabilities, allowing the
- * user to switch between activities without stopping playback.
- */
- public class MediaPlaybackService extends Service {
- /** 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
- */
- public static final int NOW = 1;
- public static final int NEXT = 2;
- public static final int LAST = 3;
- public static final int PLAYBACKSERVICE_STATUS = 1;
-
- public static final int SHUFFLE_NONE = 0;
- public static final int SHUFFLE_NORMAL = 1;
- public static final int SHUFFLE_AUTO = 2;
-
- public static final int REPEAT_NONE = 0;
- public static final int REPEAT_CURRENT = 1;
- public static final int REPEAT_ALL = 2;
- public static final String PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
- public static final String META_CHANGED = "com.android.music.metachanged";
- public static final String QUEUE_CHANGED = "com.android.music.queuechanged";
- public static final String SERVICECMD = "com.android.music.musicservicecommand";
- public static final String CMDNAME = "command";
- public static final String CMDTOGGLEPAUSE = "togglepause";
- public static final String CMDSTOP = "stop";
- public static final String CMDPAUSE = "pause";
- public static final String CMDPLAY = "play";
- public static final String CMDPREVIOUS = "previous";
- public static final String CMDNEXT = "next";
- public static final String TOGGLEPAUSE_ACTION = "com.android.music.musicservicecommand.togglepause";
- public static final String PAUSE_ACTION = "com.android.music.musicservicecommand.pause";
- public static final String PREVIOUS_ACTION = "com.android.music.musicservicecommand.previous";
- public static final String NEXT_ACTION = "com.android.music.musicservicecommand.next";
- 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 TRACK_WENT_TO_NEXT = 7;
- private static final int MAX_HISTORY_SIZE = 100;
-
- private MultiPlayer mPlayer;
- private String mFileToPlay;
- private int mShuffleMode = SHUFFLE_NONE;
- private int mRepeatMode = REPEAT_NONE;
- private int mMediaMountedCount = 0;
- private long [] mAutoShuffleList = null;
- private long [] mPlayList = null;
- private int mPlayListLen = 0;
- private Vector<Integer> mHistory = new Vector<Integer>(MAX_HISTORY_SIZE);
- private Cursor mCursor;
- private int mPlayPos = -1;
- private int mNextPlayPos = -1;
- private static final String LOGTAG = "MediaPlaybackService";
- private final Shuffler mRand = new Shuffler();
- private int mOpenFailedCounter = 0;
- String[] mCursorCols = new String[] {
- "audio._id AS _id", // index must match IDCOLIDX below
- MediaStore.Audio.Media.ARTIST,
- MediaStore.Audio.Media.ALBUM,
- MediaStore.Audio.Media.TITLE,
- MediaStore.Audio.Media.DATA,
- MediaStore.Audio.Media.MIME_TYPE,
- MediaStore.Audio.Media.ALBUM_ID,
- MediaStore.Audio.Media.ARTIST_ID,
- MediaStore.Audio.Media.IS_PODCAST, // index must match PODCASTCOLIDX below
- MediaStore.Audio.Media.BOOKMARK // index must match BOOKMARKCOLIDX below
- };
- private final static int IDCOLIDX = 0;
- private final static int PODCASTCOLIDX = 8;
- private final static int BOOKMARKCOLIDX = 9;
- private BroadcastReceiver mUnmountReceiver = 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;
- private SharedPreferences 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;
-
- private MediaAppWidgetProvider mAppWidgetProvider = MediaAppWidgetProvider.getInstance();
-
- // interval after which we stop the service when idle
- private static final int IDLE_DELAY = 60000;
- private RemoteControlClient mRemoteControlClient;
- private Handler mMediaplayerHandler = new Handler() {
- float mCurrentVolume = 1.0f;
- @Override
- public void handleMessage(Message msg) {
- MusicUtils.debugLog("mMediaplayerHandler.handleMessage " + msg.what);
- switch (msg.what) {
- case FADEDOWN:
- mCurrentVolume -= .05f;
- if (mCurrentVolume > .2f) {
- mMediaplayerHandler.sendEmptyMessageDelayed(FADEDOWN, 10);
- } else {
- mCurrentVolume = .2f;
- }
- mPlayer.setVolume(mCurrentVolume);
- break;
- case FADEUP:
- mCurrentVolume += .01f;
- if (mCurrentVolume < 1.0f) {
- mMediaplayerHandler.sendEmptyMessageDelayed(FADEUP, 10);
- } else {
- mCurrentVolume = 1.0f;
- }
- mPlayer.setVolume(mCurrentVolume);
- break;
- case SERVER_DIED:
- if (mIsSupposedToBePlaying) {
- gotoNext(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)
- openCurrentAndNext();
- }
- break;
- case TRACK_WENT_TO_NEXT:
- mPlayPos = mNextPlayPos;
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
- mCursor = getCursorForId(mPlayList[mPlayPos]);
- notifyChange(META_CHANGED);
- updateNotification();
- setNextTrack();
- break;
- case TRACK_ENDED:
- if (mRepeatMode == REPEAT_CURRENT) {
- seek(0);
- play();
- } else {
- gotoNext(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, "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, "AudioFocus: received AUDIOFOCUS_LOSS_TRANSIENT");
- if(isPlaying()) {
- mPausedByTransientLossOfFocus = true;
- }
- pause();
- break;
- case AudioManager.AUDIOFOCUS_GAIN:
- Log.v(LOGTAG, "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, "Unknown audio focus change code");
- }
- break;
- default:
- break;
- }
- }
- };
- private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- String cmd = intent.getStringExtra("command");
- MusicUtils.debugLog("mIntentReceiver.onReceive " + action + " / " + cmd);
- if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
- gotoNext(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 (CMDPLAY.equals(cmd)) {
- play();
- } else if (CMDSTOP.equals(cmd)) {
- pause();
- mPausedByTransientLossOfFocus = false;
- seek(0);
- } else if (MediaAppWidgetProvider.CMDAPPWIDGETUPDATE.equals(cmd)) {
- // Someone asked us to refresh a set of specific widgets, probably
- // because they were just added.
- int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
- mAppWidgetProvider.performUpdate(MediaPlaybackService.this, appWidgetIds);
- }
- }
- };
- private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
- public void onAudioFocusChange(int focusChange) {
- mMediaplayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
- }
- };
- public MediaPlaybackService() {
- }
- @Override
- public void onCreate() {
- super.onCreate();
- mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- ComponentName rec = new ComponentName(getPackageName(),
- MediaButtonIntentReceiver.class.getName());
- mAudioManager.registerMediaButtonEventReceiver(rec);
- // TODO update to new constructor
- // mRemoteControlClient = new RemoteControlClient(rec);
- // mAudioManager.registerRemoteControlClient(mRemoteControlClient);
- //
- // int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
- // | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
- // | RemoteControlClient.FLAG_KEY_MEDIA_PLAY
- // | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
- // | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
- // | RemoteControlClient.FLAG_KEY_MEDIA_STOP;
- // mRemoteControlClient.setTransportControlFlags(flags);
-
- mPreferences = getSharedPreferences("Music", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
- mCardId = MusicUtils.getCardId(this);
-
- registerExternalStorageListener();
- // Needs to be done in this thread, since otherwise ApplicationContext.getPowerManager() crashes.
- mPlayer = new MultiPlayer();
- mPlayer.setHandler(mMediaplayerHandler);
- reloadQueue();
- notifyChange(QUEUE_CHANGED);
- notifyChange(META_CHANGED);
- IntentFilter commandFilter = new IntentFilter();
- commandFilter.addAction(SERVICECMD);
- commandFilter.addAction(TOGGLEPAUSE_ACTION);
- commandFilter.addAction(PAUSE_ACTION);
- commandFilter.addAction(NEXT_ACTION);
- commandFilter.addAction(PREVIOUS_ACTION);
- registerReceiver(mIntentReceiver, commandFilter);
-
- 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 being destroyed while still playing.");
- }
- // release all MediaPlayer resources, including the native player and wakelocks
- Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
- i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
- i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
- sendBroadcast(i);
- mPlayer.release();
- mPlayer = null;
- mAudioManager.abandonAudioFocus(mAudioFocusListener);
- //mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
-
- // make sure there aren't any other messages coming
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- mMediaplayerHandler.removeCallbacksAndMessages(null);
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
- unregisterReceiver(mIntentReceiver);
- if (mUnmountReceiver != null) {
- unregisterReceiver(mUnmountReceiver);
- mUnmountReceiver = null;
- }
- mWakeLock.release();
- super.onDestroy();
- }
-
- private final char hexdigits [] = new char [] {
- '0', '1', '2', '3',
- '4', '5', '6', '7',
- '8', '9', 'a', 'b',
- 'c', 'd', 'e', 'f'
- };
- private void saveQueue(boolean full) {
- if (!mQueueIsSaveable) {
- return;
- }
- Editor ed = mPreferences.edit();
- //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");
- ed.putString("queue", q.toString());
- ed.putInt("cardid", mCardId);
- if (mShuffleMode != SHUFFLE_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(";");
- }
- }
- ed.putString("history", q.toString());
- }
- }
- ed.putInt("curpos", mPlayPos);
- if (mPlayer.isInitialized()) {
- ed.putLong("seekpos", mPlayer.position());
- }
- ed.putInt("repeatmode", mRepeatMode);
- ed.putInt("shufflemode", mShuffleMode);
- SharedPreferencesCompat.apply(ed);
- //Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() - start) + " ms");
- }
- private void reloadQueue() {
- String q = null;
-
- boolean newstyle = false;
- int id = mCardId;
- if (mPreferences.contains("cardid")) {
- newstyle = true;
- id = mPreferences.getInt("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.getString("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.getInt("curpos", 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,
- mCursorCols, "_id=" + mPlayList[mPlayPos] , null, null);
- }
- if (crsr != null) {
- crsr.close();
- }
- // Make sure we don't auto-skip to the next song, since that
- // also starts playback. What could happen in that case is:
- // - music is paused
- // - go to UMS and delete some files, including the currently playing one
- // - come back from UMS
- // (time passes)
- // - music app is killed for some reason (out of memory)
- // - music service is restarted, service restores state, doesn't find
- // the "current" file, goes to the next and: playback starts on its
- // own, potentially at some random inconvenient time.
- mOpenFailedCounter = 20;
- mQuietMode = true;
- openCurrentAndNext();
- mQuietMode = false;
- if (!mPlayer.isInitialized()) {
- // couldn't restore the saved state
- mPlayListLen = 0;
- return;
- }
-
- long seekpos = mPreferences.getLong("seekpos", 0);
- seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
- Log.d(LOGTAG, "restored queue, currently at position "
- + position() + "/" + duration()
- + " (requested " + seekpos + ")");
-
- int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
- if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
- repmode = REPEAT_NONE;
- }
- mRepeatMode = repmode;
- int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
- if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
- shufmode = SHUFFLE_NONE;
- }
- if (shufmode != SHUFFLE_NONE) {
- // in shuffle mode we need to restore the history too
- q = mPreferences.getString("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;
- }
- mHistory.add(n);
- 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;
- }
- }
- }
- }
- if (shufmode == SHUFFLE_AUTO) {
- if (! makeAutoShuffleList()) {
- shufmode = SHUFFLE_NONE;
- }
- }
- mShuffleMode = shufmode;
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- mServiceInUse = true;
- return mBinder;
- }
- @Override
- public void onRebind(Intent intent) {
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- mServiceInUse = true;
- }
- @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)) {
- gotoNext(true);
- } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
- if (position() < 2000) {
- prev();
- } else {
- seek(0);
- play();
- }
- } 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 (CMDPLAY.equals(cmd)) {
- play();
- } else if (CMDSTOP.equals(cmd)) {
- pause();
- mPausedByTransientLossOfFocus = false;
- seek(0);
- }
- }
-
- // 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;
- }
-
- 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);
- }
- };
- /**
- * 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 playback and clean up if the SD card is going to be unmounted.
- stop(true);
- notifyChange(QUEUE_CHANGED);
- notifyChange(META_CHANGED);
- }
- /**
- * 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)) {
- mMediaMountedCount++;
- mCardId = MusicUtils.getCardId(MediaPlaybackService.this);
- reloadQueue();
- mQueueIsSaveable = true;
- notifyChange(QUEUE_CHANGED);
- notifyChange(META_CHANGED);
- }
- }
- };
- IntentFilter iFilter = new IntentFilter();
- iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
- iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
- iFilter.addDataScheme("file");
- registerReceiver(mUnmountReceiver, iFilter);
- }
- }
- /**
- * 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" - String: the name of the album
- * "track" - String: the name of the track
- * The intent has an action that is one of
- * "com.android.music.metachanged"
- * "com.android.music.queuechanged",
- * "com.android.music.playbackcomplete"
- * "com.android.music.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 what) {
- Intent i = new Intent(what);
- i.putExtra("id", Long.valueOf(getAudioId()));
- i.putExtra("artist", getArtistName());
- i.putExtra("album",getAlbumName());
- i.putExtra("track", getTrackName());
- i.putExtra("playing", isPlaying());
- sendStickyBroadcast(i);
- if (what.equals(PLAYSTATE_CHANGED)) {
- // mRemoteControlClient.setPlaybackState(isPlaying() ?
- // RemoteControlClient.PLAYSTATE_PLAYING : RemoteControlClient.PLAYSTATE_PAUSED);
- } else if (what.equals(META_CHANGED)) {
- // RemoteControlClient.MetadataEditor ed = mRemoteControlClient.editMetadata(true);
- // ed.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, getTrackName());
- // ed.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, getAlbumName());
- // ed.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, getArtistName());
- // ed.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration());
- // Bitmap b = MusicUtils.getArtwork(this, getAudioId(), getAlbumId(), false);
- // if (b != null) {
- // ed.putBitmap(MetadataEditor.BITMAP_KEY_ARTWORK, b);
- // }
- // ed.apply();
- }
- if (what.equals(QUEUE_CHANGED)) {
- saveQueue(true);
- } else {
- saveQueue(false);
- }
-
- // Share this notification directly with our widgets
- mAppWidgetProvider.notifyChange(this, what);
- }
- 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;
- }
- // FIXME: shrink the array when the needed size is much smaller
- // than the allocated size
- }
-
- // insert the list of songs at the specified position in the playlist
- private void addToPlayList(long [] list, int position) {
- 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
- 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) {
- mCursor.close();
- mCursor = null;
- notifyChange(META_CHANGED);
- }
- }
-
- /**
- * 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 == NEXT && mPlayPos + 1 < mPlayListLen) {
- addToPlayList(list, mPlayPos + 1);
- notifyChange(QUEUE_CHANGED);
- } else {
- // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen
- addToPlayList(list, Integer.MAX_VALUE);
- notifyChange(QUEUE_CHANGED);
- if (action == NOW) {
- mPlayPos = mPlayListLen - list.length;
- openCurrentAndNext();
- play();
- notifyChange(META_CHANGED);
- return;
- }
- }
- if (mPlayPos < 0) {
- mPlayPos = 0;
- openCurrentAndNext();
- play();
- notifyChange(META_CHANGED);
- }
- }
- }
- /**
- * 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) {
- if (mShuffleMode == SHUFFLE_AUTO) {
- mShuffleMode = SHUFFLE_NORMAL;
- }
- long oldId = getAudioId();
- 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(QUEUE_CHANGED);
- }
- int oldpos = mPlayPos;
- if (position >= 0) {
- mPlayPos = position;
- } else {
- mPlayPos = mRand.nextInt(mPlayListLen);
- }
- mHistory.clear();
- saveBookmarkIfNeeded();
- openCurrentAndNext();
- if (oldId != getAudioId()) {
- notifyChange(META_CHANGED);
- }
- }
- }
-
- /**
- * Moves the item at index1 to index2.
- * @param index1
- * @param index2
- */
- public void moveQueueItem(int index1, int index2) {
- synchronized (this) {
- if (index1 >= mPlayListLen) {
- index1 = mPlayListLen - 1;
- }
- if (index2 >= mPlayListLen) {
- index2 = mPlayListLen - 1;
- }
- if (index1 < index2) {
- long tmp = mPlayList[index1];
- for (int i = index1; i < index2; i++) {
- mPlayList[i] = mPlayList[i+1];
- }
- mPlayList[index2] = tmp;
- if (mPlayPos == index1) {
- mPlayPos = index2;
- } else if (mPlayPos >= index1 && mPlayPos <= index2) {
- mPlayPos--;
- }
- } else if (index2 < index1) {
- long tmp = mPlayList[index1];
- for (int i = index1; i > index2; i--) {
- mPlayList[i] = mPlayList[i-1];
- }
- mPlayList[index2] = tmp;
- if (mPlayPos == index1) {
- mPlayPos = index2;
- } else if (mPlayPos >= index2 && mPlayPos <= index1) {
- mPlayPos++;
- }
- }
- notifyChange(QUEUE_CHANGED);
- }
- }
- /**
- * Returns the current play list
- * @return An array of integers containing the IDs of the tracks in the play list
- */
- public long [] getQueue() {
- synchronized (this) {
- int len = mPlayListLen;
- long [] list = new long[len];
- for (int i = 0; i < len; i++) {
- list[i] = mPlayList[i];
- }
- return list;
- }
- }
- private Cursor getCursorForId(long lid) {
- String id = String.valueOf(lid);
- Cursor c = getContentResolver().query(
- MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
- mCursorCols, "_id=" + id , null, null);
- if (c != null) {
- c.moveToFirst();
- }
- return c;
- }
- private void openCurrentAndNext() {
- synchronized (this) {
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
- if (mPlayListLen == 0) {
- return;
- }
- stop(false);
- mCursor = getCursorForId(mPlayList[mPlayPos]);
- while(true) {
- if (mCursor != null && mCursor.getCount() != 0 &&
- open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" +
- mCursor.getLong(IDCOLIDX))) {
- break;
- }
- // if we get here then opening the file failed. We can close the cursor now, because
- // we're either going to create a new one next, or stop trying
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
- if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
- int pos = getNextPosition(false);
- if (pos < 0) {
- gotoIdleState();
- if (mIsSupposedToBePlaying) {
- mIsSupposedToBePlaying = false;
- notifyChange(PLAYSTATE_CHANGED);
- }
- return;
- }
- mPlayPos = pos;
- stop(false);
- mPlayPos = pos;
- mCursor = getCursorForId(mPlayList[mPlayPos]);
- } else {
- mOpenFailedCounter = 0;
- if (!mQuietMode) {
- Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
- }
- Log.d(LOGTAG, "Failed to open file for playback");
- gotoIdleState();
- if (mIsSupposedToBePlaying) {
- mIsSupposedToBePlaying = false;
- notifyChange(PLAYSTATE_CHANGED);
- }
- return;
- }
- }
- // go to bookmark if needed
- if (isPodcast()) {
- 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);
- }
- setNextTrack();
- }
- }
- private void setNextTrack() {
- mNextPlayPos = getNextPosition(false);
- if (mNextPlayPos >= 0) {
- long id = mPlayList[mNextPlayPos];
- mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
- }
- }
- /**
- * Opens the specified file and readies it for playback.
- *
- * @param path The full path of the file to be opened.
- */
- public boolean open(String path) {
- synchronized (this) {
- if (path == null) {
- return false;
- }
-
- // if mCursor is null, try to associate path with a database cursor
- if (mCursor == null) {
- ContentResolver resolver = getContentResolver();
- Uri uri;
- String where;
- String selectionArgs[];
- if (path.startsWith("content://media/")) {
- uri = Uri.parse(path);
- where = null;
- selectionArgs = null;
- } else {
- uri = MediaStore.Audio.Media.getContentUriForPath(path);
- where = MediaStore.Audio.Media.DATA + "=?";
- selectionArgs = new String[] { path };
- }
-
- try {
- mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);
- if (mCursor != null) {
- if (mCursor.getCount() == 0) {
- mCursor.close();
- mCursor = null;
- } else {
- mCursor.moveToNext();
- ensurePlayListCapacity(1);
- mPlayListLen = 1;
- mPlayList[0] = mCursor.getLong(IDCOLIDX);
- mPlayPos = 0;
- }
- }
- } catch (UnsupportedOperationException ex) {
- }
- }
- mFileToPlay = path;
- mPlayer.setDataSource(mFileToPlay);
- if (mPlayer.isInitialized()) {
- mOpenFailedCounter = 0;
- return true;
- }
- stop(true);
- return false;
- }
- }
- /**
- * Starts playback of a previously opened file.
- */
- public void play() {
- mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
- mAudioManager.registerMediaButtonEventReceiver(new ComponentName(this.getPackageName(),
- MediaButtonIntentReceiver.class.getName()));
- if (mPlayer.isInitialized()) {
- // if we are at the end of the song, go to the next song first
- long duration = mPlayer.duration();
- if (mRepeatMode != REPEAT_CURRENT && duration > 2000 &&
- mPlayer.position() >= duration - 2000) {
- gotoNext(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);
- updateNotification();
- if (!mIsSupposedToBePlaying) {
- mIsSupposedToBePlaying = true;
- notifyChange(PLAYSTATE_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_AUTO);
- }
- }
- private void updateNotification() {
- RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar);
- views.setImageViewResource(R.id.icon, R.drawable.stat_notify_musicplayer);
- if (getAudioId() < 0) {
- // streaming
- views.setTextViewText(R.id.trackname, getPath());
- views.setTextViewText(R.id.artistalbum, null);
- } else {
- String artist = getArtistName();
- views.setTextViewText(R.id.trackname, getTrackName());
- if (artist == null || artist.equals(MediaStore.UNKNOWN_STRING)) {
- artist = getString(R.string.unknown_artist_name);
- }
- String album = getAlbumName();
- if (album == null || album.equals(MediaStore.UNKNOWN_STRING)) {
- album = getString(R.string.unknown_album_name);
- }
- views.setTextViewText(R.id.artistalbum,
- getString(R.string.notification_artist_album, artist, album)
- );
- }
- Notification status = new Notification();
- status.contentView = views;
- status.flags |= Notification.FLAG_ONGOING_EVENT;
- status.icon = R.drawable.stat_notify_musicplayer;
- status.contentIntent = PendingIntent.getActivity(this, 0,
- new Intent("com.android.music.PLAYBACK_VIEWER")
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0);
- startForeground(PLAYBACKSERVICE_STATUS, status);
- }
- private void stop(boolean remove_status_icon) {
- if (mPlayer.isInitialized()) {
- mPlayer.stop();
- }
- mFileToPlay = null;
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
- if (remove_status_icon) {
- gotoIdleState();
- } else {
- stopForeground(false);
- }
- if (remove_status_icon) {
- mIsSupposedToBePlaying = false;
- }
- }
- /**
- * Stops playback.
- */
- public void stop() {
- stop(true);
- }
- /**
- * Pauses playback (call play() to resume)
- */
- public void pause() {
- synchronized(this) {
- mMediaplayerHandler.removeMessages(FADEUP);
- if (isPlaying()) {
- mPlayer.pause();
- gotoIdleState();
- mIsSupposedToBePlaying = false;
- notifyChange(PLAYSTATE_CHANGED);
- saveBookmarkIfNeeded();
- }
- }
- }
- /** 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;
- }
- /*
- 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