Reworked audio route changes to make them go through Telecom Manager API if enabled to make smartwatches act as bluetooth headset properly
This commit is contained in:
parent
6d6ea9b4c4
commit
f66c90d356
6 changed files with 99 additions and 42 deletions
|
@ -34,6 +34,7 @@ import android.view.WindowManager
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.telecom.NativeCallWrapper
|
||||||
|
|
||||||
@TargetApi(26)
|
@TargetApi(26)
|
||||||
class Api26Compatibility {
|
class Api26Compatibility {
|
||||||
|
@ -133,5 +134,9 @@ class Api26Compatibility {
|
||||||
.build()
|
.build()
|
||||||
vibrator.vibrate(effect, audioAttrs)
|
vibrator.vibrate(effect, audioAttrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int) {
|
||||||
|
connection.setAudioRoute(route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import androidx.core.app.NotificationManagerCompat
|
||||||
import org.linphone.core.ChatRoom
|
import org.linphone.core.ChatRoom
|
||||||
import org.linphone.core.Content
|
import org.linphone.core.Content
|
||||||
import org.linphone.mediastream.Version
|
import org.linphone.mediastream.Version
|
||||||
|
import org.linphone.telecom.NativeCallWrapper
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
class Compatibility {
|
class Compatibility {
|
||||||
|
@ -169,6 +170,12 @@ class Compatibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int) {
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API26_O_80)) {
|
||||||
|
Api26Compatibility.changeAudioRouteForTelecomManager(connection, route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Contacts */
|
/* Contacts */
|
||||||
|
|
||||||
fun createShortcutsToContacts(context: Context) {
|
fun createShortcutsToContacts(context: Context) {
|
||||||
|
|
|
@ -72,10 +72,10 @@ class NativeCallWrapper(var callId: String) : Connection() {
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
call.microphoneMuted = state.isMuted
|
call.microphoneMuted = state.isMuted
|
||||||
when (state.route) {
|
when (state.route) {
|
||||||
CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call)
|
CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call, true)
|
||||||
CallAudioState.ROUTE_SPEAKER -> AudioRouteUtils.routeAudioToSpeaker(call)
|
CallAudioState.ROUTE_SPEAKER -> AudioRouteUtils.routeAudioToSpeaker(call, true)
|
||||||
CallAudioState.ROUTE_BLUETOOTH -> AudioRouteUtils.routeAudioToBluetooth(call)
|
CallAudioState.ROUTE_BLUETOOTH -> AudioRouteUtils.routeAudioToBluetooth(call, true)
|
||||||
CallAudioState.ROUTE_WIRED_HEADSET -> AudioRouteUtils.routeAudioToHeadset(call)
|
CallAudioState.ROUTE_WIRED_HEADSET -> AudioRouteUtils.routeAudioToHeadset(call, true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selfDestroy()
|
selfDestroy()
|
||||||
|
|
|
@ -30,8 +30,6 @@ import org.linphone.core.CoreListenerStub
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
|
||||||
class TelecomConnectionService : ConnectionService() {
|
class TelecomConnectionService : ConnectionService() {
|
||||||
private val connections = arrayListOf<NativeCallWrapper>()
|
|
||||||
|
|
||||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||||
override fun onCallStateChanged(
|
override fun onCallStateChanged(
|
||||||
core: Core,
|
core: Core,
|
||||||
|
@ -42,7 +40,7 @@ class TelecomConnectionService : ConnectionService() {
|
||||||
Log.i("[Telecom Connection Service] call [${call.callLog.callId}] state changed: $state")
|
Log.i("[Telecom Connection Service] call [${call.callLog.callId}] state changed: $state")
|
||||||
when (call.state) {
|
when (call.state) {
|
||||||
Call.State.OutgoingProgress -> {
|
Call.State.OutgoingProgress -> {
|
||||||
for (connection in connections) {
|
for (connection in TelecomHelper.get().connections) {
|
||||||
if (connection.callId.isEmpty()) {
|
if (connection.callId.isEmpty()) {
|
||||||
connection.callId = core.currentCall?.callLog?.callId ?: ""
|
connection.callId = core.currentCall?.callLog?.callId ?: ""
|
||||||
}
|
}
|
||||||
|
@ -105,7 +103,7 @@ class TelecomConnectionService : ConnectionService() {
|
||||||
connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
|
connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
|
||||||
Log.i("[Telecom Connection Service] Address is $providedHandle")
|
Log.i("[Telecom Connection Service] Address is $providedHandle")
|
||||||
|
|
||||||
connections.add(connection)
|
TelecomHelper.get().connections.add(connection)
|
||||||
connection
|
connection
|
||||||
} else {
|
} else {
|
||||||
Log.e("[Telecom Connection Service] Error: $accountHandle $componentName")
|
Log.e("[Telecom Connection Service] Error: $accountHandle $componentName")
|
||||||
|
@ -150,7 +148,7 @@ class TelecomConnectionService : ConnectionService() {
|
||||||
connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
|
connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
|
||||||
Log.i("[Telecom Connection Service] Address is $providedHandle")
|
Log.i("[Telecom Connection Service] Address is $providedHandle")
|
||||||
|
|
||||||
connections.add(connection)
|
TelecomHelper.get().connections.add(connection)
|
||||||
connection
|
connection
|
||||||
} else {
|
} else {
|
||||||
Log.e("[Telecom Connection Service] Error: $accountHandle $componentName")
|
Log.e("[Telecom Connection Service] Error: $accountHandle $componentName")
|
||||||
|
@ -163,26 +161,20 @@ class TelecomConnectionService : ConnectionService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getConnectionForCallId(callId: String): NativeCallWrapper? {
|
|
||||||
return connections.find { connection ->
|
|
||||||
connection.callId == callId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCallError(call: Call) {
|
private fun onCallError(call: Call) {
|
||||||
val connection = getConnectionForCallId(call.callLog.callId)
|
val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
|
||||||
connection ?: return
|
connection ?: return
|
||||||
|
|
||||||
connections.remove(connection)
|
TelecomHelper.get().connections.remove(connection)
|
||||||
connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR))
|
connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR))
|
||||||
connection.destroy()
|
connection.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCallEnded(call: Call) {
|
private fun onCallEnded(call: Call) {
|
||||||
val connection = getConnectionForCallId(call.callLog.callId)
|
val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
|
||||||
connection ?: return
|
connection ?: return
|
||||||
|
|
||||||
connections.remove(connection)
|
TelecomHelper.get().connections.remove(connection)
|
||||||
val reason = call.reason
|
val reason = call.reason
|
||||||
Log.i("[Telecom Connection Service] Call ended with reason: $reason")
|
Log.i("[Telecom Connection Service] Call ended with reason: $reason")
|
||||||
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
||||||
|
@ -190,7 +182,7 @@ class TelecomConnectionService : ConnectionService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCallConnected(call: Call) {
|
private fun onCallConnected(call: Call) {
|
||||||
val connection = getConnectionForCallId(call.callLog.callId)
|
val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
|
||||||
connection ?: return
|
connection ?: return
|
||||||
|
|
||||||
if (connection.state != Connection.STATE_HOLDING) {
|
if (connection.state != Connection.STATE_HOLDING) {
|
||||||
|
|
|
@ -49,6 +49,8 @@ class TelecomHelper private constructor(context: Context) {
|
||||||
|
|
||||||
private var account: PhoneAccount = initPhoneAccount(context)
|
private var account: PhoneAccount = initPhoneAccount(context)
|
||||||
|
|
||||||
|
val connections = arrayListOf<NativeCallWrapper>()
|
||||||
|
|
||||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||||
override fun onCallStateChanged(
|
override fun onCallStateChanged(
|
||||||
core: Core,
|
core: Core,
|
||||||
|
@ -137,6 +139,12 @@ class TelecomHelper private constructor(context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findConnectionForCallId(callId: String): NativeCallWrapper? {
|
||||||
|
return connections.find { connection ->
|
||||||
|
connection.callId == callId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun initPhoneAccount(context: Context): PhoneAccount {
|
private fun initPhoneAccount(context: Context): PhoneAccount {
|
||||||
val account: PhoneAccount? = findExistingAccount(context)
|
val account: PhoneAccount? = findExistingAccount(context)
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
|
|
|
@ -19,16 +19,19 @@
|
||||||
*/
|
*/
|
||||||
package org.linphone.utils
|
package org.linphone.utils
|
||||||
|
|
||||||
|
import android.telecom.CallAudioState
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.core.AudioDevice
|
import org.linphone.core.AudioDevice
|
||||||
import org.linphone.core.Call
|
import org.linphone.core.Call
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.telecom.TelecomHelper
|
||||||
|
|
||||||
class AudioRouteUtils {
|
class AudioRouteUtils {
|
||||||
companion object {
|
companion object {
|
||||||
private fun routeAudioTo(
|
private fun applyAudioRouteChange(
|
||||||
|
call: Call?,
|
||||||
types: List<AudioDevice.Type>,
|
types: List<AudioDevice.Type>,
|
||||||
call: Call? = null,
|
|
||||||
output: Boolean = true
|
output: Boolean = true
|
||||||
) {
|
) {
|
||||||
val listSize = types.size
|
val listSize = types.size
|
||||||
|
@ -71,28 +74,70 @@ class AudioRouteUtils {
|
||||||
Log.e("[Audio Route Helper] Couldn't find [$typesNames] audio device")
|
Log.e("[Audio Route Helper] Couldn't find [$typesNames] audio device")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun routeAudioToEarpiece(call: Call? = null) {
|
private fun changeCaptureDeviceToMatchAudioRoute(call: Call?, types: List<AudioDevice.Type>) {
|
||||||
routeAudioTo(arrayListOf(AudioDevice.Type.Earpiece), call)
|
when (types.first()) {
|
||||||
}
|
AudioDevice.Type.Bluetooth -> {
|
||||||
|
|
||||||
fun routeAudioToSpeaker(call: Call? = null) {
|
|
||||||
routeAudioTo(arrayListOf(AudioDevice.Type.Speaker), call)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun routeAudioToBluetooth(call: Call? = null) {
|
|
||||||
routeAudioTo(arrayListOf(AudioDevice.Type.Bluetooth), call)
|
|
||||||
if (isBluetoothAudioRecorderAvailable()) {
|
if (isBluetoothAudioRecorderAvailable()) {
|
||||||
Log.i("[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device")
|
Log.i("[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device")
|
||||||
routeAudioTo(arrayListOf(AudioDevice.Type.Bluetooth), call, false)
|
applyAudioRouteChange(call, arrayListOf(AudioDevice.Type.Bluetooth), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AudioDevice.Type.Headset, AudioDevice.Type.Headphones -> {
|
||||||
|
if (isHeadsetAudioRecorderAvailable()) {
|
||||||
|
Log.i("[Audio Route Helper] Headphones/headset device is able to record audio, also change input audio device")
|
||||||
|
applyAudioRouteChange(call, (arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset)), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun routeAudioToHeadset(call: Call? = null) {
|
private fun routeAudioTo(
|
||||||
routeAudioTo(arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset), call)
|
call: Call?,
|
||||||
if (isHeadsetAudioRecorderAvailable()) {
|
types: List<AudioDevice.Type>,
|
||||||
Log.i("[Audio Route Helper] Headphones/headset device is able to record audio, also change input audio device")
|
skipTelecom: Boolean = false
|
||||||
routeAudioTo((arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset)), call, false)
|
) {
|
||||||
|
val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
|
||||||
|
if ((call != null || currentCall != null) && !skipTelecom && TelecomHelper.exists()) {
|
||||||
|
val callToUse = call ?: currentCall
|
||||||
|
Log.i("[Audio Route Helper] Call provided & Telecom Helper exists, trying to dispatch audio route change through Telecom API")
|
||||||
|
val connection = TelecomHelper.get().findConnectionForCallId(callToUse.callLog.callId)
|
||||||
|
if (connection != null) {
|
||||||
|
val route = when (types.first()) {
|
||||||
|
AudioDevice.Type.Earpiece -> CallAudioState.ROUTE_EARPIECE
|
||||||
|
AudioDevice.Type.Speaker -> CallAudioState.ROUTE_SPEAKER
|
||||||
|
AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> CallAudioState.ROUTE_WIRED_HEADSET
|
||||||
|
AudioDevice.Type.Bluetooth, AudioDevice.Type.BluetoothA2DP -> CallAudioState.ROUTE_BLUETOOTH
|
||||||
|
else -> CallAudioState.ROUTE_WIRED_OR_EARPIECE
|
||||||
}
|
}
|
||||||
|
Log.i("[Audio Route Helper] Telecom Helper & matching connection found, dispatching audio route change through it")
|
||||||
|
// We will be called here again by NativeCallWrapper.onCallAudioStateChanged()
|
||||||
|
// but this time with skipTelecom = true
|
||||||
|
Compatibility.changeAudioRouteForTelecomManager(connection, route)
|
||||||
|
} else {
|
||||||
|
Log.w("[Audio Route Helper] Telecom Helper found but no matching connection!")
|
||||||
|
applyAudioRouteChange(callToUse, types)
|
||||||
|
changeCaptureDeviceToMatchAudioRoute(callToUse, types)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
applyAudioRouteChange(call, types)
|
||||||
|
changeCaptureDeviceToMatchAudioRoute(call, types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun routeAudioToEarpiece(call: Call? = null, skipTelecom: Boolean = false) {
|
||||||
|
routeAudioTo(call, arrayListOf(AudioDevice.Type.Earpiece), skipTelecom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun routeAudioToSpeaker(call: Call? = null, skipTelecom: Boolean = false) {
|
||||||
|
routeAudioTo(call, arrayListOf(AudioDevice.Type.Speaker), skipTelecom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun routeAudioToBluetooth(call: Call? = null, skipTelecom: Boolean = false) {
|
||||||
|
routeAudioTo(call, arrayListOf(AudioDevice.Type.Bluetooth), skipTelecom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun routeAudioToHeadset(call: Call? = null, skipTelecom: Boolean = false) {
|
||||||
|
routeAudioTo(call, arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset), skipTelecom)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean {
|
fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean {
|
||||||
|
@ -104,7 +149,7 @@ class AudioRouteUtils {
|
||||||
val conference = coreContext.core.conference
|
val conference = coreContext.core.conference
|
||||||
|
|
||||||
val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice
|
val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice
|
||||||
Log.i("[Audio Route Helper] Audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})")
|
Log.i("[Audio Route Helper] Playback audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})")
|
||||||
return audioDevice?.type == AudioDevice.Type.Speaker
|
return audioDevice?.type == AudioDevice.Type.Speaker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +162,7 @@ class AudioRouteUtils {
|
||||||
val conference = coreContext.core.conference
|
val conference = coreContext.core.conference
|
||||||
|
|
||||||
val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice
|
val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice
|
||||||
Log.i("[Audio Route Helper] Audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})")
|
Log.i("[Audio Route Helper] Playback audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})")
|
||||||
return audioDevice?.type == AudioDevice.Type.Bluetooth
|
return audioDevice?.type == AudioDevice.Type.Bluetooth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue