Android下媒體音量也支持聽(tīng)筒?

背景介紹

最近遇到了一個(gè)case飒房,發(fā)現(xiàn)Android上媒體音量下也可以支持聽(tīng)筒播放,這個(gè)行為比較奇怪媚值,因此專(zhuān)門(mén)在源碼中分析下狠毯。

分析流程

在系統(tǒng)中有這樣一個(gè)接口isSpeakerphoneOn,有時(shí)候會(huì)用來(lái)判斷是否是外放褥芒,可是這個(gè)接口真的可以判斷么嚼松?

我們可以直接在啟動(dòng)應(yīng)用的時(shí)候讀取下這個(gè)接口的值,媒體音量下默認(rèn)是外放锰扶,理論上就應(yīng)該是true献酗,那真的是么?
搞一個(gè)demo坷牛,打印下罕偎,結(jié)果如下:

speakerphone on is false

此時(shí)應(yīng)用的確是外放。這是系統(tǒng)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);
    }

從注釋上看颜及,表示優(yōu)先選擇的路由是否是speakerphone,這兒并沒(méi)有說(shuō)是當(dāng)前的路由是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中看是否優(yōu)先選擇的路由是TYPE_BUILTIN_SPEAKER器予。
從之前的信息上看浪藻,就是這兒返回的應(yīng)該不是TYPE_BUILTIN_SPEAKER捐迫,否則就應(yīng)該是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;
    }

雖然這兒想表達(dá)Sco連接的設(shè)備優(yōu)先級(jí)比較高爱葵,不過(guò)現(xiàn)在不涉及sco施戴,只關(guān)心外放,聽(tīng)筒就好了萌丈。所以邏輯還在requestedCommunicationDevice中, 其實(shí)這兒可以看到一些端倪赞哗,從邏輯上看,應(yīng)該是查詢(xún)應(yīng)用請(qǐng)求的路由辆雾,如果應(yīng)用沒(méi)有請(qǐng)求肪笋,那就應(yīng)該是空,而媒體音量下應(yīng)用一般不會(huì)請(qǐng)求路由度迂,因此這時(shí)候查詢(xún)的設(shè)備就是空藤乙,按照上面的邏輯,那就是找不到speakerphone設(shè)備惭墓,所以返回false坛梁,不過(guò)這個(gè)僅僅是我們的猜想,如果要求證還需要繼續(xù)往下看:

    /**
     * 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;
    }

這兒會(huì)按照2個(gè)策略來(lái)選擇設(shè)備:

  1. 優(yōu)先讓當(dāng)前mode owner請(qǐng)求的設(shè)備腊凶,因?yàn)槭敲襟w音量划咐,不涉及mode owner拴念,因此這個(gè)我們不滿足
  2. 在看中請(qǐng)求設(shè)備列表中的第一個(gè)設(shè)備,因?yàn)槲覀儧](méi)請(qǐng)求過(guò),那就應(yīng)該是空碗啄。注意這兒還判斷了Active蚤认,內(nèi)部就是看有沒(méi)有存活的采集或播放,也就是如果沒(méi)有存活的采集或播放缔俄,請(qǐng)求路由也會(huì)不生效,這個(gè)也是合理的器躏,既然都沒(méi)有采播俐载,空占著路由反而會(huì)影響其他有采播的應(yīng)用。這個(gè)就當(dāng)額外的一個(gè)小知識(shí)了登失。

目前看起來(lái)基本符合我們的猜想遏佣。那如何實(shí)錘呢? 我們可以dump下這個(gè)信息:

  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:[]

第一個(gè)打印的就是mCommunicationRouteClients, 這個(gè)是有一個(gè)成員揽浙,不過(guò)對(duì)應(yīng)的uid并不是demo的状婶,而且采播狀態(tài)也是false,從前面的信息我們知道如果一個(gè)應(yīng)用沒(méi)有存活的采播馅巷,那么他請(qǐng)求的route是不會(huì)生效的膛虫。
第二個(gè)就是打印的preferredCommunicationDevice,果然是null钓猬,因?yàn)槭莕ull稍刀,所以返回的是false。

不過(guò)可以看到最后一行信息有route打印敞曹,告訴我們當(dāng)前的route是earpiece账月,這個(gè)顯然不是真實(shí)的,那這個(gè)信息是什么呢澳迫?

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

是從native audiopolicymanager報(bào)上來(lái)的設(shè)備局齿,這個(gè)是默認(rèn)的device,并不是當(dāng)前使用的device橄登。

接下來(lái)我們看下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));
    }

對(duì)應(yīng)這兒的on就是false抓歼,也就是關(guān)閉speaker,接下來(lái)會(huì)到如下邏輯中:

    /**
     * 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);
    }

如果是請(qǐng)求關(guān)閉拢锹,那么就需要獲取以前請(qǐng)求的設(shè)備谣妻,如果是空或者設(shè)備不匹配,那么就直接return了面褐。
從這兒我們可以看出拌禾,如果是要使用聽(tīng)筒,那么需要先請(qǐng)求過(guò)外放展哭,如果沒(méi)有請(qǐng)求過(guò)湃窍,那么是走不了聽(tīng)筒的闻蛀。
再重復(fù)一下,如果要使用聽(tīng)筒您市,就需要先請(qǐng)求外放觉痛。
接下來(lái)繼續(xù)看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);
    }

這兒看著邏輯多,其實(shí)大部分是和sco相關(guān)的茵休,我們只關(guān)心和外放薪棒,聽(tīng)筒相關(guān)的就好¢泡海可以看到如下信息:

  1. 如果是聽(tīng)筒俐芯,那么會(huì)先移除之前請(qǐng)求的client,如果之前也沒(méi)請(qǐng)求過(guò)钉鸯,那就直接返回了
  2. 如果是外放吧史,那么會(huì)添加到mCommunicationRouteClients中。

接下來(lái)就是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中拿到優(yōu)先級(jí)最高的路由設(shè)備唠雕,前面已經(jīng)看過(guò)內(nèi)部的邏輯了贸营,優(yōu)先選擇modeOwner請(qǐng)求的設(shè)備,接下來(lái)是請(qǐng)求隊(duì)列中首個(gè)請(qǐng)求對(duì)應(yīng)的設(shè)備岩睁,不過(guò)前提是需要有存活的采播钞脂。

從我們關(guān)心的外放和聽(tīng)筒邏輯上看,如果是外放捕儒,那么這兒獲取到的優(yōu)先設(shè)備就應(yīng)該是外放冰啃,而如果是聽(tīng)筒,那么就是null肋层。
帶著這個(gè)信息我們繼續(xù)看下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();
    }

這兒會(huì)比較是否外放狀態(tài)發(fā)生變化亿笤,如果是,那么就會(huì)對(duì)外發(fā)送廣播栋猖。這兒只關(guān)注下postUpdateRingerModeServiceInt,看這個(gè)方法的原因是我們需要找到是音頻路由在哪兒設(shè)置到系統(tǒng)中生效汪榔。

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

中間就是handler處理蒲拉,我們直接看最終的實(shí)現(xiàn):

    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內(nèi)部就會(huì)觸發(fā)設(shè)置路由到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");
            }
        }
    }

音量類(lèi)型處理的可以直接忽略,看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)了痴腌,現(xiàn)在對(duì)應(yīng)的參數(shù)可以在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對(duì)上雌团,接下來(lái)就可以簡(jiǎn)單看下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();
}

這兒信息比較有限,其實(shí)再往下看也比較有限:

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();
}

這兒就是按照策略選擇路由士聪,具體這兒就先不看了锦援。

在設(shè)置音量類(lèi)型的時(shí)候也會(huì)刷新路由,這塊邏輯也可以在代碼中串起來(lái):

 /** @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);
        }
    }

這兒只需要關(guān)注2個(gè)邏輯:

  1. 設(shè)置通話音量會(huì)在audioserver中記錄剥悟,而設(shè)置媒體音量會(huì)清除記錄
  2. 設(shè)置通話音量需要有存活的采播灵寺,如果沒(méi)有曼库,那么設(shè)置的音量類(lèi)型會(huì)在閾值時(shí)間過(guò)后被修改為媒體音量
    接下來(lái)就是handler里的執(zhí)行了:
 @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);
            }
        }
    }

這兒只需要關(guān)注幾點(diǎn):

  1. 通話音量?jī)?yōu)先級(jí)比媒體音量高
  2. 音量類(lèi)型變更后,就會(huì)刷新硬件音量
  3. 最后會(huì)將音量類(lèi)型通知給DeviceBroker

而我們關(guān)心的就是最后一個(gè)略板,看看DeviceBroker收到該事件后做了哪些操作毁枯,我們預(yù)期的是會(huì)刷新路由設(shè)備:

                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;

這兒會(huì)調(diào)用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);
        }
    }

這兒就是熟悉的邏輯了,選擇一個(gè)優(yōu)先的設(shè)備作為路由設(shè)備叮称。

這下關(guān)于設(shè)置外放和聽(tīng)筒的基礎(chǔ)邏輯都介紹完了种玛,接下來(lái)我們總結(jié)下信息:

  1. isSpeakerphoneOn 是無(wú)法用來(lái)判斷是外放還是聽(tīng)筒的
  2. 系統(tǒng)會(huì)記錄isSpeakerphoneOn是true時(shí)候的請(qǐng)求信息,而為false就會(huì)移除請(qǐng)求信息瓤檐,接下來(lái)就是由系統(tǒng)自己決策路由
  3. 設(shè)置音量類(lèi)型也會(huì)刷新音頻路由
  4. media.audio_policy 中包含了當(dāng)前的路由信息
  5. 通話音量默認(rèn)路由是聽(tīng)筒赂韵,媒體音量默認(rèn)路由是外放

接下來(lái)直接在媒體音量下設(shè)置isSpeakerphoneOn false,看看能否切換到聽(tīng)筒挠蛉, 按照我們已經(jīng)儲(chǔ)備的信息右锨,結(jié)論是不可以的,因?yàn)樵诟侣酚蓵r(shí)會(huì)發(fā)現(xiàn)之前沒(méi)調(diào)用過(guò) true碌秸,所以沒(méi)有client信息绍移,會(huì)直接返回。我們驗(yàn)證下:
先看metrics信息讥电,是有調(diào)用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

對(duì)應(yīng)時(shí)間點(diǎn)就壓根沒(méi)走到updateCommunicationRoute流程中蹂窖, 而且對(duì)應(yīng)的優(yōu)先設(shè)備也是null,這個(gè)也符合我們預(yù)期恩敌, 因?yàn)閏lient是空瞬测,發(fā)現(xiàn)client為空時(shí)候就直接返回了。

再看下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:  }

的確是外放纠炮,因此準(zhǔn)確的路由信息可以看policy月趟, audio中不包含準(zhǔn)確的信息。

那接下來(lái)可能會(huì)想恢口,如果先調(diào)用isSpeakerphoneOn true孝宗,再調(diào)用false,是否可以切過(guò)去耕肩?畢竟這樣client肯定不是空了, 驗(yàn)證結(jié)果還是外放因妇,這樣也符合預(yù)期,雖然false可以設(shè)置下去了猿诸,可是由于把client移除了婚被,實(shí)際上就還是讓系統(tǒng)在媒體音量下自己決策,那走的就還是外放了:

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上看梳虽,流程和預(yù)期基本一樣址芯。

那媒體音量真的就不能走聽(tīng)筒么?從API調(diào)用上看似乎的確是,如果僅僅是這個(gè)結(jié)論的話谷炸,那似乎完全不需要這么大一個(gè)分析流程北专,幾句話就可以帶過(guò)了,實(shí)際上還有個(gè)信息我們忽略了淑廊,那就是policy逗余,policy負(fù)責(zé)為播放決策路由,那決策路由的策略依據(jù)是什么呢季惩?顯然不是音量類(lèi)型录粱,如果是的話,那Android audiopolicyservice中那么多復(fù)雜邏輯就完全不需要了画拾,那是什么呢啥繁?答案就是stream type,Android是按照stream type來(lái)決策的青抛,這樣仔細(xì)一想就會(huì)發(fā)現(xiàn)很合理旗闽,因?yàn)槁酚勺罱K是為播放服務(wù)的,從播放器角度做決策顯然是最合理的蜜另,接下來(lái)我們看下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,那策略就會(huì)選擇聽(tīng)筒举瑰。接下來(lái)驗(yàn)證下捣辆,發(fā)現(xiàn)的確媒體音量走了聽(tīng)筒。

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對(duì)應(yīng)policy中就是聽(tīng)筒此迅。

  -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:  }

現(xiàn)在就可以對(duì)Android的路由再做一個(gè)總結(jié)了:

  1. android 音頻路由的選擇依據(jù)不是音量類(lèi)型汽畴,而是播放的stream type
  2. Android媒體音量也支持聽(tīng)筒,只需要修改stream type就可以做到
  3. 普通媒體音量下切不到聽(tīng)筒耸序,是因?yàn)槊襟w音量下的stream type 默認(rèn)就是外放
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末忍些,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子坎怪,更是在濱河造成了極大的恐慌罢坝,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芋忿,死亡現(xiàn)場(chǎng)離奇詭異炸客,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)戈钢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)是尔,“玉大人殉了,你說(shuō)我怎么就攤上這事∧饷叮” “怎么了薪铜?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵众弓,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我隔箍,道長(zhǎng)谓娃,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任蜒滩,我火速辦了婚禮滨达,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘俯艰。我一直安慰自己捡遍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布竹握。 她就那樣靜靜地躺著画株,像睡著了一般。 火紅的嫁衣襯著肌膚如雪啦辐。 梳的紋絲不亂的頭發(fā)上谓传,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音芹关,去河邊找鬼续挟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛充边,可吹牛的內(nèi)容都是我干的庸推。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼浇冰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼贬媒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起肘习,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤际乘,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后漂佩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體脖含,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年投蝉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了养葵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瘩缆,死狀恐怖关拒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤着绊,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布谐算,位于F島的核電站,受9級(jí)特大地震影響归露,放射性物質(zhì)發(fā)生泄漏洲脂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一剧包、第九天 我趴在偏房一處隱蔽的房頂上張望恐锦。 院中可真熱鬧,春花似錦玄捕、人聲如沸踩蔚。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)馅闽。三九已至,卻和暖如春馍迄,著一層夾襖步出監(jiān)牢的瞬間福也,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工攀圈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留暴凑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓赘来,卻偏偏與公主長(zhǎng)得像现喳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子犬辰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容