• 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进阶篇判断3G/WIFI/WAP
    Android基础篇配置查看Android系统源码
    Android进阶篇Http协议
    Android进阶篇流量统计
    Android进阶篇自定义Menu(设置Menu的背景及文字属性)
    Android进阶篇Gson解析Json数据
    Android进阶篇PopupWindow的使用
    Android数据篇SAX解析XML
    Android进价篇重力感应
    Android进阶篇ListView和Button共存
  • 原文地址:https://www.cnblogs.com/renhui/p/9409428.html
Copyright © 2020-2023  润新知