/src/com/cyanogenmod/eleven/MusicPlaybackService.java
Java | 1514 lines | 889 code | 213 blank | 412 comment | 234 complexity | c645971ff086af0e72e3aaafda585733 MD5 | raw file
- /*
- * Copyright (C) 2012 Andrew Neal
- * Copyright (C) 2014 The CyanogenMod Project
- * Copyright (C) 2015 The SudaMod 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.cyanogenmod.eleven;
- import android.annotation.SuppressLint;
- import android.app.AlarmManager;
- import android.app.Notification;
- import android.app.NotificationManager;
- import android.app.PendingIntent;
- import android.app.Service;
- import android.appwidget.AppWidgetManager;
- import android.content.BroadcastReceiver;
- import android.content.ComponentName;
- import android.content.ContentResolver;
- import android.content.Context;
- import android.content.Intent;
- import android.content.IntentFilter;
- import android.content.SharedPreferences;
- import android.database.ContentObserver;
- import android.database.Cursor;
- import android.database.MatrixCursor;
- import android.graphics.Bitmap;
- import android.hardware.SensorManager;
- import android.media.AudioManager;
- import android.media.AudioManager.OnAudioFocusChangeListener;
- import android.media.MediaMetadata;
- import android.media.MediaPlayer;
- import android.media.audiofx.AudioEffect;
- import android.media.session.MediaSession;
- import android.media.session.PlaybackState;
- import android.net.Uri;
- import android.os.Handler;
- import android.os.HandlerThread;
- import android.os.IBinder;
- import android.os.Looper;
- import android.os.Message;
- import android.os.PowerManager;
- import android.os.RemoteException;
- import android.os.SystemClock;
- import android.provider.MediaStore;
- import android.provider.MediaStore.Audio.AlbumColumns;
- import android.provider.MediaStore.Audio.AudioColumns;
- import android.text.TextUtils;
- import android.util.Log;
- import com.cyanogenmod.eleven.Config.IdType;
- import com.cyanogenmod.eleven.appwidgets.AppWidgetLarge;
- import com.cyanogenmod.eleven.appwidgets.AppWidgetLargeAlternate;
- import com.cyanogenmod.eleven.appwidgets.AppWidgetSmall;
- import com.cyanogenmod.eleven.cache.ImageCache;
- import com.cyanogenmod.eleven.cache.ImageFetcher;
- import com.cyanogenmod.eleven.provider.MusicPlaybackState;
- import com.cyanogenmod.eleven.provider.RecentStore;
- import com.cyanogenmod.eleven.provider.SongPlayCount;
- import com.cyanogenmod.eleven.service.MusicPlaybackTrack;
- import com.cyanogenmod.eleven.utils.BitmapWithColors;
- import com.cyanogenmod.eleven.utils.Lists;
- import com.cyanogenmod.eleven.utils.PreferenceUtils;
- import com.cyanogenmod.eleven.utils.ShakeDetector;
- import com.cyanogenmod.eleven.utils.SrtManager;
- import java.io.File;
- import java.io.IOException;
- import java.lang.ref.WeakReference;
- import java.util.ArrayList;
- import java.util.LinkedList;
- import java.util.ListIterator;
- import java.util.Random;
- import java.util.TreeSet;
- /**
- * A backbround {@link Service} used to keep music playing between activities
- * and when the user moves Apollo into the background.
- */
- @SuppressLint("NewApi")
- public class MusicPlaybackService extends Service {
- private static final String TAG = "MusicPlaybackService";
- private static final boolean D = false;
- /**
- * Indicates that the music has paused or resumed
- */
- public static final String PLAYSTATE_CHANGED = "com.cyanogenmod.eleven.playstatechanged";
- /**
- * Indicates that music playback position within
- * a title was changed
- */
- public static final String POSITION_CHANGED = "com.cyanogenmod.eleven.positionchanged";
- /**
- * Indicates the meta data has changed in some way, like a track change
- */
- public static final String META_CHANGED = "com.cyanogenmod.eleven.metachanged";
- /**
- * Indicates the queue has been updated
- */
- public static final String QUEUE_CHANGED = "com.cyanogenmod.eleven.queuechanged";
- /**
- * Indicates the queue has been updated
- */
- public static final String PLAYLIST_CHANGED = "com.cyanogenmod.eleven.playlistchanged";
- /**
- * Indicates the repeat mode changed
- */
- public static final String REPEATMODE_CHANGED = "com.cyanogenmod.eleven.repeatmodechanged";
- /**
- * Indicates the shuffle mode changed
- */
- public static final String SHUFFLEMODE_CHANGED = "com.cyanogenmod.eleven.shufflemodechanged";
- /**
- * Indicates the track fails to play
- */
- public static final String TRACK_ERROR = "com.cyanogenmod.eleven.trackerror";
- /**
- * For backwards compatibility reasons, also provide sticky
- * broadcasts under the music package
- */
- public static final String ELEVEN_PACKAGE_NAME = "com.cyanogenmod.eleven";
- public static final String MUSIC_PACKAGE_NAME = "com.android.music";
- /**
- * Called to indicate a general service commmand. Used in
- * {@link MediaButtonIntentReceiver}
- */
- public static final String SERVICECMD = "com.cyanogenmod.eleven.musicservicecommand";
- /**
- * Called to go toggle between pausing and playing the music
- */
- public static final String TOGGLEPAUSE_ACTION = "com.cyanogenmod.eleven.togglepause";
- /**
- * Called to go to pause the playback
- */
- public static final String PAUSE_ACTION = "com.cyanogenmod.eleven.pause";
- /**
- * Called to go to stop the playback
- */
- public static final String STOP_ACTION = "com.cyanogenmod.eleven.stop";
- /**
- * Called to go to stop the playback for sleep mode
- */
- public static final String SLEEP_MODE_STOP_ACTION = "com.sudamod.eleven.sleepmode.stop";
- /**
- * Called to go to the previous track or the beginning of the track if partway through the track
- */
- public static final String PREVIOUS_ACTION = "com.cyanogenmod.eleven.previous";
- /**
- * Called to go to the previous track regardless of how far in the current track the playback is
- */
- public static final String PREVIOUS_FORCE_ACTION = "com.cyanogenmod.eleven.previous.force";
- /**
- * Called to go to the next track
- */
- public static final String NEXT_ACTION = "com.cyanogenmod.eleven.next";
- /**
- * Called to change the repeat mode
- */
- public static final String REPEAT_ACTION = "com.cyanogenmod.eleven.repeat";
- /**
- * Called to change the shuffle mode
- */
- public static final String SHUFFLE_ACTION = "com.cyanogenmod.eleven.shuffle";
- public static final String FROM_MEDIA_BUTTON = "frommediabutton";
- /**
- * Used to easily notify a list that it should refresh. i.e. A playlist
- * changes
- */
- public static final String REFRESH = "com.cyanogenmod.eleven.refresh";
- /**
- * Used by the alarm intent to shutdown the service after being idle
- */
- private static final String SHUTDOWN = "com.cyanogenmod.eleven.shutdown";
- /**
- * Called to notify of a timed text
- */
- public static final String NEW_LYRICS = "com.cyanogenmod.eleven.lyrics";
- /**
- * Called to update the remote control client
- */
- public static final String UPDATE_LOCKSCREEN = "com.cyanogenmod.eleven.updatelockscreen";
- 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 CMDNOTIF = "buttonId";
- private static final int IDCOLIDX = 0;
- /**
- * Moves a list to the next position in the queue
- */
- public static final int NEXT = 2;
- /**
- * Moves a list to the last position in the queue
- */
- public static final int LAST = 3;
- /**
- * Shuffles no songs, turns shuffling off
- */
- public static final int SHUFFLE_NONE = 0;
- /**
- * Shuffles all songs
- */
- public static final int SHUFFLE_NORMAL = 1;
- /**
- * Party shuffle
- */
- public static final int SHUFFLE_AUTO = 2;
- /**
- * Turns repeat off
- */
- public static final int REPEAT_NONE = 0;
- /**
- * Repeats the current track in a list
- */
- public static final int REPEAT_CURRENT = 1;
- /**
- * Repeats all the tracks in a list
- */
- public static final int REPEAT_ALL = 2;
- /**
- * Indicates when the track ends
- */
- private static final int TRACK_ENDED = 1;
- /**
- * Indicates that the current track was changed the next track
- */
- private static final int TRACK_WENT_TO_NEXT = 2;
- /**
- * Indicates the player died
- */
- private static final int SERVER_DIED = 3;
- /**
- * Indicates some sort of focus change, maybe a phone call
- */
- private static final int FOCUSCHANGE = 4;
- /**
- * Indicates to fade the volume down
- */
- private static final int FADEDOWN = 5;
- /**
- * Indicates to fade the volume back up
- */
- private static final int FADEUP = 6;
- /**
- * Notifies that there is a new timed text string
- */
- private static final int LYRICS = 7;
- /**
- * Idle time before stopping the foreground notfication (5 minutes)
- */
- private static final int IDLE_DELAY = 5 * 60 * 1000;
- /**
- * Song play time used as threshold for rewinding to the beginning of the
- * track instead of skipping to the previous track when getting the PREVIOUS
- * command
- */
- private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
- /**
- * The max size allowed for the track history
- * TODO: Comeback and rewrite/fix all the whole queue code bugs after demo
- * https://cyanogen.atlassian.net/browse/MUSIC-175
- * https://cyanogen.atlassian.net/browse/MUSIC-44
- */
- public static final int MAX_HISTORY_SIZE = 1000;
- public interface TrackErrorExtra {
- /**
- * Name of the track that was unable to play
- */
- public static final String TRACK_NAME = "trackname";
- }
- /**
- * The columns used to retrieve any info from the current track
- */
- private static final String[] PROJECTION = new String[] {
- "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
- MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
- MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
- MediaStore.Audio.Media.ARTIST_ID
- };
- /**
- * The columns used to retrieve any info from the current album
- */
- private static final String[] ALBUM_PROJECTION = new String[] {
- MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
- MediaStore.Audio.Albums.LAST_YEAR
- };
- /**
- * Keeps a mapping of the track history
- */
- private static LinkedList<Integer> mHistory = Lists.newLinkedList();
- /**
- * Used to shuffle the tracks
- */
- private static final Shuffler mShuffler = new Shuffler();
- /**
- * Service stub
- */
- private final IBinder mBinder = new ServiceStub(this);
- /**
- * 4x1 widget
- */
- private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
- /**
- * 4x2 widget
- */
- private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
- /**
- * 4x2 alternate widget
- */
- private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
- .getInstance();
- /**
- * The media player
- */
- private MultiPlayer mPlayer;
- /**
- * The path of the current file to play
- */
- private String mFileToPlay;
- /**
- * Alarm intent for removing the notification when nothing is playing
- * for some time
- */
- private AlarmManager mAlarmManager;
- private PendingIntent mShutdownIntent;
- private boolean mShutdownScheduled;
- private NotificationManager mNotificationManager;
- /**
- * The cursor used to retrieve info on the current track and run the
- * necessary queries to play audio files
- */
- private Cursor mCursor;
- /**
- * The cursor used to retrieve info on the album the current track is
- * part of, if any.
- */
- private Cursor mAlbumCursor;
- /**
- * Monitors the audio state
- */
- private AudioManager mAudioManager;
- /**
- * Settings used to save and retrieve the queue and history
- */
- private SharedPreferences mPreferences;
- /**
- * Used to know when the service is active
- */
- private boolean mServiceInUse = false;
- /**
- * Used to know if something should be playing or not
- */
- private boolean mIsSupposedToBePlaying = false;
- /**
- * Gets the last played time to determine whether we still want notifications or not
- */
- private long mLastPlayedTime;
- private int mNotifyMode = NOTIFY_MODE_NONE;
- private long mNotificationPostTime = 0;
- private static final int NOTIFY_MODE_NONE = 0;
- private static final int NOTIFY_MODE_FOREGROUND = 1;
- private static final int NOTIFY_MODE_BACKGROUND = 2;
- /**
- * Used to indicate if the queue can be saved
- */
- private boolean mQueueIsSaveable = true;
- /**
- * Used to track what type of audio focus loss caused the playback to pause
- */
- private boolean mPausedByTransientLossOfFocus = false;
- /**
- * Lock screen controls
- */
- private MediaSession mSession;
- private ComponentName mMediaButtonReceiverComponent;
- // We use this to distinguish between different cards when saving/restoring
- // playlists
- private int mCardId;
- private int mPlayPos = -1;
- private int mNextPlayPos = -1;
- private int mOpenFailedCounter = 0;
- private int mMediaMountedCount = 0;
- private int mShuffleMode = SHUFFLE_NONE;
- private int mRepeatMode = REPEAT_NONE;
- private int mServiceStartId = -1;
- private String mLyrics;
- private ArrayList<MusicPlaybackTrack> mPlaylist = new ArrayList<MusicPlaybackTrack>(100);
- private long[] mAutoShuffleList = null;
- private MusicPlayerHandler mPlayerHandler;
- private HandlerThread mHandlerThread;
- private BroadcastReceiver mUnmountReceiver = null;
- // to improve perf, instead of hitting the disk cache or file cache, store the bitmaps in memory
- private String mCachedKey;
- private BitmapWithColors[] mCachedBitmapWithColors = new BitmapWithColors[2];
- /**
- * Image cache
- */
- private ImageFetcher mImageFetcher;
- /**
- * Recently listened database
- */
- private RecentStore mRecentsCache;
- /**
- * The song play count database
- */
- private SongPlayCount mSongPlayCountCache;
- /**
- * Stores the playback state
- */
- private MusicPlaybackState mPlaybackStateStore;
- /**
- * Shake detector class used for shake to switch song feature
- */
- private ShakeDetector mShakeDetector;
- /**
- * Switch for displaying album art on lockscreen
- */
- private boolean mShowAlbumArtOnLockscreen;
- private ShakeDetector.Listener mShakeDetectorListener=new ShakeDetector.Listener() {
- @Override
- public void hearShake() {
- /*
- * on shake detect, play next song
- */
- if (D) {
- Log.d(TAG,"Shake detected!!!");
- }
- gotoNext(true);
- }
- };
- /**
- * {@inheritDoc}
- */
- @Override
- public IBinder onBind(final Intent intent) {
- if (D) Log.d(TAG, "Service bound, intent = " + intent);
- cancelShutdown();
- mServiceInUse = true;
- return mBinder;
- }
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean onUnbind(final Intent intent) {
- if (D) Log.d(TAG, "Service unbound");
- mServiceInUse = false;
- saveQueue(true);
- if (mIsSupposedToBePlaying || 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.
- } else if (mPlaylist.size() > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
- scheduleDelayedShutdown();
- return true;
- }
- stopSelf(mServiceStartId);
- return true;
- }
- /**
- * {@inheritDoc}
- */
- @Override
- public void onRebind(final Intent intent) {
- cancelShutdown();
- mServiceInUse = true;
- }
- /**
- * {@inheritDoc}
- */
- @Override
- public void onCreate() {
- if (D) Log.d(TAG, "Creating service");
- super.onCreate();
- mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- // Initialize the favorites and recents databases
- mRecentsCache = RecentStore.getInstance(this);
- // gets the song play count cache
- mSongPlayCountCache = SongPlayCount.getInstance(this);
- // gets a pointer to the playback state store
- mPlaybackStateStore = MusicPlaybackState.getInstance(this);
- // Initialize the image fetcher
- mImageFetcher = ImageFetcher.getInstance(this);
- // Initialize the image cache
- mImageFetcher.setImageCache(ImageCache.getInstance(this));
- // Start up the thread running the service. Note that we create a
- // separate thread because the service normally runs in the process's
- // main thread, which we don't want to block. We also make it
- // background priority so CPU-intensive work will not disrupt the UI.
- mHandlerThread = new HandlerThread("MusicPlayerHandler",
- android.os.Process.THREAD_PRIORITY_BACKGROUND);
- mHandlerThread.start();
- // Initialize the handler
- mPlayerHandler = new MusicPlayerHandler(this, mHandlerThread.getLooper());
- // Initialize the audio manager and register any headset controls for
- // playback
- mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
- mMediaButtonReceiverComponent = new ComponentName(getPackageName(),
- MediaButtonIntentReceiver.class.getName());
- mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);
- // Use the remote control APIs to set the playback state
- setUpMediaSession();
- // Initialize the preferences
- mPreferences = getSharedPreferences("Service", 0);
- mCardId = getCardId();
- registerExternalStorageListener();
- // Initialize the media player
- mPlayer = new MultiPlayer(this);
- mPlayer.setHandler(mPlayerHandler);
- // Initialize the intent filter and each action
- final IntentFilter filter = new IntentFilter();
- filter.addAction(SERVICECMD);
- filter.addAction(TOGGLEPAUSE_ACTION);
- filter.addAction(PAUSE_ACTION);
- filter.addAction(STOP_ACTION);
- filter.addAction(SLEEP_MODE_STOP_ACTION);
- filter.addAction(NEXT_ACTION);
- filter.addAction(PREVIOUS_ACTION);
- filter.addAction(PREVIOUS_FORCE_ACTION);
- filter.addAction(REPEAT_ACTION);
- filter.addAction(SHUFFLE_ACTION);
- // Attach the broadcast listener
- registerReceiver(mIntentReceiver, filter);
- // Get events when MediaStore content changes
- mMediaStoreObserver = new MediaStoreObserver(mPlayerHandler);
- getContentResolver().registerContentObserver(
- MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mMediaStoreObserver);
- getContentResolver().registerContentObserver(
- MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mMediaStoreObserver);
- // Initialize the delayed shutdown intent
- final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
- shutdownIntent.setAction(SHUTDOWN);
- mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0);
- // Listen for the idle state
- scheduleDelayedShutdown();
- // Bring the queue back
- reloadQueue();
- notifyChange(QUEUE_CHANGED);
- notifyChange(META_CHANGED);
- }
- private void setUpMediaSession() {
- mSession = new MediaSession(this, "Eleven");
- mSession.setCallback(new MediaSession.Callback() {
- @Override
- public void onPause() {
- pause();
- mPausedByTransientLossOfFocus = false;
- }
- @Override
- public void onPlay() {
- play();
- }
- @Override
- public void onSeekTo(long pos) {
- seek(pos);
- }
- @Override
- public void onSkipToNext() {
- gotoNext(true);
- }
- @Override
- public void onSkipToPrevious() {
- prev(false);
- }
- @Override
- public void onStop() {
- pause();
- mPausedByTransientLossOfFocus = false;
- seek(0);
- releaseServiceUiAndStop();
- }
- });
- mSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
- }
- /**
- * {@inheritDoc}
- */
- @Override
- public void onDestroy() {
- if (D) Log.d(TAG, "Destroying service");
- super.onDestroy();
- // Remove any sound effects
- final Intent audioEffectsIntent = new Intent(
- AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
- audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
- audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
- sendBroadcast(audioEffectsIntent);
- // remove any pending alarms
- mAlarmManager.cancel(mShutdownIntent);
- // Remove any callbacks from the handler
- mPlayerHandler.removeCallbacksAndMessages(null);
- // quit the thread so that anything that gets posted won't run
- mHandlerThread.quitSafely();
- // Release the player
- mPlayer.release();
- mPlayer = null;
- // Remove the audio focus listener and lock screen controls
- mAudioManager.abandonAudioFocus(mAudioFocusListener);
- mSession.release();
- // remove the media store observer
- getContentResolver().unregisterContentObserver(mMediaStoreObserver);
- // Close the cursor
- closeCursor();
- // Unregister the mount listener
- unregisterReceiver(mIntentReceiver);
- if (mUnmountReceiver != null) {
- unregisterReceiver(mUnmountReceiver);
- mUnmountReceiver = null;
- }
- // deinitialize shake detector
- stopShakeDetector(true);
- }
- /**
- * {@inheritDoc}
- */
- @Override
- public int onStartCommand(final Intent intent, final int flags, final int startId) {
- if (D) Log.d(TAG, "Got new intent " + intent + ", startId = " + startId);
- mServiceStartId = startId;
- if (intent != null) {
- final String action = intent.getAction();
- if (SHUTDOWN.equals(action)) {
- mShutdownScheduled = false;
- releaseServiceUiAndStop();
- return START_NOT_STICKY;
- }
- handleCommandIntent(intent);
- }
- // Make sure the service will shut down on its own if it was
- // just started but not bound to and nothing is playing
- scheduleDelayedShutdown();
- if (intent != null && intent.getBooleanExtra(FROM_MEDIA_BUTTON, false)) {
- MediaButtonIntentReceiver.completeWakefulIntent(intent);
- }
- return START_STICKY;
- }
- private void releaseServiceUiAndStop() {
- if (isPlaying()
- || mPausedByTransientLossOfFocus
- || mPlayerHandler.hasMessages(TRACK_ENDED)) {
- return;
- }
- if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
- cancelNotification();
- mAudioManager.abandonAudioFocus(mAudioFocusListener);
- mSession.setActive(false);
- if (!mServiceInUse) {
- saveQueue(true);
- stopSelf(mServiceStartId);
- }
- }
- private void handleCommandIntent(Intent intent) {
- final String action = intent.getAction();
- final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
- if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
- if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
- gotoNext(true);
- } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)
- || PREVIOUS_FORCE_ACTION.equals(action)) {
- prev(PREVIOUS_FORCE_ACTION.equals(action));
- } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
- if (isPlaying()) {
- pause();
- mPausedByTransientLossOfFocus = false;
- } else {
- play();
- }
- } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
- pause();
- mPausedByTransientLossOfFocus = false;
- } else if (CMDPLAY.equals(command)) {
- play();
- } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
- pause();
- mPausedByTransientLossOfFocus = false;
- seek(0);
- releaseServiceUiAndStop();
- } else if (SLEEP_MODE_STOP_ACTION.equals(action)) {
- setSleepMode(false);
- pause();
- mPausedByTransientLossOfFocus = false;
- seek(0);
- releaseServiceUiAndStop();
- } else if (REPEAT_ACTION.equals(action)) {
- cycleRepeat();
- } else if (SHUFFLE_ACTION.equals(action)) {
- cycleShuffle();
- }
- }
- /**
- * Updates the notification, considering the current play and activity state
- */
- private void updateNotification() {
- final int newNotifyMode;
- if (isPlaying()) {
- newNotifyMode = NOTIFY_MODE_FOREGROUND;
- } else if (recentlyPlayed()) {
- newNotifyMode = NOTIFY_MODE_BACKGROUND;
- } else {
- newNotifyMode = NOTIFY_MODE_NONE;
- }
- int notificationId = hashCode();
- if (mNotifyMode != newNotifyMode) {
- if (mNotifyMode == NOTIFY_MODE_FOREGROUND) {
- stopForeground(newNotifyMode == NOTIFY_MODE_NONE);
- } else if (newNotifyMode == NOTIFY_MODE_NONE) {
- mNotificationManager.cancel(notificationId);
- mNotificationPostTime = 0;
- }
- }
- if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
- startForeground(notificationId, buildNotification());
- } else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
- mNotificationManager.notify(notificationId, buildNotification());
- }
- mNotifyMode = newNotifyMode;
- }
- private void cancelNotification() {
- stopForeground(true);
- mNotificationManager.cancel(hashCode());
- mNotificationPostTime = 0;
- mNotifyMode = NOTIFY_MODE_NONE;
- }
- /**
- * @return A card ID used to save and restore playlists, i.e., the queue.
- */
- private int getCardId() {
- final ContentResolver resolver = getContentResolver();
- Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
- null, null);
- int mCardId = -1;
- if (cursor != null && cursor.moveToFirst()) {
- mCardId = cursor.getInt(0);
- cursor.close();
- cursor = null;
- }
- return mCardId;
- }
- /**
- * Called when we receive a ACTION_MEDIA_EJECT notification.
- *
- * @param storagePath The path to mount point for the removed media
- */
- public void closeExternalStorageFiles(final String storagePath) {
- 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() {
- /**
- * {@inheritDoc}
- */
- @Override
- public void onReceive(final Context context, final Intent intent) {
- final 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 = getCardId();
- reloadQueue();
- mQueueIsSaveable = true;
- notifyChange(QUEUE_CHANGED);
- notifyChange(META_CHANGED);
- }
- }
- };
- final IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_MEDIA_EJECT);
- filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
- filter.addDataScheme("file");
- registerReceiver(mUnmountReceiver, filter);
- }
- }
- private void scheduleDelayedShutdown() {
- if (D) Log.v(TAG, "Scheduling shutdown in " + IDLE_DELAY + " ms");
- mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- SystemClock.elapsedRealtime() + IDLE_DELAY, mShutdownIntent);
- mShutdownScheduled = true;
- }
- private void cancelShutdown() {
- if (D) Log.d(TAG, "Cancelling delayed shutdown, scheduled = " + mShutdownScheduled);
- if (mShutdownScheduled) {
- mAlarmManager.cancel(mShutdownIntent);
- mShutdownScheduled = false;
- }
- }
- /**
- * Stops playback
- *
- * @param goToIdle True to go to the idle state, false otherwise
- */
- private void stop(final boolean goToIdle) {
- if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
- if (mPlayer.isInitialized()) {
- mPlayer.stop();
- }
- mFileToPlay = null;
- closeCursor();
- if (goToIdle) {
- setIsSupposedToBePlaying(false, false);
- } else {
- stopForeground(false);
- }
- }
- /**
- * Removes the range of tracks specified from the play list. If a file
- * within the range is the file currently being played, playback will move
- * to the next file after the range.
- *
- * @param first The first file to be removed
- * @param last The last file to be removed
- * @return the number of tracks deleted
- */
- private int removeTracksInternal(int first, int last) {
- synchronized (this) {
- if (last < first) {
- return 0;
- } else if (first < 0) {
- first = 0;
- } else if (last >= mPlaylist.size()) {
- last = mPlaylist.size() - 1;
- }
- boolean gotonext = false;
- if (first <= mPlayPos && mPlayPos <= last) {
- mPlayPos = first;
- gotonext = true;
- } else if (mPlayPos > last) {
- mPlayPos -= last - first + 1;
- }
- final int numToRemove = last - first + 1;
- if (first == 0 && last == mPlaylist.size() - 1) {
- mPlayPos = -1;
- mNextPlayPos = -1;
- mPlaylist.clear();
- mHistory.clear();
- } else {
- for (int i = 0; i < numToRemove; i++) {
- mPlaylist.remove(first);
- }
- // remove the items from the history
- // this is not ideal as the history shouldn't be impacted by this
- // but since we are removing items from the array, it will throw
- // an exception if we keep it around. Idealistically with the queue
- // rewrite this should be all be fixed
- // https://cyanogen.atlassian.net/browse/MUSIC-44
- ListIterator<Integer> positionIterator = mHistory.listIterator();
- while (positionIterator.hasNext()) {
- int pos = positionIterator.next();
- if (pos >= first && pos <= last) {
- positionIterator.remove();
- } else if (pos > last) {
- positionIterator.set(pos - numToRemove);
- }
- }
- }
- if (gotonext) {
- if (mPlaylist.size() == 0) {
- stop(true);
- mPlayPos = -1;
- closeCursor();
- } else {
- if (mShuffleMode != SHUFFLE_NONE) {
- mPlayPos = getNextPosition(true);
- } else if (mPlayPos >= mPlaylist.size()) {
- mPlayPos = 0;
- }
- final boolean wasPlaying = isPlaying();
- stop(false);
- openCurrentAndNext();
- if (wasPlaying) {
- play();
- }
- }
- notifyChange(META_CHANGED);
- }
- return last - first + 1;
- }
- }
- /**
- * Adds a list to the playlist
- *
- * @param list The list to add
- * @param position The position to place the tracks
- */
- private void addToPlayList(final long[] list, int position, long sourceId, IdType sourceType) {
- final int addlen = list.length;
- if (position < 0) {
- mPlaylist.clear();
- position = 0;
- }
- mPlaylist.ensureCapacity(mPlaylist.size() + addlen);
- if (position > mPlaylist.size()) {
- position = mPlaylist.size();
- }
- final ArrayList<MusicPlaybackTrack> arrayList = new ArrayList<MusicPlaybackTrack>(addlen);
- for (int i = 0; i < list.length; i++) {
- arrayList.add(new MusicPlaybackTrack(list[i], sourceId, sourceType, i));
- }
- mPlaylist.addAll(position, arrayList);
- if (mPlaylist.size() == 0) {
- closeCursor();
- notifyChange(META_CHANGED);
- }
- }
- /**
- * @param trackId The track ID
- */
- private void updateCursor(final long trackId) {
- updateCursor("_id=" + trackId, null);
- }
- private void updateCursor(final String selection, final String[] selectionArgs) {
- synchronized (this) {
- closeCursor();
- mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
- PROJECTION, selection, selectionArgs);
- }
- updateAlbumCursor();
- }
- private void updateCursor(final Uri uri) {
- synchronized (this) {
- closeCursor();
- mCursor = openCursorAndGoToFirst(uri, PROJECTION, null, null);
- }
- updateAlbumCursor();
- }
- private void updateAlbumCursor() {
- long albumId = getAlbumId();
- if (albumId >= 0) {
- mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
- ALBUM_PROJECTION, "_id=" + albumId, null);
- } else {
- mAlbumCursor = null;
- }
- }
- private Cursor openCursorAndGoToFirst(Uri uri, String[] projection,
- String selection, String[] selectionArgs) {
- Cursor c = getContentResolver().query(uri, projection,
- selection, selectionArgs, null, null);
- if (c == null) {
- return null;
- }
- if (!c.moveToFirst()) {
- c.close();
- return null;
- }
- return c;
- }
- private synchronized void closeCursor() {
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
- if (mAlbumCursor != null) {
- mAlbumCursor.close();
- mAlbumCursor = null;
- }
- }
- /**
- * Called to open a new file as the current track and prepare the next for
- * playback
- */
- private void openCurrentAndNext() {
- openCurrentAndMaybeNext(true);
- }
- /**
- * Called to open a new file as the current track and prepare the next for
- * playback
- *
- * @param openNext True to prepare the next track for playback, false
- * otherwise.
- */
- private void openCurrentAndMaybeNext(final boolean openNext) {
- synchronized (this) {
- closeCursor();
- if (mPlaylist.size() == 0) {
- return;
- }
- stop(false);
- boolean shutdown = false;
- updateCursor(mPlaylist.get(mPlayPos).mId);
- while (true) {
- if (mCursor != null
- && openFile(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
- closeCursor();
- if (mOpenFailedCounter++ < 10 && mPlaylist.size() > 1) {
- final int pos = getNextPosition(false);
- if (pos < 0) {
- shutdown = true;
- break;
- }
- mPlayPos = pos;
- stop(false);
- mPlayPos = pos;
- updateCursor(mPlaylist.get(mPlayPos).mId);
- } else {
- mOpenFailedCounter = 0;
- Log.w(TAG, "Failed to open file for playback");
- shutdown = true;
- break;
- }
- }
- if (shutdown) {
- scheduleDelayedShutdown();
- if (mIsSupposedToBePlaying) {
- mIsSupposedToBePlaying = false;
- notifyChange(PLAYSTATE_CHANGED);
- }
- } else if (openNext) {
- setNextTrack();
- }
- }
- }
- private void sendErrorMessage(final String trackName) {
- final Intent i = new Intent(TRACK_ERROR);
- i.putExtra(TrackErrorExtra.TRACK_NAME, trackName);
- sendBroadcast(i);
- }
- /**
- * @param force True to force the player onto the track next, false
- * otherwise.
- * @param saveToHistory True to save the mPlayPos to the history
- * @return The next position to play.
- */
- private int getNextPosition(final boolean force) {
- // as a base case, if the playlist is empty just return -1
- if (mPlaylist == null || mPlaylist.isEmpty()) {
- return -1;
- }
- // if we're not forced to go to the next track and we are only playing the current track
- if (!force && mRepeatMode == REPEAT_CURRENT) {
- if (mPlayPos < 0) {
- return 0;
- }
- return mPlayPos;
- } else if (mShuffleMode == SHUFFLE_NORMAL) {
- final int numTracks = mPlaylist.size();
- // count the number of times a track has been played
- final int[] trackNumPlays = new int[numTracks];
- for (int i = 0; i < numTracks; i++) {
- // set it all to 0
- trackNumPlays[i] = 0;
- }
- // walk through the history and add up the number of times the track
- // has been played
- final int numHistory = mHistory.size();
- for (int i = 0; i < numHistory; i++) {
- final int idx = mHistory.get(i).intValue();
- if (idx >= 0 && idx < numTracks) {
- trackNumPlays[idx]++;
- }
- }
- // also add the currently playing track to the count
- if (mPlayPos >= 0 && mPlayPos < numTracks) {
- trackNumPlays[mPlayPos]++;
- }
- // figure out the least # of times a track has a played as well as
- // how many tracks share that count
- int minNumPlays = Integer.MAX_VALUE;
- int numTracksWithMinNumPlays = 0;
- for (int i = 0; i < trackNumPlays.length; i++) {
- // if we found a new track that has less number of plays, reset the counters
- if (trackNumPlays[i] < minNumPlays) {
- minNumPlays = trackNumPlays[i];
- numTracksWithMinNumPlays = 1;
- } else if (trackNumPlays[i] == minNumPlays) {
- // increment this track shares the # of tracks
- numTracksWithMinNumPlays++;
- }
- }
- // if we've played each track at least once and all tracks have been played an equal
- // # of times and we aren't repeating all and we're not forcing a track, then
- // return no more tracks
- if (minNumPlays > 0 && numTracksWithMinNumPlays == numTracks
- && mRepeatMode != REPEAT_ALL && !force) {
- return -1;
- }
- // else pick a track from the least number of played tracks
- int skip = mShuffler.nextInt(numTracksWithMinNumPlays);
- for (int i = 0; i < trackNumPlays.length; i++) {
- if (trackNumPlays[i] == minNumPlays) {
- if (skip == 0) {
- return i;
- } else {
- skip--;
- }
- }
- }
- // Unexpected to land here
- if (D) Log.e(TAG, "Getting the next position resulted did not get a result when it should have");
- return -1;
- } else if (mShuffleMode == SHUFFLE_AUTO) {
- doAutoShuffleUpdate();
- return mPlayPos + 1;
- } else {
- if (mPlayPos >= mPlaylist.size() - 1) {
- if (mRepeatMode == REPEAT_NONE && !force) {
- return -1;
- } else if (mRepeatMode == REPEAT_ALL || force) {
- return 0;
- }
- return -1;
- } else {
- return mPlayPos + 1;
- }
- }
- }
- /**
- * Sets the track to be played
- */
- private void setNextTrack() {
- setNextTrack(getNextPosition(false));
- }
- /**
- * Sets the next track to be played
- * @param position the target position we want
- */
- private void setNextTrack(int position) {
- mNextPlayPos = position;
- if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos);
- if (mNextPlayPos >= 0 && mPlaylist != null && mNextPlayPos < mPlaylist.size()) {
- final long id = mPlaylist.get(mNextPlayPos).mId;
- mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
- } else {
- mPlayer.setNextDataSource(null);
- }
- }
- /**
- * Creates a shuffled playlist used for party mode
- */
- private boolean makeAutoShuffleList() {
- Cursor cursor = null;
- try {
- cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
- new String[] {
- MediaStore.Audio.Media._ID
- }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
- if (cursor == null || cursor.getCount() == 0) {
- return false;
- }
- final int len = cursor.getCount();
- final long[] list = new long[len];
- for (int i = 0; i < len; i++) {
- cursor.moveToNext();
- list[i] = cursor.getLong(0);
- }
- mAutoShuffleList = list;
- return true;
- } catch (final RuntimeException e) {
- } finally {
- if (cursor != null) {
- cursor.close();
- cursor = null;
- }
- }
- return false;
- }
- /**
- * Creates the party shuffle playlist
- */
- private void doAutoShuffleUpdate() {
- boolean notify = false;
- if (mPlayPos > 10) {
- removeTracks(0, mPlayPos - 9);
- notify = true;
- }
- final int toAdd = 7 - (mPlaylist.size() - (mPlayPos < 0 ? -1 : mPlayPos));
- for (int i = 0; i < toAdd; i++) {
- int lookback = mHistory.size();
- int idx = -1;
- while (true) {
- idx = mShuffler.nextInt(mAutoShuffleList.length);
- if (!wasRecentlyUsed(idx, lookback)) {
- break;
- }
- lookback /= 2;
- }
- mHistory.add(idx);
- if (mHistory.size() > MAX_HISTORY_SIZE) {
- mHistory.remove(0);
- }
- mPlaylist.add(new MusicPlaybackTrack(mAutoShuffleList[idx], -1, IdType.NA, -1));
- notify = true;
- }
- if (notify) {
- notifyChange(QUEUE_CHANGED);
- }
- }
- /**/
- private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
- if (lookbacksize == 0) {
- return false;
- }
- final int histsize = mHistory.size();
- if (histsize < lookbacksize) {
- lookbacksize = histsize;
- }
- final int maxidx = histsize - 1;
- for (int i = 0; i < lookbacksize; i++) {
- final long entry = mHistory.get(maxidx - i);
- if (entry == idx) {
- return true;
- }
- }
- return false;
- }
- /**
- * Notify the change-receivers that something has changed.
- */
- private void notifyChange(final String what) {
- if (D) Log.d(TAG, "notifyChange: what = " + what);
- // Update the lockscreen controls
- updateMediaSession(what);
- if (what.equals(POSITION_CHANGED)) {
- return;
- }
- final Intent intent = new Intent(what);
- intent.putExtra("id", getAudioId());
- intent.putExtra("artist", getArtistName());
- intent.putExtra("album", getAlbumName());
- intent.putExtra("track", getTrackName());
- intent.putExtra("playing", isPlaying());
- if (NEW_LYRICS.equals(what)) {
- intent.putExtra("lyrics", mLyrics);
- }
- sendStickyBroadcast(intent);
- final Intent musicIntent = new Intent(intent);
- musicIntent.setAction(what.replace(ELEVEN_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
- sendStickyBroadcast(musicIntent);
- if (what.equals(META_CHANGED)) {
- // Add the track to the recently played list.
- mRecentsCache.addSongId(getAudioId());
- mSongPlayCountCache.bumpSongCount(getAudioId());
- } else if (what.equals(QUEUE_CHANGED)) {
- saveQueue(true);
- if (isPlaying()) {
- // if we are in shuffle mode and our next track is still valid,
- // try to re-use the track
- // We need to reimplement the queue to prevent hacky solutions like this
- // https://cyanogen.atlassian.net/browse/MUSIC-175
- // https://cyanogen.atlassian.net/browse/MUSIC-44
- if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size()
- && getShuffleMode() != SHUFFLE_NONE) {
- setNextTrack(mNextPlayPos);
- } else {
- setNextTrack();
- }
- }
- } else {
- saveQueue(false);
- }
- if (what.equals(PLAYSTATE_CHANGED)) {
- updateNotification();
- }
- // Update the app-widgets
- mAppWidgetSmall.notifyChange(this, what);
- mAppWidgetLarge.notifyChange(this, what);
- mAppWidgetLargeAlternate.notifyChange(this, what);
- }
- private void updateMediaSession(final String what) {
- int playState = mIsSupposedToBePlaying
- ? PlaybackState.STATE_PLAYING
- : PlaybackState.STATE_PAUSED;
- if (what.equals(PLAYSTATE_CHANGED) || what.equals(POSITION_CHANGED)) {
- mSession.setPlaybackState(new PlaybackState.Builder()
- .setState(playState, position(), 1.0f).build());
- } else if (what.equals(META_CHANGED) || what.equals(QUEUE_CHANGED)) {
- Bitmap albumArt = getAlbumArt(false).getBitmap();
- if (albumArt != null) {
- // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need
- // to make sure not to hand out our cache copy
- Bitmap.Config config = albumArt.getConfig();
- if (config == null) {
- config