vanilla/src/org/kreed/vanilla/PlaybackService.java
2010-02-27 22:29:33 -06:00

682 lines
18 KiB
Java

/*
* Copyright (C) 2010 Christopher Eby <kreed@kreed.org>
*
* This file is part of Vanilla Music Player.
*
* Vanilla Music Player is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Vanilla Music Player is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kreed.vanilla;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Random;
import org.kreed.vanilla.IMusicPlayerWatcher;
import org.kreed.vanilla.IPlaybackService;
import org.kreed.vanilla.R;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
public class PlaybackService extends Service implements Runnable, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, SharedPreferences.OnSharedPreferenceChangeListener {
private static final int NOTIFICATION_ID = 2;
public static final String APPWIDGET_SMALL_UPDATE = "org.kreed.vanilla.action.APPWIDGET_SMALL_UPDATE";
public static final String TOGGLE_PLAYBACK = "org.kreed.vanilla.action.TOGGLE_PLAYBACK";
public static final String NEXT_SONG = "org.kreed.vanilla.action.NEXT_SONG";
public static final int STATE_NORMAL = 0;
public static final int STATE_NO_MEDIA = 1;
public static final int STATE_PLAYING = 2;
private RemoteCallbackList<IMusicPlayerWatcher> mWatchers;
public IPlaybackService.Stub mBinder = new IPlaybackService.Stub() {
public Song[] getCurrentSongs()
{
return new Song[] { getSong(-1), getSong(0), getSong(1) };
}
public Song getSong(int delta)
{
return PlaybackService.this.getSong(delta);
}
public int getState()
{
return mState;
}
public int getPosition()
{
if (mMediaPlayer == null)
return 0;
return mMediaPlayer.getCurrentPosition();
}
public int getDuration()
{
if (mMediaPlayer == null)
return 0;
return mMediaPlayer.getDuration();
}
public void nextSong()
{
if (mHandler == null)
return;
mHandler.sendMessage(mHandler.obtainMessage(SET_SONG, 1, 0));
}
public void previousSong()
{
if (mHandler == null)
return;
mHandler.sendMessage(mHandler.obtainMessage(SET_SONG, -1, 0));
}
public void togglePlayback()
{
if (mHandler == null)
return;
mHandler.sendMessage(mHandler.obtainMessage(PLAY_PAUSE, 0, 0));
}
public void registerWatcher(IMusicPlayerWatcher watcher)
{
if (watcher != null)
mWatchers.register(watcher);
}
public void unregisterWatcher(IMusicPlayerWatcher watcher)
{
if (watcher != null)
mWatchers.unregister(watcher);
}
public void seekToProgress(int progress)
{
if (mMediaPlayer == null || !mMediaPlayer.isPlaying())
return;
long position = (long)mMediaPlayer.getDuration() * progress / 1000;
mMediaPlayer.seekTo((int)position);
}
};
@Override
public IBinder onBind(Intent intent)
{
return mBinder;
}
@Override
public void onCreate()
{
mWatchers = new RemoteCallbackList<IMusicPlayerWatcher>();
new Thread(this).start();
}
@Override
public void onStart(Intent intent, int flags)
{
if (mHandler == null)
return;
int id = intent.getIntExtra("songId", -1);
mHandler.sendMessage(mHandler.obtainMessage(QUEUE_ITEM, id, 0));
}
@Override
public void onDestroy()
{
super.onDestroy();
if (mMediaPlayer != null) {
MediaPlayer player = mMediaPlayer;
mMediaPlayer = null;
if (player.isPlaying())
player.pause();
player.release();
}
unregisterReceiver(mReceiver);
mNotificationManager.cancel(NOTIFICATION_ID);
resetWidgets();
if (mWakeLock != null && mWakeLock.isHeld())
mWakeLock.release();
}
public void startForegroundCompat(int id, Notification notification)
{
if (mStartForeground == null) {
setForeground(true);
mNotificationManager.notify(id, notification);
} else {
try {
mStartForeground.invoke(this, Integer.valueOf(id), notification);
} catch (InvocationTargetException e) {
Log.w("VanillaMusic", e);
} catch (IllegalAccessException e) {
Log.w("VanillaMusic", e);
}
}
}
public void stopForegroundCompat(boolean cancelNotification)
{
if (mStopForeground == null) {
setForeground(false);
} else {
try {
mStopForeground.invoke(this, Boolean.FALSE);
} catch (InvocationTargetException e) {
Log.w("VanillaMusic", e);
} catch (IllegalAccessException e) {
Log.w("VanillaMusic", e);
}
}
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context content, Intent intent)
{
String action = intent.getAction();
if (Intent.ACTION_HEADSET_PLUG.equals(action) && mHandler != null) {
int plugged = intent.getIntExtra("state", 0) == 130 ? 1 : 0;
mHandler.sendMessage(mHandler.obtainMessage(HEADSET_PLUGGED, plugged, 0));
} else if (Intent.ACTION_MEDIA_SCANNER_FINISHED.equals(action)
|| Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action)) {
mHandler.sendEmptyMessage(RETRIEVE_SONGS);
} else if (TOGGLE_PLAYBACK.equals(action)) {
if (mHandler.hasMessages(DO)) {
mHandler.removeMessages(DO);
Intent launcher = new Intent(PlaybackService.this, NowPlayingActivity.class);
launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(launcher);
} else {
mHandler.sendMessageDelayed(mHandler.obtainMessage(DO, PLAY_PAUSE, 0), 250);
}
} else if (NEXT_SONG.equals(action)) {
if (mHandler.hasMessages(DO)) {
mHandler.removeMessages(DO);
Intent launcher = new Intent(PlaybackService.this, NowPlayingActivity.class);
launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(launcher);
} else {
mHandler.sendMessageDelayed(mHandler.obtainMessage(DO, SET_SONG, 1), 250);
}
} else if (APPWIDGET_SMALL_UPDATE.equals(intent.getAction())) {
OneCellWidget.update(PlaybackService.this, getSong(0));
return;
}
}
};
private PhoneStateListener mCallListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber)
{
int inCall = -1;
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:
case TelephonyManager.CALL_STATE_OFFHOOK:
inCall = 1;
break;
case TelephonyManager.CALL_STATE_IDLE:
inCall = 0;
break;
}
if (mHandler != null)
mHandler.sendMessage(mHandler.obtainMessage(CALL, inCall, 0));
}
};
public void onSharedPreferenceChanged(SharedPreferences settings, String key)
{
if (mHandler != null)
mHandler.sendMessage(mHandler.obtainMessage(PREF_CHANGED, key));
}
private boolean mHeadsetPause;
private boolean mHeadsetOnly;
private boolean mUseRemotePlayer;
private boolean mNotifyWhilePaused;
private boolean mScrobble;
private Handler mHandler;
private MediaPlayer mMediaPlayer;
private Random mRandom;
private PowerManager.WakeLock mWakeLock;
private Notification mNotification;
private SharedPreferences mSettings;
private AudioManager mAudioManager;
private NotificationManager mNotificationManager;
private int[] mSongs;
private ArrayList<Song> mSongTimeline;
private int mCurrentSong = -1;
private int mQueuePos = 0;
private int mState = STATE_NORMAL;
private boolean mPlayingBeforeCall;
private Method mIsWiredHeadsetOn;
private Method mIsBluetoothScoOn;
private Method mIsBluetoothA2dpOn;
private Method mStartForeground;
private Method mStopForeground;
private static final int SET_SONG = 0;
private static final int PLAY_PAUSE = 1;
private static final int HEADSET_PLUGGED = 2;
private static final int PREF_CHANGED = 3;
private static final int QUEUE_ITEM = 4;
private static final int TRACK_CHANGED = 5;
private static final int RELEASE_WAKE_LOCK = 6;
private static final int HANDLE_PLAY = 7;
private static final int HANDLE_PAUSE = 8;
private static final int RETRIEVE_SONGS = 9;
private static final int CALL = 10;
private static final int DO = 11;
public void run()
{
Looper.prepare();
mMediaPlayer = new MediaPlayer();
mSongTimeline = new ArrayList<Song>();
mRandom = new Random();
mHandler = new Handler() {
public void handleMessage(Message message)
{
switch (message.what) {
case SET_SONG:
setCurrentSong(message.arg1);
break;
case PLAY_PAUSE:
setPlaying(!mMediaPlayer.isPlaying());
break;
case HEADSET_PLUGGED:
if (mHeadsetPause) {
boolean plugged = message.arg1 == 1;
if (!plugged && mMediaPlayer.isPlaying())
pause();
}
break;
case PREF_CHANGED:
loadPreference((String)message.obj);
break;
case QUEUE_ITEM:
if (message.arg1 == -1) {
mQueuePos = 0;
} else {
Song song = new Song(message.arg1);
String text = getResources().getString(R.string.enqueued, song.title);
Toast.makeText(ContextApplication.getContext(), text, Toast.LENGTH_SHORT).show();
int i = mCurrentSong + 1 + mQueuePos++;
if (i < mSongTimeline.size())
mSongTimeline.set(i, song);
else
mSongTimeline.add(song);
}
break;
case TRACK_CHANGED:
setCurrentSong(+1);
break;
case RELEASE_WAKE_LOCK:
if (mWakeLock.isHeld())
mWakeLock.release();
break;
case HANDLE_PLAY:
setState(STATE_PLAYING);
startForegroundCompat(NOTIFICATION_ID, mNotification);
break;
case HANDLE_PAUSE:
setState(STATE_NORMAL);
stopForegroundCompat(false);
break;
case RETRIEVE_SONGS:
retrieveSongs();
break;
case CALL:
boolean inCall = message.arg1 == 1;
if (inCall) {
if (mState == STATE_PLAYING) {
mPlayingBeforeCall = true;
pause();
}
} else {
if (mPlayingBeforeCall) {
play();
mPlayingBeforeCall = false;
}
}
break;
case DO:
if (message.arg1 == DO) {
Log.w("VanillaMusic", "handleMessage.DO should not be called with arg1 = DO");
return;
}
message.what = message.arg1;
message.arg1 = message.arg2;
handleMessage(message);
break;
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_HEADSET_PLUG);
filter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
filter.addAction(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
filter.addAction(TOGGLE_PLAYBACK);
filter.addAction(NEXT_SONG);
filter.addAction(APPWIDGET_SMALL_UPDATE);
registerReceiver(mReceiver, filter);
PowerManager powerManager = (PowerManager)getSystemService(POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "VanillaMusicSongChangeLock");
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(mCallListener, PhoneStateListener.LISTEN_CALL_STATE);
mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
try {
mIsWiredHeadsetOn = mAudioManager.getClass().getMethod("isWiredHeadsetOn", (Class[]) null);
mIsBluetoothScoOn = mAudioManager.getClass().getMethod("isBluetoothScoOn", (Class[]) null);
mIsBluetoothA2dpOn = mAudioManager.getClass().getMethod("isBluetoothA2dpOn", (Class[]) null);
} catch (NoSuchMethodException e) {
Log.d("VanillaMusic", "falling back to pre-2.0 AudioManager APIs");
}
mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
try {
mStartForeground = getClass().getMethod("startForeground", int.class, Notification.class);
mStopForeground = getClass().getMethod("stopForeground", boolean.class);
} catch (NoSuchMethodException e) {
Log.d("VanillaMusic", "falling back to pre-2.0 Service APIs");
}
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setWakeMode(this, PowerManager.PARTIAL_WAKE_LOCK);
mMediaPlayer.setOnCompletionListener(this);
mMediaPlayer.setOnErrorListener(this);
retrieveSongs();
mSettings = PreferenceManager.getDefaultSharedPreferences(this);
mSettings.registerOnSharedPreferenceChangeListener(this);
mHeadsetPause = mSettings.getBoolean("headset_pause", true);
mHeadsetOnly = mSettings.getBoolean("headset_only", false);
mUseRemotePlayer = mSettings.getBoolean("remote_player", true);
mNotifyWhilePaused = mSettings.getBoolean("notify_while_paused", true);
mScrobble = mSettings.getBoolean("scrobble", false);
setCurrentSong(1);
Looper.loop();
}
private void loadPreference(String key)
{
if ("headset_pause".equals(key)) {
mHeadsetPause = mSettings.getBoolean("headset_pause", true);
} else if ("headset_only".equals(key)) {
mHeadsetOnly = mSettings.getBoolean(key, false);
if (mHeadsetOnly && isSpeakerOn() && mMediaPlayer.isPlaying())
pause();
} else if ("remote_player".equals(key)) {
mUseRemotePlayer = mSettings.getBoolean(key, true);
updateNotification();
} else if ("notify_while_paused".equals(key)){
mNotifyWhilePaused = mSettings.getBoolean(key, true);
updateNotification();
} else if ("scrobble".equals(key)) {
mScrobble = mSettings.getBoolean("scrobble", false);
}
}
public void setState(int state)
{
if (mState == state)
return;
int oldState = mState;
mState = state;
updateNotification();
int i = mWatchers.beginBroadcast();
while (--i != -1) {
try {
mWatchers.getBroadcastItem(i).stateChanged(oldState, mState);
} catch (RemoteException e) {
// Null elements will be removed automatically
}
}
mWatchers.finishBroadcast();
if (mScrobble) {
Intent intent = new Intent("net.jjc1138.android.scrobbler.action.MUSIC_STATUS");
intent.putExtra("playing", mState == STATE_PLAYING);
intent.putExtra("id", getSong(0).id);
sendBroadcast(intent);
}
}
private void retrieveSongs()
{
mSongs = Song.getAllSongs();
if (mSongs == null) {
setState(STATE_NO_MEDIA);
} else {
if (mState == STATE_NO_MEDIA)
setState(STATE_NORMAL);
}
}
private void updateNotification()
{
Song song = getSong(0);
if (song == null || !mNotifyWhilePaused && mState == STATE_NORMAL) {
mNotificationManager.cancel(NOTIFICATION_ID);
return;
}
String title = song.title;
if (mState != STATE_PLAYING)
title += ' ' + getResources().getString(R.string.paused);
RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar);
views.setImageViewResource(R.id.icon, R.drawable.status_icon);
views.setTextViewText(R.id.title, title);
views.setTextViewText(R.id.artist, song.artist);
Notification notification = new Notification();
notification.contentView = views;
notification.icon = R.drawable.status_icon;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
Intent intent = new Intent(this, mUseRemotePlayer ? RemoteActivity.class : NowPlayingActivity.class);
notification.contentIntent = PendingIntent.getActivity(ContextApplication.getContext(), 0, intent, 0);
mNotification = notification;
mNotificationManager.notify(NOTIFICATION_ID, mNotification);
}
private boolean isSpeakerOn()
{
try {
return !(Boolean)mIsWiredHeadsetOn.invoke(mAudioManager, (Object[])null)
&& !(Boolean)mIsBluetoothScoOn.invoke(mAudioManager, (Object[])null)
&& !(Boolean)mIsBluetoothA2dpOn.invoke(mAudioManager, (Object[])null);
} catch (InvocationTargetException e) {
Log.w("VanillaMusic", e);
} catch (IllegalAccessException e) {
Log.w("VanillaMusic", e);
}
// Why is there no true equivalent to this in Android 2.0?
return (mAudioManager.getRouting(mAudioManager.getMode()) & AudioManager.ROUTE_SPEAKER) != 0;
}
private void play()
{
if (mHeadsetOnly && isSpeakerOn())
return;
mMediaPlayer.start();
mHandler.sendEmptyMessage(HANDLE_PLAY);
}
private void pause()
{
mMediaPlayer.pause();
mHandler.sendEmptyMessage(HANDLE_PAUSE);
}
private void setPlaying(boolean play)
{
if (play)
play();
else
pause();
}
private void setCurrentSong(int delta)
{
Song song = getSong(delta);
if (song == null)
return;
mCurrentSong += delta;
updateNotification();
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(song.path);
mMediaPlayer.prepare();
if (mState == STATE_PLAYING)
play();
} catch (IOException e) {
Log.e("VanillaMusic", "IOException", e);
}
int i = mWatchers.beginBroadcast();
while (--i != -1) {
try {
mWatchers.getBroadcastItem(i).songChanged(song);
} catch (RemoteException e) {
// Null elements will be removed automatically
}
}
mWatchers.finishBroadcast();
getSong(+2);
while (mCurrentSong > 15) {
mSongTimeline.remove(0);
--mCurrentSong;
}
updateWidgets();
}
public void onCompletion(MediaPlayer player)
{
mWakeLock.acquire();
mHandler.sendEmptyMessage(TRACK_CHANGED);
mHandler.sendEmptyMessage(RELEASE_WAKE_LOCK);
}
public boolean onError(MediaPlayer player, int what, int extra)
{
Log.e("VanillaMusic", "MediaPlayer error: " + what + " " + extra);
mMediaPlayer.reset();
mHandler.sendEmptyMessage(TRACK_CHANGED);
return true;
}
private Song randomSong()
{
return new Song(mSongs[mRandom.nextInt(mSongs.length)]);
}
private synchronized Song getSong(int delta)
{
if (mSongTimeline == null)
return null;
int pos = mCurrentSong + delta;
if (pos < 0)
return null;
int size = mSongTimeline.size();
if (pos > size)
return null;
if (pos == size) {
if (mSongs == null)
return null;
mSongTimeline.add(randomSong());
}
return mSongTimeline.get(pos);
}
private void updateWidgets()
{
Song song = getSong(0);
OneCellWidget.update(this, song);
}
private void resetWidgets()
{
OneCellWidget.reset(this);
}
}