Android 12+ 蓝牙外放问题总结

2022-10-25 16:56:16 浏览数 (1)

背景

Android 12 上发现存在蓝牙外放问题,原因是存在多个应用设置通话音量,在建立SCO连接时,如果本应用不是通话音量的mode owner,则系统会拒绝为该应用建立sco。

也就是只有mode owner对应的应用才可以建立sco。

本篇从系统机制和dumpsys 角度分析根本原因

原因分析

首先提一下,sco连接失败的代码表现是执行startBluetoothSco后,收不到任何蓝牙状态的广播,就如同完全没执行过一样。

那分析就从该函数开始

代码语言:javascript复制
    public void startBluetoothSco(){
        final IAudioService service = getService();
        try {
            service.startBluetoothSco(mICallBack,
                    getContext().getApplicationInfo().targetSdkVersion);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

继续看AudioService

代码语言:javascript复制
    /** @see AudioManager#startBluetoothSco() */
    public void startBluetoothSco(IBinder cb, int targetSdkVersion) {
        if (!checkAudioSettingsPermission("startBluetoothSco()")) {
            return;
        }

        final int uid = Binder.getCallingUid();
        final int pid = Binder.getCallingPid();
        final int scoAudioMode =
                (targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2) ?
                        BtHelper.SCO_MODE_VIRTUAL_CALL : BtHelper.SCO_MODE_UNDEFINED;
        final String eventSource = new StringBuilder("startBluetoothSco()")
                .append(") from u/pid:").append(uid).append("/")
                .append(pid).toString();

        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
                .setUid(uid)
                .setPid(pid)
                .set(MediaMetrics.Property.EVENT, "startBluetoothSco")
                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
                        BtHelper.scoAudioModeToString(scoAudioMode))
                .record();
        startBluetoothScoInt(cb, pid, scoAudioMode, eventSource);

    }

这儿会走到startBluetoothScoInt里,没其他真正逻辑:

代码语言:javascript复制
    void startBluetoothScoInt(IBinder cb, int pid, int scoAudioMode, @NonNull String eventSource) {
        MediaMetrics.Item mmi = new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
                .set(MediaMetrics.Property.EVENT, "startBluetoothScoInt")
                .set(MediaMetrics.Property.SCO_AUDIO_MODE,
                        BtHelper.scoAudioModeToString(scoAudioMode));

        if (!checkAudioSettingsPermission("startBluetoothSco()") ||
                !mSystemReady) {
            mmi.set(MediaMetrics.Property.EARLY_RETURN, "permission or systemReady").record();
            return;
        }
        final long ident = Binder.clearCallingIdentity();
        mDeviceBroker.startBluetoothScoForClient(cb, pid, scoAudioMode, eventSource);
        Binder.restoreCallingIdentity(ident);
        mmi.record();
    }

这儿调用就会走到AudioDeviceBroker里

代码语言:javascript复制
  /*package*/ void startBluetoothScoForClient(IBinder cb, int pid, int scoAudioMode,
                @NonNull String eventSource) {

        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "startBluetoothScoForClient_Sync, pid: "   pid);
        }

        synchronized (mSetModeLock) {
            synchronized (mDeviceStateLock) {
                AudioDeviceAttributes device =
                        new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, "");

                postSetCommunicationRouteForClient(new CommunicationClientInfo(
                        cb, pid, device, scoAudioMode, eventSource));
            }
        }
    }

这儿会抛到Looper里,对应的实现如下:

代码语言:javascript复制
@GuardedBy("mDeviceStateLock")
    /*package*/ void setCommunicationRouteForClient(
                            IBinder cb, int pid, AudioDeviceAttributes device,
                            int scoAudioMode, String eventSource) {

        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "setCommunicationRouteForClient: device: "   device);
        }
        AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                                        "setCommunicationRouteForClient for pid: "   pid
                                          " device: "   device
                                          " from API: "   eventSource)).printLog(TAG));

        final boolean wasBtScoRequested = isBluetoothScoRequested();
        CommunicationRouteClient client;


        // Save previous client route in case of failure to start BT SCO audio
        AudioDeviceAttributes prevClientDevice = null;
        client = getCommunicationRouteClientForPid(pid);
        if (client != null) {
            prevClientDevice = client.getDevice();
        }

        if (device != null) {
            client = addCommunicationRouteClient(cb, pid, device);
            if (client == null) {
                Log.w(TAG, "setCommunicationRouteForClient: could not add client for pid: "
                          pid   " and device: "   device);
            }
        } else {
            client = removeCommunicationRouteClient(cb, true);
        }
        if (client == null) {
            return;
        }

        boolean isBtScoRequested = isBluetoothScoRequested();
        if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
            if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
                Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for pid: "
                          pid);
                // clean up or restore previous client selection
                if (prevClientDevice != null) {
                    addCommunicationRouteClient(cb, pid, prevClientDevice);
                } else {
                    removeCommunicationRouteClient(cb, true);
                }
                postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
            }
        } else if (!isBtScoRequested && wasBtScoRequested) {
            mBtHelper.stopBluetoothSco(eventSource);
        }

        sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE, SENDMSG_QUEUE, eventSource);
    }

这儿的逻辑简单看就是:

先查下是否已经有sco请求了 如果以前没有sco请求,现在要请求了,那就开始请求sco 这样理解上看没毛病,具体看看就会发现还有一些细节,比如isBluetoothScoRequested,是如何判断是否有sco请求的呢?

代码语言:javascript复制
    /*package*/ boolean isBluetoothScoRequested() {
        return isDeviceRequestedForCommunication(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
    }

    private boolean isDeviceRequestedForCommunication(int deviceType) {
        synchronized (mDeviceStateLock) {
            AudioDeviceAttributes device = requestedCommunicationDevice();
            return device != null && device.getType() == deviceType;
        }
    }

    @GuardedBy("mDeviceStateLock")
    private AudioDeviceAttributes requestedCommunicationDevice() {
        CommunicationRouteClient crc = topCommunicationRouteClient();
        AudioDeviceAttributes device = crc != null ? crc.getDevice() : null;
        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "requestedCommunicationDevice, device: "
                      device   "mAudioModeOwner: "   mAudioModeOwner.toString());
        }
        return device;
    }

 // 注意了!!!!!!!!!,这儿是最最关键的地方
    @GuardedBy("mDeviceStateLock")
    private CommunicationRouteClient topCommunicationRouteClient() {
        for (CommunicationRouteClient crc : mCommunicationRouteClients) {
            if (crc.getPid() == mAudioModeOwner.mPid) {
                return crc;
            }
        }
        if (!mCommunicationRouteClients.isEmpty() && mAudioModeOwner.mPid == 0) {
            return mCommunicationRouteClients.get(0);
        }
        return null;
    }

判断routeclient 的标准就是是否是modeowner,或者当前还没有modeownner,如果通话音量被其他应用设置了后,那明显不会是modeownner,因此这儿就会返回null。再倒推回去,isBluetoothScoRequested的返回值就是false。

那接下来获取client:getCommunicationRouteClientForPid,如果是首次设置,那就是null,接下来就是通过addCommunicationRouteClient进行add,这儿就是往列表中添加,那一定会添加成功。好的,那接下来就开始判断本次是否是启动sco了:isBluetoothScoRequested

按照刚才的逻辑,由于不是modeowner,那返回必然是false。也就是本次也不认为是sco请求。

如果本地和之前都没有sco请求,那就不操作sco了,直接更新路由:

代码语言:javascript复制
private void onUpdateCommunicationRoute(String eventSource) {
        AudioDeviceAttributes preferredCommunicationDevice = preferredCommunicationDevice();
        //preferredCommunicationDevice 会返回null
        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "onUpdateCommunicationRoute, preferredCommunicationDevice: "
                      preferredCommunicationDevice   " eventSource: "   eventSource);
        }
        AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
                "onUpdateCommunicationRoute, preferredCommunicationDevice: "
                  preferredCommunicationDevice   " eventSource: "   eventSource)));

        if (preferredCommunicationDevice == null
                || preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
            // 返回是null,所以即使是打开sco,到了这儿也变成了关闭sco了
            AudioSystem.setParameters("BT_SCO=off");
        } else {
            AudioSystem.setParameters("BT_SCO=on");
        }
        if (preferredCommunicationDevice == null) {
            AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice();
            if (defaultDevice != null) {
                setPreferredDevicesForStrategySync(
                        mCommunicationStrategyId, Arrays.asList(defaultDevice));
                setPreferredDevicesForStrategySync(
                        mAccessibilityStrategyId, Arrays.asList(defaultDevice));
            } else {
                removePreferredDevicesForStrategySync(mCommunicationStrategyId);
                removePreferredDevicesForStrategySync(mAccessibilityStrategyId);
            }
        } else {
            setPreferredDevicesForStrategySync(
                    mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice));
            setPreferredDevicesForStrategySync(
                    mAccessibilityStrategyId, Arrays.asList(preferredCommunicationDevice));
        }
        onUpdatePhoneStrategyDevice(preferredCommunicationDevice);
    }

接下来再获取preferedcommunicationdevice:

代码语言:javascript复制
  @Nullable private AudioDeviceAttributes preferredCommunicationDevice() {
        boolean btSCoOn = mBluetoothScoOn && mBtHelper.isBluetoothScoOn();
        if (btSCoOn) {
            // Use the SCO device known to BtHelper so that it matches exactly
            // what has been communicated to audio policy manager. The device
            // returned by requestedCommunicationDevice() can be a dummy SCO device if legacy
            // APIs are used to start SCO audio.
            AudioDeviceAttributes device = mBtHelper.getHeadsetAudioDevice();
            if (device != null) {
                return device;
            }
        }
        AudioDeviceAttributes device = requestedCommunicationDevice();
        if (device == null || device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
            // Do not indicate BT SCO selection if SCO is requested but SCO is not ON
            return null;
        }
        return device;
    }

如果调用了setBluetoothScoOn,那mBluetoothScoOn 可以是true,可是isBluetoothScoOn就会是false,因为sco 还没打开。

那这儿就会还是返回null。

返回null后,就会不管是不是开启sco,统一执行关闭sco:AudioSystem.setParameters("BT_SCO=off")

到了这儿总结下,如果不是modeowner,那么不管开不开sco,都会变成关闭sco。那接下来用log论证下:

代码语言:javascript复制
dumpsys media.metrics:

1951: {audio.bluetooth, (10-10 16:47:45.360), (10097, 25565, 10097), (event#=startBluetoothSco, scoAudioMode=SCO_MODE_UNDEFINED)}

1952: {audio.bluetooth, (10-10 16:47:45.361), (android.uid.system, 0, 1000), (event#=startBluetoothScoInt, scoAudioMode=SCO_MODE_UNDEFINED)}

dumpsys audio

10-10 16:47:45:360 onUpdateCommunicationRoute, preferredCommunicationDevice: null eventSource: startBluetoothSco()) from u/pid:10097/25565
10-10 16:47:45:362 removePreferredDevicesForStrategySync, strategy: 14
10-10 16:47:45:362 removePreferredDevicesForStrategySync, strategy: 17

dumpsys media.audio_flinger
    10-10 16:47:13.696 UID  1000, 1 KVP received: BT_SCO=off
    10-10 16:47:26.100 UID  1000, 1 KVP received: A2dpSuspended=false
    10-10 16:47:33.188 UID  1000, 1 KVP received: BT_SCO=off
    10-10 16:47:45.110 UID  1000, 1 KVP received: BT_SCO=off
    10-10 16:47:45.361 UID  1000, 1 KVP received: BT_SCO=off

看到 虽然执行的是startBluetoothSco, 却一直在发送关闭sco命令。代码和log 对的上了。

0 人点赞