안드로이드

[Android] 통화관련 블루투스 Audio (Audio Control)

코딩하는후운 2023. 1. 6. 18:46
반응형

통화관련 블루투스 Audio

제대로된 방법이 아닐 수 있습니다 !!


통화 관련 SDK를 이용하고 있고, 블루투스를 이용하여 Audio Control을 해야 하는 상황.

참고로 OS 12이상에서는 BLUETOOTH_CONNECT 권한 필요(연결된 디바이스 가져오려면)

1. 휴대폰의 블루투스가 Enable / Disabled 되어있는지 확인하기

bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
publishEnableBluetooth(bluetoothAdapter?.isEnabled == true)

publishEnable함수는 EventBus를 통해 뷰쪽으로 값 전달 하는 함수.

 

2. 브로드 캐스트 액션 등록 - 블루투스 STATE값 가져오기.

val intentFilter = IntentFilter()
intentFilter.addAction(ACTION_STATE_CHANGED)
intentFilter.addAction(ACTION_AUDIO_STATE_CHANGED)
intentFilter.addAction(ACTION_CONNECTION_STATE_CHANGED)

registerReceiver(
    bluetoothStateReceiver,
    intentFilter
)
private val bluetoothStateReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            ACTION_STATE_CHANGED ->
                when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, ERROR)) {
                    STATE_TURNING_OFF -> {
                        ExLog.d(TAG, "************* STATE_TURNING_OFF")
                        publishEnableBluetooth(false)
                    }
                    STATE_ON -> {
                        ExLog.d(TAG, "************* STATE_ON")
                        publishEnableBluetooth(true)
                    }
                }
            ACTION_AUDIO_STATE_CHANGED ->
                when (intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, ERROR)) {
                    STATE_AUDIO_CONNECTED -> {
                        ExLog.d(TAG, "************* STATE_AUDIO_CONNECTED")
                    }
                    STATE_AUDIO_DISCONNECTED -> {
                        ExLog.d(TAG, "************* STATE_AUDIO_DISCONNECTED")
                        if (currentAudioType == SelectButtonType.BLUETOOTH) {
                            publishAudioType(
                                if (isCurrentTypeAndRingPlay()) SelectButtonType.SPEAKER else SelectButtonType.PHONE
                            )
                        }
                    }
                }
            ACTION_CONNECTION_STATE_CHANGED -> {
                when (intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, ERROR)) {
                    STATE_CONNECTED -> {
                        isBluetoothConnected = true
                        ExLog.d(TAG, "************* BluetoothHeadset STATE_CONNECTED")
                        getProfileProxy()
                    }
                    STATE_DISCONNECTED -> {
                        isBluetoothConnected = false
                        ExLog.d(TAG, "************* BluetoothHeadset STATE_DISCONNECTED")
                        publishAudioType(
                            if (isCurrentTypeAndRingPlay()) SelectButtonType.SPEAKER else SelectButtonType.PHONE
                        )
                    }
                }
            }
        }
    }
}

isCurrentTypeAndRingPlay는
벨소리가 울리는 상황인지 & 현재 오디오타입이 스피커인지 판단해주는 함수.

ACTION_STATE_CHANGED : 휴대폰 블루투스 Enable/Disable변경시 호출된다.
ACTION_AUDIO_STATE_CHANGED : 블루투스의 오디오가 실제 연결/연결끊김 발생시 호출된다.
ACTION_CONNECTION_STATE_CHANGED : 블루투스가 실제 휴대폰에 연결/연결끊김 발생시 호출 (이미 연결되어진 상태라면 호출 안되는 듯!)

 

3. getProfileProxy
호출 할 때마다 listener이 불리며, 연결된 디바이스를 가져와서 블루투스 or 다른곳으로 송출할지 판단함

bluetoothAdapter?.getProfileProxy(this, profileListener, BluetoothProfile.HEADSET)

private val profileListener = object : BluetoothProfile.ServiceListener {
    @SuppressLint("MissingPermission")
    override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
        ExLog.d(TAG, "************* bluetooth onServiceConnected : $profile")
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = proxy as BluetoothHeadset
            bluetoothHeadset
                ?.connectedDevices
                ?.takeIf {
                    0 < it.size &&
                        ringBleManager.isBluetoothDevice() == true //블루투스 기기 연결 여부
                }
                ?.let {
                    //실제 블루투스가 연결되어있는 상태라면
                    ExLog.d(TAG, "************* bluetooth device Connected")
                    ringBleManager.isBluetoothConnected = true
                    ringBleManager.publishAudioType(
                        BLUETOOTH,
                        ringMediaManager.isRingPlayingAndReceiveCall() //추후 소스 분리하여 manager파일 생김.
                    )
                }
                ?: run {
                    ExLog.d(TAG, "************* bluetooth device not Connected")
                    ringBleManager.isBluetoothConnected = false
                    ringBleManager.publishAudioType(
                        if (isCurrentTypeAndRingPlay()) SPEAKER else PHONE,
                        ringMediaManager.isRingPlayingAndReceiveCall()
                    )
                }
        }
    }

    override fun onServiceDisconnected(profile: Int) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null
            publishAudioType(
                if (isCurrentTypeAndRingPlay()) SelectButtonType.SPEAKER else SelectButtonType.PHONE
            )
        }
    }
}

연결된 디바이스 타입이 BLUETOOTH_SCO라면
디바이스 연결 되었다고 판단하여 값 true로 설정하고 AudioManager를 통해 startBluetoothSCO를 해준다.

4. AudioManager를 통해 출력 변경

/**
 * AudioManager를 통해 벨소리 및 통화중 Phone/Speaker/Bluetooth로 오디오 출력 설정
 */
private fun requestCurrentAudioType(audioType: SelectButtonType) {
    if (audioType == currentAudioType) {
        ExLog.d(TAG, "************* SAME A U D I O T Y P E : $audioType")
        return
    }
    resetAudioType()

    AudioHelper.audioManager?.apply {
        currentAudioType = when (audioType) {
            SelectButtonType.BLUETOOTH -> {
                if (!isBluetoothConnected) {
                    publishAudioType(SelectButtonType.PHONE)
                    return@apply
                }
                setAudioOutDevice(this, getAudioDevice(this, AudioDeviceInfo.TYPE_BLUETOOTH_SCO))
                SelectButtonType.BLUETOOTH
            }
            SelectButtonType.SPEAKER -> {
                setAudioOutDevice(this, getAudioDevice(this, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER))
                SelectButtonType.SPEAKER
            }
            else -> { //phone
                setAudioOutDevice(this, getAudioDevice(this, AudioDeviceInfo.TYPE_BUILTIN_EARPIECE))
                SelectButtonType.PHONE
            }
        }
    }
}

resetAudioType으로 초기화 시켜준 뒤에 변경 하였습니다.

/**
 * AudioManager 설정 Reset
 */
private fun resetAudioType() {
    AudioHelper.audioManager?.apply {
        changeSco(this, false)
        isSpeakerphoneOn = false
        mode = AudioManager.MODE_NORMAL
        currentAudioType = SelectButtonType.PHONE
    }
}

4-1. setAudioOutDevice

/**
 * 가져온 AudioDeviceInfo로 CommunicationDevice Set해준다. (OS 12이상)
 */
private fun setAudioOutDevice(audioManager: AudioManager, target: AudioDeviceInfo?) {
    //need some delay for bluetooth sco

    var audioChangeDelay: Long = 15
    if (System.currentTimeMillis() - lastAudioChangeTime <= 300) audioChangeDelay = 300

    lastAudioChangeTime = System.currentTimeMillis()
    Handler(Looper.getMainLooper()).postDelayed({

        audioManager.run {
            var targetDevice = target
            //no device selected. scan all output devices and select one automatically
            if (targetDevice == null) {
                var wiredHeadsetDevice: AudioDeviceInfo? =
                    getAudioDevice(this, AudioDeviceInfo.TYPE_WIRED_HEADSET)
                if (wiredHeadsetDevice == null) wiredHeadsetDevice =
                    getAudioDevice(this, AudioDeviceInfo.TYPE_WIRED_HEADPHONES)
                val bluetoothDevice: AudioDeviceInfo? =
                    getAudioDevice(this, AudioDeviceInfo.TYPE_BLUETOOTH_SCO)
                val speakerDevice: AudioDeviceInfo? =
                    getAudioDevice(this, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
                val earpieceDevice: AudioDeviceInfo? =
                    getAudioDevice(this, AudioDeviceInfo.TYPE_BUILTIN_EARPIECE)

                //update global variables
                var isBluetoothAvailable = bluetoothDevice != null
                //disable BT if wired headset connected
                if (wiredHeadsetDevice != null) isBluetoothAvailable = false


                //choose an output device
                targetDevice =
                    if (
                        isRingPlaying &&
                        !isOutGoingCall &&
                        bluetoothDevice == null &&
                        wiredHeadsetDevice == null
                    ) {
                        speakerDevice
                    } else if (
                        isRingPlaying &&
                        !isOutGoingCall &&
                        wiredHeadsetDevice == null &&
                        !isBluetoothEnabled
                    ) {
                        speakerDevice
                    } else {
                        wiredHeadsetDevice ?: if (
                            isBluetoothAvailable &&
                            isBluetoothEnabled &&
                            bluetoothDevice != null
                        ) {
                            bluetoothDevice
                        } else {
                            earpieceDevice ?: speakerDevice
                        }
                    }

            }

            //set output device
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                try {
                    clearCommunicationDevice()
                    if (targetDevice != null) {
                        setCommunicationDevice(targetDevice)
                    }
                } catch (e: Exception) {
                    ExLog.d(TAG, e.message.toString())
                }
            }

            isSpeakerphoneOn = if (targetDevice?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
                changeSco(this, false)
                true
            } else {
                if (targetDevice?.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
                    changeSco(this, true)
                } else {
                    changeSco(this, false)
                }
                false
            }
            mode = if (
                isRingPlaying &&
                !isOutGoingCall
            ) {
                AudioManager.MODE_RINGTONE
            } else {
                AudioManager.MODE_IN_COMMUNICATION
            }
        }
    }, audioChangeDelay)
}

4-2. 원하는 타입의 디바이스 가져오기

/**
 * 원하는 타입의 디바이스를 가져온다.
 */
private fun getAudioDevice(audioManager: AudioManager, type: Int): AudioDeviceInfo? {
    audioManager.run {
        val audioDevices = getDevices(AudioManager.GET_DEVICES_OUTPUTS)
        if (audioDevices != null) {
            for (deviceInfo in audioDevices) {
                if (type == deviceInfo.type) return deviceInfo
            }
        }
    }
    return null
}

 


화면단에서는 뷰모델에서
EventBus로 값을 받고, Ui만 업데이트 시켜줍니다.

실제 뷰에서 오디오 타입을 변경할 때에는 EventBus로 다시 AudioSet할 수 있도록 전달합니다.

 

정답은 아니겠지만..
힘들게 로직 작업을 한 내용이라 잊지 않기 위해 메모!

 

 


BroadcastReceiver - ACTION_AUDIO_STATE_CHANGED - STATE_AUDIO_DISCONNECTED
가 블루투스 연결시 or 다른 오디오로 송출할때 같이 호출이되어 문제가 발생하여 
STATE_AUDIO_DISCONNECTED에서 연결 끊어주던 로직은 제거하였습니다.

한 파일안에서 너무 많은것을 작업하여
RingMediaManager와 RingBluetoothManager로 소스 분리하였습니다.

갤럭시 왓치도 블루투스 기기 연결 되었다고 판단이되어
왓치와 블루투스 이어폰(?) 판단하는 함수 추가

/**
 * 블루투스 SCO 기기가 연결 되어있는지 판단
 */
fun isBluetoothDevice(): Boolean? {
    return (context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager)?.run {
        val bluetoothDevice: AudioDeviceInfo? =
            getAudioDevice(this, AudioDeviceInfo.TYPE_BLUETOOTH_SCO)

        ExLog.d(TAG, "************* bluetooth device isBluetoothDevice (${bluetoothDevice?.productName})")

        return@run when {
            isBluetoothEnabled &&
                bluetoothDevice != null -> {
                true
            }
            else -> {
                null
            }
        }
    }
}
반응형