问题背景:话机连接了头戴式的耳机,在通话过程中短按按钮是挂断电话,长按按钮是通话静音。客户需求是把长按改成挂断功能,短按是静音功能。
android版本:8.1
在通话中,测试打印信息,可以看到button的Keycode 是79, 对应着按键KEYCODE_HEADSETHOOK。
Phonewindowmanager -->interceptKeyBeforeQueueing() -->case KEYCODE_HEADSETHOOK
mBroadcastWakeLock.acquire(); Message msg = mHandler.obtainMessage(MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK, new KeyEvent(event)); msg.setAsynchronous(true); msg.sendToTarget();
在这里将message发送出去,在handlemessage里处理MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK
case MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK: dispatchMediaKeyWithWakeLock((KeyEvent)msg.obj); break;
在dispatchMediaKeyWithWakeLock()方法里,
void dispatchMediaKeyWithWakeLock(KeyEvent event) { ... dispatchMediaKeyWithWakeLockToAudioService(event); ... }
接着在把event传给了dispatchMediaKeyWithWakeLockToAudioService(event)。
接着调用了
MediaSessionLegacyHelper.getHelper(mContext).sendMediaButtonEvent(event, true);
继续传递event给了mediasession,MediaSessionLegacyHelper.getHelper(mContext)获得了一个MediaSessionLegacyHelper对象,接着看MediaSessionLegacyHelper的sendMediaButtonEvent()
public void sendMediaButtonEvent(KeyEvent keyEvent, boolean needWakeLock) { if (keyEvent == null) { Log.w(TAG, "Tried to send a null key event. Ignoring."); return; } mSessionManager.dispatchMediaKeyEvent(keyEvent, needWakeLock); if (DEBUG) { Log.d(TAG, "dispatched media key " + keyEvent); } }
又把event传给了msessionmanager,的dispatchMediaKeyEvent
public void dispatchMediaKeyEvent(@NonNull KeyEvent keyEvent, boolean needWakeLock) { try { mService.dispatchMediaKeyEvent(keyEvent, needWakeLock); } catch (RemoteException e) { Log.e(TAG, "Failed to send key event.", e); } }
这个mService的对象是ISessionManager,发现这个是应用了AIDL的进程通信方式,ISessionManager只是个接口,它的实现类是class SessionManagerImpl extends ISessionManager.Stub{},这个SessionManagerImpl是MediaSessionService.java的内部类,MediaSessionService是一个系统服务,控制了很多关于media的功能。
接着看SessionManagerImpl 的dispatchMediaKeyEvent()
@Override public void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) { 。。。 if (!isGlobalPriorityActive && isVoiceKey(keyEvent.getKeyCode())) { Log.i(TAG, "dispatchMediaKeyEvent: handleVoiceKeyEventLocked"); handleVoiceKeyEventLocked(keyEvent, needWakeLock); } else { Log.i(TAG, "dispatchMediaKeyEventLocked"); dispatchMediaKeyEventLocked(keyEvent, needWakeLock); } 。。。 }
中间省略了一些代码,在传递event的时候,做了个判断传递的方式是否是voicekey, 我们的headset是只有一个按钮,于是接着走dispatchMediaKeyEventLocked()
private void dispatchMediaKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock) { MediaSessionRecord session = mCurrentFullUserRecord.getMediaButtonSessionLocked(); if (session != null) { 。。。。 // If we don't need a wakelock use -1 as the id so we won't release it later. session.sendMediaButton(keyEvent, needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1, mKeyEventReceiver, Process.SYSTEM_UID, getContext().getPackageName()); 。。。。 } }
这里会把keyevent传递个一个session,这个session是什么呢?我也不知道,应该是类似于一个token之类的,记录了当前media信息的一个类MediaSessionRecord.java
进MediaSessionRecord.java看看,其中有许多设置方法,找到sendMediaButton
public void sendMediaButton(KeyEvent ke, int sequenceId, ResultReceiver cb, int uid, String packageName) { updateCallingPackage(uid, packageName); mSessionCb.sendMediaButton(ke, sequenceId, cb); }
这里的mSessionCb也是个特殊的类,在这一段,会发现有很多进程间通信的痕迹,各种AIDL输出。
class SessionCb { private final ISessionCallback mCb; public SessionCb(ISessionCallback cb) { mCb = cb; } public boolean sendMediaButton(KeyEvent keyEvent, int sequenceId, ResultReceiver cb) { Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); try { mCb.onMediaButton(mediaButtonIntent, sequenceId, cb); return true; } catch (RemoteException e) { Slog.e(TAG, "Remote failure in sendMediaRequest.", e); } return false; }
在这里,sendMediaButton又接着把keyevent转换为一个Intent,传给了mCb.onMediaButton
这个mCb是个AIDL实现,ISessionCallback是个接口,需要找到真实的继承它的类,全局搜索找到
public static class CallbackStub extends ISessionCallback.Stub 实现了这个接口,这里在MediaSession.java的内部类,看看路径,会发现MediaSessionRecord.java在framwork/service/子目录下,而MediaSession.java却在framwork/base/media/子目录下,跨进程通信很明显必然要用到AIDL。
在onMediaButton里
@Override public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber, ResultReceiver cb) { MediaSession session = mMediaSession.get(); try { if (session != null) { session.dispatchMediaButton(mediaButtonIntent); } } }
会继续走dispatchMediaButton
private void dispatchMediaButton(Intent mediaButtonIntent) { postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent); }
postToCallback就把intent传给了CallbackMessageHandler
在这个handler里,处理了msg和intent
handlemessage里
case MSG_MEDIA_BUTTON: mCallback.onMediaButtonEvent((Intent) msg.obj); break;
这个mCallback回调,是在创建CallbackMessageHandler的时候传来的,
public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) { super(looper, null, true); mCallback = callback; mCallback.mHandler = this; }
这里,需要找到是谁调用了构造方法,才能从callback里找到那个调用onMediaButtonEvent的地方。
全局搜索之后,就在MediaSession.java里的setCallback有调用:
public void setCallback(@Nullable Callback callback, @Nullable Handler handler) { 。。。 if (handler == null) { handler = new Handler(); } callback.mSession = this; CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(), callback); mCallback = msgHandler; 。。。 }
这里就需要再找找谁调用了mediasession的setcallback方法,全局搜索,发现只要在HeadsetMediaButton.java里有调用这个方法,并且这里是属于一个叫mMediaSessionHandler的handler里,
case MSG_MEDIA_SESSION_INITIALIZE: { MediaSession session = new MediaSession( mContext, HeadsetMediaButton.class.getSimpleName()); session.setCallback(mSessionCallback); session.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS); session.setPlaybackToLocal(AUDIO_ATTRIBUTES); mSession = session; break; }
那就是它了,HeadsetMediaButton.class。
在它的构造方法里,有个发送message的动作,从一开始创建就存在去产生了Mediasession
public HeadsetMediaButton( Context context, CallsManager callsManager, TelecomSystem.SyncRoot lock) { mContext = context; mCallsManager = callsManager; mLock = lock; // Create a MediaSession but don't enable it yet. This is a // replacement for MediaButtonReceiver mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_INITIALIZE).sendToTarget(); }
在setcallback里设置的是mSessionCallback,于是继续看它是什么。
private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() { @Override public boolean onMediaButtonEvent(Intent intent) { KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); Log.v(this, "SessionCallback.onMediaButton()... event = %s.", event); if ((event != null) && ((event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) || (event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))) { synchronized (mLock) { Log.v(this, "SessionCallback: HEADSETHOOK/MEDIA_PLAY_PAUSE"); boolean consumed = handleCallMediaButton(event); Log.v(this, "==> handleCallMediaButton(): consumed = %b.", consumed); return consumed; } } return true; } };
这个回调,有个我们熟悉的方法,onMediaButtonEvent,
在MediaSession里处理MSG_MEDIA_BUTTON这个case的时候,就是调用了mcallback的onMediaButtonEvent,而这个回调实现对象就是这里的mSessionCallback,这里onMediaButtonEvent把intent给处理了,走到handleCallMediaButton。
private boolean handleCallMediaButton(KeyEvent event) { if (event.isLongPress()) { return mCallsManager.onMediaButton(LONG_PRESS); } else if (event.getAction() == KeyEvent.ACTION_UP) { // We should not judge SHORT_PRESS by ACTION_UP event repeatCount, because it always // return 0. // Actually ACTION_DOWN event repeatCount only increases when LONG_PRESS performed. if (mLastHookEvent != null && mLastHookEvent.getRepeatCount() == 0) { return mCallsManager.onMediaButton(SHORT_PRESS); } } return true; }
这里走到了CallManager,是的,这是管理通话控制的地方,在callmanager里
boolean onMediaButton(int type) { if (hasAnyCalls()) { Call ringingCall = getFirstCallWithState(CallState.RINGING); if (HeadsetMediaButton.SHORT_PRESS == type) { if (ringingCall == null) { Call callToHangup = getFirstCallWithState(CallState.RINGING, CallState.DIALING, CallState.PULLING, CallState.ACTIVE, CallState.ON_HOLD); Log.addEvent(callToHangup, LogUtils.Events.INFO, "media btn short press - end call."); if (callToHangup != null) { callToHangup.disconnect(); return true; } } else { ringingCall.answer(VideoProfile.STATE_AUDIO_ONLY); return true; } } else if (HeadsetMediaButton.LONG_PRESS == type) { if (ringingCall != null) { Log.addEvent(getForegroundCall(), LogUtils.Events.INFO, "media btn long press - reject"); ringingCall.reject(false, null); } else { Log.addEvent(getForegroundCall(), LogUtils.Events.INFO, "media btn long press - mute"); mCallAudioManager.toggleMute(); } return true; } } return false; }
就是我们要找的地方,短按,和长按的处理,在这里,SHORT_PRESS 是接听和挂断,LONG_PRESS是拒绝和静音控制。
于是,我们只需要在这里更改对应if条件下的控制,就能完成短按和长按的客户定制功能。
总结:整个流程,从按键监听,到走到callmanager里,饶了很久,中间遇到了很多Mediasession和进程间通信知识,这里只是记录一下解bug的过程,感觉像猜谜游戏一样,这也是一段技术成长的过程。挺有意义的。