package org.music.player; 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.graphics.Bitmap; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.media.AudioManager; import android.media.MediaPlayer; import android.os.Build; 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.Process; import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.widget.RemoteViews; import android.widget.Toast; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.util.ArrayList; import org.music.player.R; /** * Handles music playback and pretty much all the other work. */ public final class PlaybackService extends Service implements Handler.Callback , MediaPlayer.OnCompletionListener , MediaPlayer.OnErrorListener , SharedPreferences.OnSharedPreferenceChangeListener , SongTimeline.Callback , SensorEventListener { /** * Name of the state file. */ private static final String STATE_FILE = "state"; /** * Header for state file to help indicate if the file is in the right * format. */ private static final long STATE_FILE_MAGIC = 0x1533574DC74B6ECL; /** * State file version that indicates data order. */ private static final int STATE_VERSION = 6; private static final int NOTIFICATION_ID = 2; /** * Action for startService: toggle playback on/off. */ public static final String ACTION_TOGGLE_PLAYBACK = "org.music.player.action.TOGGLE_PLAYBACK"; /** * Action for startService: start playback if paused. */ public static final String ACTION_PLAY = "org.music.player.action.PLAY"; /** * Action for startService: pause playback if playing. */ public static final String ACTION_PAUSE = "org.music.player.action.PAUSE"; /** * Action for startService: toggle playback on/off. * * Unlike {@link PlaybackService#ACTION_TOGGLE_PLAYBACK}, the toggle does * not occur immediately. Instead, it is delayed so that if two of these * actions are received within 400 ms, the playback activity is opened * instead. */ public static final String ACTION_TOGGLE_PLAYBACK_DELAYED = "org.music.player.action.TOGGLE_PLAYBACK_DELAYED"; /** * Action for startService: toggle playback on/off. * * This works the same way as ACTION_PLAY_PAUSE but prevents the notification * from being hidden regardless of notification visibility settings. */ public static final String ACTION_TOGGLE_PLAYBACK_NOTIFICATION = "org.music.player.action.TOGGLE_PLAYBACK_NOTIFICATION"; /** * Action for startService: advance to the next song. */ public static final String ACTION_NEXT_SONG = "org.music.player.action.NEXT_SONG"; /** * Action for startService: advance to the next song. * * Unlike {@link PlaybackService#ACTION_NEXT_SONG}, the toggle does * not occur immediately. Instead, it is delayed so that if two of these * actions are received within 400 ms, the playback activity is opened * instead. */ public static final String ACTION_NEXT_SONG_DELAYED = "org.music.player.action.NEXT_SONG_DELAYED"; /** * Action for startService: advance to the next song. * * Like ACTION_NEXT_SONG, but starts playing automatically if paused * when this is called. */ public static final String ACTION_NEXT_SONG_AUTOPLAY = "org.music.player.action.NEXT_SONG_AUTOPLAY"; /** * Action for startService: go back to the previous song. */ public static final String ACTION_PREVIOUS_SONG = "org.music.player.action.PREVIOUS_SONG"; /** * Action for startService: go back to the previous song. * * Like ACTION_PREVIOUS_SONG, but starts playing automatically if paused * when this is called. */ public static final String ACTION_PREVIOUS_SONG_AUTOPLAY = "org.music.player.action.PREVIOUS_SONG_AUTOPLAY"; /** * Change the shuffle mode. */ public static final String ACTION_CYCLE_SHUFFLE = "org.music.player.CYCLE_SHUFFLE"; /** * Change the repeat mode. */ public static final String ACTION_CYCLE_REPEAT = "org.music.player.CYCLE_REPEAT"; /** * Pause music and hide the notifcation. */ public static final String ACTION_CLOSE_NOTIFICATION = "org.music.player.CLOSE_NOTIFICATION"; public static final int NEVER = 0; public static final int WHEN_PLAYING = 1; public static final int ALWAYS = 2; /** * Notification click action: open LaunchActivity. */ private static final int NOT_ACTION_MAIN_ACTIVITY = 0; /** * Notification click action: open MiniPlaybackActivity. */ private static final int NOT_ACTION_MINI_ACTIVITY = 1; /** * Notification click action: skip to next song. */ private static final int NOT_ACTION_NEXT_SONG = 2; /** * If a user action is triggered within this time (in ms) after the * idle time fade-out occurs, playback will be resumed. */ private static final long IDLE_GRACE_PERIOD = 60000; /** * Minimum time in milliseconds between shake actions. */ private static final int MIN_SHAKE_PERIOD = 500; /** * Defer release of mWakeLock for this time (in ms). */ private static final int WAKE_LOCK_DELAY = 60000; /** * If set, music will play. */ public static final int FLAG_PLAYING = 0x1; /** * Set when there is no media available on the device. */ public static final int FLAG_NO_MEDIA = 0x2; /** * Set when the current song is unplayable. */ public static final int FLAG_ERROR = 0x4; /** * Set when the user needs to select songs to play. */ public static final int FLAG_EMPTY_QUEUE = 0x8; public static final int SHIFT_FINISH = 4; /** * These two bits will be one of SongTimeline.FINISH_*. */ public static final int MASK_FINISH = 0x7 << SHIFT_FINISH; public static final int SHIFT_SHUFFLE = 7; /** * These two bits will be one of SongTimeline.SHUFFLE_*. */ public static final int MASK_SHUFFLE = 0x3 << SHIFT_SHUFFLE; /** * The PlaybackService state, indicating if the service is playing, * repeating, etc. * * The format of this is 0b00000000_00000000_00000000f_feeedcba, * where each bit is: * a: {@link PlaybackService#FLAG_PLAYING} * b: {@link PlaybackService#FLAG_NO_MEDIA} * c: {@link PlaybackService#FLAG_ERROR} * d: {@link PlaybackService#FLAG_EMPTY_QUEUE} * eee: {@link PlaybackService#MASK_FINISH} * ff: {@link PlaybackService#MASK_SHUFFLE} */ int mState; /** * Object used for state-related locking. */ final Object[] mStateLock = new Object[0]; /** * Object used for PlaybackService startup waiting. */ private static final Object[] sWait = new Object[0]; /** * The appplication-wide instance of the PlaybackService. */ public static PlaybackService sInstance; private static final ArrayList<PlaybackActivity> sActivities = new ArrayList<PlaybackActivity>(5); /** * Cached app-wide SharedPreferences instance. */ private static SharedPreferences sSettings; boolean mHeadsetPause; private boolean mScrobble; /** * If true, emulate the music status broadcasts sent by the stock android * music player. */ private boolean mStockBroadcast; private int mNotificationMode; /** * If true, audio will not be played through the speaker. */ private boolean mHeadsetOnly; /** * If true, start playing when the headset is plugged in. */ boolean mHeadsetPlay; /** * True if the initial broadcast sent when registering HEADSET_PLUG has * been receieved. */ boolean mPlugInitialized; /** * The time to wait before considering the player idle. */ private int mIdleTimeout; /** * The intent for the notification to execute, created by * {@link PlaybackService#createNotificationAction(SharedPreferences)}. */ private PendingIntent mNotificationAction; /** * Use white text instead of black default text in notification. */ private boolean mInvertNotification; private Looper mLooper; private Handler mHandler; MediaPlayer mMediaPlayer; private boolean mMediaPlayerInitialized; private PowerManager.WakeLock mWakeLock; private NotificationManager mNotificationManager; private AudioManager mAudioManager; /** * The SensorManager service. */ private SensorManager mSensorManager; /** * The equalizer wrapper. */ private CompatEq mEqualizer; SongTimeline mTimeline; private Song mCurrentSong; boolean mPlayingBeforeCall; /** * Stores the saved position in the current song from saved state. Should * be seeked to when the song is loaded into MediaPlayer. Used only during * initialization. The song that the saved position is for is stored in * {@link #mPendingSeekSong}. */ private int mPendingSeek; /** * The id of the song that the mPendingSeek position is for. -1 indicates * an invalid song. Value is undefined when mPendingSeek is 0. */ private long mPendingSeekSong; public Receiver mReceiver; public InCallListener mCallListener; private String mErrorMessage; /** * The volume adjustment set in the volume preference. */ private float mUserVolume; /** * The linear scale volume set on the MediaPlayer. */ private float mBaseVolume; /** * If true, the volume is being reduced for the idle fade-out. */ private boolean mFadeInProgress; /** * Elapsed realtime at which playback was paused by idle timeout. -1 * indicates that no timeout has occurred. */ private long mIdleStart = -1; /** * True if the last audio focus loss can be ducked. */ private boolean mDuckedLoss; /** * Magnitude of last sensed acceleration. */ private double mAccelLast; /** * Filtered acceleration used for shake detection. */ private double mAccelFiltered; /** * Elapsed realtime of last shake action. */ private long mLastShakeTime; /** * Minimum jerk required for shake. */ private double mShakeThreshold; /** * What to do when an accelerometer shake is detected. */ private Action mShakeAction; /** * If true, the notification should not be hidden when pausing regardless * of user settings. */ private boolean mForceNotificationVisible; @Override public void onCreate() { HandlerThread thread = new HandlerThread("PlaybackService", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mTimeline = new SongTimeline(this); mTimeline.setCallback(this); int state = loadState(); mMediaPlayer = new MediaPlayer(); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mMediaPlayer.setOnCompletionListener(this); mMediaPlayer.setOnErrorListener(this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { try { mEqualizer = new CompatEq(mMediaPlayer); } catch (IllegalArgumentException e) { // equalizer not supported } } mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { CompatFroyo.createAudioFocus(); } SharedPreferences settings = getSettings(this); settings.registerOnSharedPreferenceChangeListener(this); mNotificationMode = Integer.parseInt(settings.getString(PrefKeys.NOTIFICATION_MODE, "1")); mScrobble = settings.getBoolean(PrefKeys.SCROBBLE, false); mUserVolume = (float)Math.pow(settings.getInt(PrefKeys.VOLUME, 100) / 100.0, 3); mIdleTimeout = settings.getBoolean(PrefKeys.USE_IDLE_TIMEOUT, false) ? settings.getInt(PrefKeys.IDLE_TIMEOUT, 3600) : 0; Song.mDisableCoverArt = settings.getBoolean(PrefKeys.DISABLE_COVER_ART, false); mHeadsetOnly = settings.getBoolean(PrefKeys.HEADSET_ONLY, false); mStockBroadcast = settings.getBoolean(PrefKeys.STOCK_BROADCAST, false); mHeadsetPlay = settings.getBoolean(PrefKeys.HEADSET_PLAY, false); mInvertNotification = settings.getBoolean(PrefKeys.NOTIFICATION_INVERTED_COLOR, false); mNotificationAction = createNotificationAction(settings); mHeadsetPause = getSettings(this).getBoolean(PrefKeys.HEADSET_PAUSE, true); mShakeAction = settings.getBoolean(PrefKeys.ENABLE_SHAKE, false) ? Action.getAction(settings, PrefKeys.SHAKE_ACTION, Action.NextSong) : Action.Nothing; mShakeThreshold = settings.getInt(PrefKeys.SHAKE_THRESHOLD, 80) / 10.0f; updateVolume(); PowerManager powerManager = (PowerManager)getSystemService(POWER_SERVICE); mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "VanillaMusicLock"); try { mCallListener = new InCallListener(); TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); telephonyManager.listen(mCallListener, PhoneStateListener.LISTEN_CALL_STATE); } catch (SecurityException e) { // don't have READ_PHONE_STATE } mReceiver = new Receiver(); IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); filter.addAction(Intent.ACTION_HEADSET_PLUG); filter.addAction(Intent.ACTION_SCREEN_ON); registerReceiver(mReceiver, filter); getContentResolver().registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mObserver); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { CompatIcs.registerRemote(this, mAudioManager); } mLooper = thread.getLooper(); mHandler = new Handler(mLooper, this); initWidgets(); updateState(state); setCurrentSong(0); sInstance = this; synchronized (sWait) { sWait.notifyAll(); } mAccelFiltered = 0.0f; mAccelLast = SensorManager.GRAVITY_EARTH; setupSensor(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String action = intent.getAction(); if (ACTION_TOGGLE_PLAYBACK.equals(action)) { playPause(); } else if (ACTION_TOGGLE_PLAYBACK_NOTIFICATION.equals(action)) { mForceNotificationVisible = true; synchronized (mStateLock) { if ((mState & FLAG_PLAYING) != 0) pause(); else play(); } } else if (ACTION_TOGGLE_PLAYBACK_DELAYED.equals(action)) { if (mHandler.hasMessages(CALL_GO, Integer.valueOf(0))) { mHandler.removeMessages(CALL_GO, Integer.valueOf(0)); Intent launch = new Intent(this, LibraryActivity.class); launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); launch.setAction(Intent.ACTION_MAIN); startActivity(launch); } else { mHandler.sendMessageDelayed(mHandler.obtainMessage(CALL_GO, 0, 0, Integer.valueOf(0)), 400); } } else if (ACTION_NEXT_SONG.equals(action)) { setCurrentSong(1); userActionTriggered(); } else if (ACTION_NEXT_SONG_AUTOPLAY.equals(action)) { setCurrentSong(1); play(); } else if (ACTION_NEXT_SONG_DELAYED.equals(action)) { if (mHandler.hasMessages(CALL_GO, Integer.valueOf(1))) { mHandler.removeMessages(CALL_GO, Integer.valueOf(1)); Intent launch = new Intent(this, LibraryActivity.class); launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); launch.setAction(Intent.ACTION_MAIN); startActivity(launch); } else { mHandler.sendMessageDelayed(mHandler.obtainMessage(CALL_GO, 1, 0, Integer.valueOf(1)), 400); } } else if (ACTION_PREVIOUS_SONG.equals(action)) { setCurrentSong(-1); userActionTriggered(); } else if (ACTION_PREVIOUS_SONG_AUTOPLAY.equals(action)) { setCurrentSong(-1); play(); } else if (ACTION_PLAY.equals(action)) { play(); } else if (ACTION_PAUSE.equals(action)) { pause(); } else if (ACTION_CYCLE_REPEAT.equals(action)) { cycleFinishAction(); } else if (ACTION_CYCLE_SHUFFLE.equals(action)) { cycleShuffle(); } else if (ACTION_CLOSE_NOTIFICATION.equals(action)) { mForceNotificationVisible = false; pause(); stopForeground(true); // sometimes required to clear notification mNotificationManager.cancel(NOTIFICATION_ID); } MediaButtonReceiver.registerMediaButton(this); } return START_NOT_STICKY; } @Override public void onDestroy() { sInstance = null; mLooper.quit(); // clear the notification stopForeground(true); if (mMediaPlayer != null) { saveState(mMediaPlayer.getCurrentPosition()); mMediaPlayer.release(); mMediaPlayer = null; } MediaButtonReceiver.unregisterMediaButton(this); try { unregisterReceiver(mReceiver); } catch (IllegalArgumentException e) { // we haven't registered the receiver yet } if (mSensorManager != null && mShakeAction != Action.Nothing) mSensorManager.unregisterListener(this); if (mWakeLock != null && mWakeLock.isHeld()) mWakeLock.release(); super.onDestroy(); } /** * Return the SharedPreferences instance containing the PlaybackService * settings, creating it if necessary. */ public static SharedPreferences getSettings(Context context) { if (sSettings == null) sSettings = PreferenceManager.getDefaultSharedPreferences(context); return sSettings; } /** * Setup the accelerometer. */ private void setupSensor() { if (mShakeAction == Action.Nothing || (mState & FLAG_PLAYING) == 0) { if (mSensorManager != null) mSensorManager.unregisterListener(this); } else { if (mSensorManager == null) mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_UI); } } /** * Set the volume gain on the MediaPlayer/Equalizer */ private void updateVolume() { float base = mUserVolume; if (base > 1.0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { // In Gingerbread and above, MediaPlayer no longer accepts volumes // > 1.0. So we use an equalizer instead. CompatEq eq = mEqualizer; if (eq != null) { short gain = (short)(2000 * Math.log10(base)); for (short i = eq.getNumberOfBands(); --i != -1; ) { eq.setBandLevel(i, gain); } } base = 1.0f; } mBaseVolume = base; if (mMediaPlayer != null) mMediaPlayer.setVolume(base, base); } private void loadPreference(String key) { SharedPreferences settings = getSettings(this); if (PrefKeys.HEADSET_PAUSE.equals(key)) { mHeadsetPause = settings.getBoolean(PrefKeys.HEADSET_PAUSE, true); } else if (PrefKeys.NOTIFICATION_ACTION.equals(key)) { mNotificationAction = createNotificationAction(settings); updateNotification(); } else if (PrefKeys.NOTIFICATION_INVERTED_COLOR.equals(key)) { mInvertNotification = settings.getBoolean(PrefKeys.NOTIFICATION_INVERTED_COLOR, false); updateNotification(); } else if (PrefKeys.NOTIFICATION_MODE.equals(key)){ mNotificationMode = Integer.parseInt(settings.getString(PrefKeys.NOTIFICATION_MODE, "1")); // This is the only way to remove a notification created by // startForeground(), even if we are not currently in foreground // mode. stopForeground(true); updateNotification(); } else if (PrefKeys.SCROBBLE.equals(key)) { mScrobble = settings.getBoolean(PrefKeys.SCROBBLE, false); } else if (PrefKeys.VOLUME.equals(key)) { mUserVolume = (float)Math.pow(settings.getInt(key, 100) / 100.0, 3); updateVolume(); } else if (PrefKeys.MEDIA_BUTTON.equals(key) || PrefKeys.MEDIA_BUTTON_BEEP.equals(key)) { MediaButtonReceiver.reloadPreference(this); } else if (PrefKeys.USE_IDLE_TIMEOUT.equals(key) || PrefKeys.IDLE_TIMEOUT.equals(key)) { mIdleTimeout = settings.getBoolean(PrefKeys.USE_IDLE_TIMEOUT, false) ? settings.getInt(PrefKeys.IDLE_TIMEOUT, 3600) : 0; userActionTriggered(); } else if (PrefKeys.DISABLE_COVER_ART.equals(key)) { Song.mDisableCoverArt = settings.getBoolean(PrefKeys.DISABLE_COVER_ART, false); } else if (PrefKeys.NOTIFICATION_INVERTED_COLOR.equals(key)) { updateNotification(); } else if (PrefKeys.HEADSET_ONLY.equals(key)) { mHeadsetOnly = settings.getBoolean(key, false); if (mHeadsetOnly && isSpeakerOn()) unsetFlag(FLAG_PLAYING); } else if (PrefKeys.STOCK_BROADCAST.equals(key)) { mStockBroadcast = settings.getBoolean(key, false); } else if (PrefKeys.HEADSET_PLAY.equals(key)) { mHeadsetPlay = settings.getBoolean(key, false); } else if (PrefKeys.ENABLE_SHAKE.equals(key) || PrefKeys.SHAKE_ACTION.equals(key)) { mShakeAction = settings.getBoolean(PrefKeys.ENABLE_SHAKE, false) ? Action.getAction(settings, PrefKeys.SHAKE_ACTION, Action.NextSong) : Action.Nothing; setupSensor(); } else if (PrefKeys.SHAKE_THRESHOLD.equals(key)) { mShakeThreshold = settings.getInt(PrefKeys.SHAKE_THRESHOLD, 80) / 10.0f; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { CompatFroyo.dataChanged(this); } } /** * Set a state flag. */ public void setFlag(int flag) { synchronized (mStateLock) { updateState(mState | flag); } } /** * Unset a state flag. */ public void unsetFlag(int flag) { synchronized (mStateLock) { updateState(mState & ~flag); } } /** * Return true if audio would play through the speaker. */ @SuppressWarnings("deprecation") private boolean isSpeakerOn() { // Android seems very intent on making this difficult to detect. In // Android 1.5, this worked great with AudioManager.getRouting(), // which definitively answered if audio would play through the speakers. // Android 2.0 deprecated this method and made it no longer function. // So this hacky alternative was created. But with Android 4.0, // isWiredHeadsetOn() was deprecated, though it still works. But for // how much longer? // // I'd like to remove this feature so I can avoid fighting Android to // keep it working, but some users seem to really like it. I think the // best solution to this problem is for Android to have separate media // volumes for speaker, headphones, etc. That way the speakers can be // muted system-wide. There is not much I can do about that here, // though. return !mAudioManager.isWiredHeadsetOn() && !mAudioManager.isBluetoothA2dpOn() && !mAudioManager.isBluetoothScoOn(); } /** * Modify the service state. * * @param state Union of PlaybackService.STATE_* flags * @return The new state */ private int updateState(int state) { if ((state & (FLAG_NO_MEDIA|FLAG_ERROR|FLAG_EMPTY_QUEUE)) != 0 || mHeadsetOnly && isSpeakerOn()) state &= ~FLAG_PLAYING; int oldState = mState; mState = state; if (state != oldState) { mHandler.sendMessage(mHandler.obtainMessage(PROCESS_STATE, oldState, state)); mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_CHANGE, state, 0)); } return state; } private void processNewState(int oldState, int state) { int toggled = oldState ^ state; if ((toggled & FLAG_PLAYING) != 0) { if ((state & FLAG_PLAYING) != 0) { if (mMediaPlayerInitialized) mMediaPlayer.start(); if (mNotificationMode != NEVER) startForeground(NOTIFICATION_ID, createNotification(mCurrentSong, mState)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { CompatFroyo.requestAudioFocus(mAudioManager); } mHandler.removeMessages(RELEASE_WAKE_LOCK); try { if (mWakeLock != null) mWakeLock.acquire(); } catch (SecurityException e) { // Don't have WAKE_LOCK permission } } else { if (mMediaPlayerInitialized) mMediaPlayer.pause(); if (mNotificationMode == ALWAYS || mForceNotificationVisible) { stopForeground(false); mNotificationManager.notify(NOTIFICATION_ID, createNotification(mCurrentSong, mState)); } else { stopForeground(true); } // Delay release of the wake lock. This allows the headset // button to continue to function for a short period after // pausing. mHandler.sendEmptyMessageDelayed(RELEASE_WAKE_LOCK, WAKE_LOCK_DELAY); } setupSensor(); } if ((toggled & FLAG_NO_MEDIA) != 0 && (state & FLAG_NO_MEDIA) != 0) { Song song = mCurrentSong; if (song != null && mMediaPlayerInitialized) { mPendingSeek = mMediaPlayer.getCurrentPosition(); mPendingSeekSong = song.id; } } if ((toggled & MASK_SHUFFLE) != 0) mTimeline.setShuffleMode(shuffleMode(state)); if ((toggled & MASK_FINISH) != 0) mTimeline.setFinishAction(finishAction(state)); } private void broadcastChange(int state, Song song, long uptime) { if (state != -1) { ArrayList<PlaybackActivity> list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).setState(uptime, state); } if (song != null) { ArrayList<PlaybackActivity> list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).setSong(uptime, song); } updateWidgets(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { CompatIcs.updateRemote(this, mCurrentSong, mState); } if (mStockBroadcast) stockMusicBroadcast(); if (mScrobble) scrobble(); } /** * Check if there are any instances of each widget. */ private void initWidgets() { AppWidgetManager manager = AppWidgetManager.getInstance(this); OneCellWidget.checkEnabled(this, manager); FourSquareWidget.checkEnabled(this, manager); FourLongWidget.checkEnabled(this, manager); FourWhiteWidget.checkEnabled(this, manager); WidgetD.checkEnabled(this, manager); WidgetE.checkEnabled(this, manager); } /** * Update the widgets with the current song and state. */ private void updateWidgets() { AppWidgetManager manager = AppWidgetManager.getInstance(this); Song song = mCurrentSong; int state = mState; OneCellWidget.updateWidget(this, manager, song, state); FourLongWidget.updateWidget(this, manager, song, state); FourSquareWidget.updateWidget(this, manager, song, state); FourWhiteWidget.updateWidget(this, manager, song, state); WidgetD.updateWidget(this, manager, song, state); WidgetE.updateWidget(this, manager, song, state); } /** * Send a broadcast emulating that of the stock music player. */ private void stockMusicBroadcast() { Song song = mCurrentSong; Intent intent = new Intent("com.android.music.playstatechanged"); intent.putExtra("playing", (mState & FLAG_PLAYING) != 0); if (song != null) { intent.putExtra("track", song.title); intent.putExtra("album", song.album); intent.putExtra("artist", song.artist); intent.putExtra("songid", song.id); intent.putExtra("albumid", song.albumId); } sendBroadcast(intent); } private void scrobble() { Song song = mCurrentSong; Intent intent = new Intent("net.jjc1138.android.scrobbler.action.MUSIC_STATUS"); intent.putExtra("playing", (mState & FLAG_PLAYING) != 0); if (song != null) intent.putExtra("id", (int)song.id); sendBroadcast(intent); } private void updateNotification() { if ((mForceNotificationVisible || mNotificationMode == ALWAYS || mNotificationMode == WHEN_PLAYING && (mState & FLAG_PLAYING) != 0) && mCurrentSong != null) mNotificationManager.notify(NOTIFICATION_ID, createNotification(mCurrentSong, mState)); else mNotificationManager.cancel(NOTIFICATION_ID); } /** * Start playing if currently paused. * * @return The new state after this is called. */ public int play() { synchronized (mStateLock) { if ((mState & FLAG_EMPTY_QUEUE) != 0) { setFinishAction(SongTimeline.FINISH_RANDOM); setCurrentSong(0); Toast.makeText(this, R.string.random_enabling, Toast.LENGTH_SHORT).show(); } int state = updateState(mState | FLAG_PLAYING); userActionTriggered(); return state; } } /** * Pause if currently playing. * * @return The new state after this is called. */ public int pause() { synchronized (mStateLock) { int state = updateState(mState & ~FLAG_PLAYING); userActionTriggered(); return state; } } /** * If playing, pause. If paused, play. * * @return The new state after this is called. */ public int playPause() { mForceNotificationVisible = false; synchronized (mStateLock) { if ((mState & FLAG_PLAYING) != 0) return pause(); else return play(); } } /** * Change the end action (e.g. repeat, random). * * @param action The new action. One of SongTimeline.FINISH_*. * @return The new state after this is called. */ public int setFinishAction(int action) { synchronized (mStateLock) { return updateState(mState & ~MASK_FINISH | action << SHIFT_FINISH); } } /** * Cycle repeat mode. Disables random mode. * * @return The new state after this is called. */ public int cycleFinishAction() { synchronized (mStateLock) { int mode = finishAction(mState) + 1; if (mode > SongTimeline.FINISH_RANDOM) mode = SongTimeline.FINISH_STOP; return setFinishAction(mode); } } /** * Change the shuffle mode. * * @param mode The new mode. One of SongTimeline.SHUFFLE_*. * @return The new state after this is called. */ public int setShuffleMode(int mode) { synchronized (mStateLock) { return updateState(mState & ~MASK_SHUFFLE | mode << SHIFT_SHUFFLE); } } /** * Cycle shuffle mode. * * @return The new state after this is called. */ public int cycleShuffle() { synchronized (mStateLock) { int mode = shuffleMode(mState) + 1; if (mode > SongTimeline.SHUFFLE_ALBUMS) mode = SongTimeline.SHUFFLE_NONE; return setShuffleMode(mode); } } /** * Move to the next or previous song or album in the timeline. * * @param delta One of SongTimeline.SHIFT_*. 0 can also be passed to * initialize the current song with media player, notification, * broadcasts, etc. * @return The new current song */ private Song setCurrentSong(int delta) { if (mMediaPlayer == null) return null; if (mMediaPlayer.isPlaying()) mMediaPlayer.stop(); Song song; if (delta == 0) song = mTimeline.getSong(0); else song = mTimeline.shiftCurrentSong(delta); mCurrentSong = song; if (song == null || song.id == -1 || song.path == null) { if (MediaUtils.isSongAvailable(getContentResolver())) { int flag = finishAction(mState) == SongTimeline.FINISH_RANDOM ? FLAG_ERROR : FLAG_EMPTY_QUEUE; synchronized (mStateLock) { updateState((mState | flag) & ~FLAG_NO_MEDIA); } return null; } else { // we don't have any songs : / synchronized (mStateLock) { updateState((mState | FLAG_NO_MEDIA) & ~FLAG_EMPTY_QUEUE); } return null; } } else if ((mState & (FLAG_NO_MEDIA|FLAG_EMPTY_QUEUE)) != 0) { synchronized (mStateLock) { updateState(mState & ~(FLAG_EMPTY_QUEUE|FLAG_NO_MEDIA)); } } mHandler.removeMessages(PROCESS_SONG); mMediaPlayerInitialized = false; mHandler.sendMessage(mHandler.obtainMessage(PROCESS_SONG, song)); mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_CHANGE, -1, 0, song)); return song; } private void processSong(Song song) { try { mMediaPlayerInitialized = false; mMediaPlayer.reset(); mMediaPlayer.setDataSource(song.path); mMediaPlayer.prepare(); mMediaPlayerInitialized = true; if (mPendingSeek != 0 && mPendingSeekSong == song.id) { mMediaPlayer.seekTo(mPendingSeek); mPendingSeek = 0; } if ((mState & FLAG_PLAYING) != 0) mMediaPlayer.start(); if ((mState & FLAG_ERROR) != 0) { mErrorMessage = null; updateState(mState & ~FLAG_ERROR); } } catch (IOException e) { mErrorMessage = getResources().getString(R.string.song_load_failed, song.path); updateState(mState | FLAG_ERROR); Toast.makeText(this, mErrorMessage, Toast.LENGTH_LONG).show(); Log.e("VanillaMusic", "IOException", e); } updateNotification(); mTimeline.purge(); } @Override public void onCompletion(MediaPlayer player) { if (finishAction(mState) == SongTimeline.FINISH_REPEAT_CURRENT) { setCurrentSong(0); } else if (finishAction(mState) == SongTimeline.FINISH_STOP_CURRENT) { unsetFlag(FLAG_PLAYING); setCurrentSong(+1); } else if (mTimeline.isEndOfQueue()) { unsetFlag(FLAG_PLAYING); } else { setCurrentSong(+1); } } @Override public boolean onError(MediaPlayer player, int what, int extra) { Log.e("VanillaMusic", "MediaPlayer error: " + what + ' ' + extra); return true; } /** * Returns the song <code>delta</code> places away from the current * position. * * @see SongTimeline#getSong(int) */ public Song getSong(int delta) { if (mTimeline == null) return null; if (delta == 0) return mCurrentSong; return mTimeline.getSong(delta); } private class Receiver extends BroadcastReceiver { @Override public void onReceive(Context content, Intent intent) { String action = intent.getAction(); if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(action)) { if (mHeadsetPause) unsetFlag(FLAG_PLAYING); } else if (Intent.ACTION_HEADSET_PLUG.equals(action)) { if (mHeadsetPlay && mPlugInitialized && intent.getIntExtra("state", 0) == 1) setFlag(FLAG_PLAYING); else if (!mPlugInitialized) mPlugInitialized = true; } else if (Intent.ACTION_SCREEN_ON.equals(action)) { userActionTriggered(); } } } private class InCallListener extends PhoneStateListener { @Override public void onCallStateChanged(int state, String incomingNumber) { switch (state) { case TelephonyManager.CALL_STATE_RINGING: case TelephonyManager.CALL_STATE_OFFHOOK: { MediaButtonReceiver.setInCall(true); if (!mPlayingBeforeCall) { synchronized (mStateLock) { if (mPlayingBeforeCall = (mState & FLAG_PLAYING) != 0) unsetFlag(FLAG_PLAYING); } } break; } case TelephonyManager.CALL_STATE_IDLE: { MediaButtonReceiver.setInCall(false); if (mPlayingBeforeCall) { setFlag(FLAG_PLAYING); mPlayingBeforeCall = false; } break; } } } } public void onMediaChange() { if (MediaUtils.isSongAvailable(getContentResolver())) { if ((mState & FLAG_NO_MEDIA) != 0) setCurrentSong(0); } else { setFlag(FLAG_NO_MEDIA); } ArrayList<PlaybackActivity> list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).onMediaChange(); } @Override public void onSharedPreferenceChanged(SharedPreferences settings, String key) { loadPreference(key); } /** * Calls {@link PowerManager.WakeLock#release()} on mWakeLock. */ private static final int RELEASE_WAKE_LOCK = 1; /** * Run the given query and add the results to the timeline. * * obj is the QueryTask. arg1 is the add mode (one of SongTimeline.MODE_*) */ private static final int QUERY = 2; /** * This message is sent with a delay specified by a user preference. After * this delay, assuming no new IDLE_TIMEOUT messages cancel it, playback * will be stopped. */ private static final int IDLE_TIMEOUT = 4; /** * Decrease the volume gradually over five seconds, pausing when 0 is * reached. * * arg1 should be the progress in the fade as a percentage, 1-100. */ private static final int FADE_OUT = 7; /** * If arg1 is 0, calls {@link PlaybackService#playPause()}. * Otherwise, calls {@link PlaybackService#setCurrentSong(int)} with arg1. */ private static final int CALL_GO = 8; private static final int BROADCAST_CHANGE = 10; private static final int SAVE_STATE = 12; private static final int PROCESS_SONG = 13; private static final int PROCESS_STATE = 14; @Override public boolean handleMessage(Message message) { switch (message.what) { case CALL_GO: if (message.arg1 == 0) playPause(); else setCurrentSong(message.arg1); break; case SAVE_STATE: // For unexpected terminations: crashes, task killers, etc. // In most cases onDestroy will handle this saveState(0); break; case PROCESS_SONG: processSong((Song)message.obj); break; case QUERY: runQuery((QueryTask)message.obj); break; case IDLE_TIMEOUT: if ((mState & FLAG_PLAYING) != 0) { mHandler.sendMessage(mHandler.obtainMessage(FADE_OUT, 100, 0)); mFadeInProgress = true; } break; case FADE_OUT: { int progress = message.arg1 - 1; float volume; if (progress == 0) { mIdleStart = SystemClock.elapsedRealtime(); unsetFlag(FLAG_PLAYING); volume = mBaseVolume; mFadeInProgress = false; } else { // Approximate an exponential curve with x^4 // http://www.dr-lex.be/info-stuff/volumecontrols.html volume = Math.max((float)(Math.pow(progress / 100f, 4) * mBaseVolume), .01f); mHandler.sendMessageDelayed(mHandler.obtainMessage(FADE_OUT, progress, 0), 50); } if (mMediaPlayer != null) mMediaPlayer.setVolume(volume, volume); break; } case PROCESS_STATE: processNewState(message.arg1, message.arg2); break; case BROADCAST_CHANGE: broadcastChange(message.arg1, (Song)message.obj, message.getWhen()); break; case RELEASE_WAKE_LOCK: if (mWakeLock != null && mWakeLock.isHeld()) mWakeLock.release(); break; default: return false; } return true; } /** * Returns the current service state. The state comprises several individual * flags. */ public int getState() { synchronized (mStateLock) { return mState; } } /** * Returns the current position in current song in milliseconds. */ public int getPosition() { if (!mMediaPlayerInitialized) return 0; return mMediaPlayer.getCurrentPosition(); } /** * Seek to a position in the current song. * * @param progress Proportion of song completed (where 1000 is the end of the song) */ public void seekToProgress(int progress) { if (!mMediaPlayerInitialized) return; long position = (long)mMediaPlayer.getDuration() * progress / 1000; mMediaPlayer.seekTo((int)position); } @Override public IBinder onBind(Intent intents) { return null; } @Override public void activeSongReplaced(int delta, Song song) { ArrayList<PlaybackActivity> list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).replaceSong(delta, song); if (delta == 0) setCurrentSong(0); } /** * Delete all the songs in the given media set. Should be run on a * background thread. * * @param type One of the TYPE_* constants, excluding playlists. * @param id The MediaStore id of the media to delete. * @return The number of songs deleted. */ public int deleteMedia(int type, long id) { int count = 0; ContentResolver resolver = getContentResolver(); String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA }; Cursor cursor = MediaUtils.buildQuery(type, id, projection, null).runQuery(resolver); if (cursor != null) { while (cursor.moveToNext()) { if (new File(cursor.getString(1)).delete()) { long songId = cursor.getLong(0); String where = MediaStore.Audio.Media._ID + '=' + songId; resolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where, null); mTimeline.removeSong(songId); ++count; } } cursor.close(); } return count; } /** * Move to next or previous song or album in the queue. * * @param delta One of SongTimeline.SHIFT_*. * @return The new current song. */ public Song shiftCurrentSong(int delta) { Song song = setCurrentSong(delta); userActionTriggered(); return song; } /** * Resets the idle timeout countdown. Should be called by a user action * has been triggered (new song chosen or playback toggled). * * If an idle fade out is actually in progress, aborts it and resets the * volume. */ public void userActionTriggered() { mHandler.removeMessages(FADE_OUT); mHandler.removeMessages(IDLE_TIMEOUT); if (mIdleTimeout != 0) mHandler.sendEmptyMessageDelayed(IDLE_TIMEOUT, mIdleTimeout * 1000); if (mFadeInProgress) { mMediaPlayer.setVolume(mBaseVolume, mBaseVolume); mFadeInProgress = false; } long idleStart = mIdleStart; if (idleStart != -1 && SystemClock.elapsedRealtime() - idleStart < IDLE_GRACE_PERIOD) { mIdleStart = -1; setFlag(FLAG_PLAYING); } } /** * Run the query and add the results to the timeline. Should be called in the * worker thread. * * @param query The query to run. */ public void runQuery(QueryTask query) { int count = mTimeline.addSongs(this, query); int text; switch (query.mode) { case SongTimeline.MODE_PLAY: case SongTimeline.MODE_PLAY_POS_FIRST: case SongTimeline.MODE_PLAY_ID_FIRST: text = R.plurals.playing; if (count != 0 && (mState & FLAG_PLAYING) == 0) setFlag(FLAG_PLAYING); break; case SongTimeline.MODE_PLAY_NEXT: case SongTimeline.MODE_ENQUEUE: case SongTimeline.MODE_ENQUEUE_ID_FIRST: case SongTimeline.MODE_ENQUEUE_POS_FIRST: text = R.plurals.enqueued; break; default: throw new IllegalArgumentException("Invalid add mode: " + query.mode); } Toast.makeText(this, getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT).show(); } /** * Run the query in the background and add the results to the timeline. * * @param query The query. */ public void addSongs(QueryTask query) { mHandler.sendMessage(mHandler.obtainMessage(QUERY, query)); } /** * Enqueues all the songs with the same album/artist/genre as the current * song. * * This will clear the queue and place the first song from the group after * the playing song. * * @param type The media type, one of MediaUtils.TYPE_ALBUM, TYPE_ARTIST, * or TYPE_GENRE */ public void enqueueFromCurrent(int type) { Song current = mCurrentSong; if (current == null) return; long id; switch (type) { case MediaUtils.TYPE_ARTIST: id = current.artistId; break; case MediaUtils.TYPE_ALBUM: id = current.albumId; break; case MediaUtils.TYPE_GENRE: id = MediaUtils.queryGenreForSong(getContentResolver(), current.id); break; default: throw new IllegalArgumentException("Unsupported media type: " + type); } String selection = "_id!=" + current.id; QueryTask query = MediaUtils.buildQuery(type, id, Song.FILLED_PROJECTION, selection); query.mode = SongTimeline.MODE_PLAY_NEXT; addSongs(query); } /** * Clear the song queue. */ public void clearQueue() { mTimeline.clearQueue(); } /** * Return the error message set when FLAG_ERROR is set. */ public String getErrorMessage() { return mErrorMessage; } @Override public void timelineChanged() { mHandler.removeMessages(SAVE_STATE); mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000); } @Override public void positionInfoChanged() { ArrayList<PlaybackActivity> list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).onPositionInfoChanged(); } private final ContentObserver mObserver = new ContentObserver(null) { @Override public void onChange(boolean selfChange) { MediaUtils.onMediaChange(); onMediaChange(); } }; /** * Return the PlaybackService instance, creating one if needed. */ public static PlaybackService get(Context context) { if (sInstance == null) { context.startService(new Intent(context, PlaybackService.class)); while (sInstance == null) { try { synchronized (sWait) { sWait.wait(); } } catch (InterruptedException ignored) { } } } return sInstance; } /** * Returns true if a PlaybackService instance is active. */ public static boolean hasInstance() { return sInstance != null; } /** * Add an Activity to the registered PlaybackActivities. * * @param activity The Activity to be added */ public static void addActivity(PlaybackActivity activity) { sActivities.add(activity); } /** * Remove an Activity from the registered PlaybackActivities * * @param activity The Activity to be removed */ public static void removeActivity(PlaybackActivity activity) { sActivities.remove(activity); } /** * Initializes the service state, loading songs saved from the disk into the * song timeline. * * @return The loaded value for mState. */ public int loadState() { int state = 0; try { DataInputStream in = new DataInputStream(openFileInput(STATE_FILE)); if (in.readLong() == STATE_FILE_MAGIC && in.readInt() == STATE_VERSION) { mPendingSeek = in.readInt(); mPendingSeekSong = in.readLong(); mTimeline.readState(in); state |= mTimeline.getShuffleMode() << SHIFT_SHUFFLE; state |= mTimeline.getFinishAction() << SHIFT_FINISH; } in.close(); } catch (EOFException e) { Log.w("VanillaMusic", "Failed to load state", e); } catch (IOException e) { Log.w("VanillaMusic", "Failed to load state", e); } return state; } /** * Save the service state to disk. * * @param pendingSeek The pendingSeek to store. Should be the current * MediaPlayer position or 0. */ public void saveState(int pendingSeek) { try { DataOutputStream out = new DataOutputStream(openFileOutput(STATE_FILE, 0)); Song song = mCurrentSong; out.writeLong(STATE_FILE_MAGIC); out.writeInt(STATE_VERSION); out.writeInt(pendingSeek); out.writeLong(song == null ? -1 : song.id); mTimeline.writeState(out); out.close(); } catch (IOException e) { Log.w("VanillaMusic", "Failed to save state", e); } } /** * Returns the shuffle mode for the given state. * * @param state The PlaybackService state to process. * @return The shuffle mode. One of SongTimeline.SHUFFLE_*. */ public static int shuffleMode(int state) { return (state & MASK_SHUFFLE) >> SHIFT_SHUFFLE; } /** * Returns the finish action for the given state. * * @param state The PlaybackService state to process. * @return The finish action. One of SongTimeline.FINISH_*. */ public static int finishAction(int state) { return (state & MASK_FINISH) >> SHIFT_FINISH; } /** * Create a PendingIntent for use with the notification. * * @param prefs Where to load the action preference from. */ public PendingIntent createNotificationAction(SharedPreferences prefs) { switch (Integer.parseInt(prefs.getString(PrefKeys.NOTIFICATION_ACTION, "0"))) { case NOT_ACTION_NEXT_SONG: { Intent intent = new Intent(this, PlaybackService.class); intent.setAction(PlaybackService.ACTION_NEXT_SONG_AUTOPLAY); return PendingIntent.getService(this, 0, intent, 0); } case NOT_ACTION_MINI_ACTIVITY: { Intent intent = new Intent(this, MiniPlaybackActivity.class); return PendingIntent.getActivity(this, 0, intent, 0); } default: Log.w("VanillaMusic", "Unknown value for notification_action. Defaulting to 0."); // fall through case NOT_ACTION_MAIN_ACTIVITY: { Intent intent = new Intent(this, LibraryActivity.class); intent.setAction(Intent.ACTION_MAIN); return PendingIntent.getActivity(this, 0, intent, 0); } } } /** * Create a song notification. Call through the NotificationManager to * display it. * * @param song The Song to display information about. * @param state The state. Determines whether to show paused or playing icon. */ public Notification createNotification(Song song, int state) { boolean playing = (state & FLAG_PLAYING) != 0; RemoteViews views = new RemoteViews(getPackageName(), R.layout.notification); Bitmap cover = song.getCover(this); if (cover == null) { views.setImageViewResource(R.id.cover, R.drawable.fallback_cover); } else { views.setImageViewBitmap(R.id.cover, cover); } String title = song.title; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { int playButton = playing ? R.drawable.pause : R.drawable.play; views.setImageViewResource(R.id.play_pause, playButton); ComponentName service = new ComponentName(this, PlaybackService.class); Intent playPause = new Intent(PlaybackService.ACTION_TOGGLE_PLAYBACK_NOTIFICATION); playPause.setComponent(service); views.setOnClickPendingIntent(R.id.play_pause, PendingIntent.getService(this, 0, playPause, 0)); Intent next = new Intent(PlaybackService.ACTION_NEXT_SONG); next.setComponent(service); views.setOnClickPendingIntent(R.id.next, PendingIntent.getService(this, 0, next, 0)); Intent close = new Intent(PlaybackService.ACTION_CLOSE_NOTIFICATION); close.setComponent(service); views.setOnClickPendingIntent(R.id.close, PendingIntent.getService(this, 0, close, 0)); } else if (!playing) { title = getResources().getString(R.string.notification_title_paused, song.title); } views.setTextViewText(R.id.title, title); views.setTextViewText(R.id.artist, song.artist); if (mInvertNotification) { views.setTextColor(R.id.title, 0xffffffff); views.setTextColor(R.id.artist, 0xffffffff); } Notification notification = new Notification(); notification.contentView = views; notification.icon = R.drawable.status_icon; notification.flags |= Notification.FLAG_ONGOING_EVENT; notification.contentIntent = mNotificationAction; return notification; } public void onAudioFocusChange(int type) { Log.d("VanillaMusic", "audio focus change: " + type); switch (type) { case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: mDuckedLoss = (mState & FLAG_PLAYING) != 0; unsetFlag(FLAG_PLAYING); break; case AudioManager.AUDIOFOCUS_LOSS: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: mDuckedLoss = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { // On Honeycomb and above we have controls in the notification. // Ensure they are shown when music is paused from focus loss // so music can easily be started again if desired. mForceNotificationVisible = true; } unsetFlag(FLAG_PLAYING); break; case AudioManager.AUDIOFOCUS_GAIN: if (mDuckedLoss) { mDuckedLoss = false; setFlag(FLAG_PLAYING); } break; } } @Override public void onSensorChanged(SensorEvent se) { double x = se.values[0]; double y = se.values[1]; double z = se.values[2]; double accel = Math.sqrt(x*x + y*y + z*z); double delta = accel - mAccelLast; mAccelLast = accel; double filtered = mAccelFiltered * 0.9f + delta; mAccelFiltered = filtered; if (filtered > mShakeThreshold) { long now = SystemClock.elapsedRealtime(); if (now - mLastShakeTime > MIN_SHAKE_PERIOD) { mLastShakeTime = now; performAction(mShakeAction, null); } } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } /** * Execute the given action. * * @param action The action to execute. * @param receiver Optional. If non-null, update the PlaybackActivity with * new song or state from the executed action. The activity will still be * updated by the broadcast if not passed here; passing it just makes the * update immediate. */ public void performAction(Action action, PlaybackActivity receiver) { switch (action) { case Nothing: break; case Library: Intent intent = new Intent(this, LibraryActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); break; case PlayPause: { int state = playPause(); if (receiver != null) receiver.setState(state); break; } case NextSong: { Song song = shiftCurrentSong(SongTimeline.SHIFT_NEXT_SONG); if (receiver != null) receiver.setSong(song); break; } case PreviousSong: { Song song = shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_SONG); if (receiver != null) receiver.setSong(song); break; } case NextAlbum: { Song song = shiftCurrentSong(SongTimeline.SHIFT_NEXT_ALBUM); if (receiver != null) receiver.setSong(song); break; } case PreviousAlbum: { Song song = shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_ALBUM); if (receiver != null) receiver.setSong(song); break; } case Repeat: { int state = cycleFinishAction(); if (receiver != null) receiver.setState(state); break; } case Shuffle: { int state = cycleShuffle(); if (receiver != null) receiver.setState(state); break; } case EnqueueAlbum: enqueueFromCurrent(MediaUtils.TYPE_ALBUM); break; case EnqueueArtist: enqueueFromCurrent(MediaUtils.TYPE_ARTIST); break; case EnqueueGenre: enqueueFromCurrent(MediaUtils.TYPE_GENRE); break; case ClearQueue: clearQueue(); Toast.makeText(this, R.string.queue_cleared, Toast.LENGTH_SHORT).show(); break; case ToggleControls: // Handled in FullPlaybackActivity.performAction break; default: throw new IllegalArgumentException("Invalid action: " + action); } } /** * Returns the position of the current song in the song timeline. */ public int getTimelinePosition() { return mTimeline.getPosition(); } /** * Returns the number of songs in the song timeline. */ public int getTimelineLength() { return mTimeline.getLength(); } }