mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-04 07:25:25 +01:00
Reintroduce preliminary telecom support for 1:1 calling.
This commit is contained in:
committed by
Michelle Tang
parent
1b6cfe9fc6
commit
a62f07db11
@@ -212,7 +212,14 @@ class ActiveCallManager(
|
||||
|
||||
fun sendAudioCommand(audioCommand: AudioManagerCommand) {
|
||||
if (signalAudioManager == null) {
|
||||
signalAudioManager = create(application, this)
|
||||
val canUseTelecom = if (audioCommand is AudioManagerCommand.Initialize) {
|
||||
!audioCommand.isGroupCall
|
||||
} else {
|
||||
Log.w(TAG, "First AudioCommand received was not Initialize, skipping Telecom usage, command: ${audioCommand.javaClass.simpleName}")
|
||||
false
|
||||
}
|
||||
|
||||
signalAudioManager = create(context = application, eventListener = this, canUseTelecom = canUseTelecom)
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sending audio command [" + audioCommand.javaClass.simpleName + "] to " + signalAudioManager?.javaClass?.simpleName)
|
||||
@@ -310,17 +317,23 @@ class ActiveCallManager(
|
||||
@get:RequiresApi(30)
|
||||
override val serviceType: Int
|
||||
get() {
|
||||
var type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
val telecom = Build.VERSION.SDK_INT >= 34 && AndroidTelecomUtil.hasActiveController()
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
return if (telecom) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||
} else {
|
||||
var type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
}
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.CAMERA)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
}
|
||||
|
||||
type
|
||||
}
|
||||
|
||||
if (Permissions.hasAll(this, Manifest.permission.CAMERA)) {
|
||||
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.telecom.CallAudioState
|
||||
import android.telecom.Connection
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder
|
||||
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCommand
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
|
||||
/**
|
||||
* Signal implementation for the telecom system connection. Provides an interaction point for the system to
|
||||
* inform us about changes in the telecom system. Created and returned by [AndroidCallConnectionService].
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
class AndroidCallConnection(
|
||||
private val context: Context,
|
||||
private val recipientId: RecipientId,
|
||||
isOutgoing: Boolean,
|
||||
private val isVideoCall: Boolean
|
||||
) : Connection() {
|
||||
|
||||
private var needToResetAudioRoute = isOutgoing && !isVideoCall
|
||||
private var initialAudioRoute: SignalAudioManager.AudioDevice? = null
|
||||
|
||||
init {
|
||||
connectionProperties = PROPERTY_SELF_MANAGED
|
||||
connectionCapabilities = CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL or
|
||||
CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL or
|
||||
CAPABILITY_MUTE
|
||||
}
|
||||
|
||||
override fun onShowIncomingCallUi() {
|
||||
Log.i(TAG, "onShowIncomingCallUi()")
|
||||
ActiveCallManager.update(context, CallNotificationBuilder.TYPE_INCOMING_CONNECTING, recipientId, isVideoCall)
|
||||
setRinging()
|
||||
}
|
||||
|
||||
override fun onCallAudioStateChanged(state: CallAudioState) {
|
||||
Log.i(TAG, "onCallAudioStateChanged($state)")
|
||||
|
||||
val activeDevice = state.route.toDevices().firstOrNull() ?: SignalAudioManager.AudioDevice.EARPIECE
|
||||
val availableDevices = state.supportedRouteMask.toDevices()
|
||||
|
||||
AppDependencies.signalCallManager.onAudioDeviceChanged(activeDevice, availableDevices)
|
||||
|
||||
if (needToResetAudioRoute) {
|
||||
if (initialAudioRoute == null) {
|
||||
initialAudioRoute = activeDevice
|
||||
} else if (activeDevice == SignalAudioManager.AudioDevice.SPEAKER_PHONE) {
|
||||
Log.i(TAG, "Resetting audio route from SPEAKER_PHONE to $initialAudioRoute")
|
||||
AndroidTelecomUtil.selectAudioDevice(recipientId, initialAudioRoute!!)
|
||||
needToResetAudioRoute = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnswer(videoState: Int) {
|
||||
Log.i(TAG, "onAnswer($videoState)")
|
||||
if (Permissions.hasAll(context, android.Manifest.permission.RECORD_AUDIO)) {
|
||||
AppDependencies.signalCallManager.acceptCall(false)
|
||||
} else {
|
||||
val intent = CallIntent.Builder(context)
|
||||
.withAddedIntentFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.withAction(if (isVideoCall) CallIntent.Action.ANSWER_VIDEO else CallIntent.Action.ANSWER_AUDIO)
|
||||
.build()
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSilence() {
|
||||
ActiveCallManager.sendAudioManagerCommand(context, AudioManagerCommand.SilenceIncomingRinger())
|
||||
}
|
||||
|
||||
override fun onReject() {
|
||||
Log.i(TAG, "onReject()")
|
||||
ActiveCallManager.denyCall()
|
||||
}
|
||||
|
||||
override fun onDisconnect() {
|
||||
Log.i(TAG, "onDisconnect()")
|
||||
ActiveCallManager.hangup()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(AndroidCallConnection::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toDevices(): Set<SignalAudioManager.AudioDevice> {
|
||||
val devices = mutableSetOf<SignalAudioManager.AudioDevice>()
|
||||
|
||||
if (this and CallAudioState.ROUTE_BLUETOOTH != 0) {
|
||||
devices += SignalAudioManager.AudioDevice.BLUETOOTH
|
||||
}
|
||||
|
||||
if (this and CallAudioState.ROUTE_EARPIECE != 0) {
|
||||
devices += SignalAudioManager.AudioDevice.EARPIECE
|
||||
}
|
||||
|
||||
if (this and CallAudioState.ROUTE_WIRED_HEADSET != 0) {
|
||||
devices += SignalAudioManager.AudioDevice.WIRED_HEADSET
|
||||
}
|
||||
|
||||
if (this and CallAudioState.ROUTE_SPEAKER != 0) {
|
||||
devices += SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.telecom.Connection
|
||||
import android.telecom.ConnectionRequest
|
||||
import android.telecom.ConnectionService
|
||||
import android.telecom.PhoneAccountHandle
|
||||
import android.telecom.TelecomManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Signal implementation of the Android telecom [ConnectionService]. The system binds to this service
|
||||
* when we inform the [TelecomManager] of a new incoming or outgoing call. It'll then call the appropriate
|
||||
* create/failure method to let us know how to proceed.
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
class AndroidCallConnectionService : ConnectionService() {
|
||||
|
||||
override fun onCreateIncomingConnection(
|
||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
request: ConnectionRequest
|
||||
): Connection {
|
||||
val (recipientId: RecipientId, callId: Long, isVideoCall: Boolean) = request.getOurExtras()
|
||||
|
||||
Log.i(TAG, "onCreateIncomingConnection($recipientId)")
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val displayName = recipient.getDisplayName(this)
|
||||
val connection = AndroidCallConnection(
|
||||
context = applicationContext,
|
||||
recipientId = recipientId,
|
||||
isOutgoing = false,
|
||||
isVideoCall = isVideoCall
|
||||
).apply {
|
||||
setInitializing()
|
||||
if (SignalStore.settings.messageNotificationsPrivacy.isDisplayContact && recipient.e164.isPresent) {
|
||||
setAddress(Uri.fromParts("tel", recipient.e164.get(), null), TelecomManager.PRESENTATION_ALLOWED)
|
||||
setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
|
||||
}
|
||||
videoState = request.videoState
|
||||
extras = request.extras
|
||||
setRinging()
|
||||
}
|
||||
AndroidTelecomUtil.connections[recipientId] = connection
|
||||
AppDependencies.signalCallManager.setTelecomApproved(callId, recipientId)
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
override fun onCreateIncomingConnectionFailed(
|
||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
request: ConnectionRequest
|
||||
) {
|
||||
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
|
||||
|
||||
Log.i(TAG, "onCreateIncomingConnectionFailed($recipientId)")
|
||||
AppDependencies.signalCallManager.dropCall(callId)
|
||||
}
|
||||
|
||||
override fun onCreateOutgoingConnection(
|
||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
request: ConnectionRequest
|
||||
): Connection {
|
||||
val (recipientId: RecipientId, callId: Long, isVideoCall: Boolean) = request.getOurExtras()
|
||||
|
||||
Log.i(TAG, "onCreateOutgoingConnection($recipientId)")
|
||||
val connection = AndroidCallConnection(
|
||||
context = applicationContext,
|
||||
recipientId = recipientId,
|
||||
isOutgoing = true,
|
||||
isVideoCall = isVideoCall
|
||||
).apply {
|
||||
videoState = request.videoState
|
||||
extras = request.extras
|
||||
setDialing()
|
||||
}
|
||||
AndroidTelecomUtil.connections[recipientId] = connection
|
||||
AppDependencies.signalCallManager.setTelecomApproved(callId, recipientId)
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
override fun onCreateOutgoingConnectionFailed(
|
||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
request: ConnectionRequest
|
||||
) {
|
||||
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
|
||||
|
||||
Log.i(TAG, "onCreateOutgoingConnectionFailed($recipientId)")
|
||||
AppDependencies.signalCallManager.dropCall(callId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(AndroidCallConnectionService::class.java)
|
||||
const val KEY_RECIPIENT_ID = "org.thoughtcrime.securesms.RECIPIENT_ID"
|
||||
const val KEY_CALL_ID = "org.thoughtcrime.securesms.CALL_ID"
|
||||
const val KEY_VIDEO_CALL = "org.thoughtcrime.securesms.VIDEO_CALL"
|
||||
}
|
||||
|
||||
private fun ConnectionRequest.getOurExtras(): ServiceExtras {
|
||||
val ourExtras: Bundle = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS) ?: extras
|
||||
|
||||
val recipientId: RecipientId = RecipientId.from(ourExtras.getString(KEY_RECIPIENT_ID)!!)
|
||||
val callId: Long = ourExtras.getLong(KEY_CALL_ID)
|
||||
val isVideoCall: Boolean = ourExtras.getBoolean(KEY_VIDEO_CALL, false)
|
||||
|
||||
return ServiceExtras(recipientId, callId, isVideoCall)
|
||||
}
|
||||
|
||||
private data class ServiceExtras(val recipientId: RecipientId, val callId: Long, val isVideoCall: Boolean)
|
||||
}
|
||||
@@ -1,23 +1,15 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import android.telecom.CallAudioState
|
||||
import android.telecom.Connection
|
||||
import android.telecom.DisconnectCause
|
||||
import android.telecom.DisconnectCause.REJECTED
|
||||
import android.telecom.DisconnectCause.UNKNOWN
|
||||
import android.telecom.PhoneAccount
|
||||
import android.telecom.PhoneAccountHandle
|
||||
import android.telecom.TelecomManager
|
||||
import android.telecom.VideoProfile
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.telecom.CallsManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.webrtc.AudioOutputOption
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -25,172 +17,194 @@ import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
|
||||
/**
|
||||
* Wrapper around various [TelecomManager] methods to make dealing with SDK versions easier. Also
|
||||
* maintains a global list of all Signal [AndroidCallConnection]s associated with their [RecipientId].
|
||||
* There should really only be one ever, but there may be times when dealing with glare or a busy that two
|
||||
* may kick off.
|
||||
* Wrapper around Jetpack [CallsManager] to manage telecom integration. Maintains a global map of
|
||||
* [TelecomCallController] instances associated with their [RecipientId].
|
||||
*/
|
||||
@SuppressLint("NewApi", "InlinedApi")
|
||||
@SuppressLint("NewApi")
|
||||
object AndroidTelecomUtil {
|
||||
|
||||
private val TAG = Log.tag(AndroidTelecomUtil::class.java)
|
||||
private val context = AppDependencies.application
|
||||
private var systemRejected = false
|
||||
private var accountRegistered = false
|
||||
private var registered = false
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val callsManager: CallsManager by lazy { CallsManager(context) }
|
||||
|
||||
@JvmStatic
|
||||
val telecomSupported: Boolean
|
||||
get() {
|
||||
if (Build.VERSION.SDK_INT >= 26 && !systemRejected && isTelecomAllowedForDevice()) {
|
||||
if (!accountRegistered) {
|
||||
if (Build.VERSION.SDK_INT >= 34 && !systemRejected && isTelecomAllowedForDevice()) {
|
||||
if (!registered) {
|
||||
registerPhoneAccount()
|
||||
}
|
||||
|
||||
if (accountRegistered) {
|
||||
val phoneAccount = ContextCompat.getSystemService(context, TelecomManager::class.java)!!.getPhoneAccount(getPhoneAccountHandle())
|
||||
if (phoneAccount != null && phoneAccount.isEnabled) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return registered
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val connections: MutableMap<RecipientId, AndroidCallConnection> = mutableMapOf()
|
||||
private val controllers: MutableMap<RecipientId, TelecomCallController> = mutableMapOf()
|
||||
|
||||
@JvmStatic
|
||||
fun registerPhoneAccount() {
|
||||
if (Build.VERSION.SDK_INT >= 26 && !systemRejected) {
|
||||
Log.i(TAG, "Registering phone account")
|
||||
val phoneAccount = PhoneAccount.Builder(getPhoneAccountHandle(), "Signal")
|
||||
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED or PhoneAccount.CAPABILITY_VIDEO_CALLING)
|
||||
.build()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 34 && !systemRejected) {
|
||||
Log.i(TAG, "Registering with CallsManager")
|
||||
try {
|
||||
ContextCompat.getSystemService(context, TelecomManager::class.java)!!.registerPhoneAccount(phoneAccount)
|
||||
Log.i(TAG, "Phone account registered successfully")
|
||||
accountRegistered = true
|
||||
callsManager.registerAppWithTelecom(
|
||||
CallsManager.CAPABILITY_BASELINE or CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING
|
||||
)
|
||||
Log.i(TAG, "CallsManager registration successful")
|
||||
registered = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to register telecom account", e)
|
||||
Log.w(TAG, "Unable to register with CallsManager", e)
|
||||
systemRejected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@RequiresApi(26)
|
||||
fun getPhoneAccountHandle(): PhoneAccountHandle {
|
||||
return PhoneAccountHandle(ComponentName(context, AndroidCallConnectionService::class.java), context.packageName, Process.myUserHandle())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun addIncomingCall(recipientId: RecipientId, callId: Long, remoteVideoOffer: Boolean): Boolean {
|
||||
if (telecomSupported) {
|
||||
val telecomBundle = bundleOf(
|
||||
TelecomManager.EXTRA_INCOMING_CALL_EXTRAS to bundleOf(
|
||||
AndroidCallConnectionService.KEY_RECIPIENT_ID to recipientId.serialize(),
|
||||
AndroidCallConnectionService.KEY_CALL_ID to callId,
|
||||
AndroidCallConnectionService.KEY_VIDEO_CALL to remoteVideoOffer,
|
||||
TelecomManager.EXTRA_INCOMING_VIDEO_STATE to if (remoteVideoOffer) VideoProfile.STATE_BIDIRECTIONAL else VideoProfile.STATE_AUDIO_ONLY
|
||||
),
|
||||
TelecomManager.EXTRA_INCOMING_VIDEO_STATE to if (remoteVideoOffer) VideoProfile.STATE_BIDIRECTIONAL else VideoProfile.STATE_AUDIO_ONLY
|
||||
Log.i(TAG, "addIncomingCall(recipientId=$recipientId, callId=$callId, videoOffer=$remoteVideoOffer)")
|
||||
val controller = TelecomCallController(
|
||||
context = context,
|
||||
recipientId = recipientId,
|
||||
callId = callId,
|
||||
isVideoCall = remoteVideoOffer,
|
||||
isOutgoing = false,
|
||||
callsManager = callsManager
|
||||
)
|
||||
try {
|
||||
Log.i(TAG, "Adding incoming call $telecomBundle")
|
||||
ContextCompat.getSystemService(context, TelecomManager::class.java)!!.addNewIncomingCall(getPhoneAccountHandle(), telecomBundle)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Unable to add incoming call", e)
|
||||
systemRejected = true
|
||||
return false
|
||||
synchronized(controllers) {
|
||||
controllers[recipientId] = controller
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun reject(recipientId: RecipientId) {
|
||||
if (telecomSupported) {
|
||||
connections[recipientId]?.setDisconnected(DisconnectCause(REJECTED))
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun activateCall(recipientId: RecipientId) {
|
||||
if (telecomSupported) {
|
||||
connections[recipientId]?.setActive()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun terminateCall(recipientId: RecipientId) {
|
||||
if (telecomSupported) {
|
||||
connections[recipientId]?.let { connection ->
|
||||
if (connection.disconnectCause == null) {
|
||||
connection.setDisconnected(DisconnectCause(UNKNOWN))
|
||||
scope.launch {
|
||||
try {
|
||||
Log.i(TAG, "Incoming call controller starting for recipientId=$recipientId callId=$callId")
|
||||
controller.start()
|
||||
Log.i(TAG, "Incoming call controller scope ended normally for recipientId=$recipientId callId=$callId")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "addIncomingCall failed for recipientId=$recipientId callId=$callId", e)
|
||||
systemRejected = true
|
||||
AppDependencies.signalCallManager.dropCall(callId)
|
||||
} finally {
|
||||
Log.i(TAG, "Removing incoming controller for recipientId=$recipientId")
|
||||
synchronized(controllers) {
|
||||
controllers.remove(recipientId)
|
||||
}
|
||||
}
|
||||
connection.destroy()
|
||||
connections.remove(recipientId)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun addOutgoingCall(recipientId: RecipientId, callId: Long, isVideoCall: Boolean): Boolean {
|
||||
if (telecomSupported) {
|
||||
val telecomBundle = bundleOf(
|
||||
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE to getPhoneAccountHandle(),
|
||||
TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE to if (isVideoCall) VideoProfile.STATE_BIDIRECTIONAL else VideoProfile.STATE_AUDIO_ONLY,
|
||||
TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS to bundleOf(
|
||||
AndroidCallConnectionService.KEY_RECIPIENT_ID to recipientId.serialize(),
|
||||
AndroidCallConnectionService.KEY_CALL_ID to callId,
|
||||
AndroidCallConnectionService.KEY_VIDEO_CALL to isVideoCall
|
||||
)
|
||||
Log.i(TAG, "addOutgoingCall(recipientId=$recipientId, callId=$callId, isVideoCall=$isVideoCall)")
|
||||
val controller = TelecomCallController(
|
||||
context = context,
|
||||
recipientId = recipientId,
|
||||
callId = callId,
|
||||
isVideoCall = isVideoCall,
|
||||
isOutgoing = true,
|
||||
callsManager = callsManager
|
||||
)
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Adding outgoing call $telecomBundle")
|
||||
ContextCompat.getSystemService(context, TelecomManager::class.java)!!.placeCall(recipientId.generateTelecomE164(), telecomBundle)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Unable to add outgoing call", e)
|
||||
systemRejected = true
|
||||
return false
|
||||
synchronized(controllers) {
|
||||
controllers[recipientId] = controller
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
Log.i(TAG, "Outgoing call controller starting for recipientId=$recipientId callId=$callId")
|
||||
controller.start()
|
||||
Log.i(TAG, "Outgoing call controller scope ended normally for recipientId=$recipientId callId=$callId")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "addOutgoingCall failed for recipientId=$recipientId callId=$callId", e)
|
||||
systemRejected = true
|
||||
AppDependencies.signalCallManager.dropCall(callId)
|
||||
} finally {
|
||||
Log.i(TAG, "Removing outgoing controller for recipientId=$recipientId")
|
||||
synchronized(controllers) {
|
||||
controllers.remove(recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun selectAudioDevice(recipientId: RecipientId, device: SignalAudioManager.AudioDevice) {
|
||||
@JvmStatic
|
||||
fun activateCall(recipientId: RecipientId) {
|
||||
if (telecomSupported) {
|
||||
val connection: AndroidCallConnection? = connections[recipientId]
|
||||
Log.i(TAG, "Selecting audio route: $device connection: ${connection != null}")
|
||||
if (connection?.callAudioState != null) {
|
||||
when (device) {
|
||||
SignalAudioManager.AudioDevice.SPEAKER_PHONE -> connection.setAudioRouteIfDifferent(CallAudioState.ROUTE_SPEAKER)
|
||||
SignalAudioManager.AudioDevice.BLUETOOTH -> connection.setAudioRouteIfDifferent(CallAudioState.ROUTE_BLUETOOTH)
|
||||
SignalAudioManager.AudioDevice.WIRED_HEADSET -> connection.setAudioRouteIfDifferent(CallAudioState.ROUTE_WIRED_HEADSET)
|
||||
else -> connection.setAudioRouteIfDifferent(CallAudioState.ROUTE_EARPIECE)
|
||||
}
|
||||
Log.i(TAG, "activateCall(recipientId=$recipientId) hasController=${synchronized(controllers) { controllers.containsKey(recipientId) }}")
|
||||
synchronized(controllers) {
|
||||
controllers[recipientId]?.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun getSelectedAudioDevice(recipientId: RecipientId): SignalAudioManager.AudioDevice {
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun terminateCall(recipientId: RecipientId, disconnectCause: Int = DisconnectCause.REMOTE) {
|
||||
if (telecomSupported) {
|
||||
val connection: AndroidCallConnection? = connections[recipientId]
|
||||
if (connection?.callAudioState != null) {
|
||||
return when (connection.callAudioState.route) {
|
||||
CallAudioState.ROUTE_SPEAKER -> SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
CallAudioState.ROUTE_BLUETOOTH -> SignalAudioManager.AudioDevice.BLUETOOTH
|
||||
CallAudioState.ROUTE_WIRED_HEADSET -> SignalAudioManager.AudioDevice.WIRED_HEADSET
|
||||
else -> SignalAudioManager.AudioDevice.EARPIECE
|
||||
}
|
||||
Log.i(TAG, "terminateCall(recipientId=$recipientId, cause=$disconnectCause) hasController=${synchronized(controllers) { controllers.containsKey(recipientId) }}")
|
||||
synchronized(controllers) {
|
||||
controllers[recipientId]?.disconnect(disconnectCause)
|
||||
}
|
||||
}
|
||||
return SignalAudioManager.AudioDevice.NONE
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun reject(recipientId: RecipientId) {
|
||||
if (telecomSupported) {
|
||||
Log.i(TAG, "reject(recipientId=$recipientId) hasController=${synchronized(controllers) { controllers.containsKey(recipientId) }}")
|
||||
synchronized(controllers) {
|
||||
controllers[recipientId]?.disconnect(DisconnectCause.REJECTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getActiveAudioDevice(recipientId: RecipientId): SignalAudioManager.AudioDevice {
|
||||
return synchronized(controllers) {
|
||||
controllers[recipientId]?.currentAudioDevice ?: SignalAudioManager.AudioDevice.NONE
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAudioDevice(recipientId: RecipientId, device: SignalAudioManager.AudioDevice) {
|
||||
if (telecomSupported) {
|
||||
synchronized(controllers) {
|
||||
val controller = controllers[recipientId]
|
||||
Log.i(TAG, "Selecting audio route: $device controller: ${controller != null}")
|
||||
controller?.requestEndpointChange(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAvailableAudioOutputOptions(): List<AudioOutputOption>? {
|
||||
if (!telecomSupported) return null
|
||||
return synchronized(controllers) {
|
||||
controllers.values.firstOrNull()?.getAvailableAudioOutputOptions()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getCurrentEndpointDeviceId(): Int {
|
||||
return synchronized(controllers) {
|
||||
controllers.values.firstOrNull()?.getCurrentEndpointDeviceId() ?: -1
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getCurrentActiveAudioDevice(): SignalAudioManager.AudioDevice {
|
||||
return synchronized(controllers) {
|
||||
controllers.values.firstOrNull()?.currentAudioDevice ?: SignalAudioManager.AudioDevice.NONE
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasActiveController(): Boolean {
|
||||
return synchronized(controllers) { controllers.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun isTelecomAllowedForDevice(): Boolean {
|
||||
@@ -200,16 +214,3 @@ object AndroidTelecomUtil {
|
||||
return RingRtcDynamicConfiguration.isTelecomAllowedForDevice()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@RequiresApi(26)
|
||||
private fun Connection.setAudioRouteIfDifferent(newRoute: Int) {
|
||||
if (callAudioState.route != newRoute) {
|
||||
setAudioRoute(newRoute)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RecipientId.generateTelecomE164(): Uri {
|
||||
val pseudoNumber = toLong().toString().padEnd(10, '0').replaceRange(3..5, "555")
|
||||
return Uri.fromParts("tel", "+1$pseudoNumber", null)
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, remotePeer, isRemoteVideoOffer);
|
||||
}
|
||||
webRtcInteractor.retrieveTurnServers(remotePeer);
|
||||
webRtcInteractor.initializeAudioForCall();
|
||||
webRtcInteractor.initializeAudioForCall(false);
|
||||
|
||||
if (!webRtcInteractor.addNewIncomingCall(remotePeer.getId(), remotePeer.getCallId().longValue(), offerType == OfferMessage.Type.VIDEO_CALL)) {
|
||||
Log.i(tag, "Unable to add new incoming call");
|
||||
|
||||
@@ -178,7 +178,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
||||
|
||||
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, currentState.getCallInfoState().getCallRecipient(), true);
|
||||
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
|
||||
webRtcInteractor.initializeAudioForCall();
|
||||
webRtcInteractor.initializeAudioForCall(true);
|
||||
|
||||
try {
|
||||
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
|
||||
|
||||
@@ -122,7 +122,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
|
||||
|
||||
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState, RemotePeer.GROUP_CALL_ID.longValue());
|
||||
|
||||
webRtcInteractor.initializeAudioForCall();
|
||||
webRtcInteractor.initializeAudioForCall(true);
|
||||
|
||||
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext(), recipient.resolve());
|
||||
|
||||
@@ -256,7 +256,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
|
||||
|
||||
webRtcInteractor.setCallInProgressNotification(TYPE_ESTABLISHED, currentState.getCallInfoState().getCallRecipient(), true);
|
||||
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
|
||||
webRtcInteractor.initializeAudioForCall();
|
||||
webRtcInteractor.initializeAudioForCall(true);
|
||||
|
||||
try {
|
||||
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
|
||||
|
||||
@@ -72,11 +72,11 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
boolean isVideoCall = offerType == OfferMessage.Type.VIDEO_CALL;
|
||||
|
||||
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer, isVideoCall);
|
||||
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context, isVideoCall, false));
|
||||
webRtcInteractor.initializeAudioForCall(false);
|
||||
webRtcInteractor.setDefaultAudioDevice(remotePeer.getId(),
|
||||
isVideoCall ? SignalAudioManager.AudioDevice.SPEAKER_PHONE : SignalAudioManager.AudioDevice.EARPIECE,
|
||||
false);
|
||||
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context, isVideoCall, false));
|
||||
webRtcInteractor.initializeAudioForCall();
|
||||
webRtcInteractor.startOutgoingRinger();
|
||||
|
||||
if (!webRtcInteractor.addNewOutgoingCall(remotePeer.getId(), remotePeer.getCallId().longValue(), isVideoCall)) {
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.telecom.DisconnectCause
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.telecom.CallAttributesCompat
|
||||
import androidx.core.telecom.CallEndpointCompat
|
||||
import androidx.core.telecom.CallsManager
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.AudioOutputOption
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
|
||||
sealed class TelecomCommand {
|
||||
object Activate : TelecomCommand()
|
||||
data class Disconnect(val cause: Int) : TelecomCommand()
|
||||
data class ChangeEndpoint(val device: SignalAudioManager.AudioDevice) : TelecomCommand()
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
class TelecomCallController(
|
||||
private val context: Context,
|
||||
private val recipientId: RecipientId,
|
||||
private val callId: Long,
|
||||
private val isVideoCall: Boolean,
|
||||
private val isOutgoing: Boolean,
|
||||
private val callsManager: CallsManager
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(TelecomCallController::class.java)
|
||||
}
|
||||
|
||||
private val commandChannel = Channel<TelecomCommand>(Channel.BUFFERED)
|
||||
|
||||
@Volatile
|
||||
var currentAudioDevice: SignalAudioManager.AudioDevice = SignalAudioManager.AudioDevice.NONE
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
private var cachedEndpoints: List<CallEndpointCompat> = emptyList()
|
||||
|
||||
@Volatile
|
||||
private var disconnected: Boolean = false
|
||||
|
||||
fun getAvailableAudioOutputOptions(): List<AudioOutputOption> {
|
||||
return cachedEndpoints
|
||||
.map { AudioOutputOption(it.name.toString(), it.type.toAudioDevice(), it.type) }
|
||||
.distinctBy { it.deviceType }
|
||||
.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
|
||||
}
|
||||
|
||||
fun getCurrentEndpointDeviceId(): Int {
|
||||
return cachedEndpoints.firstOrNull { it.type.toAudioDevice() == currentAudioDevice }?.type ?: -1
|
||||
}
|
||||
|
||||
fun activate() {
|
||||
Log.i(TAG, "activate() recipientId=$recipientId callId=$callId")
|
||||
commandChannel.trySend(TelecomCommand.Activate)
|
||||
}
|
||||
|
||||
fun disconnect(cause: Int) {
|
||||
if (disconnected) {
|
||||
Log.i(TAG, "disconnect(cause=$cause) already disconnected, ignoring")
|
||||
return
|
||||
}
|
||||
disconnected = true
|
||||
|
||||
Log.i(TAG, "disconnect(cause=$cause) recipientId=$recipientId callId=$callId")
|
||||
commandChannel.trySend(TelecomCommand.Disconnect(cause))
|
||||
}
|
||||
|
||||
fun requestEndpointChange(device: SignalAudioManager.AudioDevice) {
|
||||
Log.i(TAG, "requestEndpointChange($device) recipientId=$recipientId")
|
||||
commandChannel.trySend(TelecomCommand.ChangeEndpoint(device))
|
||||
}
|
||||
|
||||
suspend fun start() {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val displayName = if (SignalStore.settings.messageNotificationsPrivacy.isDisplayContact) recipient.getDisplayName(context) else context.getString(R.string.Recipient_signal_call)
|
||||
val address = Uri.fromParts("sip", recipientId.serialize(), null)
|
||||
|
||||
val direction = if (isOutgoing) CallAttributesCompat.DIRECTION_OUTGOING else CallAttributesCompat.DIRECTION_INCOMING
|
||||
val callType = if (isVideoCall) CallAttributesCompat.CALL_TYPE_VIDEO_CALL else CallAttributesCompat.CALL_TYPE_AUDIO_CALL
|
||||
|
||||
val attributes = CallAttributesCompat(
|
||||
displayName = displayName,
|
||||
address = address,
|
||||
direction = direction,
|
||||
callType = callType
|
||||
)
|
||||
|
||||
Log.i(TAG, "start() recipientId=$recipientId callId=$callId isOutgoing=$isOutgoing isVideo=$isVideoCall")
|
||||
|
||||
callsManager.addCall(
|
||||
callAttributes = attributes,
|
||||
onAnswer = { callType -> onAnswer(callType) },
|
||||
onDisconnect = { cause -> onDisconnect(cause) },
|
||||
onSetActive = { onSetActive() },
|
||||
onSetInactive = { onSetInactive() }
|
||||
) {
|
||||
Log.i(TAG, "addCall block entered, callControlScope active for callId=$callId")
|
||||
|
||||
if (isOutgoing) {
|
||||
Log.i(TAG, "Posting outgoing call notification immediately for callId=$callId")
|
||||
try {
|
||||
val notification = CallNotificationBuilder.getCallInProgressNotification(
|
||||
context,
|
||||
CallNotificationBuilder.TYPE_OUTGOING_RINGING,
|
||||
recipient,
|
||||
isVideoCall,
|
||||
true
|
||||
)
|
||||
NotificationManagerCompat.from(context).notify(CallNotificationBuilder.WEBRTC_NOTIFICATION, notification)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Failed to post outgoing call notification", e)
|
||||
}
|
||||
}
|
||||
|
||||
AppDependencies.signalCallManager.setTelecomApproved(callId, recipientId)
|
||||
Log.i(TAG, "setTelecomApproved fired for callId=$callId recipientId=$recipientId")
|
||||
|
||||
var needToResetAudioRoute = isOutgoing && !isVideoCall
|
||||
var initialEndpoint: SignalAudioManager.AudioDevice? = null
|
||||
|
||||
launch {
|
||||
currentCallEndpoint.collect { endpoint ->
|
||||
val activeDevice = endpoint.type.toAudioDevice()
|
||||
Log.i(TAG, "currentCallEndpoint changed: ${endpoint.name} (type=${endpoint.type}) -> $activeDevice")
|
||||
currentAudioDevice = activeDevice
|
||||
|
||||
val available = cachedEndpoints.map { it.type.toAudioDevice() }.toSet()
|
||||
AppDependencies.signalCallManager.onAudioDeviceChanged(activeDevice, available)
|
||||
|
||||
if (needToResetAudioRoute) {
|
||||
if (initialEndpoint == null) {
|
||||
initialEndpoint = activeDevice
|
||||
} else if (activeDevice == SignalAudioManager.AudioDevice.SPEAKER_PHONE) {
|
||||
Log.i(TAG, "Resetting audio route from SPEAKER_PHONE to $initialEndpoint")
|
||||
val resetTarget = cachedEndpoints.firstOrNull { it.type.toAudioDevice() == initialEndpoint }
|
||||
if (resetTarget != null) {
|
||||
requestEndpointChange(resetTarget)
|
||||
}
|
||||
needToResetAudioRoute = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
isMuted.collect { muted ->
|
||||
Log.i(TAG, "isMuted changed: $muted for callId=$callId")
|
||||
AppDependencies.signalCallManager.setMuteAudio(muted)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
availableEndpoints.collect { endpoints ->
|
||||
cachedEndpoints = endpoints
|
||||
val available = endpoints.map { it.type.toAudioDevice() }.toSet()
|
||||
Log.i(TAG, "availableEndpoints changed: $available (${endpoints.size} endpoints)")
|
||||
AppDependencies.signalCallManager.onAudioDeviceChanged(currentAudioDevice, available)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
for (command in commandChannel) {
|
||||
when (command) {
|
||||
is TelecomCommand.Activate -> {
|
||||
val result = setActive()
|
||||
Log.i(TAG, "setActive result: $result")
|
||||
needToResetAudioRoute = false
|
||||
}
|
||||
is TelecomCommand.Disconnect -> {
|
||||
val result = disconnect(DisconnectCause(command.cause))
|
||||
Log.i(TAG, "disconnect result: $result")
|
||||
break
|
||||
}
|
||||
is TelecomCommand.ChangeEndpoint -> {
|
||||
val targetDevice = command.device
|
||||
val target = cachedEndpoints.firstOrNull { it.type.toAudioDevice() == targetDevice }
|
||||
if (target != null) {
|
||||
val result = requestEndpointChange(target)
|
||||
Log.i(TAG, "requestEndpointChange($targetDevice) result: $result")
|
||||
} else {
|
||||
Log.w(TAG, "No endpoint found for device: $targetDevice, available: ${cachedEndpoints.map { it.type.toAudioDevice() }}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAnswer(callType: Int) {
|
||||
val hasRecordAudio = Permissions.hasAll(context, android.Manifest.permission.RECORD_AUDIO)
|
||||
Log.i(TAG, "onAnswer(callType=$callType) recipientId=$recipientId hasRecordAudio=$hasRecordAudio")
|
||||
if (hasRecordAudio) {
|
||||
AppDependencies.signalCallManager.acceptCall(false)
|
||||
} else {
|
||||
Log.i(TAG, "Missing RECORD_AUDIO permission, launching CallIntent activity")
|
||||
val intent = CallIntent.Builder(context)
|
||||
.withAddedIntentFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.withAction(if (isVideoCall) CallIntent.Action.ANSWER_VIDEO else CallIntent.Action.ANSWER_AUDIO)
|
||||
.build()
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDisconnect(cause: DisconnectCause) {
|
||||
Log.i(TAG, "onDisconnect(code=${cause.code}, reason=${cause.reason})")
|
||||
when (cause.code) {
|
||||
DisconnectCause.REJECTED -> {
|
||||
Log.i(TAG, "Call rejected via system UI")
|
||||
ActiveCallManager.denyCall()
|
||||
}
|
||||
DisconnectCause.LOCAL -> {
|
||||
Log.i(TAG, "Local hangup via system UI")
|
||||
ActiveCallManager.hangup()
|
||||
}
|
||||
DisconnectCause.REMOTE,
|
||||
DisconnectCause.MISSED,
|
||||
DisconnectCause.CANCELED -> {
|
||||
Log.i(TAG, "Remote/missed/canceled disconnect, no action needed (handled by Signal processors)")
|
||||
}
|
||||
DisconnectCause.ERROR -> {
|
||||
Log.w(TAG, "Disconnect due to error, performing local hangup as fallback")
|
||||
ActiveCallManager.hangup()
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown disconnect cause: ${cause.code}, performing local hangup")
|
||||
ActiveCallManager.hangup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSetActive() {
|
||||
Log.i(TAG, "onSetActive()")
|
||||
}
|
||||
|
||||
private fun onSetInactive() {
|
||||
Log.i(TAG, "onSetInactive()")
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
private fun Int.toAudioDevice(): SignalAudioManager.AudioDevice {
|
||||
return when (this) {
|
||||
CallEndpointCompat.TYPE_EARPIECE -> SignalAudioManager.AudioDevice.EARPIECE
|
||||
CallEndpointCompat.TYPE_SPEAKER -> SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
CallEndpointCompat.TYPE_BLUETOOTH -> SignalAudioManager.AudioDevice.BLUETOOTH
|
||||
CallEndpointCompat.TYPE_WIRED_HEADSET -> SignalAudioManager.AudioDevice.WIRED_HEADSET
|
||||
CallEndpointCompat.TYPE_STREAMING -> SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
else -> SignalAudioManager.AudioDevice.EARPIECE
|
||||
}
|
||||
}
|
||||
@@ -290,7 +290,7 @@ public abstract class WebRtcActionProcessor {
|
||||
RemotePeer peer = currentState.getCallInfoState().getPeerByCallId(new CallId(callId));
|
||||
if (peer == null || !peer.callIdEquals(currentState.getCallInfoState().getActivePeer())) {
|
||||
Log.w(tag, "Received telecom approval after call terminated. callId: " + callId + " recipient: " + recipientId);
|
||||
webRtcInteractor.terminateCall(recipientId);
|
||||
webRtcInteractor.terminateCall(recipientId, android.telecom.DisconnectCause.LOCAL);
|
||||
return currentState;
|
||||
}
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ public class WebRtcInteractor {
|
||||
ActiveCallManager.sendAudioManagerCommand(context, new AudioManagerCommand.SilenceIncomingRinger());
|
||||
}
|
||||
|
||||
void initializeAudioForCall() {
|
||||
ActiveCallManager.sendAudioManagerCommand(context, new AudioManagerCommand.Initialize());
|
||||
void initializeAudioForCall(boolean isGroupCall) {
|
||||
ActiveCallManager.sendAudioManagerCommand(context, new AudioManagerCommand.Initialize(isGroupCall));
|
||||
}
|
||||
|
||||
void startIncomingRinger(@Nullable Uri ringtoneUri, boolean vibrate) {
|
||||
@@ -186,6 +186,10 @@ public class WebRtcInteractor {
|
||||
AndroidTelecomUtil.terminateCall(recipientId);
|
||||
}
|
||||
|
||||
public void terminateCall(RecipientId recipientId, int disconnectCause) {
|
||||
AndroidTelecomUtil.terminateCall(recipientId, disconnectCause);
|
||||
}
|
||||
|
||||
public boolean addNewIncomingCall(RecipientId recipientId, long callId, boolean remoteVideoOffer) {
|
||||
return AndroidTelecomUtil.addIncomingCall(recipientId, callId, remoteVideoOffer);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user