package com.linroid.sky31radio.service; 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.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.audiofx.AudioEffect; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.os.RemoteException; import android.support.v4.app.NotificationCompat; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.widget.RemoteViews; import com.linroid.sky31radio.App; import com.linroid.sky31radio.IRadioService; import com.linroid.sky31radio.R; import com.linroid.sky31radio.model.Program; import com.linroid.sky31radio.ui.HomeActivity; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import timber.log.Timber; /** * Created by linroid on 1/14/15. */ public class RadioPlaybackService extends Service implements AudioManager.OnAudioFocusChangeListener { private static final int NOTIFICATION_ID = 1; public static final String KEY_PROGRAM_LIST = "program_list"; public static final String KEY_PROGRAM_POSITION = "program_position"; public static final String KEY_PROGRAM = "program"; public static final String KEY_IS_PLAYING = "is_playing"; public static final String EXTRA_POSITION = "position"; public static final String EXTRA_PERCENT = "percent"; public static final String ACTION_PROGRAM_CHANGED = "com.linroid.radio.intent.action.PROGRAM_CHANGED"; public static final String ACTION_PLAYING_STATUS_CHANGED = "com.linroid.radio.intent.action.PLAYING_STATUS_CHANGED"; public static final String ACTION_PLAY = "com.linroid.radio.intent.action.PLAY"; public static final String ACTION_PAUSE = "com.linroid.radio.intent.action.PAUSE"; public static final String ACTION_NEXT = "com.linroid.radio.intent.action.NEXT"; public static final String ACTION_PREVIOUS = "com.linroid.radio.intent.action.PREVIOUS"; public static final String ACTION_SEEK_TO_POSITION = "com.linroid.radio.intent.action.SEEK_TO_POSITION"; public static final String ACTION_SEEK_TO_PERCENT = "com.linroid.radio.intent.action.SEEK_TO_PERCENT"; public static final String ACTION_STOP = "com.linroid.radio.intent.action.STOP"; public static final String ACTION_SELECT_PROGRAM_LIST = "com.linroid.radio.intent.action.SELECT_PROGRAM_LIST"; PlayerReceiver receiver; RadioPlayer player; NotificationManager notificationManager; WifiManager.WifiLock wifiLock; AudioManager audioManager; @Inject Picasso picasso; @Override public IBinder onBind(Intent intent) { Timber.i("onBind: Intent %s", intent.toString()); return mBinder; } @Override public void onCreate() { super.onCreate(); App app = (App) getApplication(); app.inject(this); player = new RadioPlayer(); receiver = new PlayerReceiver(); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_SELECT_PROGRAM_LIST); filter.addAction(ACTION_PLAY); filter.addAction(ACTION_PAUSE); filter.addAction(ACTION_NEXT); filter.addAction(ACTION_STOP); filter.addAction(ACTION_PREVIOUS); filter.addAction(ACTION_SEEK_TO_POSITION); filter.addAction(ACTION_SEEK_TO_PERCENT); filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver(receiver, filter); audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "wifi_lock"); wifiLock.acquire(); int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // could not get audio focus. } TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); telephonyManager.listen(mPhoneListener, PhoneStateListener.LISTEN_CALL_STATE); } @Override public boolean onUnbind(Intent intent) { Timber.w("onUnbind"); return super.onUnbind(intent); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if(intent==null){ Timber.e("onStartCommand: Null intent"); return super.onStartCommand(intent, flags, startId); } Timber.i("onStartCommand, intent: %s", intent.toString()); Bundle data = intent.getExtras(); List<Program> programList = data.getParcelableArrayList(KEY_PROGRAM_LIST); int playingIndex = data.getInt(KEY_PROGRAM_POSITION); player.setProgramList(programList, playingIndex); player.loadProgram(); return super.onStartCommand(intent, flags, startId); } private void sendProgramChangedBroadcast(Program program){ buildNotification(); Intent broadCastIntent = new Intent(); broadCastIntent.setAction(ACTION_PROGRAM_CHANGED); broadCastIntent.putExtra(RadioPlaybackService.KEY_PROGRAM, program); sendBroadcast(broadCastIntent); } private void sendPlayingStatusChangedBroadcast(boolean isPlaying){ if(isPlaying){ final Intent audioEffectIntent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); audioEffectIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, player.mediaPlayer.getAudioSessionId()); audioEffectIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); sendBroadcast(audioEffectIntent); } buildNotification(); Intent statusChangedIntent = new Intent(); statusChangedIntent.setAction(ACTION_PLAYING_STATUS_CHANGED); statusChangedIntent.putExtra(RadioPlaybackService.KEY_IS_PLAYING, isPlaying); sendBroadcast(statusChangedIntent); } @Override public void onDestroy() { Timber.d("onDestroy"); stopForeground(true); notificationManager.cancel(NOTIFICATION_ID); player.destroy(); player = null; unregisterReceiver(receiver); wifiLock.release(); } private void buildNotification(){ // Notification.MediaStyle style = new Notification.MediaStyle(); // style.setShowActionsInCompactView(0, 1, 2); int playButtonIconResId; String playButtonAction; final boolean isPlaying = player.isPlaying(); if(isPlaying){ playButtonAction = ACTION_PAUSE; playButtonIconResId = R.drawable.ic_stat_action_pause; }else{ playButtonAction = ACTION_PLAY; playButtonIconResId = R.drawable.ic_stat_action_play_arrow; } Intent intent = new Intent(this, HomeActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent contentIntent = PendingIntent.getActivity(this, 1, intent, 0); Program playingProgram = player.getPlayingProgram(); final NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.ic_stat_playing) .setContentTitle(playingProgram.getTitle()) .setContentText(playingProgram.getAuthor()) .setContentIntent(contentIntent) .setShowWhen(true); // .setContent(collapseViews); //// .addAction(R.drawable.ic_stat_action_skip_previous, null, createAction(ACTION_PREVIOUS)) //// .addAction(playButtonIconResId, null, createAction(playButtonAction)) //// .addAction(R.drawable.ic_stat_action_skip_next, null, createAction(ACTION_NEXT)); RemoteViews collapsedViews = new RemoteViews(getPackageName(), R.layout.collapsed_notification); collapsedViews.setTextViewText(R.id.program_title, playingProgram.getTitle()); collapsedViews.setImageViewResource(R.id.btn_play_pause, playButtonIconResId); collapsedViews.setOnClickPendingIntent(R.id.btn_play_pause, createAction(playButtonAction)); collapsedViews.setOnClickPendingIntent(R.id.btn_skip_next, createAction(ACTION_NEXT)); builder.setContent(collapsedViews); final Notification notification = builder.build(); notification.contentView = collapsedViews; if(Build.VERSION.SDK_INT >= 16){ RemoteViews expandedViews = new RemoteViews(getPackageName(), R.layout.expanded_notification); expandedViews.setTextViewText(R.id.program_title, playingProgram.getTitle()); expandedViews.setTextViewText(R.id.program_author, playingProgram.getAuthor()); expandedViews.setOnClickPendingIntent(R.id.btn_skip_next, createAction(ACTION_NEXT)); expandedViews.setOnClickPendingIntent(R.id.btn_skip_previous, createAction(ACTION_PREVIOUS)); expandedViews.setOnClickPendingIntent(R.id.btn_play_pause, createAction(playButtonAction)); expandedViews.setOnClickPendingIntent(R.id.stop, createAction(ACTION_STOP)); expandedViews.setImageViewResource(R.id.btn_play_pause, playButtonIconResId); notification.bigContentView = expandedViews; } picasso.load(playingProgram.getThumbnail()) .into(new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { // builder.setLargeIcon(bitmap); // Notification notification = builder.build(); notification.contentView.setImageViewBitmap(R.id.thumbnail, bitmap); if(Build.VERSION.SDK_INT >= 16){ notification.bigContentView.setImageViewBitmap(R.id.thumbnail, bitmap); } if(isPlaying) { notification.flags |= Notification.FLAG_NO_CLEAR; notificationManager.notify(NOTIFICATION_ID, notification); startForeground(NOTIFICATION_ID, notification); }else { stopForeground(false); // builder.setAutoCancel(true); notification.flags = 0; notification.deleteIntent = createAction(ACTION_STOP); notificationManager.notify(NOTIFICATION_ID, notification); } } @Override public void onBitmapFailed(Drawable errorDrawable) { } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { } }); } private PendingIntent createAction(String action){ Intent intent = new Intent(); intent.setAction(action); return PendingIntent.getBroadcast(this, action.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); } @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: // resume playback Timber.i("AudioManager.AUDIOFOCUS_GAIN"); // player.play(); break; case AudioManager.AUDIOFOCUS_LOSS: // Lost focus for an unbounded amount of time: stop playback and release media player Timber.i("AudioManager.AUDIOFOCUS_LOSS"); if(player!=null){ player.stop(); } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // Lost focus for a short time, but we have to stop // playback. We don't release the media player because playback // is likely to resume Timber.i("AudioManager.AUDIOFOCUS_LOSS_TRANSIENT"); if(player!=null){ player.pause(); } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // Lost focus for a short time, but it's ok to keep playing // at an attenuated level Timber.i("AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"); break; } } private class RadioPlayer implements MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener{ private boolean isPlaying = false; private List<Program> programList = new ArrayList<>(); private int playingIndex; MediaPlayer mediaPlayer; public RadioPlayer() { mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setLooping(true); mediaPlayer.setOnBufferingUpdateListener(this); mediaPlayer.setOnPreparedListener(this); mediaPlayer.setScreenOnWhilePlaying(true); mediaPlayer.setOnErrorListener(this); mediaPlayer.setOnCompletionListener(this); mediaPlayer.setWakeMode(RadioPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); } public boolean isPlaying(){ return mediaPlayer!=null && (isPlaying||mediaPlayer.isPlaying()); } public void play() { Timber.d("player play"); isPlaying = true; if (!mediaPlayer.isPlaying()) { mediaPlayer.start(); } sendPlayingStatusChangedBroadcast(true); } public void pause() { isPlaying = false; if (mediaPlayer != null) { mediaPlayer.pause(); } sendPlayingStatusChangedBroadcast(false); Timber.d("player paused"); } public void stop() { Timber.d("stop"); isPlaying = false; if (mediaPlayer != null) { mediaPlayer.reset(); mediaPlayer.release(); } mediaPlayer = null; stopSelf(); } private void next() { if(playingIndex==programList.size()-1){ return; } playingIndex = (playingIndex + 1) % programList.size(); loadProgram(); } private void previous() { playingIndex = (playingIndex - 1) % programList.size(); playingIndex = playingIndex<=0 ? programList.size()-1 : playingIndex; loadProgram(); } public void loadProgram() { isPlaying = true; Program program = programList.get(playingIndex); if(program.getAudio()==null){ return; } String url = program.getAudio().getSrc(); Timber.i("loadProgram: %s(%s)", program.getTitle(), url); mediaPlayer.reset(); try { mediaPlayer.setDataSource(url); mediaPlayer.prepareAsync(); } catch (IOException | IllegalStateException e) { Timber.e(e, "load program failed"); } sendProgramChangedBroadcast(program); } public void setProgramList(List<Program> programList, int playingIndex) { this.programList = programList; this.playingIndex = playingIndex; } public int getDuration() { return mediaPlayer.getDuration(); } public void seekToPercent(float percent) { int position = (int) (mediaPlayer.getDuration() * percent / 100); seekToPosition(position); } public void seekToPosition(int position) { mediaPlayer.seekTo(position); sendPlayingStatusChangedBroadcast(isPlaying()); } public void destroy() { isPlaying = false; if (mediaPlayer != null) { mediaPlayer.stop(); mediaPlayer.release(); mediaPlayer = null; } } public long getPosition() { return mediaPlayer.getCurrentPosition(); } public Program getPlayingProgram() { return programList.get(playingIndex); } public int getSessionId() { return mediaPlayer.getAudioSessionId(); } @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { if(percent == 100){ Timber.d("buffer complete:%d%%", percent); } } @Override public void onPrepared(MediaPlayer mp) { Timber.v("onPrepared"); this.play(); } @Override public boolean onError(MediaPlayer mp, int what, int extra) { Timber.e("onError"); return true; } @Override public void onCompletion(MediaPlayer mp) { Timber.i("onCompletion, %d/%d", mp.getCurrentPosition(), mp.getDuration()); player.next(); } } IBinder mBinder = new IRadioService.Stub() { @Override public void play() throws RemoteException { player.play(); } @Override public void pause() throws RemoteException { player.pause(); } @Override public void stop() throws RemoteException { player.stop(); } @Override public void seekToPosition(int position) throws RemoteException { player.seekToPosition(position); } @Override public void seekToPercent(int percent) throws RemoteException { player.seekToPercent(percent); } @Override public void next() throws RemoteException { player.next(); } @Override public void previous() throws RemoteException { player.previous(); } @Override public long getDuration() throws RemoteException { return player.getDuration(); } @Override public long getPosition() throws RemoteException { return player.getPosition(); } @Override public int getPlayerSessionId() throws RemoteException { return player.getSessionId(); } @Override public boolean isPlaying() throws RemoteException { return player!=null && player.isPlaying(); } @Override public Program getPlayingProgram() throws RemoteException { return player.getPlayingProgram(); } }; private class PlayerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Timber.i("onReceive, Intent: %s, Extras:%s", intent.toString(), intent.getExtras()==null ? null : intent.getExtras().toString()); switch (intent.getAction()) { case ACTION_PLAY: player.play(); break; case ACTION_NEXT: player.next(); break; case ACTION_PREVIOUS: player.previous(); break; case ACTION_SEEK_TO_POSITION: int position = intent.getIntExtra(EXTRA_POSITION, 0); Timber.d("ACTION_SEEK_TO_POSITION, position:%d", position); player.seekToPosition(position); break; case ACTION_SEEK_TO_PERCENT: int percent = intent.getIntExtra(EXTRA_PERCENT, 0); player.seekToPosition(percent); break; case ACTION_STOP: player.stop(); break; case ACTION_PAUSE: player.pause(); break; case AudioManager.ACTION_AUDIO_BECOMING_NOISY: player.pause(); break; } } } PhoneStateListener mPhoneListener = new PhoneStateListener(){ @Override public void onCallStateChanged(int state, String incomingNumber) { switch (state) { case TelephonyManager.CALL_STATE_IDLE: break; case TelephonyManager.CALL_STATE_OFFHOOK: case TelephonyManager.CALL_STATE_RINGING: if (player.isPlaying()) { player.pause(); } break; default: break; } } }; }