/* * Copyright (C) 2010 Christopher Eby * * 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 . */ 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 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(); 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 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(); 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); } }