项目源码
https://github.com/dogmeng/littleyunmusic
功能实现:
第一部分 整体各部分之间的协作流程.
1.对音乐的操作控制放在service中处理
2.利用代理类PlayerProxy处理activity和service的交互
3.前台对后台的调用,利用在onServiceConnected中返回的AIDL内部类的实现类对象.后台对前台的调用,利用在onServiceConnected中对后台设置监听类对象,同样,此监听类也需要生成AIDL文件
下面看具体代码:
1.音乐service:具体参见源码中MusicService类和MusicControl类
为保证音乐能在程序退出至后台时,依然播放,这里使用的是跨进程的service,当然不跨进程也可以,具体看需求.在AndroidManifest.xml中配置
<service
android:name=".service.MusicService"
android:enabled="true"
android:exported="true"
android:process=":musicservice"
>
</service>
在service中要做的工作有:初始化MediaPlayer,实现播放,暂停,上一首,下一首,设置播放模式,失去焦点暂停,重获焦点播放,与前台通知交互等功能.
常见的自动播放下一首,是在onComplete中play下一首即可.为了保证音乐无缝切换,此处使用了MediaPlayer的setNextMediaPlayer(MediaPlayer mp).不过这样会导致实现播放控制方面稍显复杂.这里我先声明三个MediaPlayer引用:
private MediaPlayer mPlayer,mNextPlayer1,mNextPlayer2;
在MusicControl的构造方法也就是MusicService的onCreate中初始化其中两个对象,mNextPlayer1 = new MediaPlayer();mNextPlayer2 = new MediaPlayer();
mPlayer指向的是当前正在使用的MediaPlayer, 默认mPlayer = mNextPlayer1;
在当前歌曲播放完时会回调onCompletion,在onCompletion 中改变mPlayer 的指向,并且setNextPlayer();这样就可以交替使用mNextPlayer1和mNextPlayer2,实现自动播放下一首了.
class CompletionListener implements OnCompletionListener{
MediaPlayer nextPlayer;
public CompletionListener(MediaPlayer next){
nextPlayer = next;
}
@Override
public void onCompletion(MediaPlayer arg0) {
// TODO Auto-generated method stub
if(arg0 == mPlayer && nextPlayer!=null){
//因为之前已经设置过setNextPlayer,所以会自动播放设置好的player,即此时正在播放的
//player为nextPlayer
mPlayer = nextPlayer;
//设置当前播放歌曲的位置
mCurrentPosition = nextPosition;
//加入播放历史,在自动循环列表模式下,避免自动获取下一首的歌曲为当前已播放的歌曲
mHistoryPositions.add(mCurrentPosition);
if(mHistoryPositions.size()>mPlayList.size())
mHistoryPositions.remove(0);
mCurrentSong = mPlayList.get(mCurrentPosition);
mCurrentSongId = mCurrentSong.songId;
//回调activity的方法,改变页面信息
if (mPlayer.isPlaying() && mUiListener != null) {
try {
mUiListener.onMusicStart();
//更新前台通知信息
mainHandler.obtainMessage(MAINHANDLER_UPEATE, true).sendToTarget();
} catch (RemoteException e) {
}
}
//设置下一首资源setNextPlayer();
}else{
mContext.cancelNotification();
mWakeLock.releaseWakeLock();
}
}
}
失去焦点和重获焦点的类:
class AudioFocusManager implements AudioManager.OnAudioFocusChangeListener {
private boolean isPausedByFocusLossTransient = false;
private int mVolumeWhenFocusLossTransientCanDuck;
private WeakReferenceplayerControl;
public AudioFocusManager(MusicControl control) {
playerControl = new WeakReference(control);
}
@Override
public void onAudioFocusChange(int focusChange) {
int volume;
switch (focusChange) {
// 暫時丢失焦点,如来电,應暫停播放,但不清除
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
if(playerControl.get().isPlaying()){
isPausedByFocusLossTransient = true;
}
playerControl.get().pause();
break;
// 重新获得焦点
case AudioManager.AUDIOFOCUS_GAIN:
if(isPausedByFocusLossTransient){
isPausedByFocusLossTransient = false;
playerControl.get().continuePlay(mPlayer);
}
volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (mVolumeWhenFocusLossTransientCanDuck > 0 && volume == mVolumeWhenFocusLossTransientCanDuck / 2) {
// 恢复音量
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
mVolumeWhenFocusLossTransientCanDuck = 0;
break;
// 永久丢失焦点,如被其他播放器抢占,清理資源
case AudioManager.AUDIOFOCUS_LOSS:
playerControl.get().stop();
isPausedByFocusLossTransient = true;
break;
// 瞬间丢失焦点,如通知,但是允許持續播放音樂(以很小的聲音)
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 音量减小为一半
volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (playerControl.get().isPlaying() && volume > 0) {
mVolumeWhenFocusLossTransientCanDuck = volume;
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck / 2, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
break;
}
}
}
当点击播放音乐时,开启前台通知:
public void updateNotification(SingleSongBean music,boolean isPlaying){
if(isPlaying){
if(notifyMode == NOTIFICATION_STOP){
//如果没有开启过,就先开启前台服务;
startForeground(id, creatNotification(music,isPlaying));
notifyMode = NOTIFICATION_START;
}else{
//如果已经开启前台服务,就更新通知内容
notificationManager.notify(id, creatNotification(music,true));
}
}else{
if(notifyMode == NOTIFICATION_STOP){
}else{
//暂停前台服务
stopForeground(false);
notificationManager.notify(id, creatNotification(music,false));
}
}
}
public Notification creatNotification(SingleSongBean music,boolean isPlaying){
RemoteViews remoteViews = new RemoteViews(this.getPackageName(), R.layout.notification);
RemoteViews remoteViewsSmall = new RemoteViews(this.getPackageName(), R.layout.notification_small);
String title = music.songName;
String detail = music.albumName+"_"+music.artistName;
remoteViews.setImageViewResource(R.id.notification_image,R.drawable.album);
remoteViews.setTextViewText(R.id.music_title, title);
remoteViews.setTextViewText(R.id.music_detail, detail);
remoteViewsSmall.setImageViewResource(R.id.notification_image,R.drawable.album);
remoteViewsSmall.setTextViewText(R.id.music_title, title);
remoteViewsSmall.setTextViewText(R.id.music_detail, detail);
Intent playIntent = new Intent(NOTIFICATION_PLAY_PAUSE);
PendingIntent playPendingIntent = PendingIntent.getBroadcast(this, 0, playIntent, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setImageViewResource(R.id.play, isPlaying? R.drawable.pause : R.drawable.play);
remoteViews.setOnClickPendingIntent(R.id.play, playPendingIntent);
remoteViewsSmall.setImageViewResource(R.id.play, isPlaying? R.drawable.pause : R.drawable.play);
remoteViewsSmall.setOnClickPendingIntent(R.id.play, playPendingIntent);
Intent preIntent = new Intent(NOTIFICATION_PRE);
PendingIntent nextPendingIntent = PendingIntent.getBroadcast(this, 0, preIntent, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.play_prev, nextPendingIntent);
remoteViewsSmall.setOnClickPendingIntent(R.id.play_prev, nextPendingIntent);
Intent nextIntent = new Intent(NOTIFICATION_NEX);
PendingIntent nextPIntent = PendingIntent.getBroadcast(this, 0, nextIntent, 0);
remoteViews.setOnClickPendingIntent(R.id.play_next, nextPIntent);
remoteViewsSmall.setOnClickPendingIntent(R.id.play_next, nextPIntent);
Intent cancelIntent = new Intent(NOTIFICATION_CANCEL);
PendingIntent cancelPIntent = PendingIntent.getBroadcast(this, 0, cancelIntent, 0);
remoteViews.setOnClickPendingIntent(R.id.notification_close, cancelPIntent);
remoteViewsSmall.setOnClickPendingIntent(R.id.notification_close, cancelPIntent);
Intent intent = new Intent(this, PlayActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
if(mNotification == null){
NotificationCompat.Builder builder = new NotificationCompat.Builder(this.getApplicationContext()) .setContentIntent(pendingIntent) .setSmallIcon(R.drawable.album) .setWhen(System.currentTimeMillis());
mNotification = builder.build();
//notification的bigContentView在通知出现在状态栏列表第一行,或者用户向下拉时
//contentView 非第一行非下拉时的显示状态
mNotification.bigContentView = remoteViews;
mNotification.contentView = remoteViewsSmall;
}else {
mNotification.bigContentView = remoteViews;
mNotification.contentView = remoteViewsSmall;
}
notificationTarget = new NotificationTarget(this, remoteViews, R.id.notification_image, mNotification, id);
notificationTargetSmall = new NotificationTarget(this, remoteViewsSmall, R.id.notification_image, mNotification, id);
if(music.islocal == 0){
Glide.with(getApplicationContext()).load(Uri.parse(music.album_art)).asBitmap().placeholder(R.drawable.album).into(notificationTarget);
Glide.with(getApplicationContext()).load(Uri.parse(music.album_art)).asBitmap().placeholder(R.drawable.album).into(notificationTargetSmall);
}else{
Glide.with(getApplicationContext()).load(music.album_art).asBitmap().placeholder(R.drawable.album).into(notificationTarget);
Glide.with(getApplicationContext()).load(music.album_art).asBitmap().placeholder(R.drawable.album).into(notificationTargetSmall);
}
return mNotification;
}
前台服务通过发送广播,来调用service方法Service中的其他方法,可参看源码在activity中利用PlayerProxy类对service进行操作,主要是实现一层封装,利用单例模式,最终调用的还是service中的方法.开启和绑定service
public void startAndBindPlayService(Context context, ServiceConnection connection){
intent = new Intent(context, MusicService.class);
if(mControl == null){ context.startService(intent); }
this.context = context;
context.bindService(intent, connection, Service.BIND_AUTO_CREATE); }
在ServiceConnection中获取后台的AIDL的Stub对象,进而可以调用service中的方法(这些方法运行在后台的binder线程池中);同时,把前台的AIDL的Stub对象,传入后台中去,当后台音乐状态改变时,即调用Stub对象相对应的方法,这些方法运行在前台binder线程池中,所以,此处进行了线程切换,使其在主线程刷新界面.也可以使用广播来处理,广播本来也是跨进程的一种方式.当后台发送广播时,系统会在已注册的广播中寻找匹配的广播接收器来响应.
public class MusicServiceConnection implements ServiceConnection{
private MusicControlInterface control;
private UIChangedListener uiListener;
private Handler mHandler = new Handler(Looper.getMainLooper());
private BaseActivity activity;
public MusicServiceConnection(final UIChangedListenerImpl listener,BaseActivity activity){
this.activity = activity;
uiListener = new UIChangedListener.Stub() {
@Override
public void onMusicStart() throws RemoteException {
// TODO Auto-generated method stub
mHandler.post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
listener.onMusicStart();
}
});
}
@Override
public void onMusicPause() throws RemoteException {
// TODO Auto-generated method stub
mHandler.post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
listener.onMusicPause();
}
});
}
@Override
public void onBufferingUpdate(int percent) throws RemoteException {
// TODO Auto-generated method stub
}
@Override
public void onDeleteAll() throws RemoteException {
// TODO Auto-generated method stub
mHandler.post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
listener.onDeleteAll();
}
});
}
};
}
@Override
public void onServiceConnected(ComponentName arg0, IBinder arg1) {
// TODO Auto-generated method stub
control =
MusicControlInterface.Stub.asInterface(arg1);PlayerProxy.getIntance().setService(control);
try {
List song = PlayerProxy.getIntance().getPlayList();
if(song!=null&&song.size()>0){
activity.showQuickControl(true);
activity.isNow = false;
}else{
activity.showQuickControl(false);
activity.isNow = true;
}
} catch (RemoteException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
control.setUiListener(uiListener);
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
// TODO Auto-generated method stub
PlayerProxy.getIntance().startAndBindPlayService(activity,this);
}
}
这样前台和后台就可以互相传递数据,进行交互