【线控】:机电行业特定短语。指机电控制里边的一种物理控制方式,主要是指信号发生器与信号接收器之间的连接方式是通过线缆或其他动作传到物体进行连接的。
Android线控:我们经常使用耳机上面的上一曲、下一曲、播放、暂停等操作。大体逻辑是耳机上的物理按键按下后会作为系统系统键值处理。之前看过android4.4的源码最近看android6.0的源码发现两者处理的方式有明显不同,本篇主要从adnroid4.4跟android6.0两套源码对比分析下线控的注册过程 后续分析源码中线控的下发逻辑。
【Android 4.4.3线控注册过程】
一。android应用软件中线控的注册
1.1 实现BroadcastReceiver 类,如下:
public class MediaKeyReceiverextends BroadcastReceiver { private String Tag = "MediaKeyReceiver001"; @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) { KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); switch (keyEvent.getKeyCode()) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: //播放暂停 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { L.i(Tag, "KEYCODE_MEDIA_PLAY_PAUSE ACTION_UP"); } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { L.i(Tag, "KEYCODE_MEDIA_PLAY_PAUSE ACTION_DOWN"); } break; case KeyEvent.KEYCODE_MEDIA_NEXT://下一曲 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { L.i(Tag, "KEYCODE_MEDIA_NEXT ACTION_UP"); } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { L.i(Tag, "KEYCODE_MEDIA_NEXT ACTION_DOWN"); } break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS://上一曲 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { L.i(Tag, "KEYCODE_MEDIA_PREVIOUS ACTION_UP"); } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { L.i(Tag, "KEYCODE_MEDIA_PREVIOUS ACTION_DOWN"); } break; } } } }
1.2 在AndroidManifest.xml中注册自己的BroadcastReceiver
<receiver android:name=".MediaKeyReceiver"> <intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" /> </intent-filter> </receiver>
1.3 注册线控广播
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); ComponentName name = new ComponentName(getPackageName(), MediaKeyReceiver.class.getName()); audioManager.registerMediaButtonEventReceiver(name);
二。Framework中线控的注册过程。
注册线控使用AudioManager的registerMediaButtonEventReceiver(ComponentName eventReceiver) ,传递一个ComponentName对象给AudioManager,源码如下:
/**
* Register a component to be the sole receiver of MEDIA_BUTTON intents.
*
* @param eventReceiver identifier of a {@link android.content.BroadcastReceiver}
* that will receive the media button intent. This broadcast receiver must be declared
* in the application manifest. The package of the component must match that of
* the context you're registering from.
*/
public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
if (eventReceiver == null) {
return;
}
if (!eventReceiver.getPackageName().equals(mContext.getPackageName())) {
Log.e(TAG, "registerMediaButtonEventReceiver() error: " +
"receiver and context package names don't match");
return;
}
// construct a PendingIntent for the media button and register it
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
// the associated intent will be handled by the component being registered
mediaButtonIntent.setComponent(eventReceiver);
PendingIntent pi = PendingIntent.getBroadcast(mContext,
0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/);
registerMediaButtonIntent(pi, eventReceiver);
}
可以看到此处会做下判断,如果当前传递ComponentName的包名跟AudioManager的包名不一致的话不作处理(正常情况下包名都是一致的)。此处构造一个PendingIntent对象,后面我们会看到这个对象是做什么用的。
/** * @hide no-op if (pi == null) or (eventReceiver == null) */ public void registerMediaButtonIntent(PendingIntent pi, ComponentName eventReceiver) { if (pi == null) { Log.e(TAG, "Cannot call registerMediaButtonIntent() with a null parameter"); return; } IAudioService service = getService(); try { // pi != null service.registerMediaButtonIntent(pi, eventReceiver, eventReceiver == null ? mToken : null); } catch (RemoteException e) { Log.e(TAG, "Dead object in registerMediaButtonIntent" + e); } }
可以看到最后实际上调用的是AudioManager的隐藏的registerMediaButtonIntent方法将PendingIntent跟ComponentName参数传递给AudioService,AudioService又传递给MediaFocusControl
public void registerMediaButtonIntent(PendingIntent pi, ComponentName c, IBinder token) { mMediaFocusControl.registerMediaButtonIntent(pi, c, token); }
直接看MediaFocusControl中的处理
/** * see AudioManager.registerMediaButtonIntent(PendingIntent pi, ComponentName c) * precondition: mediaIntent != null */ protected void registerMediaButtonIntent(PendingIntent mediaIntent, ComponentName eventReceiver, IBinder token) { Log.i(TAG, " Remote Control registerMediaButtonIntent() for " + mediaIntent); synchronized (mAudioFocusLock) { synchronized (mRCStack) { if (pushMediaButtonReceiver_syncAfRcs(mediaIntent, eventReceiver, token)) { // new RC client, assume every type of information shall be queried checkUpdateRemoteControlDisplay_syncAfRcs(RC_INFO_ALL); } } } }
/** * Helper function: * Set the new remote control receiver at the top of the RC focus stack. * Called synchronized on mAudioFocusLock, then mRCStack * precondition: mediaIntent != null * * @return true if mRCStack was changed, false otherwise */ private boolean pushMediaButtonReceiver_syncAfRcs(PendingIntent mediaIntent, ComponentName target, IBinder token) { // already at top of stack? if (!mRCStack.empty() && mRCStack.peek().mMediaIntent.equals(mediaIntent)) { return false; } if (mAppOps.noteOp(AppOpsManager.OP_TAKE_MEDIA_BUTTONS, Binder.getCallingUid(), mediaIntent.getCreatorPackage()) != AppOpsManager.MODE_ALLOWED) { return false; } RemoteControlStackEntry rcse = null; boolean wasInsideStack = false; try { for (int index = mRCStack.size() - 1; index >= 0; index--) { rcse = mRCStack.elementAt(index); if (rcse.mMediaIntent.equals(mediaIntent)) { // ok to remove element while traversing the stack since we're leaving the loop mRCStack.removeElementAt(index); wasInsideStack = true; break; } } } catch (ArrayIndexOutOfBoundsException e) { // not expected to happen, indicates improper concurrent modification Log.e(TAG, "Wrong index accessing media button stack, lock error? ", e); } if (!wasInsideStack) { rcse = new RemoteControlStackEntry(this, mediaIntent, target, token); } mRCStack.push(rcse); // rcse is never null // post message to persist the default media button receiver if (target != null) { mEventHandler.sendMessage(mEventHandler.obtainMessage( MSG_PERSIST_MEDIABUTTONRECEIVER, 0, 0, target/*obj*/)); } // RC stack was modified return true; }
可以看到当前线控消息已经在保存的栈并且在栈顶时不作处理,如果没有在栈顶,则移除站内的线控消息同时创建新的线控对象保存在栈顶。到此线控的注册过程完成。
【Android 6.0.1线控注册过程】
一。android应用软件中线控的注册
通过android4.4的注册过程看到是通过广播方式来接收线控消息,通过PendingIntent传递,可能因为广播本身是不可靠的通讯方式所以android5.0后修改为Binder来通讯,通过MediaSession来管理。
1.1 MediaSession的初始化
public class MediaKeyService extends Service { private MediaSession mediaSession; private AudioManager audioManager; @RequiresApi(api = 26) @Override public void onCreate() { super.onCreate(); audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); initMediaSession(); } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onDestroy() { super.onDestroy(); if (mediaSession!=null){ mediaSession.release(); } } private void play(){ //自己的播放逻辑,需要更新MediaSession状态 updatePlayState(false); } private void pause(){ //自己的暂停逻辑,需要更新MediaSession状态 updatePlayState(true); } private void updatePlayState(boolean isPlaying){ if (mediaSession==null) return; PlaybackState.Builder stateBuilder = new PlaybackState.Builder() .setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS); if (isPlaying) { stateBuilder.setState(PlaybackState.STATE_PLAYING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, SystemClock.elapsedRealtime()); } else { stateBuilder.setState(PlaybackState.STATE_PAUSED, PlaybackState.PLAYBACK_POSITION_UNKNOWN, SystemClock.elapsedRealtime()); } mediaSession.setPlaybackState(stateBuilder.build()); } private void initMediaSession() { mediaSession = new MediaSession(this, "com.android.music"); mediaSession.setCallback(new MediaSession.Callback() { @Override public void onSkipToPrevious() { //上一曲 } @Override public void onSkipToNext() {//下一曲 } @Override public void onPlay() {//播放 } @Override public void onPause() {//暂停 } });
mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
} AudioManager.OnAudioFocusChangeListener audioFocusLinister = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_LOSS://焦点被别的应用抢占,失去焦点此时不再接收线控消息 mediaSession.setActive(false); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT://短暂的失去音频焦点此时不再接收线控消息 mediaSession.setActive(false); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK://焦点会短暂失去,但是可以继续压低音量播放,如果是压低音量播放此处可以继续接收线控消息 mediaSession.setActive(true); break; case AudioManager.AUDIOFOCUS_GAIN://完全获得焦点,接收线控消息 mediaSession.setActive(true); break; default: } } }; }
以上为伪代码,在Service的onCreate中初始化MediaSession在onDestory()中注销,当需要监听线控消息的时候调用 mediaSession.setActive(true)接口来激活,不需要监听线控消息的时候调用 mediaSession.setActive(false)来注销。因为线控消息一般跟AudioFocus同步,即获取焦点的时候激活线控回调,失去焦点时注销。所以我在AudioFocus的回调中处理。
细心的同学会注意到代码里面有更新当前的播放状态updatePlayState(),可能有疑问为什么要将当前的播放状态传递给MediaSession。一般手机线控播放暂停是同一个按键,在android 4.4直接接收的是按键值,KEYCODE_MEDIA_PLAY_PAUSE键代表了播放暂停键需要根据自己当前的状态来做处理,在android 6.0中没有播放暂停的接口,所以需要将自己当前播放状态上传至MediaSession中,在消息分发过程中会判断当前的播放状态来分发。
二。Framework中线控的注册过程
public MediaSession(@NonNull Context context, @NonNull String tag, int userId) { if (context == null) { throw new IllegalArgumentException("context cannot be null."); } if (TextUtils.isEmpty(tag)) { throw new IllegalArgumentException("tag cannot be null or empty"); } mMaxBitmapSize = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize); mCbStub = new CallbackStub(this); MediaSessionManager manager = (MediaSessionManager) context .getSystemService(Context.MEDIA_SESSION_SERVICE); try { mBinder = manager.createSession(mCbStub, tag, userId); mSessionToken = new Token(mBinder.getController()); mController = new MediaController(context, mSessionToken); } catch (RemoteException e) { throw new RuntimeException("Remote error creating session.", e); } }
本文对比分析了Android 4.4与6.0中线控功能的实现方式。4.4版采用广播方式,通过PendingIntent传递;而6.0版改用MediaSession管理,采用Binder通讯,提升了线控消息的可靠性。
198






