Android下媒体音量也支持听筒?

背景介绍

最近遇到了一个case,发现Android上媒体音量下也可以支持听筒播放,这个行为比较奇怪,因此专门在源码中分析下。

分析流程

在系统中有这样一个接口isSpeakerphoneOn,有时候会用来判断是否是外放,可是这个接口真的可以判断么?

我们可以直接在启动应用的时候读取下这个接口的值,媒体音量下默认是外放,理论上就应该是true,那真的是么?
搞一个demo,打印下,结果如下:

speakerphone on is false

此时应用的确是外放。这是系统bug么?我们从代码上看下:

  /**
     * Indicates if preferred route selection for communication is speakerphone.
     * @return true if speakerphone is active, false otherwise.
     */
    /*package*/ boolean isSpeakerphoneOn() {
        return isDeviceOnForCommunication(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
    }

从注释上看,表示优先选择的路由是否是speakerphone,这儿并没有说是当前的路由是speakerphone。

    /**
     * Indicates if the device which type is passed as argument is currently either resquested
     * to be used for communication or selected for an other reason (e.g bluetooth SCO audio
     * is active for SCO device).
     * @param deviceType the device type the query applies to.
     * @return true if this device type is requested for communication.
     */
    private boolean isDeviceOnForCommunication(int deviceType) {
        synchronized (mDeviceStateLock) {
            AudioDeviceAttributes device = preferredCommunicationDevice();
            return device != null && device.getType() == deviceType;
        }
    }

这儿从preferredCommunicationDevice中看是否优先选择的路由是TYPE_BUILTIN_SPEAKER。
从之前的信息上看,就是这儿返回的应该不是TYPE_BUILTIN_SPEAKER,否则就应该是true了。

    /**
     * Determines which preferred device for phone strategy should be sent to audio policy manager
     * as a function of current SCO audio activation state and active communication route requests.
     * SCO audio state has the highest priority as it can result from external activation by
     * telephony service.
     * @return selected forced usage for communication.
     */
    @GuardedBy("mDeviceStateLock")
    @Nullable private AudioDeviceAttributes preferredCommunicationDevice() {
        boolean btSCoOn = mBtHelper.isBluetoothScoOn();
        synchronized (mBluetoothAudioStateLock) {
            btSCoOn = btSCoOn && mBluetoothScoOn;
        }

        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 placeholder 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;
    }

虽然这儿想表达Sco连接的设备优先级比较高,不过现在不涉及sco,只关心外放,听筒就好了。所以逻辑还在requestedCommunicationDevice中, 其实这儿可以看到一些端倪,从逻辑上看,应该是查询应用请求的路由,如果应用没有请求,那就应该是空,而媒体音量下应用一般不会请求路由,因此这时候查询的设备就是空,按照上面的逻辑,那就是找不到speakerphone设备,所以返回false,不过这个仅仅是我们的猜想,如果要求证还需要继续往下看:

    /**
     * Returns the device currently requested for communication use case.
     * Use the device requested by the communication route client selected by
     * {@link #topCommunicationRouteClient()} if any or none otherwise.
     * @return AudioDeviceAttributes the requested device for communication.
     */
    @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 + " mAudioModeOwner: " + mAudioModeOwner.toString());
        }
        return device;
    }

答案就在topCommunicationRouteClient中了

    /**
     * Returns the communication client with the highest priority:
     * - 1) the client which is currently also controlling the audio mode
     * - 2) the first client in the stack if there is no audio mode owner
     * - 3) no client otherwise
     * @return CommunicationRouteClient the client driving the communication use case routing.
     */
    @GuardedBy("mDeviceStateLock")
    private CommunicationRouteClient topCommunicationRouteClient() {
        for (CommunicationRouteClient crc : mCommunicationRouteClients) {
            if (crc.getUid() == mAudioModeOwner.mUid) {
                return crc;
            }
        }
        if (!mCommunicationRouteClients.isEmpty() && mAudioModeOwner.mPid == 0
                && mCommunicationRouteClients.get(0).isActive()) {
            return mCommunicationRouteClients.get(0);
        }
        return null;
    }

这儿会按照2个策略来选择设备:

  1. 优先让当前mode owner请求的设备,因为是媒体音量,不涉及mode owner,因此这个我们不满足
  2. 在看中请求设备列表中的第一个设备,因为我们没请求过,那就应该是空。注意这儿还判断了Active,内部就是看有没有存活的采集或播放,也就是如果没有存活的采集或播放,请求路由也会不生效,这个也是合理的,既然都没有采播,空占着路由反而会影响其他有采播的应用。这个就当额外的一个小知识了。

目前看起来基本符合我们的猜想。那如何实锤呢? 我们可以dump下这个信息:

  Communication route clients:
    [CommunicationRouteClient: mUid: 10372 mDevice: AudioDeviceAttributes: role:output type:speaker addr: name: profiles:[] descriptors:[] mIsPrivileged: false mPlaybackActive: false mRecordingActive: false]

  Computed Preferred communication device: null

  Applied Preferred communication device: null
  Active communication device: AudioDeviceAttributes: role:output type:earpiece addr: name:Pixel 4 profiles:[] descriptors:[]

第一个打印的就是mCommunicationRouteClients, 这个是有一个成员,不过对应的uid并不是demo的,而且采播状态也是false,从前面的信息我们知道如果一个应用没有存活的采播,那么他请求的route是不会生效的。
第二个就是打印的preferredCommunicationDevice,果然是null,因为是null,所以返回的是false。

不过可以看到最后一行信息有route打印,告诉我们当前的route是earpiece,这个显然不是真实的,那这个信息是什么呢?

   mActiveCommunicationDevice = AudioManager.getDeviceInfoFromTypeAndAddress(
                device.getType(), device.getAddress());

是从native audiopolicymanager报上来的设备,这个是默认的device,并不是当前使用的device。

接下来我们看下SetSpeakerphoneOn(false):

    /**
     * Turns speakerphone on/off
     * @param on true to enable speakerphone
     * @param eventSource for logging purposes
     */
    /*package*/ void setSpeakerphoneOn(
            IBinder cb, int uid, boolean on, boolean isPrivileged, String eventSource) {

        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "setSpeakerphoneOn, on: " + on + " uid: " + uid);
        }
        postSetCommunicationDeviceForClient(new CommunicationDeviceInfo(
                cb, uid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
                on, BtHelper.SCO_MODE_UNDEFINED, eventSource, isPrivileged));
    }

对应这儿的on就是false,也就是关闭speaker,接下来会到如下逻辑中:

    /**
     * Sets or resets the communication device for matching client. If no client matches and the
     * request is to reset for a given device (deviceInfo.mOn == false), the method is a noop.
     * @param deviceInfo information on the device and requester {@link #CommunicationDeviceInfo}
     */
    @GuardedBy("mDeviceStateLock")
    /*package*/ void onSetCommunicationDeviceForClient(CommunicationDeviceInfo deviceInfo) {
        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "onSetCommunicationDeviceForClient: " + deviceInfo);
        }
        if (!deviceInfo.mOn) {
            CommunicationRouteClient client = getCommunicationRouteClientForUid(deviceInfo.mUid);
            if (client == null || (deviceInfo.mDevice != null
                    && !deviceInfo.mDevice.equals(client.getDevice()))) {
                return;
            }
        }

        AudioDeviceAttributes device = deviceInfo.mOn ? deviceInfo.mDevice : null;
        setCommunicationRouteForClient(deviceInfo.mCb, deviceInfo.mUid, device,
                deviceInfo.mScoAudioMode, deviceInfo.mIsPrivileged, deviceInfo.mEventSource);
    }

如果是请求关闭,那么就需要获取以前请求的设备,如果是空或者设备不匹配,那么就直接return了。
从这儿我们可以看出,如果是要使用听筒,那么需要先请求过外放,如果没有请求过,那么是走不了听筒的。
再重复一下,如果要使用听筒,就需要先请求外放。
接下来继续看setCommunicationRouteForClient:

 @GuardedBy("mDeviceStateLock")
    /*package*/ void setCommunicationRouteForClient(
                            IBinder cb, int uid, AudioDeviceAttributes device,
                            int scoAudioMode, boolean isPrivileged, String eventSource) {

        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "setCommunicationRouteForClient: device: " + device
                    + ", eventSource: " + eventSource);
        }
        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                                        "setCommunicationRouteForClient for uid: " + uid
                                        + " device: " + device + " isPrivileged: " + isPrivileged
                                        + " 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;
        boolean prevPrivileged = false;
        client = getCommunicationRouteClientForUid(uid);
        if (client != null) {
            prevClientDevice = client.getDevice();
            prevPrivileged = client.isPrivileged();
        }

        if (device != null) {
            client = addCommunicationRouteClient(cb, uid, device, isPrivileged);
            if (client == null) {
                Log.w(TAG, "setCommunicationRouteForClient: could not add client for uid: "
                        + uid + " and device: " + device);
            }
        } else {
            client = removeCommunicationRouteClient(cb, true);
        }
        if (client == null) {
            return;
        }
        if (!mScoManagedByAudio) {
            boolean isBtScoRequested = isBluetoothScoRequested();
            if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
                if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
                    Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: "
                            + uid);
                    // clean up or restore previous client selection
                    if (prevClientDevice != null) {
                        addCommunicationRouteClient(cb, uid, prevClientDevice, prevPrivileged);
                    } else {
                        removeCommunicationRouteClient(cb, true);
                    }
                    postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
                }
            } else if (!isBtScoRequested && wasBtScoRequested) {
                mBtHelper.stopBluetoothSco(eventSource);
            }
        }
        // In BT classic for communication, the device changes from a2dp to sco device, but for
        // LE Audio it stays the same and we must trigger the proper stream volume alignment, if
        // LE Audio communication device is activated after the audio system has already switched to
        // MODE_IN_CALL mode.
        if (isBluetoothLeAudioRequested() && device != null) {
            final int streamType = mAudioService.getBluetoothContextualVolumeStream();
            final int leAudioVolIndex = getVssVolumeForDevice(streamType, device.getInternalType());
            final int leAudioMaxVolIndex = getMaxVssVolumeForStream(streamType);
            if (AudioService.DEBUG_COMM_RTE) {
                Log.v(TAG, "setCommunicationRouteForClient restoring LE Audio device volume lvl.");
            }
            postSetLeAudioVolumeIndex(leAudioVolIndex, leAudioMaxVolIndex, streamType);
        }

        updateCommunicationRoute(eventSource);
    }

这儿看着逻辑多,其实大部分是和sco相关的,我们只关心和外放,听筒相关的就好。可以看到如下信息:

  1. 如果是听筒,那么会先移除之前请求的client,如果之前也没请求过,那就直接返回了
  2. 如果是外放,那么会添加到mCommunicationRouteClients中。

接下来就是updateCommunicationRoute:

  /**
     * Configures audio policy manager and audio HAL according to active communication route.
     * Always called from message Handler.
     */
    // @GuardedBy("mSetModeLock")
    @GuardedBy("mDeviceStateLock")
    private void updateCommunicationRoute(String eventSource) {
        AudioDeviceAttributes preferredCommunicationDevice = preferredCommunicationDevice();
        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "updateCommunicationRoute, preferredCommunicationDevice: "
                    + preferredCommunicationDevice + " eventSource: " + eventSource);
        }
        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                "updateCommunicationRoute, preferredCommunicationDevice: "
                + preferredCommunicationDevice + " eventSource: " + eventSource)));

        if (preferredCommunicationDevice == null) {
            AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice();
            if (defaultDevice != null) {
                mDeviceInventory.setPreferredDevicesForStrategyInt(
                        mCommunicationStrategyId, Arrays.asList(defaultDevice));
                mDeviceInventory.setPreferredDevicesForStrategyInt(
                        mAccessibilityStrategyId, Arrays.asList(defaultDevice));
            } else {
                mDeviceInventory.removePreferredDevicesForStrategyInt(mCommunicationStrategyId);
                mDeviceInventory.removePreferredDevicesForStrategyInt(mAccessibilityStrategyId);
            }
            mDeviceInventory.applyConnectedDevicesRoles();
            mDeviceInventory.reapplyExternalDevicesRoles();
        } else {
            mDeviceInventory.setPreferredDevicesForStrategyInt(
                    mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice));
            mDeviceInventory.setPreferredDevicesForStrategyInt(
                    mAccessibilityStrategyId, Arrays.asList(preferredCommunicationDevice));
        }
        onUpdatePhoneStrategyDevice(preferredCommunicationDevice);
    }

这儿从preferredCommunicationDevice中拿到优先级最高的路由设备,前面已经看过内部的逻辑了,优先选择modeOwner请求的设备,接下来是请求队列中首个请求对应的设备,不过前提是需要有存活的采播。

从我们关心的外放和听筒逻辑上看,如果是外放,那么这儿获取到的优先设备就应该是外放,而如果是听筒,那么就是null。
带着这个信息我们继续看下onUpdatePhoneStrategyDevice:

    // @GuardedBy("mSetModeLock")
    @GuardedBy("mDeviceStateLock")
    private void onUpdatePhoneStrategyDevice(AudioDeviceAttributes device) {
        boolean wasSpeakerphoneActive = isSpeakerphoneActive();
        mPreferredCommunicationDevice = device;
        updateActiveCommunicationDevice();
        if (wasSpeakerphoneActive != isSpeakerphoneActive()) {
            try {
                mContext.sendBroadcastAsUser(
                        new Intent(AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED)
                                .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
                                          UserHandle.ALL);
            } catch (Exception e) {
                Log.w(TAG, "failed to broadcast ACTION_SPEAKERPHONE_STATE_CHANGED: " + e);
            }
        }
        mAudioService.postUpdateRingerModeServiceInt();
        dispatchCommunicationDevice();
    }

这儿会比较是否外放状态发生变化,如果是,那么就会对外发送广播。这儿只关注下postUpdateRingerModeServiceInt,看这个方法的原因是我们需要找到是音频路由在哪儿设置到系统中生效。

    /*package*/ void postUpdateRingerModeServiceInt() {
        sendMsg(mAudioHandler, MSG_UPDATE_RINGER_MODE, SENDMSG_QUEUE, 0, 0, null, 0);
    }

中间就是handler处理,我们直接看最终的实现:

    private void setRingerModeInt(int ringerMode, boolean persist) {
        final boolean change;
        synchronized(mSettingsLock) {
            change = mRingerMode != ringerMode;
            mRingerMode = ringerMode;
            muteRingerModeStreams();
        }

        // Post a persist ringer mode msg
        if (persist) {
            sendMsg(mAudioHandler, MSG_PERSIST_RINGER_MODE,
                    SENDMSG_REPLACE, 0, 0, null, PERSIST_DELAY);
        }
        if (change) {
            // Send sticky broadcast
            broadcastRingerMode(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, ringerMode);
        }
    }

muteRingerModeStreams内部就会触发设置路由到native:

@GuardedBy("mSettingsLock")
    private void muteRingerModeStreams() {
        // Mute stream if not previously muted by ringer mode and (ringer mode
        // is not RINGER_MODE_NORMAL OR stream is zen muted) and stream is affected by ringer mode.
        // Unmute stream if previously muted by ringer/zen mode and ringer mode
        // is RINGER_MODE_NORMAL or stream is not affected by ringer mode.
        int numStreamTypes = AudioSystem.getNumStreamTypes();

        if (mNm == null) {
            mNm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        }

        final int ringerMode = mRingerMode; // Read ringer mode as reading primitives is atomic
        final boolean ringerModeMute = ringerMode == AudioManager.RINGER_MODE_VIBRATE
                || ringerMode == AudioManager.RINGER_MODE_SILENT;
        final boolean shouldRingSco = ringerMode == AudioManager.RINGER_MODE_VIBRATE
                && mDeviceBroker.isBluetoothScoActive();
        final boolean shouldRingBle = ringerMode == AudioManager.RINGER_MODE_VIBRATE
                && (mDeviceBroker.isBluetoothBleHeadsetActive()
                || mDeviceBroker.isBluetoothBleSpeakerActive());
        // Ask audio policy engine to force use Bluetooth SCO/BLE channel if needed
        final String eventSource = "muteRingerModeStreams() from u/pid:" + Binder.getCallingUid()
                + "/" + Binder.getCallingPid();
        int forceUse = AudioSystem.FORCE_NONE;
        if (shouldRingSco) {
            forceUse = AudioSystem.FORCE_BT_SCO;
        } else if (shouldRingBle) {
            forceUse = AudioSystem.FORCE_BT_BLE;
        }
        sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE, AudioSystem.FOR_VIBRATE_RINGING,
                forceUse, eventSource, 0);

        for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
            final boolean isMuted = isStreamMutedByRingerOrZenMode(streamType);
            final boolean muteAllowedBySco =
                    !((shouldRingSco || shouldRingBle) && streamType == AudioSystem.STREAM_RING);
            final boolean shouldZenMute = isStreamAffectedByCurrentZen(streamType);
            final boolean shouldMute = shouldZenMute || (ringerModeMute
                    && isStreamAffectedByRingerMode(streamType) && muteAllowedBySco);
            if (isMuted == shouldMute) continue;
            if (!shouldMute) {
                // unmute
                // ring and notifications volume should never be 0 when not silenced
                if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING
                        || mStreamVolumeAlias[streamType] == AudioSystem.STREAM_NOTIFICATION) {
                    synchronized (VolumeStreamState.class) {
                        final VolumeStreamState vss = mStreamStates[streamType];
                        for (int i = 0; i < vss.mIndexMap.size(); i++) {
                            int device = vss.mIndexMap.keyAt(i);
                            int value = vss.mIndexMap.valueAt(i);
                            if (value == 0) {
                                vss.setIndex(10, device, TAG, true /*hasModifyAudioSettings*/);
                            }
                        }
                        // Persist volume for stream ring when it is changed here
                      final int device = getDeviceForStream(streamType);
                      sendMsg(mAudioHandler,
                              MSG_PERSIST_VOLUME,
                              SENDMSG_QUEUE,
                              device,
                              0,
                              mStreamStates[streamType],
                              PERSIST_DELAY);
                    }
                }
                sRingerAndZenModeMutedStreams &= ~(1 << streamType);
                sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent(
                        sRingerAndZenModeMutedStreams, "muteRingerModeStreams"));
                mStreamStates[streamType].mute(false, "muteRingerModeStreams");
            } else {
                // mute
                sRingerAndZenModeMutedStreams |= (1 << streamType);
                sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent(
                        sRingerAndZenModeMutedStreams, "muteRingerModeStreams"));
                mStreamStates[streamType].mute(true, "muteRingerModeStreams");
            }
        }
    }

音量类型处理的可以直接忽略,看MSG_SET_FORCE_USE就可以:

 case MSG_SET_FORCE_USE:
                {
                    final String eventSource = (String) msg.obj;
                    final int useCase = msg.arg1;
                    final int config = msg.arg2;
                    if (useCase == AudioSystem.FOR_MEDIA) {
                        Log.wtf(TAG, "Invalid force use FOR_MEDIA in AudioService from "
                                + eventSource);
                        break;
                    }
                    new MediaMetrics.Item(MediaMetrics.Name.AUDIO_FORCE_USE
                            + MediaMetrics.SEPARATOR + AudioSystem.forceUseUsageToString(useCase))
                            .set(MediaMetrics.Property.EVENT, "setForceUse")
                            .set(MediaMetrics.Property.FORCE_USE_DUE_TO, eventSource)
                            .set(MediaMetrics.Property.FORCE_USE_MODE,
                                    AudioSystem.forceUseConfigToString(config))
                            .record();
                    sForceUseLogger.enqueue(
                            new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource));
                    mAudioSystem.setForceUse(useCase, config);
                }
                    break;

就是这儿的mAudioSystem.setForceUse(useCase, config)了,现在对应的参数可以在dumpsys中看到:

11-02 20:46:58:546 setCommunicationRouteForClient for uid: 10376 device: AudioDeviceAttributes: role:output type:speaker addr: name: profiles:[] descriptors:[] isPrivileged: false from API: setSpeakerphoneOn(true) from u/pid:10376/7775
11-02 20:46:58:546 updateCommunicationRoute, preferredCommunicationDevice: AudioDeviceAttributes: role:output type:speaker addr: name: profiles:[] descriptors:[] eventSource: setSpeakerphoneOn(true) from u/pid:10376/7775

11-02 20:46:59:413 setForceUse(FOR_VIBRATE_RINGING, FORCE_NONE) due to muteRingerModeStreams() from u/pid:1000/1788
11-02 20:46:59:484 setForceUse(FOR_VIBRATE_RINGING, FORCE_NONE) due to muteRingerModeStreams() from u/pid:1000/1788
11-02 20:46:59:503 setForceUse(FOR_VIBRATE_RINGING, FORCE_NONE) due to muteRingerModeStreams() from u/pid:1000/1788

差不多都可以和log对上,接下来就可以简单看下native了:

Status AudioPolicyService::setForceUse(AudioPolicyForceUse usageAidl,
                                       AudioPolicyForcedConfig configAidl)
{
    audio_policy_force_use_t usage = VALUE_OR_RETURN_BINDER_STATUS(
            aidl2legacy_AudioPolicyForceUse_audio_policy_force_use_t(usageAidl));
    audio_policy_forced_cfg_t config = VALUE_OR_RETURN_BINDER_STATUS(
            aidl2legacy_AudioPolicyForcedConfig_audio_policy_forced_cfg_t(configAidl));

    if (mAudioPolicyManager == NULL) {
        return binderStatusFromStatusT(NO_INIT);
    }

    if (!modifyAudioRoutingAllowed()) {
        return binderStatusFromStatusT(PERMISSION_DENIED);
    }

    if (usage < 0 || usage >= AUDIO_POLICY_FORCE_USE_CNT) {
        return binderStatusFromStatusT(BAD_VALUE);
    }
    if (config < 0 || config >= AUDIO_POLICY_FORCE_CFG_CNT) {
        return binderStatusFromStatusT(BAD_VALUE);
    }
    ALOGV("setForceUse()");
    audio_utils::lock_guard _l(mMutex);
    AutoCallerClear acc;
    mAudioPolicyManager->setForceUse(usage, config);
    onCheckSpatializer_l();
    return Status::ok();
}

这儿信息比较有限,其实再往下看也比较有限:

void AudioPolicyManager::setForceUse(audio_policy_force_use_t usage,
                                     audio_policy_forced_cfg_t config)
{
    ALOGV("setForceUse() usage %d, config %d, mPhoneState %d", usage, config, mEngine->getPhoneState());
    if (config == mEngine->getForceUse(usage)) {
        return;
    }

    if (mEngine->setForceUse(usage, config) != NO_ERROR) {
        ALOGW("setForceUse() could not set force cfg %d for usage %d", config, usage);
        return;
    }
    bool forceVolumeReeval = (usage == AUDIO_POLICY_FORCE_FOR_COMMUNICATION) ||
            (usage == AUDIO_POLICY_FORCE_FOR_DOCK) ||
            (usage == AUDIO_POLICY_FORCE_FOR_SYSTEM);

    // check for device and output changes triggered by new force usage
    checkForDeviceAndOutputChanges();

    // force client reconnection to reevaluate flag AUDIO_FLAG_AUDIBILITY_ENFORCED
    if (usage == AUDIO_POLICY_FORCE_FOR_SYSTEM) {
        invalidateStreams({AUDIO_STREAM_SYSTEM, AUDIO_STREAM_ENFORCED_AUDIBLE});
    }

    //FIXME: workaround for truncated touch sounds
    // to be removed when the problem is handled by system UI
    uint32_t delayMs = 0;
    if (usage == AUDIO_POLICY_FORCE_FOR_COMMUNICATION) {
        delayMs = TOUCH_SOUND_FIXED_DELAY_MS;
    }

    updateCallAndOutputRouting(forceVolumeReeval, delayMs);
    updateInputRouting();
}

这儿就是按照策略选择路由,具体这儿就先不看了。

在设置音量类型的时候也会刷新路由,这块逻辑也可以在代码中串起来:

 /** @see AudioManager#setMode(int) */
    public void setMode(int mode, IBinder cb, String callingPackage) {
        int pid = Binder.getCallingPid();
        int uid = Binder.getCallingUid();
        if (DEBUG_MODE) {
            Log.v(TAG, "setMode(mode=" + mode + ", pid=" + pid
                    + ", uid=" + uid + ", caller=" + callingPackage + ")");
        }
        if (!checkAudioSettingsPermission("setMode()")) {
            return;
        }
        if (cb == null) {
            Log.e(TAG, "setMode() called with null binder");
            return;
        }
        if (mode < AudioSystem.MODE_CURRENT || mode >= AudioSystem.NUM_MODES) {
            Log.w(TAG, "setMode() invalid mode: " + mode);
            return;
        }

        if (mode == AudioSystem.MODE_CURRENT) {
            mode = getMode();
        }

        if (mode == AudioSystem.MODE_CALL_SCREENING && !mIsCallScreeningModeSupported) {
            Log.w(TAG, "setMode(MODE_CALL_SCREENING) not permitted "
                    + "when call screening is not supported");
            return;
        }

        final boolean hasModifyPhoneStatePermission = mContext.checkCallingOrSelfPermission(
                MODIFY_PHONE_STATE)
                == PackageManager.PERMISSION_GRANTED;
        if ((mode == AudioSystem.MODE_IN_CALL
                || mode == AudioSystem.MODE_CALL_REDIRECT
                || mode == AudioSystem.MODE_COMMUNICATION_REDIRECT)
                && !hasModifyPhoneStatePermission) {
            Log.w(TAG, "MODIFY_PHONE_STATE Permission Denial: setMode("
                    + AudioSystem.modeToString(mode) + ") from pid=" + pid
                    + ", uid=" + Binder.getCallingUid());
            return;
        }

        SetModeDeathHandler currentModeHandler = null;
        synchronized (mDeviceBroker.mSetModeLock) {
            for (SetModeDeathHandler h : mSetModeDeathHandlers) {
                if (h.getPid() == pid) {
                    currentModeHandler = h;
                    break;
                }
            }

            if (mode == AudioSystem.MODE_NORMAL) {
                if (currentModeHandler != null) {
                    if (!currentModeHandler.isPrivileged()
                            && currentModeHandler.getMode() == AudioSystem.MODE_IN_COMMUNICATION) {
                        mAudioHandler.removeEqualMessages(
                                MSG_CHECK_MODE_FOR_UID, currentModeHandler);
                    }
                    mSetModeDeathHandlers.remove(currentModeHandler);
                    if (DEBUG_MODE) {
                        Log.v(TAG, "setMode(" + mode + ") removing hldr for pid: " + pid);
                    }
                    try {
                        currentModeHandler.getBinder().unlinkToDeath(currentModeHandler, 0);
                    } catch (NoSuchElementException e) {
                        Log.w(TAG, "setMode link does not exist ...");
                    }
                }
            } else {
                if (currentModeHandler != null) {
                    currentModeHandler.setMode(mode);
                    if (DEBUG_MODE) {
                        Log.v(TAG, "setMode(" + mode + ") updating hldr for pid: " + pid);
                    }
                } else {
                    currentModeHandler = new SetModeDeathHandler(cb, pid, uid,
                            hasModifyPhoneStatePermission, callingPackage, mode);
                    // Register for client death notification
                    try {
                        cb.linkToDeath(currentModeHandler, 0);
                    } catch (RemoteException e) {
                        // Client has died!
                        Log.w(TAG, "setMode() could not link to " + cb + " binder death");
                        return;
                    }
                    mSetModeDeathHandlers.add(currentModeHandler);
                    if (DEBUG_MODE) {
                        Log.v(TAG, "setMode(" + mode + ") adding handler for pid=" + pid);
                    }
                }
                if (mode == AudioSystem.MODE_IN_COMMUNICATION) {
                    // Force active state when entering/updating the stack to avoid glitches when
                    // an app starts playing/recording after settng the audio mode,
                    // and send a reminder to check activity after a grace period.
                    if (!currentModeHandler.isPrivileged()) {
                        currentModeHandler.setPlaybackActive(true);
                        currentModeHandler.setRecordingActive(true);
                        sendMsg(mAudioHandler,
                                MSG_CHECK_MODE_FOR_UID,
                                SENDMSG_QUEUE,
                                0,
                                0,
                                currentModeHandler,
                                CHECK_MODE_FOR_UID_PERIOD_MS);
                    }
                }
            }

            sendMsg(mAudioHandler,
                    MSG_UPDATE_AUDIO_MODE,
                    SENDMSG_REPLACE,
                    mode,
                    pid,
                    callingPackage,
                    0);
        }
    }

这儿只需要关注2个逻辑:

  1. 设置通话音量会在audioserver中记录,而设置媒体音量会清除记录
  2. 设置通话音量需要有存活的采播,如果没有,那么设置的音量类型会在阈值时间过后被修改为媒体音量
    接下来就是handler里的执行了:
 @GuardedBy("mDeviceBroker.mSetModeLock")
    void onUpdateAudioMode(int requestedMode, int requesterPid, String requesterPackage,
                           boolean force) {
        if (requestedMode == AudioSystem.MODE_CURRENT) {
            requestedMode = getMode();
        }
        int mode = AudioSystem.MODE_NORMAL;
        int uid = 0;
        int pid = 0;
        SetModeDeathHandler currentModeHandler = getAudioModeOwnerHandler();
        if (currentModeHandler != null) {
            mode = currentModeHandler.getMode();
            uid = currentModeHandler.getUid();
            pid = currentModeHandler.getPid();
        }
        if (DEBUG_MODE) {
            Log.v(TAG, "onUpdateAudioMode() new mode: " + mode + ", current mode: "
                    + mMode.get() + " requested mode: " + requestedMode);
        }
        if (mode != mMode.get() || force) {
            int status = AudioSystem.SUCCESS;
            final long identity = Binder.clearCallingIdentity();
            try {
                status = mAudioSystem.setPhoneState(mode, uid);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
            if (status == AudioSystem.AUDIO_STATUS_OK) {
                if (DEBUG_MODE) {
                    Log.v(TAG, "onUpdateAudioMode: mode successfully set to " + mode);
                }
                sendMsg(mAudioHandler, MSG_DISPATCH_AUDIO_MODE, SENDMSG_REPLACE, mode, 0,
                        /*obj*/ null, /*delay*/ 0);
                int previousMode = mMode.getAndSet(mode);
                // Note: newModeOwnerPid is always 0 when actualMode is MODE_NORMAL
                mModeLogger.enqueue(new PhoneStateEvent(requesterPackage, requesterPid,
                        requestedMode, pid, mode));

                final int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE);
                final int device = getDeviceForStream(streamType);
                final int streamAlias = mStreamVolumeAlias[streamType];

                if (DEBUG_MODE) {
                    Log.v(TAG, "onUpdateAudioMode: streamType=" + streamType
                            + ", streamAlias=" + streamAlias);
                }

                final int index = mStreamStates[streamAlias].getIndex(device);
                final int maxIndex = mStreamStates[streamAlias].getMaxIndex();
                setStreamVolumeInt(streamAlias, index, device, true,
                        requesterPackage, true /*hasModifyAudioSettings*/);

                updateStreamVolumeAlias(true /*updateVolumes*/, requesterPackage);

                // change of mode may require volume to be re-applied on some devices
                updateAbsVolumeMultiModeDevices(previousMode, mode);

                setLeAudioVolumeOnModeUpdate(mode, device, streamAlias, index, maxIndex);

                synchronized (mCachedAbsVolDrivingStreamsLock) {
                    mCachedAbsVolDrivingStreams.replaceAll((absDev, stream) -> {
                        int streamToDriveAbs = getBluetoothContextualVolumeStream();
                        if (stream != streamToDriveAbs) {
                            mAudioSystem.setDeviceAbsoluteVolumeEnabled(absDev, /*address=*/
                                    "", /*enabled*/true, streamToDriveAbs);
                        }
                        return streamToDriveAbs;
                    });
                }

                // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                // connections not started by the application changing the mode when pid changes
                mDeviceBroker.postSetModeOwner(mode, pid, uid);
            } else {
                Log.w(TAG, "onUpdateAudioMode: failed to set audio mode to: " + mode);
            }
        }
    }

这儿只需要关注几点:

  1. 通话音量优先级比媒体音量高
  2. 音量类型变更后,就会刷新硬件音量
  3. 最后会将音量类型通知给DeviceBroker

而我们关心的就是最后一个,看看DeviceBroker收到该事件后做了哪些操作,我们预期的是会刷新路由设备:

                case MSG_I_SET_MODE_OWNER:
                    synchronized (mSetModeLock) {
                        synchronized (mDeviceStateLock) {
                            boolean wasBtScoRequested = isBluetoothScoRequested();
                            mAudioModeOwner = (AudioModeInfo) msg.obj;
                            if (mAudioModeOwner.mMode != AudioSystem.MODE_RINGTONE) {
                                onUpdateCommunicationRouteClient(
                                        wasBtScoRequested, "setNewModeOwner");
                            }
                        }
                    }
                    break;

这儿会调用onUpdateCommunicationRouteClient:

    /**
     * Select new communication device from communication route client at the top of the stack
     * and restore communication route including restarting SCO audio if needed.
     */
    // @GuardedBy("mSetModeLock")
    @GuardedBy("mDeviceStateLock")
    private void onUpdateCommunicationRouteClient(boolean wasBtScoRequested, String eventSource) {
        CommunicationRouteClient crc = topCommunicationRouteClient();
        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " + crc
                    + " wasBtScoRequested: " + wasBtScoRequested + " eventSource: " + eventSource);
        }
        if (crc != null) {
            setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(),
                    BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource);
        } else {
            if (!mScoManagedByAudio && !isBluetoothScoRequested() && wasBtScoRequested) {
                mBtHelper.stopBluetoothSco(eventSource);
            }
            updateCommunicationRoute(eventSource);
        }
    }

这儿就是熟悉的逻辑了,选择一个优先的设备作为路由设备。

这下关于设置外放和听筒的基础逻辑都介绍完了,接下来我们总结下信息:

  1. isSpeakerphoneOn 是无法用来判断是外放还是听筒的
  2. 系统会记录isSpeakerphoneOn是true时候的请求信息,而为false就会移除请求信息,接下来就是由系统自己决策路由
  3. 设置音量类型也会刷新音频路由
  4. media.audio_policy 中包含了当前的路由信息
  5. 通话音量默认路由是听筒,媒体音量默认路由是外放

接下来直接在媒体音量下设置isSpeakerphoneOn false,看看能否切换到听筒, 按照我们已经储备的信息,结论是不可以的,因为在更新路由时会发现之前没调用过 true,所以没有client信息,会直接返回。我们验证下:
先看metrics信息,是有调用setSpeakerphoneOn false

 1971: {audio.device.setSpeakerphoneOn, (11-03 17:52:06.777), (10376, 22449, 10376), (state=off)}
 1972: {audio.device.setSpeakerphoneOn, (11-03 17:52:07.780), (10376, 22449, 10376), (state=off)}
 1973: {audio.device.setSpeakerphoneOn, (11-03 17:52:08.784), (10376, 22449, 10376), (state=off)}
 1974: {audio.device.setSpeakerphoneOn, (11-03 17:52:09.787), (10376, 22449, 10376), (state=off)}
 1975: {audio.device.setSpeakerphoneOn, (11-03 17:52:10.789), (10376, 22449, 10376), (state=off)}
 1976: {audio.device.setSpeakerphoneOn, (11-03 17:52:11.792), (10376, 22449, 10376), (state=off)}
 1977: {audio.device.setSpeakerphoneOn, (11-03 17:52:12.795), (10376, 22449, 10376), (state=off)}
 1978: {audio.device.setSpeakerphoneOn, (11-03 17:52:13.798), (10376, 22449, 10376), (state=off)}
 1979: {audio.device.setSpeakerphoneOn, (11-03 17:52:14.802), (10376, 22449, 10376), (state=off)}
 1980: {audio.device.setSpeakerphoneOn, (11-03 17:52:15.805), (10376, 22449, 10376), (state=off)}
 1981: {audio.device.setSpeakerphoneOn, (11-03 17:52:16.807), (10376, 22449, 10376), (state=off)}
 1982: {audio.device.setSpeakerphoneOn, (11-03 17:52:17.810), (10376, 22449, 10376), (state=off)}

再看audio信息:

Computed Preferred communication device: null

Applied Preferred communication device: null

11-03 17:41:18:485 updateCommunicationRoute, preferredCommunicationDevice: null eventSource: setNewModeOwner
11-03 17:44:48:352 updateCommunicationRoute, preferredCommunicationDevice: null eventSource: setNewModeOwner
11-03 17:48:08:757 updateCommunicationRoute, preferredCommunicationDevice: null eventSource: setNewModeOwner

对应时间点就压根没走到updateCommunicationRoute流程中, 而且对应的优先设备也是null,这个也符合我们预期, 因为client是空,发现client为空时候就直接返回了。

再看下policy:

  -STRATEGY_MEDIA (id: 19)
      Selected Device: {AUDIO_DEVICE_OUT_SPEAKER, @:}
       Group: 20 stream: AUDIO_STREAM_ASSISTANT
        Attributes: { Content type: AUDIO_CONTENT_TYPE_SPEECH Usage: AUDIO_USAGE_ASSISTANT Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_MEDIA Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_GAME Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_ASSISTANT Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Any }
       Group: 11 stream: AUDIO_STREAM_SYSTEM
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_ASSISTANCE_SONIFICATION Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }

的确是外放,因此准确的路由信息可以看policy, audio中不包含准确的信息。

那接下来可能会想,如果先调用isSpeakerphoneOn true,再调用false,是否可以切过去?毕竟这样client肯定不是空了, 验证结果还是外放,这样也符合预期,虽然false可以设置下去了,可是由于把client移除了,实际上就还是让系统在媒体音量下自己决策,那走的就还是外放了:

11-03 18:05:34:581 setCommunicationRouteForClient for uid: 10376 device: AudioDeviceAttributes: role:output type:speaker addr: name: profiles:[] descriptors:[] isPrivileged: false from API: setSpeakerphoneOn(true) from u/pid:10376/22943
11-03 18:05:34:582 updateCommunicationRoute, preferredCommunicationDevice: AudioDeviceAttributes: role:output type:speaker addr: name: profiles:[] descriptors:[] eventSource: setSpeakerphoneOn(true) from u/pid:10376/22943
11-03 18:05:34:833 setCommunicationRouteForClient for uid: 10376 device: null isPrivileged: false from API: setSpeakerphoneOn(false) from u/pid:10376/22943
11-03 18:05:34:834 updateCommunicationRoute, preferredCommunicationDevice: null eventSource: setSpeakerphoneOn(false) from u/pid:10376/22943

从log上看,流程和预期基本一样。

那媒体音量真的就不能走听筒么?从API调用上看似乎的确是,如果仅仅是这个结论的话,那似乎完全不需要这么大一个分析流程,几句话就可以带过了,实际上还有个信息我们忽略了,那就是policy,policy负责为播放决策路由,那决策路由的策略依据是什么呢?显然不是音量类型,如果是的话,那Android audiopolicyservice中那么多复杂逻辑就完全不需要了,那是什么呢?答案就是stream type,Android是按照stream type来决策的,这样仔细一想就会发现很合理,因为路由最终是为播放服务的,从播放器角度做决策显然是最合理的,接下来我们看下Android的策略:

Policy Engine dump:
  Product Strategies dump:
    -STRATEGY_PHONE (id: 14)
      Selected Device: {AUDIO_DEVICE_OUT_EARPIECE, @:}
       Group: 13 stream: AUDIO_STREAM_VOICE_CALL
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_VOICE_COMMUNICATION Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 3 stream: AUDIO_STREAM_BLUETOOTH_SCO
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_UNKNOWN Source: AUDIO_SOURCE_INVALID Flags: 0x4 Tags:  }

    -STRATEGY_SONIFICATION (id: 15)
      Selected Device: {AUDIO_DEVICE_OUT_EARPIECE, @:}
       Group: 10 stream: AUDIO_STREAM_RING
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 2 stream: AUDIO_STREAM_ALARM
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_ALARM Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }

    -STRATEGY_ENFORCED_AUDIBLE (id: 16)
      Selected Device: {AUDIO_DEVICE_OUT_SPEAKER, @:}
       Group: 5 stream: AUDIO_STREAM_ENFORCED_AUDIBLE
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_UNKNOWN Source: AUDIO_SOURCE_INVALID Flags: 0x1 Tags:  }

    -STRATEGY_ACCESSIBILITY (id: 17)
      Selected Device: {AUDIO_DEVICE_OUT_SPEAKER, @:}
       Group: 1 stream: AUDIO_STREAM_ACCESSIBILITY
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_ASSISTANCE_ACCESSIBILITY Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }

    -STRATEGY_SONIFICATION_RESPECTFUL (id: 18)
      Selected Device: {AUDIO_DEVICE_OUT_EARPIECE, @:}
       Group: 7 stream: AUDIO_STREAM_NOTIFICATION
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_NOTIFICATION Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 7 stream: AUDIO_STREAM_NOTIFICATION
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_NOTIFICATION_EVENT Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }

    -STRATEGY_MEDIA (id: 19)
      Selected Device: {AUDIO_DEVICE_OUT_SPEAKER, @:}
       Group: 20 stream: AUDIO_STREAM_ASSISTANT
        Attributes: { Content type: AUDIO_CONTENT_TYPE_SPEECH Usage: AUDIO_USAGE_ASSISTANT Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_MEDIA Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_GAME Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_ASSISTANT Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 6 stream: AUDIO_STREAM_MUSIC
        Attributes: { Any }
       Group: 11 stream: AUDIO_STREAM_SYSTEM
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_ASSISTANCE_SONIFICATION Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }

    -STRATEGY_DTMF (id: 21)
      Selected Device: {AUDIO_DEVICE_OUT_SPEAKER, @:}
       Group: 4 stream: AUDIO_STREAM_DTMF
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }

    -STRATEGY_CALL_ASSISTANT (id: 22)
      Selected Device: {AUDIO_DEVICE_OUT_TELEPHONY_TX, @:}
       Group: 23 stream: AUDIO_STREAM_CALL_ASSISTANT
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_CALL_ASSISTANT Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }

    -STRATEGY_TRANSMITTED_THROUGH_SPEAKER (id: 24)
      Selected Device: {AUDIO_DEVICE_OUT_SPEAKER, @:}
       Group: 12 stream: AUDIO_STREAM_TTS
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_UNKNOWN Source: AUDIO_SOURCE_INVALID Flags: 0x8 Tags:  }
       Group: 12 stream: AUDIO_STREAM_TTS
        Attributes: { Content type: AUDIO_CONTENT_TYPE_ULTRASOUND Usage: AUDIO_USAGE_UNKNOWN Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }

    -STRATEGY_REROUTING (id: 25)
      Selected Device: {AUDIO_DEVICE_OUT_SPEAKER, @:}
       Group: 9 stream: AUDIO_STREAM_REROUTING
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_VIRTUAL_SOURCE Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags: reserved_internal_strategy }

    -STRATEGY_PATCH (id: 26)
      Selected Device: {AUDIO_DEVICE_OUT_SPEAKER, @:}
       Group: 8 stream: AUDIO_STREAM_PATCH
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_UNKNOWN Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags: reserved_internal_strategy }

可以看到,如果选AUDIO_STREAM_VOICE_CALL的stream type,那策略就会选择听筒。接下来验证下,发现的确媒体音量走了听筒。

Audio mode: 
- Requested mode = MODE_NORMAL
- Actual mode = MODE_NORMAL
- Mode owner: 
   None
- Mode owner stack: 
   Empty

  Computed Preferred communication device: null

  Applied Preferred communication device: null
  Active communication device: AudioDeviceAttributes: role:output type:earpiece addr: name:Pixel 4 profiles:[] descriptors:[]
  mCommunicationStrategyId: 14
  mAccessibilityStrategyId: 17

这儿的mCommunicationStrategyId: 14对应policy中就是听筒。

  -STRATEGY_PHONE (id: 14)
      Selected Device: {AUDIO_DEVICE_OUT_EARPIECE, @:}
       Group: 13 stream: AUDIO_STREAM_VOICE_CALL
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_VOICE_COMMUNICATION Source: AUDIO_SOURCE_INVALID Flags: 0x0 Tags:  }
       Group: 3 stream: AUDIO_STREAM_BLUETOOTH_SCO
        Attributes: { Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_UNKNOWN Source: AUDIO_SOURCE_INVALID Flags: 0x4 Tags:  }

现在就可以对Android的路由再做一个总结了:

  1. android 音频路由的选择依据不是音量类型,而是播放的stream type
  2. Android媒体音量也支持听筒,只需要修改stream type就可以做到
  3. 普通媒体音量下切不到听筒,是因为媒体音量下的stream type 默认就是外放
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容