Android 关于解决MediaButton学习到的media控制流程
问题背景:话机连接了头戴式的耳机,在通话过程中短按按钮是挂断电话,长按按钮是通话静音。客户需求是把长按改成挂断功能,短按是静音功能。
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的过程,感觉像猜谜游戏一样,这也是一段技术成长的过程。挺有意义的。
Android 关于解决MediaButton学习到的media控制流程的更多相关文章
- Android自动化测试之Monkeyrunner学习笔记(一)
Android自动化测试之Monkeyrunner学习笔记(一) 因项目需要,开始研究Android自动化测试方法,对其中的一些工具.方法和框架做了一些简单的整理,其中包括Monkey.Monkeyr ...
- Android(java)学习笔记267:Android线程池形态
1. 线程池简介 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力. 假设一个服务器完成一项任务所需时间为:T1 创建线程时间, ...
- Android(java)学习笔记264:Android下的属性动画高级用法(Property Animation)
1. 大家好,在上一篇文章当中,我们学习了Android属性动画的基本用法,当然也是最常用的一些用法,这些用法足以覆盖我们平时大多情况下的动画需求了.但是,正如上篇文章当中所说到的,属性动画对补间动画 ...
- Android(java)学习笔记71:生产者和消费者之等待唤醒机制
1. 首先我们根据梳理我们之前Android(java)学习笔记70中关于生产者和消费者程序思路: 2. 下面我们就要重点介绍这个等待唤醒机制: (1)第一步:还是先通过代码体现出等待唤醒机制 pac ...
- Android(java)学习笔记160:Framework运行环境之 Android进程产生过程
1.前面Android(java)学习笔记159提到Dalvik虚拟机启动初始化过程,就下来就是启动zygote进程: zygote进程是所有APK应用进程的父进程:每当执行一个Android应用程序 ...
- 20172327 2018-2019-1 《第一行代码Android》第一章学习总结
学号 2018-2019-1 <第一行代码Android>第一章学习总结 教材学习内容总结 - Android系统架构: 1.Linux内核层 Android系统是基于Linux内核的,这 ...
- Android系统源码学习步骤
Android系统是基于Linux内核来开发的,在分析它在运行时库层的源代码时,我们会经常碰到诸如管道(pipe).套接字(socket)和虚拟文件系统(VFS)等知识. 此外,Android系统还在 ...
- Android(java)学习笔记211:Android线程池形态
1. 线程池简介 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力. 假设一个服务器完成一项任务所需时间为:T1 创建线程时间, ...
- Android(java)学习笔记208:Android下的属性动画高级用法(Property Animation)
1. 大家好,在上一篇文章当中,我们学习了Android属性动画的基本用法,当然也是最常用的一些用法,这些用法足以覆盖我们平时大多情况下的动画需求了.但是,正如上篇文章当中所说到的,属性动画对补间动画 ...
随机推荐
- Eclipse oxygen 版本汉化教程
Eclipse oxygen 版本汉化步骤如下: 第一步:打开Eclipse 第二步:浏览器打开网址 http://www.eclipse.org/babel/downloads.php 1.复制对应 ...
- Centos 7创建一个服务
首先创建服务文件 vim /etc/systemd/system/node.service #内容如下 [Unit] Description=ethereum-go Monitor Daemon Af ...
- Unity3D-RayMarch-几何图元1-添加基本着色模型
效果图: 使用phong着色模型,将环境光.物体的漫反射光.镜面光三种光效加合而得到上图的效果 raymarch 的shader代码: // Upgrade NOTE: replaced '_Obje ...
- java编程高级进阶
Java内存模型 对hadoop namenode -format执行过程的探究 intellij idea 高级用法之:集成JIRA.UML类图插件.集成SSH.集成FTP.Database管理 强 ...
- EasyPR源码剖析(7):车牌判断之SVM
前面的文章中我们主要介绍了车牌定位的相关技术,但是定位出来的相关区域可能并非是真实的车牌区域,EasyPR通过SVM支持向量机,一种机器学习算法来判定截取的图块是否是真的“车牌”,本节主要对相关的技术 ...
- tomcat简单使用
下载解压tomcat[root@localhost]# tar zxf apache-tomcat-8.5.4.tar.gz -C /usr/local/tomcat yum自带JDK,注意区别JRE ...
- Python 列表(list)的使用
文章目录 一.创建list 二.访问list中元素 三.更新元素 四.删除元素 五.求list长度 六.连接列表 七.截取列表 八.复制列表 一.创建list myList = [2,3,1,5,6, ...
- 一次HTTP请求响应涉及了哪些?
HTTP请求和响应步骤 TCP/IP协议 TCP三次握手 HTTP协议 HTTP请求报文 HTTP响应报文 TCP四次挥手 HTTP请求和响应步骤 以上完整表示了HTTP请求和响应的7个步骤,下面从T ...
- new Image().src资源重复请求问题
const img = new Image(); img.setAttribute("crossOrigin", 'Anonymous'); img.src = url + '?t ...
- 移动端canvas文字图片合成并生成图片(canvas宽度自适应移动端屏幕)
这是我之前做的一个关于文字图片合成的代码,供大家参考,不足支出还望体谅:具体的注释在代码里都有,有什么不懂了可以留言互相交流.<!DOCTYPE html> <html lang=& ...