Add disk control function to Android car Bluetooth music

1. Steering wheel event conversion

Assuming that the steering wheel is converted through the lin bus, it will eventually come to the Android side as a standard keyevent:

/** Key code constant: Play/Pause media key. */
public static final int KEYCODE_MEDIA_PLAY_PAUSE= 85;
/** Key code constant: Stop media key. */              
public static final int KEYCODE_MEDIA_STOP      = 86;
/** Key code constant: Play Next media key. */
public static final int KEYCODE_MEDIA_NEXT      = 87;
/** Key code constant: Play Previous media key. */
public static final int KEYCODE_MEDIA_PREVIOUS  = 88;

Press the media control key of the steering wheel, the inputdispatcher of the Android system (for related introduction, please refer to my blog:

Simple analysis of Android InputFlinger ) will report events with the keycode defined above. After reporting in

PhoneWindowManager will respond:

//PhoneWindowManager.java
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
    
    
    ...
        case KeyEvent.KEYCODE_MEDIA_STOP:
        case KeyEvent.KEYCODE_MEDIA_NEXT:
    	case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
    	  if 	(MediaSessionLegacyHelper.getHelper(mContext).
                 isGlobalPriorityActive()) {
    
    
                    result &= ~ACTION_PASS_TO_USER;
                }
                if ((result & ACTION_PASS_TO_USER) == 0) {
    
    
                    
                    mBroadcastWakeLock.acquire();
                    Message msg = mHandler.obtainMessage(
                        MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK,
                            new KeyEvent(event));
                    msg.setAsynchronous(true);
                    msg.sendToTarget();
                }
                break;	
    ...
}
...
case MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK:
	dispatchMediaKeyWithWakeLock((KeyEvent)msg.obj);
...
 //太多流转的代码就不列举了,不是本文重点
 //直接看MediaSessionManager.java
 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);
        }
    }

2.MediaSession

​ In the previous section, we discussed that the operation of event distribution is transferred to MediaSessionService through MediaSessionManager.

//MediaSessionService.java
public void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
    
    
    ...
        //一般全局优先的都是电话之类的
        //电话不是active状态的时候
        //dispatchMediaKeyEventLocked继续分发、/
       if (!isGlobalPriorityActive &&
         isVoiceKey(keyEvent.getKeyCode())) {
    
    
           handleVoiceKeyEventLocked(keyEvent, needWakeLock);
       } else {
    
    
           dispatchMediaKeyEventLocked(keyEvent, needWakeLock);
       }
    ...
}

Note here that the dispatchMediaKeyEventLocked call only has keyEvent parameters. This just shows what happened? The buttons to press are play, pause, next, previous etc. So who should this distribution go to? Let's see:

private void dispatchMediaKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock) {
    
    
    //找出
    MediaSessionRecord session = mCurrentFullUserRecord.getMediaButtonSessionLocked();
    if (session != null) {
    
    
        //回调它
    } else if (
    	mCurrentFullUserRecord.mLastMediaButtonReceiver != null
                    || mCurrentFullUserRecord.mRestoredMediaButtonReceiver != null
    ){
    
    
        //回调它
    }
}

//MediaSessionService.FullUserRecord
private MediaSessionRecord getMediaButtonSessionLocked() {
    
    
            return isGlobalPriorityActiveLocked()
                    ? mGlobalPrioritySession : mPriorityStack.getMediaButtonSession();
        }

getMediaButtonSessionLocked: If the global high-priority session is not active, it will return:

mPriorityStack.getMediaButtonSession();

See what mPriorityStack is?

mPriorityStack = new MediaSessionStack(mAudioPlaybackMonitor, this);

It is a session stack. There are a bunch of MediaSession stored in this stack. The mAudioPlaybackMonitor of this stack (finally has something to do with audio):

mAudioPlaybackMonitor = AudioPlaybackMonitor.getInstance(getContext(), mAudioService);

mAudioPlaybackMonitor is a singleton. After the whole system is up, there is only one. It is the AudioPlaybackMonitor of AudioService.

What does mPriorityStack use it for?

//MediaSessionStack.java 
//(开始我的业余翻译)
//如果有需要,更新media button session
//media button session是那个会接收media button事件的session
//我们会把media button事件发送给the lastly played app(最后播放的app)
//如果那个app有media session,它的session将收到media buttion事件
    public void updateMediaButtonSessionIfNeeded() {
    
    
        //getSortedAudioPlaybackClientUids的注释:
        //返回经过排序的有激活的audio playback的uid列表
        //The UID whose audio playback becomes active at the last comes first.
        //最后播放的放在最前面,后进先出嘛,所以这里的数据结构是栈
        //感觉自己好聪明
        IntArray audioPlaybackUids = mAudioPlaybackMonitor.getSortedAudioPlaybackClientUids();                                                                               
        for (int i = 0; i < audioPlaybackUids.size(); i++) {
    
    
            //找到udi对应的mediasession
            MediaSessionRecord mediaButtonSession =
                    findMediaButtonSession(audioPlaybackUids.get(i));
            if (mediaButtonSession != null) {
    
    
            // Found the media button session.    
                //然后cleanup
                mAudioPlaybackMonitor.cleanUpAudioPlaybackUids(
                    mediaButtonSession.getUid());
                if (mMediaButtonSession != mediaButtonSession) {
    
    
                    //更新mMediaButtonSession的值
                    updateMediaButtonSession(mediaButtonSession);
                }
                return;
            }
        }
    }

mPriorityStack uses mAudioPlaybackMonitor to map with mediasession, and refreshes according to the change of audio playback status.

The previous updateMediaButtonSessionIfNeeded function will be called when the app calls addSession:

public void addSession(MediaSessionRecord record) {
    
    
    mSessions.add(record);
    updateMediaButtonSessionIfNeeded();
}

Add the MediaSession created by the application to mSessions of MediaSessionStack for management, and update

mMediaButtonSession. In this way, when the previous mPriorityStack.getMediaButtonSession(); is called, a MediaSession that can respond to disk control events can be given.

3. Why doesn't car bluetooth music work?

1 and 2 above mentioned so many contextual summaries, it seems that the app wants to use the disk control, new a MediaSession, addSession, add a callback, and monitor the disk control events. But the actual situation is not like this. The specific reason, let me tell you slowly.

Such a function was mentioned in the second section above: getSortedAudioPlaybackClientUids.

//AudioPlaybackMonitor.java
public IntArray getSortedAudioPlaybackClientUids() {
    
    
        Log.d(TAG,"getSortedAudioPlaybackClientUids");
        IntArray sortedAudioPlaybackClientUids = new IntArray();
        synchronized (mLock) {
    
     sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids);
        }
        return sortedAudioPlaybackClientUids;
    }

The main association is this mSortedAudioPlaybackClientUids.

When playing Bluetooth music in the car (connect to the mobile phone, play on the mobile phone, the role of the Bluetooth on the car side is the sink). The size of mSortedAudioPlaybackClientUids is 0. I rub, why is this!!! let me find out why so it is!

Where is mSortedAudioPlaybackClientUids added?

//直接搜索mSortedAudioPlaybackClientUids.add
//只有一个地方
//AudioPlaybackMonitor.java
//alled when the {@link AudioPlaybackConfiguration} is updated.
public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs,
                                         boolean flush) {
    
    
    ...
        mSortedAudioPlaybackClientUids.add(0, config.getClientUid());
    ...
}

Ok, as usual, walk through the call stack. Let’s not talk about the process, just go straight to the conclusion:

1. AudioTrack, MediaPlayer, and SoundPool all extend PlayerBase. They have obtained some public capabilities, such as

//PlayerBase.java
void baseStart() {
    
    
    ...
                mState = AudioPlaybackConfiguration.PLAYER_STATE_STARTED;
    //请注意,并记住这个playerEvent方法
                getService().playerEvent(mPlayerIId, mState);
   ...
    }

getService returns IAudioService.

Yes, that is the audio_service started in SystemServer.

2. The play method of AudioTrack will call the baseStart method mentioned in 1 of PlayerBase, as will the start method of MediaPlayer, and of course the play method of SoundPool.

That is to say, as long as the app calls one of the three in Android to play the sound, it will automatically call the baseStart method of PlayerBase, that is, the playerEvent method of AudioService

3. See what the playerEvent method called in the first step baseStart looks like?

//AudioService.java 
public void playerEvent(int piid, int event) {
    
    
        mPlaybackMonitor.playerEvent(piid, event, Binder.getCallingUid());                           
    }
//PlaybackActivityMonitor.java
public void playerEvent(int piid, int event, int binderUid) {
    
    
    ...
        dispatchPlaybackChange(event == AudioPlaybackConfiguration.PLAYER_STATE_RELEASED);
    ...
}
private void dispatchPlaybackChange(boolean iplayerReleased) {
    
    
    ...
        pmc.mDispatcherCb.dispatchPlaybackConfigChange(...省略)
    ...
}

Because of the successful meeting, I ignored many details. A lot of code is omitted.

That is to say, as long as the app calls Java's AudioTrack or MediaPlayer or SoundPool, it will call the dispatchPlaybackConfigChange method we discussed earlier.

As mentioned earlier, dispatchPlaybackConfigChange will add the currently playing MediaSession

mSortedAudioPlaybackClientUids。

I won't talk about the others first. Today we are talking about car bluetooth music!

Why won't the car bluetooth music be added? Don't they use java's AudioTrack, MediaPlayer, SoundPool? Note, I emphasized java!!!

Let me show the evidence I found:

//system/bt/btif/src/btif_avrcp_audio_track.cc
//这里再次明确下,我们本文聊的场景是车机蓝牙音乐,就是手机连接车机,手机的音频
//数据通过a2dp送到车机,车机的蓝牙角色是sink
void* BtifAvrcpAudioTrackCreate(参数省略){
    
    
    ...
    track = new android::AudioTrack(参数省略);
    ...
}

Let’s not talk about the others, after all, I have already entered the field of Bluetooth, which is not what I am good at.

What I want to say here is that they call the native AudioTrack class. This is the problem, and Google probably didn't take this into account. In short, the playback status of the app that uses the native AudioTrack class in the AudioTrack.cpp file will not be tracked (here is the meaning of tracking).

4. Solve problems

Now that we have found the problem. We just try to call the playerEvent method of AudioService (don't ask why it's not playerBase, I have to let AudioTrack inherit the PlayerBase class, it's hard!).

We checked the source code of PlayerBase and found:

//PlayerBase.cpp
PlayerBase::PlayerBase() {
    
    
    ...
        sp<IBinder> binder = defaultServiceManager()->checkService(String16("audio"));
    	mAudioManager = interface_cast<IAudioManager>(binder);
    ...
}

Yes, you read that right, cpp code can directly bind java service.

Hold on

Let me say a digression (the interface file used by the binder call):

//IAudioManager.h
class IAudioManager : public IInterface
{
    
    
public:
	// These transaction IDs must be kept in sync with the method order from
    // IAudioService.aidl.
    // transaction IDs for the unsupported methods are commented out
        ADJUSTSUGGESTEDSTREAMVOLUME           = IBinder::FIRST_CALL_TRANSACTION,
        ADJUSTSTREAMVOLUME                    = IBinder::FIRST_CALL_TRANSACTION + 1,
        PLAYER_EVENT                          = IBinder::FIRST_CALL_TRANSACTION + 70,
}

The previous two lines of comments allowed me to solve a problem that the binder call was unsuccessful.

The definition of the IAudioManager method should be consistent with the IAudioService.aidl of the java layer. Mainly method offset

For example this:

IBinder::FIRST_CALL_TRANSACTION + 70,//我说的偏移就是这个70

FIRST_CALL_TRANSACTION points to the first method ADJUSTSUGGESTEDSTREAMVOLUME, if the IAudioService.aidl of the java layer has an added method (for example, AudioManager adds a new interface to the app call, AudioService needs to add the corresponding method implementation, and IAudioService.aidl needs to add the corresponding method definition) . At this time, the offset corresponding to PLAYER_EVENT, for example, the original is 69, and I changed it to 70 here because there is an extra method in front. I can think of a stupid way to count the number of methods.

Back to the solution to this problem:

//AudioTrack.cpp
AudioTrack::AudioTrack(
...
    //这个参数是我自己加的默认参数,默认值就是false.这样,只有蓝牙改成true才会启动这个功能。这样能保证改动的影响最小。
   bool isNeedTrackInNative 
   ) {
    
    
   	...
    if(mIsNeedTrackInNative) {
    
    
    	sp<IBinder> binder = defaultServiceManager()->checkService(String16("audio"));
    	mAudioManager = interface_cast<IAudioManager>(binder);
    }
    ...
   }

If the binding is successful, the audio service can be called happily.

status_t AudioTrack::start()                                 
{
    
    
    ...
        mAudioManager->playerEvent(mPIId,PLAYER_STATE_STARTED);
    ...
}

void AudioTrack::stop()
{
    
    
    ...
        mAudioManager->playerEvent(mPIId,PLAYER_STATE_STOPPED);
    ...
}

void AudioTrack::pause()
{
    
    
 	...
        mAudioManager->playerEvent(mPIId,PLAYER_STATE_PAUSED);
    ...
}

AudioTrack::~AudioTrack()
{
    
    
    ...
        mAudioManager->releasePlayer(mPIId);
    	mAudioManager.clear();
    ...
}

Then, the corresponding BtifAvrcpAudioTrackCreate call also has a uniquely defined parameter for him. In this regard, the default parameter mechanism of C++ is really easy to use. The definition of the function can be changed, and parameters can be added, without affecting the code that called the function before. Niubi Niubi!

Okay, the problem is solved, the article is finished, call it a day!

Guess you like

Origin blog.csdn.net/bberdong/article/details/88370351