问题背景:话机连接了头戴式的耳机,在通话过程中短按按钮是挂断电话,长按按钮是通话静音。客户需求是把长按改成挂断功能,短按是静音功能。

android版本:8.1

在通话中,测试打印信息,可以看到button的Keycode 是79, 对应着按键KEYCODE_HEADSETHOOK。

Phonewindowmanager -->interceptKeyBeforeQueueing() -->case KEYCODE_HEADSETHOOK

  1. mBroadcastWakeLock.acquire();
  2. Message msg = mHandler.obtainMessage(MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK, new KeyEvent(event));
  3. msg.setAsynchronous(true);
  4. msg.sendToTarget();

在这里将message发送出去,在handlemessage里处理MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK

  1. case MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK:
  2. dispatchMediaKeyWithWakeLock((KeyEvent)msg.obj);
  3. break;

在dispatchMediaKeyWithWakeLock()方法里,

  1. void dispatchMediaKeyWithWakeLock(KeyEvent event) {
  2. ...
  3. dispatchMediaKeyWithWakeLockToAudioService(event);
  4. ...
  5. }

接着在把event传给了dispatchMediaKeyWithWakeLockToAudioService(event)。

接着调用了

  1. MediaSessionLegacyHelper.getHelper(mContext).sendMediaButtonEvent(event, true);

继续传递event给了mediasession,MediaSessionLegacyHelper.getHelper(mContext)获得了一个MediaSessionLegacyHelper对象,接着看MediaSessionLegacyHelper的sendMediaButtonEvent()

  1. public void sendMediaButtonEvent(KeyEvent keyEvent, boolean needWakeLock) {
  2. if (keyEvent == null) {
  3. Log.w(TAG, "Tried to send a null key event. Ignoring.");
  4. return;
  5. }
  6. mSessionManager.dispatchMediaKeyEvent(keyEvent, needWakeLock);
  7. if (DEBUG) {
  8. Log.d(TAG, "dispatched media key " + keyEvent);
  9. }
  10. }

又把event传给了msessionmanager,的dispatchMediaKeyEvent

  1. public void dispatchMediaKeyEvent(@NonNull KeyEvent keyEvent, boolean needWakeLock) {
  2. try {
  3. mService.dispatchMediaKeyEvent(keyEvent, needWakeLock);
  4. } catch (RemoteException e) {
  5. Log.e(TAG, "Failed to send key event.", e);
  6. }
  7. }

这个mService的对象是ISessionManager,发现这个是应用了AIDL的进程通信方式,ISessionManager只是个接口,它的实现类是class SessionManagerImpl extends ISessionManager.Stub{},这个SessionManagerImpl是MediaSessionService.java的内部类,MediaSessionService是一个系统服务,控制了很多关于media的功能。

接着看SessionManagerImpl 的dispatchMediaKeyEvent()

  1. @Override
  2. public void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
  3. 。。。
  4. if (!isGlobalPriorityActive && isVoiceKey(keyEvent.getKeyCode())) {
  5. Log.i(TAG, "dispatchMediaKeyEvent: handleVoiceKeyEventLocked");
  6.  
  7. handleVoiceKeyEventLocked(keyEvent, needWakeLock);
  8. } else {
  9. Log.i(TAG, "dispatchMediaKeyEventLocked");
  10. dispatchMediaKeyEventLocked(keyEvent, needWakeLock);
  11. }
  12. 。。。
  13. }

中间省略了一些代码,在传递event的时候,做了个判断传递的方式是否是voicekey, 我们的headset是只有一个按钮,于是接着走dispatchMediaKeyEventLocked()

  1. private void dispatchMediaKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock) {
  2. MediaSessionRecord session = mCurrentFullUserRecord.getMediaButtonSessionLocked();
  3. if (session != null) {
  4.  
  5. 。。。。
  6. // If we don't need a wakelock use -1 as the id so we won't release it later.
  7. session.sendMediaButton(keyEvent,
  8. needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
  9. mKeyEventReceiver, Process.SYSTEM_UID,
  10. getContext().getPackageName());
  11. 。。。。
  12. }
  13. }

这里会把keyevent传递个一个session,这个session是什么呢?我也不知道,应该是类似于一个token之类的,记录了当前media信息的一个类MediaSessionRecord.java

进MediaSessionRecord.java看看,其中有许多设置方法,找到sendMediaButton

  1. public void sendMediaButton(KeyEvent ke, int sequenceId,
  2. ResultReceiver cb, int uid, String packageName) {
  3. updateCallingPackage(uid, packageName);
  4. mSessionCb.sendMediaButton(ke, sequenceId, cb);
  5. }

这里的mSessionCb也是个特殊的类,在这一段,会发现有很多进程间通信的痕迹,各种AIDL输出。

  1. class SessionCb {
  2. private final ISessionCallback mCb;
  3.  
  4. public SessionCb(ISessionCallback cb) {
  5. mCb = cb;
  6. }
  7.  
  8. public boolean sendMediaButton(KeyEvent keyEvent, int sequenceId, ResultReceiver cb) {
  9. Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
  10. mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
  11. try {
  12. mCb.onMediaButton(mediaButtonIntent, sequenceId, cb);
  13. return true;
  14. } catch (RemoteException e) {
  15. Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
  16. }
  17. return false;
  18. }

在这里,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里

  1. @Override
  2. public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber,
  3. ResultReceiver cb) {
  4. MediaSession session = mMediaSession.get();
  5. try {
  6. if (session != null) {
  7. session.dispatchMediaButton(mediaButtonIntent);
  8. }
  9. }
  10. }

会继续走dispatchMediaButton

  1. private void dispatchMediaButton(Intent mediaButtonIntent) {
  2. postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
  3. }

postToCallback就把intent传给了CallbackMessageHandler

在这个handler里,处理了msg和intent

handlemessage里

  1. case MSG_MEDIA_BUTTON:
  2. mCallback.onMediaButtonEvent((Intent) msg.obj);
  3. break;

这个mCallback回调,是在创建CallbackMessageHandler的时候传来的,

  1. public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
  2. super(looper, null, true);
  3. mCallback = callback;
  4. mCallback.mHandler = this;
  5. }

这里,需要找到是谁调用了构造方法,才能从callback里找到那个调用onMediaButtonEvent的地方。

全局搜索之后,就在MediaSession.java里的setCallback有调用:

  1. public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
  2. 。。。
  3.  
  4. if (handler == null) {
  5. handler = new Handler();
  6. }
  7. callback.mSession = this;
  8. CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
  9. callback);
  10. mCallback = msgHandler;
  11. 。。。
  12. }

这里就需要再找找谁调用了mediasession的setcallback方法,全局搜索,发现只要在HeadsetMediaButton.java里有调用这个方法,并且这里是属于一个叫mMediaSessionHandler的handler里,

  1. case MSG_MEDIA_SESSION_INITIALIZE: {
  2. MediaSession session = new MediaSession(
  3. mContext,
  4. HeadsetMediaButton.class.getSimpleName());
  5. session.setCallback(mSessionCallback);
  6. session.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY
  7. | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
  8. session.setPlaybackToLocal(AUDIO_ATTRIBUTES);
  9. mSession = session;
  10. break;
  11. }

那就是它了,HeadsetMediaButton.class。

在它的构造方法里,有个发送message的动作,从一开始创建就存在去产生了Mediasession

  1. public HeadsetMediaButton(
  2. Context context,
  3. CallsManager callsManager,
  4. TelecomSystem.SyncRoot lock) {
  5. mContext = context;
  6. mCallsManager = callsManager;
  7. mLock = lock;
  8.  
  9. // Create a MediaSession but don't enable it yet. This is a
  10. // replacement for MediaButtonReceiver
  11. mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_INITIALIZE).sendToTarget();
  12. }

在setcallback里设置的是mSessionCallback,于是继续看它是什么。

  1. private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() {
  2. @Override
  3. public boolean onMediaButtonEvent(Intent intent) {
  4. KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
  5. Log.v(this, "SessionCallback.onMediaButton()... event = %s.", event);
  6. if ((event != null) && ((event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) ||
  7. (event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))) {
  8. synchronized (mLock) {
  9. Log.v(this, "SessionCallback: HEADSETHOOK/MEDIA_PLAY_PAUSE");
  10. boolean consumed = handleCallMediaButton(event);
  11. Log.v(this, "==> handleCallMediaButton(): consumed = %b.", consumed);
  12. return consumed;
  13. }
  14. }
  15. return true;
  16. }
  17. };

这个回调,有个我们熟悉的方法,onMediaButtonEvent,

在MediaSession里处理MSG_MEDIA_BUTTON这个case的时候,就是调用了mcallback的onMediaButtonEvent,而这个回调实现对象就是这里的mSessionCallback,这里onMediaButtonEvent把intent给处理了,走到handleCallMediaButton。

  1. private boolean handleCallMediaButton(KeyEvent event) {
  2.  
  3. if (event.isLongPress()) {
  4. return mCallsManager.onMediaButton(LONG_PRESS);
  5. } else if (event.getAction() == KeyEvent.ACTION_UP) {
  6. // We should not judge SHORT_PRESS by ACTION_UP event repeatCount, because it always
  7. // return 0.
  8. // Actually ACTION_DOWN event repeatCount only increases when LONG_PRESS performed.
  9. if (mLastHookEvent != null && mLastHookEvent.getRepeatCount() == 0) {
  10. return mCallsManager.onMediaButton(SHORT_PRESS);
  11. }
  12. }
  13.  
  14. return true;
  15. }

这里走到了CallManager,是的,这是管理通话控制的地方,在callmanager里

  1. boolean onMediaButton(int type) {
  2. if (hasAnyCalls()) {
  3. Call ringingCall = getFirstCallWithState(CallState.RINGING);
  4. if (HeadsetMediaButton.SHORT_PRESS == type) {
  5. if (ringingCall == null) {
  6. Call callToHangup = getFirstCallWithState(CallState.RINGING, CallState.DIALING,
  7. CallState.PULLING, CallState.ACTIVE, CallState.ON_HOLD);
  8. Log.addEvent(callToHangup, LogUtils.Events.INFO,
  9. "media btn short press - end call.");
  10. if (callToHangup != null) {
  11. callToHangup.disconnect();
  12. return true;
  13. }
  14. } else {
  15. ringingCall.answer(VideoProfile.STATE_AUDIO_ONLY);
  16. return true;
  17. }
  18. } else if (HeadsetMediaButton.LONG_PRESS == type) {
  19. if (ringingCall != null) {
  20. Log.addEvent(getForegroundCall(),
  21. LogUtils.Events.INFO, "media btn long press - reject");
  22. ringingCall.reject(false, null);
  23. } else {
  24. Log.addEvent(getForegroundCall(), LogUtils.Events.INFO,
  25. "media btn long press - mute");
  26. mCallAudioManager.toggleMute();
  27. }
  28. return true;
  29. }
  30. }
  31. return false;
  32. }

就是我们要找的地方,短按,和长按的处理,在这里,SHORT_PRESS 是接听和挂断,LONG_PRESS是拒绝和静音控制。

于是,我们只需要在这里更改对应if条件下的控制,就能完成短按和长按的客户定制功能。

总结:整个流程,从按键监听,到走到callmanager里,饶了很久,中间遇到了很多Mediasession和进程间通信知识,这里只是记录一下解bug的过程,感觉像猜谜游戏一样,这也是一段技术成长的过程。挺有意义的。

Android 关于解决MediaButton学习到的media控制流程的更多相关文章

  1. Android自动化测试之Monkeyrunner学习笔记(一)

    Android自动化测试之Monkeyrunner学习笔记(一) 因项目需要,开始研究Android自动化测试方法,对其中的一些工具.方法和框架做了一些简单的整理,其中包括Monkey.Monkeyr ...

  2. Android(java)学习笔记267:Android线程池形态

    1. 线程池简介  多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力.     假设一个服务器完成一项任务所需时间为:T1 创建线程时间, ...

  3. Android(java)学习笔记264:Android下的属性动画高级用法(Property Animation)

    1. 大家好,在上一篇文章当中,我们学习了Android属性动画的基本用法,当然也是最常用的一些用法,这些用法足以覆盖我们平时大多情况下的动画需求了.但是,正如上篇文章当中所说到的,属性动画对补间动画 ...

  4. Android(java)学习笔记71:生产者和消费者之等待唤醒机制

    1. 首先我们根据梳理我们之前Android(java)学习笔记70中关于生产者和消费者程序思路: 2. 下面我们就要重点介绍这个等待唤醒机制: (1)第一步:还是先通过代码体现出等待唤醒机制 pac ...

  5. Android(java)学习笔记160:Framework运行环境之 Android进程产生过程

    1.前面Android(java)学习笔记159提到Dalvik虚拟机启动初始化过程,就下来就是启动zygote进程: zygote进程是所有APK应用进程的父进程:每当执行一个Android应用程序 ...

  6. 20172327 2018-2019-1 《第一行代码Android》第一章学习总结

    学号 2018-2019-1 <第一行代码Android>第一章学习总结 教材学习内容总结 - Android系统架构: 1.Linux内核层 Android系统是基于Linux内核的,这 ...

  7. Android系统源码学习步骤

    Android系统是基于Linux内核来开发的,在分析它在运行时库层的源代码时,我们会经常碰到诸如管道(pipe).套接字(socket)和虚拟文件系统(VFS)等知识. 此外,Android系统还在 ...

  8. Android(java)学习笔记211:Android线程池形态

    1. 线程池简介  多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力.     假设一个服务器完成一项任务所需时间为:T1 创建线程时间, ...

  9. Android(java)学习笔记208:Android下的属性动画高级用法(Property Animation)

    1. 大家好,在上一篇文章当中,我们学习了Android属性动画的基本用法,当然也是最常用的一些用法,这些用法足以覆盖我们平时大多情况下的动画需求了.但是,正如上篇文章当中所说到的,属性动画对补间动画 ...

随机推荐

  1. pyecharts 安装学习

    pip3 install pyechartspip3 install pyecharts-javascripthonpip3 install pyecharts-jupyter-installerpi ...

  2. 简单的StringBuffer实现

    package com.letv.test.base; import java.util.Arrays; public class StringBuffer { private char[] valu ...

  3. mac 删除文件不经过废纸篓解决办法

    mac 删除文件不经过废纸篓,提示“此项目将被立刻删除,您不能撤销此操作.”,解决办法. 终端机运行两个命令: rm -R ~/.Trash killall Finder 退出终端机. ------- ...

  4. VNF网络性能提升解决方案及实践

    VNF网络性能提升解决方案及实践 2016年7月 作者:    王智民 贡献者:     创建时间:    2016-7-20 稳定程度:    初稿 修改历史 版本 日期 修订人 说明 1.0 20 ...

  5. ASP.NET Core Web API 如何 数据分页 以及遇到'OFFSET' 附近有语法错误

    最近领导叫我做的一个B/S端的小项目,突发奇想想用到core web api 今天写数据分页的时候,就想着 用linq分页查询吧,直接上代码 _context.Skip(Size * (PageNum ...

  6. Ax用Excel导出表的字段属性信息

    static void CKT_ExportTableColnum(Args _args){ LJD_QaHalf_Figure _LJD_QaHalf_Figure; SysDictTable sd ...

  7. 3200 [HNOI2009]有趣的数列

    题面 dalao们都说这是一题简单的卡特兰数,画一画就出来了 emmmmm…… 讲讲怎么分解质因数来算组合数 先打个表 void prim(){ ex[]=ex[]=; ;i<=*n;i++){ ...

  8. Apollo

    下载源码: https://github.com/nobodyiam/apollo-build-scripts#%E4%B8%80%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9 ...

  9. C# 互通操作 (一)

    回顾一下自己学习的内容然后从互通的基础案例开始写起. 这次实现一个很简单的互通demo,就是 在unity里  在c#里调用windows窗体的MessageBox 消息提示 public class ...

  10. 【Python】 Python3 环境搭建

    Python是一种计算机程序设计语言.是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越来越多被用于独立的.大型项目的开发. Windo ...