目前Android系统中主流的音乐播放器都支持线控的功能,线控设备包括有线耳机和蓝牙耳机或蓝牙车机,当不方便操作手机的时候可以通过线控来控制音乐的播放暂停以及切歌。
同时当音乐播放的时候部分手机(如小米)会在系统的锁屏页面上展示各种歌曲信息,如歌曲名,歌手名,专辑图片甚至歌词,同时还可以提供一些播放控制的操作。
AudioManager配合RemoteControlClient
在Android 5.0之前的版本中,Android推荐使用AudioManager的一系列功能来实现线控和锁屏信息显示功能。实现线控很简单,通过下面代码即可。
mAudioManager = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE);
mComponentName = new ComponentName(mContext.getPackageName(), MediaButtonReceiver.class.getName());
mContext.getPackageManager().setComponentEnabledSetting(mComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setComponent(mComponentName);
mPendingIntent = PendingIntent.getBroadcast(mContext, 0, mediaButtonIntent, PendingIntent.FLAG_CANCEL_CURRENT);
mAudioManager.registerMediaButtonEventReceiver(mComponentName);
registerMediaButtonEventReceiver是抢占线控焦点的方法,Android系统同时只能为一个应用发送线控信息,只有抢占到线控焦点后才能让线控为自己的app所用。
MediaButtonReceiver是一个BroadcastReceiver,用来处理接收到的线控信息以实现具体的音乐控制,这个BroadcastReceiver是需要在Manifest.xml中注册的。
<receiver
android:name=".MediaButtonReceiver"
android:enabled="true"
android:exported="false">
<intent-filter android:priority="2147483647">
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
监听android.intent.action.MEDIA_BUTTON这个action,通过intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
来获取具体的KeyEvent,从KeyEvent中通过event.getKeyCode()
取出keycode,判断是哪种按键信息,如KeyEvent.KEYCODE_HEADSETHOOK
、KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
、KeyEvent.KEY_MEDIA_PLAY
、KeyEvent.KEY_MEDIA_PAUSE
等等;通过event.getAction()
取出按键操作进行判断是何种行为,如KeyEvent.ACTION_UP
、KeyEvent.ACTION_DOWN
,然后进行相应的操作。
当使用完毕后需要通过mAudioManager.unregisterMediaButtonEventReceiver(mComponentName)
解注册。
实现线控功能后要想再显示锁屏信息,就要用到RemoteControlClient了,这也是Android5.0之前推荐的系统API。首先需要初始化RemoteControlClient并向AudioManager进行注册
mRemoteControlClient = new RemoteControlClient(mPendingIntent);
mAudioManager.registerRemoteControlClient(mRemoteControlClient);
这里的mPendingIntent在上面注册线控时使用过,这里再次使用是为了共用MediaButtonReceiver来接收处理来自系统锁屏页面的音乐控制操作。但是能够接收哪些按键信息是需要特别指定的。
int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
| RemoteControlClient.FLAG_KEY_MEDIA_NEXT
| RemoteControlClient.FLAG_KEY_MEDIA_PLAY
| RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
| RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
| RemoteControlClient.FLAG_KEY_MEDIA_RATING;
mRemoteControlClient.setTransportControlFlags(flags);
接下来要做的就是发送歌曲信息了。
//设置当前播放状态和播放时间
mRemoteControlClient.setPlaybackState(getPlayState(), MusicUtil.getCursongTime() * 1000, 1.0f);
//设置歌曲信息,包括歌曲名,歌手名,专辑名,歌曲时长,专辑图等
MetadataEditor md = mRemoteControlClient.editMetadata(true);
md.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, song.getName());
md.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, song.getSinger());
md.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, song.getAlbum());
md.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, song.getDuration());
md.putBitmap(MetadataEditor.BITMAP_KEY_ARTWORK, mAlbumCover);
//适配MIUI系统锁屏歌词,MIUI自定义1000为歌词信息
md.putString(1000, mLyric.toLrcString());
md.apply();
使用完毕后需要通过mAudioManager.unregisterRemoteControlClient(mRemoteControlClient)
解注册RemoteControlClient。
MediaSession
Android5.0及以后的版本RemoteControlClient被Deprecate,Android推荐使用最新的MediaSession来统一管理线控和歌曲信息展示,这样一来,比使用AudioManager加RemoteControlClient要方便的多。由于MediaSession只有Android5.0以后才提供,要适配Android5.0之前的版本还是要兼容一下RemoteControlClient,但是我们惊喜的发现,support V4包已经加入了MediaSessionCompat的API,使用方法和MediaSession一样,这样我们就可以完全摒弃RemoteControlClient。只是它是非线程安全的,这点下面我们会讲到。首先看一下初始化过程。
//这里同样要指明相应的MediaBottonReceiver,用来接收处理线控信息
//Android5.0之前的版本线控信息直接通过BroadcastReceiver处理
mComponentName = new ComponentName(mContext.getPackageName(), MediaButtonReceiver.class.getName());
mContext.getPackageManager().setComponentEnabledSetting(mComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setComponent(mComponentName);
mPendingIntent = PendingIntent.getBroadcast(mContext, 0, mediaButtonIntent, PendingIntent.FLAG_CANCEL_CURRENT);
//由于非线程安全,这里要把所有的事件都放到主线程中处理,使用这个handler保证都处于主线程
mHandler = new Handler(Looper.getMainLooper());
mMediaSession = new MediaSessionCompat(mContext, "mbr", mComponentName, null);
//指明支持的按键信息类型
mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mMediaSession.setMediaButtonReceiver(mPendingIntent);
//这里指定可以接收的来自锁屏页面的按键信息
PlaybackStateCompat state = new PlaybackStateCompat.Builder().setActions(
PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_STOP).build();
mMediaSession.setPlaybackState(state);
//在Android5.0及以后的版本中线控信息在这里处理
mMediaSession.setCallback(new MediaSessionCompat.Callback() {
@Override
public boolean onMediaButtonEvent(Intent intent) {
//通过Callback返回按键信息,为复用MediaButtonReceiver,直接调用它的onReceive()方法
MediaButtonReceiver mMediaButtonReceiver = new MediaButtonReceiver();
mMediaButtonReceiver.onReceive(mContext, intent);
return true;
}
}, mHandler); //把mHandler当做参数传入,保证按键事件处理在主线程
//把MediaSession置为active,这样才能开始接收各种信息
if (!mMediaSession.isActive()) {
mMediaSession.setActive(true);
}
这里需要注意的两个问题:
1.从上面的初始化过程中可以看到,Android5.0之前和之后的版本处理按键信息的地方是不同的,为了适配所有系统版本,我们把两种注册方式都加入。
2.由于MediaSessionCompat为非线程安全,要求所有对MediaSessionCompat的调用都处于同一线程。然而Android5.0系统中提供的MediaSession确是线程安全的,看起来为了适配低版本还是要有所牺牲的。
初始化过后线控就可以使用了。接下来处理屏显信息的发送。
//同步当前的播放状态和播放时间
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder();
stateBuilder.setState(getPlayState(), getCurrentPlayTime(), 1.0f);
mMediaSession.setPlaybackState(stateBuilder.build());
//同步歌曲信息
MediaMetadataCompat.Builder md = new MediaMetadataCompat.Builder();
md.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getName());
md.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getSinger());
md.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbum());
md.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.getDuration());
md.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, mAlbumCover);
mMediaSession.setMetadata(md.build());
使用完毕后要通过mMediaSession.release()
释放这个MediaSession。
遇到的坑
1.线控焦点的抢占
线控焦点是需要抢的!!!由于系统同时只会允许一个APP占用线控焦点,所以如果你抢占线控焦点后其他APP又去抢占,那我们的APP就无法收到线控控制信息了。这个时候我们就需要在合适的时机把线控焦点抢回来。合适的夺抢时机有两个:
(1) 当歌曲发起播放或从暂停恢复播放的时候去抢一下线控焦点,因为我们要播放音乐了,这个时候抢占无可厚非。
(2) Android系统建议线控焦点和音频焦点要同时使用,即抢占音频焦点的同时也要抢占线控焦点,音频焦点的丢失基本上也意味着线控焦点的丢失。不同于线控焦点,音频焦点的丢失和恢复都是可以被系统通知的,所以我们就可以根据音频焦点的状态来判断线控焦点的状态,当音频焦点丢失的时候不做任何操作,而当音频焦点恢复的时候就是我们重新抢占线控焦点的时候了。
2.双锁屏的问题
一些音乐APP如QQ音乐和轻听等会自定义自己的锁屏页面,这个锁屏页面是可以通过开关来打开后关闭的,这时候问题来了,为了避免同时出现两个锁屏页面,打开自定义锁屏的时候需要关闭系统锁屏页面,关闭自定义锁屏的时候需要重新打开系统锁屏,那如何收放自如的开关系统的锁屏页面呢?
大家也许注意到,上面再初始化MediaSessionCompat的时候调用了mMediaSession.setActive(true)
开激活它,那是不是调用mMediaSession.setActive(false)
一下就可以关掉系统锁屏了呢?试了一下,果然没有问题,锁屏页面可以随着setActive方法自由开启和关闭,但是发现一个问题,关闭锁屏后,线控也失效了。。。原因很简单,线控和屏显用的都是这一套MediaSession,线控自然也会随这个setActive方法开启和关闭。后来又试过mMediaSession.setActive(false)
后再调用mMediaSession.setActive(true)
把线控启动回来,但这时屏显也会跟着一起回来,而显示的是之前的歌曲信息。
这时就需要下猛药,直接调用mMediaSession.release()
来关闭当前的MediaSessionCompat,然后马上重新初始化一个新的MediaSessionCompat,由于之前的屏显信息已经销毁掉,新的MediaSessionCompat就不会重新展示屏显,同时由于重新注册线控,可以重新接收线控信息。
3.MIUI的锁屏歌词显示
在介绍MediaSessionCompat发送屏显信息的时候,貌似没有跟RemoteControlClient一样发送适配MIUI屏显的歌词信息,这是因为构造屏显信息结构体的时候,MediaSessionCompat所使用的MediaMetadataCompat.Builder的putString()方法传递的Key是一个String,而RemoteControlClient所使用的MetadataEditor的putString()方法传递的Key是一个int,MIUI自定义的歌词item的Key就是一个int值1000,而把这个“1000”转为String传给MediaMetadataCompat.Builder显然不合适,那该怎么办呢?
经过和MIUI开发人员的确实得知MIUI并没有为MediaSession适配歌词item后,我们只能自己寻找出路。通过查看RemoteControlClient的源码发现它有一个私有成员就是MediaSession!!原来MediaSession本来就是存在的,并非是Android5.0后新出来的API,只不过之前都是通过RemoteControlClient进行了封装,了解了这一点后看到了一线希望,两种方法的屏显信息结构体MediaMetadataCompat.Builder和MetadataEditor是否也有什么关联呢?查看RemoteControlClient用到的MetadataEditor源码,发现有这么一段:
public synchronized MetadataEditor putString(int key, String value)
throws IllegalArgumentException {
super.putString(key, value);
if (mMetadataBuilder != null) {
// MediaMetadata supports all the same fields as MetadataEditor
String metadataKey = MediaMetadata.getKeyFromMetadataEditorKey(key);
// But just in case, don't add things we don't understand
if (metadataKey != null) {
mMetadataBuilder.putText(metadataKey, value);
}
}
return this;
}
在putString方法中,int类型的key值会通过MediaMetadata.getKeyFromMetadataEditorKey(key)
方法转换为String型,然后再放到一个mMetadataBuilder中,而这个mMetadataBuilder正是MediaSessionCompat所用到的MediaMetadataCompat.Builder类型!!这么看来,int类型的key值是有办法映射到String类型的,只需要通过MediaMetadata.getKeyFromMetadataEditorKey(key)
方法。继续查看源码发现这个转换方法被hide掉,无法直接调用,只能反射了。
MediaMetadataCompat.Builder md = new MediaMetadataCompat.Builder();
try {
if (mLyric != null && Util4Common.isSupportMIUILockScreen()) {
Class<?> MediaMetadataInstance = null;
try {
MediaMetadataInstance = Class.forName(MediaMetadata.class.getName());
} catch (Exception e) {
e.printStackTrace();
}
try {
Method method = MediaMetadataInstance.getMethod("getKeyFromMetadataEditorKey", int.class);
String key = (String) method.invoke(null, 1000);
md.putString(key, mLyric.toLrcString());
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
mMediaSession.setMetadata(md.build());
经测试完美解决问题。
本文不是原创,忘记在哪看的了