Integrate calling with Android Telecom system.

This commit is contained in:
Cody Henthorne
2022-02-23 13:16:25 -05:00
committed by Alex Hart
parent 2ed39e4448
commit d6b6884c69
31 changed files with 920 additions and 332 deletions
+9
View File
@@ -91,6 +91,8 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<application android:name=".ApplicationContext" <application android:name=".ApplicationContext"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@@ -628,6 +630,13 @@
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/> <service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/> <service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/> <service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
<service android:name=".service.webrtc.AndroidCallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<service android:name=".components.voice.VoiceNotePlaybackService"> <service android:name=".components.voice.VoiceNotePlaybackService">
<intent-filter> <intent-filter>
@@ -35,30 +35,28 @@ import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer; import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs; import org.signal.glide.SignalGlideCodecs;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.signal.ringrtc.CallManager; import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage; import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider; import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.LogDatabase; import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider; import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource; import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmJobService; import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob; import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob; import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob; import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob; import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
@@ -66,6 +64,7 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil; import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
@@ -78,6 +77,7 @@ import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener; import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.AppStartup;
@@ -91,7 +91,6 @@ import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider; import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import java.io.IOException;
import java.net.SocketException; import java.net.SocketException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.security.Security; import java.security.Security;
@@ -196,6 +195,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge())) .addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this)) .addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
.addPostRender(RetrieveReleaseChannelJob::enqueue) .addPostRender(RetrieveReleaseChannelJob::enqueue)
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
.execute(); .execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -82,7 +82,7 @@ class WebRtcViewModel(state: WebRtcServiceState) {
} }
val state: State = state.callInfoState.callState val state: State = state.callInfoState.callState
val groupState: GroupCallState = state.callInfoState.groupCallState val groupState: GroupCallState = state.callInfoState.groupState
val recipient: Recipient = state.callInfoState.callRecipient val recipient: Recipient = state.callInfoState.callRecipient
val isRemoteVideoOffer: Boolean = state.getCallSetupState(state.callInfoState.activePeer?.callId).isRemoteVideoOffer val isRemoteVideoOffer: Boolean = state.getCallSetupState(state.callInfoState.activePeer?.callId).isRemoteVideoOffer
val callConnectedTime: Long = state.callInfoState.callConnectedTime val callConnectedTime: Long = state.callInfoState.callConnectedTime
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.emoji.EmojiFiles;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor; import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.util.AppSignatureUtil; import org.thoughtcrime.securesms.util.AppSignatureUtil;
import org.thoughtcrime.securesms.util.ByteUnit; import org.thoughtcrime.securesms.util.ByteUnit;
import org.thoughtcrime.securesms.util.DeviceProperties; import org.thoughtcrime.securesms.util.DeviceProperties;
@@ -74,6 +75,7 @@ public class LogSectionSystemInfo implements LogSection {
builder.append("Days Installed: ").append(VersionTracker.getDaysSinceFirstInstalled(context)).append("\n"); builder.append("Days Installed: ").append(VersionTracker.getDaysSinceFirstInstalled(context)).append("\n");
builder.append("Build Variant : ").append(BuildConfig.BUILD_DISTRIBUTION_TYPE).append(BuildConfig.BUILD_ENVIRONMENT_TYPE).append(BuildConfig.BUILD_VARIANT_TYPE).append("\n"); builder.append("Build Variant : ").append(BuildConfig.BUILD_DISTRIBUTION_TYPE).append(BuildConfig.BUILD_ENVIRONMENT_TYPE).append(BuildConfig.BUILD_VARIANT_TYPE).append("\n");
builder.append("Emoji Version : ").append(getEmojiVersionString(context)).append("\n"); builder.append("Emoji Version : ").append(getEmojiVersionString(context)).append("\n");
builder.append("Telecom : ").append(AndroidTelecomUtil.getTelecomSupported()).append("\n");
builder.append("User-Agent : ").append(StandardUserAgentInterceptor.USER_AGENT).append("\n"); builder.append("User-Agent : ").append(StandardUserAgentInterceptor.USER_AGENT).append("\n");
builder.append("App : "); builder.append("App : ");
try { try {
@@ -0,0 +1,97 @@
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.util.logging.Log
import org.thoughtcrime.securesms.WebRtcCallActivity
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.permissions.Permissions
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, val recipientId: RecipientId, val isOutgoing: Boolean = false) : Connection() {
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()")
WebRtcCallService.update(context, CallNotificationBuilder.TYPE_INCOMING_RINGING, recipientId)
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()
ApplicationDependencies.getSignalCallManager().onAudioDeviceChanged(activeDevice, availableDevices)
}
override fun onAnswer(videoState: Int) {
Log.i(TAG, "onAnswer($videoState)")
if (Permissions.hasAll(context, android.Manifest.permission.RECORD_AUDIO)) {
ApplicationDependencies.getSignalCallManager().acceptCall(false)
} else {
val intent = Intent(context, WebRtcCallActivity::class.java)
intent.action = WebRtcCallActivity.ANSWER_ACTION
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
}
override fun onSilence() {
WebRtcCallService.sendAudioManagerCommand(context, AudioManagerCommand.SilenceIncomingRinger())
}
override fun onReject() {
Log.i(TAG, "onReject()")
WebRtcCallService.denyCall(context)
}
override fun onDisconnect() {
Log.i(TAG, "onDisconnect()")
WebRtcCallService.hangup(context)
}
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
}
@@ -0,0 +1,104 @@
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.ApplicationDependencies
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) = request.getOurExtras()
Log.i(TAG, "onCreateIncomingConnection($recipientId)")
val recipient = Recipient.resolved(recipientId)
val displayName = recipient.getDisplayName(this)
val connection = AndroidCallConnection(applicationContext, recipientId).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
ApplicationDependencies.getSignalCallManager().setTelecomApproved(callId)
return connection
}
override fun onCreateIncomingConnectionFailed(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: ConnectionRequest
) {
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
Log.i(TAG, "onCreateIncomingConnectionFailed($recipientId)")
ApplicationDependencies.getSignalCallManager().dropCall(callId)
}
override fun onCreateOutgoingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: ConnectionRequest
): Connection {
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
Log.i(TAG, "onCreateOutgoingConnection($recipientId)")
val connection = AndroidCallConnection(applicationContext, recipientId, true).apply {
videoState = request.videoState
extras = request.extras
setDialing()
}
AndroidTelecomUtil.connections[recipientId] = connection
ApplicationDependencies.getSignalCallManager().setTelecomApproved(callId)
return connection
}
override fun onCreateOutgoingConnectionFailed(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: ConnectionRequest
) {
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
Log.i(TAG, "onCreateOutgoingConnectionFailed($recipientId)")
ApplicationDependencies.getSignalCallManager().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"
}
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)
return ServiceExtras(recipientId, callId)
}
private data class ServiceExtras(val recipientId: RecipientId, val callId: Long)
}
@@ -0,0 +1,201 @@
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 org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.RecipientId
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.
*/
@SuppressLint("NewApi", "InlinedApi")
object AndroidTelecomUtil {
private val TAG = Log.tag(AndroidTelecomUtil::class.java)
private val context = ApplicationDependencies.getApplication()
private var systemRejected = false
private var accountRegistered = false
@JvmStatic
val telecomSupported: Boolean
get() {
if (Build.VERSION.SDK_INT >= 26 && !systemRejected) {
if (!accountRegistered) {
registerPhoneAccount()
}
if (accountRegistered) {
val phoneAccount = ContextCompat.getSystemService(context, TelecomManager::class.java)!!.getPhoneAccount(getPhoneAccountHandle())
if (phoneAccount != null && phoneAccount.isEnabled) {
return true
}
}
}
return false
}
@JvmStatic
val connections: MutableMap<RecipientId, AndroidCallConnection> = 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()
try {
ContextCompat.getSystemService(context, TelecomManager::class.java)!!.registerPhoneAccount(phoneAccount)
Log.i(TAG, "Phone account registered successfully")
accountRegistered = true
} catch (e: Exception) {
Log.w(TAG, "Unable to register telecom account", 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,
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
)
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
}
}
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))
}
connection.destroy()
connections.remove(recipientId)
}
}
}
@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
),
)
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
}
}
return true
}
fun selectAudioDevice(recipientId: RecipientId, device: SignalAudioManager.AudioDevice) {
if (telecomSupported) {
val connection: AndroidCallConnection? = connections[recipientId]
Log.i(TAG, "Selecting audio route: $device connection: ${connection != null}")
if (connection != 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)
}
}
}
}
fun getSelectedAudioDevice(recipientId: RecipientId): SignalAudioManager.AudioDevice {
if (telecomSupported) {
val connection: AndroidCallConnection? = connections[recipientId]
if (connection != 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
}
}
}
return SignalAudioManager.AudioDevice.NONE
}
}
@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)
}
@@ -69,7 +69,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
} }
@Override @Override
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) {
remotePeer.answering(); remotePeer.answering();
Log.i(tag, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); Log.i(tag, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
@@ -78,8 +78,17 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
webRtcInteractor.retrieveTurnServers(remotePeer); webRtcInteractor.retrieveTurnServers(remotePeer);
webRtcInteractor.initializeAudioForCall(); webRtcInteractor.initializeAudioForCall();
if (!webRtcInteractor.addNewIncomingCall(remotePeer.getId(), remotePeer.getCallId().longValue(), offerType == OfferMessage.Type.VIDEO_CALL)) {
Log.i(tag, "Unable to add new incoming call");
return handleDropCall(currentState, remotePeer.getCallId().longValue());
}
return currentState.builder() return currentState.builder()
.actionProcessor(new IncomingCallActionProcessor(webRtcInteractor)) .actionProcessor(new IncomingCallActionProcessor(webRtcInteractor))
.changeCallSetupState(remotePeer.getCallId())
.waitForTelecom(AndroidTelecomUtil.getTelecomSupported())
.telecomApproved(false)
.commit()
.changeCallInfoState() .changeCallInfoState()
.callRecipient(remotePeer.getRecipient()) .callRecipient(remotePeer.getRecipient())
.activePeer(remotePeer) .activePeer(remotePeer)
@@ -10,7 +10,6 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager; import org.thoughtcrime.securesms.webrtc.locks.LockManager;
@@ -40,6 +39,7 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
ApplicationDependencies.getAppForegroundObserver().removeListener(webRtcInteractor.getForegroundListener()); ApplicationDependencies.getAppForegroundObserver().removeListener(webRtcInteractor.getForegroundListener());
webRtcInteractor.startAudioCommunication(); webRtcInteractor.startAudioCommunication();
webRtcInteractor.activateCall(activePeer.getId());
activePeer.connected(); activePeer.connected();
@@ -75,9 +75,9 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
} }
if (currentState.getCallSetupState(activePeer).isAcceptWithVideo() || currentState.getLocalDeviceState().getCameraState().isEnabled()) { if (currentState.getCallSetupState(activePeer).isAcceptWithVideo() || currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.setDefaultAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE, false); webRtcInteractor.setDefaultAudioDevice(activePeer.getId(), SignalAudioManager.AudioDevice.SPEAKER_PHONE, false);
} else { } else {
webRtcInteractor.setDefaultAudioDevice(SignalAudioManager.AudioDevice.EARPIECE, false); webRtcInteractor.setDefaultAudioDevice(activePeer.getId(), SignalAudioManager.AudioDevice.EARPIECE, false);
} }
return currentState; return currentState;
@@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
@@ -40,7 +41,8 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor {
protected @NonNull WebRtcServiceState handleSetUserAudioDevice(@NonNull WebRtcServiceState currentState, @NonNull SignalAudioManager.AudioDevice userDevice) { protected @NonNull WebRtcServiceState handleSetUserAudioDevice(@NonNull WebRtcServiceState currentState, @NonNull SignalAudioManager.AudioDevice userDevice) {
Log.i(tag, "handleSetUserAudioDevice(): userDevice: " + userDevice); Log.i(tag, "handleSetUserAudioDevice(): userDevice: " + userDevice);
webRtcInteractor.setUserAudioDevice(userDevice); RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
webRtcInteractor.setUserAudioDevice(activePeer != null ? activePeer.getId() : null, userDevice);
return currentState; return currentState;
} }
@@ -26,11 +26,8 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
private static final String TAG = Log.tag(GroupJoiningActionProcessor.class); private static final String TAG = Log.tag(GroupJoiningActionProcessor.class);
private final CallSetupActionProcessorDelegate callSetupDelegate;
public GroupJoiningActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) { public GroupJoiningActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG); super(webRtcInteractor, TAG);
callSetupDelegate = new CallSetupActionProcessorDelegate(webRtcInteractor, TAG);
} }
@Override @Override
@@ -97,6 +94,7 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
.changeLocalDeviceState() .changeLocalDeviceState()
.commit() .commit()
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor)); .actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor));
} else if (device.getJoinState() == GroupCall.JoinState.JOINING) { } else if (device.getJoinState() == GroupCall.JoinState.JOINING) {
builder.changeCallInfoState() builder.changeCallInfoState()
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING) .groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
@@ -34,11 +34,11 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
beginCallDelegate = new BeginCallActionProcessorDelegate(webRtcInteractor, TAG); beginCallDelegate = new BeginCallActionProcessorDelegate(webRtcInteractor, TAG);
} }
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) {
Log.i(TAG, "handleStartIncomingCall():"); Log.i(TAG, "handleStartIncomingCall():");
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState, remotePeer.getCallId().longValue()); currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState, remotePeer.getCallId().longValue());
return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer); return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer, offerType);
} }
@Override @Override
@@ -20,9 +20,12 @@ import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CallState; import org.thoughtcrime.securesms.ringrtc.CallState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.CallSetupState;
import org.thoughtcrime.securesms.service.webrtc.state.VideoState; import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.NetworkUtil; import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager; import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
@@ -59,8 +62,37 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
@NonNull List<PeerConnection.IceServer> iceServers, @NonNull List<PeerConnection.IceServer> iceServers,
boolean isAlwaysTurn) boolean isAlwaysTurn)
{ {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer(); RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
boolean hideIp = !activePeer.getRecipient().isSystemContact() || isAlwaysTurn;
Log.i(TAG, "handleTurnServerUpdate(): call_id: " + activePeer.getCallId());
currentState = currentState.builder()
.changeCallSetupState(activePeer.getCallId())
.iceServers(iceServers)
.alwaysTurn(isAlwaysTurn)
.build();
return proceed(currentState);
}
@Override
protected @NonNull WebRtcServiceState handleSetTelecomApproved(@NonNull WebRtcServiceState currentState, long callId) {
return proceed(super.handleSetTelecomApproved(currentState, callId));
}
private @NonNull WebRtcServiceState proceed(@NonNull WebRtcServiceState currentState) {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
CallSetupState callSetupState = currentState.getCallSetupState(activePeer.getCallId());
if (callSetupState.getIceServers().isEmpty() || (callSetupState.shouldWaitForTelecomApproval() && !callSetupState.isTelecomApproved())) {
Log.i(TAG, "Unable to proceed without ice server and telecom approval" +
" iceServers: " + Util.hasItems(callSetupState.getIceServers()) +
" waitForTelecom: " + callSetupState.shouldWaitForTelecomApproval() +
" telecomApproved: " + callSetupState.isTelecomApproved());
return currentState;
}
boolean hideIp = !activePeer.getRecipient().isSystemContact() || callSetupState.isAlwaysTurnServers();
VideoState videoState = currentState.getVideoState(); VideoState videoState = currentState.getVideoState();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient())); CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
@@ -72,7 +104,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
videoState.requireLocalSink(), videoState.requireLocalSink(),
callParticipant.getVideoSink(), callParticipant.getVideoSink(),
videoState.requireCamera(), videoState.requireCamera(),
iceServers, callSetupState.getIceServers(),
hideIp, hideIp,
NetworkUtil.getCallingBandwidthMode(context), NetworkUtil.getCallingBandwidthMode(context),
null, null,
@@ -87,6 +119,11 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
return currentState; return currentState;
} }
@Override
protected @NonNull WebRtcServiceState handleDropCall(@NonNull WebRtcServiceState currentState, long callId) {
return callSetupDelegate.handleDropCall(currentState, callId);
}
@Override @Override
protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) { protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer(); RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
@@ -120,10 +157,11 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
Log.i(TAG, "handleDenyCall():"); Log.i(TAG, "handleDenyCall():");
try { try {
webRtcInteractor.rejectIncomingCall(activePeer.getId());
webRtcInteractor.getCallManager().hangup(); webRtcInteractor.getCallManager().hangup();
SignalDatabase.sms().insertMissedCall(activePeer.getId(), System.currentTimeMillis(), currentState.getCallSetupState(activePeer).isRemoteVideoOffer()); SignalDatabase.sms().insertMissedCall(activePeer.getId(), System.currentTimeMillis(), currentState.getCallSetupState(activePeer).isRemoteVideoOffer());
return terminate(currentState, activePeer); return terminate(currentState, activePeer);
} catch (CallException e) { } catch (CallException e) {
return callFailure(currentState, "hangup() failed: ", e); return callFailure(currentState, "hangup() failed: ", e);
} }
} }
@@ -174,7 +212,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
} }
@Override @Override
protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) { protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
return activeCallDelegate.handleRemoteVideoEnable(currentState, enable); return activeCallDelegate.handleRemoteVideoEnable(currentState, enable);
} }
@@ -199,7 +237,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
} }
@Override @Override
protected @NonNull WebRtcServiceState handleSetupFailure(@NonNull WebRtcServiceState currentState, @NonNull CallId callId) { protected @NonNull WebRtcServiceState handleSetupFailure(@NonNull WebRtcServiceState currentState, @NonNull CallId callId) {
return activeCallDelegate.handleSetupFailure(currentState, callId); return activeCallDelegate.handleSetupFailure(currentState, callId);
} }
@@ -227,8 +227,8 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
long ringId = currentState.getCallSetupState(RemotePeer.GROUP_CALL_ID).getRingId(); long ringId = currentState.getCallSetupState(RemotePeer.GROUP_CALL_ID).getRingId();
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId, SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId,
System.currentTimeMillis(), System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE); CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE);
try { try {
webRtcInteractor.getCallManager().cancelGroupRing(groupId.get().getDecodedId(), webRtcInteractor.getCallManager().cancelGroupRing(groupId.get().getDecodedId(),
@@ -10,7 +10,6 @@ import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId; import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallManager; import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper; import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -19,10 +18,12 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata; import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
import org.thoughtcrime.securesms.service.webrtc.state.CallSetupState;
import org.thoughtcrime.securesms.service.webrtc.state.VideoState; import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.NetworkUtil; import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
@@ -68,13 +69,18 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
boolean isVideoCall = offerType == OfferMessage.Type.VIDEO_CALL; boolean isVideoCall = offerType == OfferMessage.Type.VIDEO_CALL;
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer); webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer);
webRtcInteractor.setDefaultAudioDevice(isVideoCall ? SignalAudioManager.AudioDevice.SPEAKER_PHONE webRtcInteractor.setDefaultAudioDevice(remotePeer.getId(),
: SignalAudioManager.AudioDevice.EARPIECE, isVideoCall ? SignalAudioManager.AudioDevice.SPEAKER_PHONE : SignalAudioManager.AudioDevice.EARPIECE,
false); false);
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context)); webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall(); webRtcInteractor.initializeAudioForCall();
webRtcInteractor.startOutgoingRinger(); webRtcInteractor.startOutgoingRinger();
if (!webRtcInteractor.addNewOutgoingCall(remotePeer.getId(), remotePeer.getCallId().longValue(), isVideoCall)) {
Log.i(TAG, "Unable to add new outgoing call");
return handleDropCall(currentState, remotePeer.getCallId().longValue());
}
RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, Recipient.resolved(remotePeer.getId()), SignalDatabase.threads().getThreadIdIfExistsFor(remotePeer.getId())); RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, Recipient.resolved(remotePeer.getId()), SignalDatabase.threads().getThreadIdIfExistsFor(remotePeer.getId()));
SignalDatabase.sms().insertOutgoingCall(remotePeer.getId(), isVideoCall); SignalDatabase.sms().insertOutgoingCall(remotePeer.getId(), isVideoCall);
@@ -84,6 +90,8 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
return builder.changeCallSetupState(remotePeer.getCallId()) return builder.changeCallSetupState(remotePeer.getCallId())
.enableVideoOnCreate(isVideoCall) .enableVideoOnCreate(isVideoCall)
.waitForTelecom(AndroidTelecomUtil.getTelecomSupported())
.telecomApproved(false)
.commit() .commit()
.changeCallInfoState() .changeCallInfoState()
.activePeer(remotePeer) .activePeer(remotePeer)
@@ -98,11 +106,40 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
@NonNull List<PeerConnection.IceServer> iceServers, @NonNull List<PeerConnection.IceServer> iceServers,
boolean isAlwaysTurn) boolean isAlwaysTurn)
{ {
try { RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
VideoState videoState = currentState.getVideoState();
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
Log.i(TAG, "handleTurnServerUpdate(): call_id: " + activePeer.getCallId());
currentState = currentState.builder()
.changeCallSetupState(activePeer.getCallId())
.iceServers(iceServers)
.alwaysTurn(isAlwaysTurn)
.build();
return proceed(currentState);
}
@Override
protected @NonNull WebRtcServiceState handleSetTelecomApproved(@NonNull WebRtcServiceState currentState, long callId) {
return proceed(super.handleSetTelecomApproved(currentState, callId));
}
private @NonNull WebRtcServiceState proceed(@NonNull WebRtcServiceState currentState) {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
CallSetupState callSetupState = currentState.getCallSetupState(activePeer);
if (callSetupState.getIceServers().isEmpty() || (callSetupState.shouldWaitForTelecomApproval() && !callSetupState.isTelecomApproved())) {
Log.i(TAG, "Unable to proceed without ice server and telecom approval" +
" iceServers: " + Util.hasItems(callSetupState.getIceServers()) +
" waitForTelecom: " + callSetupState.shouldWaitForTelecomApproval() +
" telecomApproved: " + callSetupState.isTelecomApproved());
return currentState;
}
VideoState videoState = currentState.getVideoState();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
try {
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(), webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
context, context,
videoState.getLockableEglBase().require(), videoState.getLockableEglBase().require(),
@@ -110,8 +147,8 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
videoState.requireLocalSink(), videoState.requireLocalSink(),
callParticipant.getVideoSink(), callParticipant.getVideoSink(),
videoState.requireCamera(), videoState.requireCamera(),
iceServers, callSetupState.getIceServers(),
isAlwaysTurn, callSetupState.isAlwaysTurnServers(),
NetworkUtil.getCallingBandwidthMode(context), NetworkUtil.getCallingBandwidthMode(context),
null, null,
currentState.getCallSetupState(activePeer).isEnableVideoOnCreate()); currentState.getCallSetupState(activePeer).isEnableVideoOnCreate());
@@ -125,6 +162,11 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
.build(); .build();
} }
@Override
protected @NonNull WebRtcServiceState handleDropCall(@NonNull WebRtcServiceState currentState, long callId) {
return callSetupDelegate.handleDropCall(currentState, callId);
}
@Override @Override
protected @NonNull WebRtcServiceState handleRemoteRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { protected @NonNull WebRtcServiceState handleRemoteRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId()); Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId());
@@ -34,7 +34,7 @@ public class PreJoinActionProcessor extends DeviceAwareActionProcessor {
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor)); return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
} }
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) {
Log.i(TAG, "handleStartIncomingCall():"); Log.i(TAG, "handleStartIncomingCall():");
EglBaseWrapper.replaceHolder(EglBaseWrapper.OUTGOING_PLACEHOLDER, remotePeer.getCallId().longValue()); EglBaseWrapper.replaceHolder(EglBaseWrapper.OUTGOING_PLACEHOLDER, remotePeer.getCallId().longValue());
@@ -45,7 +45,7 @@ public class PreJoinActionProcessor extends DeviceAwareActionProcessor {
.build(); .build();
webRtcInteractor.postStateUpdate(currentState); webRtcInteractor.postStateUpdate(currentState);
return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer); return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer, offerType);
} }
@Override @Override
@@ -295,6 +295,14 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleSetUserAudioDevice(s, desiredDevice)); process((s, p) -> p.handleSetUserAudioDevice(s, desiredDevice));
} }
public void setTelecomApproved(long callId) {
process((s, p) -> p.handleSetTelecomApproved(s, callId));
}
public void dropCall(long callId) {
process((s, p) -> p.handleDropCall(s, callId));
}
public void peekGroupCall(@NonNull RecipientId id) { public void peekGroupCall(@NonNull RecipientId id) {
if (callManager == null) { if (callManager == null) {
Log.i(TAG, "Unable to peekGroupCall, call manager is null"); Log.i(TAG, "Unable to peekGroupCall, call manager is null");
@@ -401,7 +409,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
if (isOutgoing) { if (isOutgoing) {
return p.handleStartOutgoingCall(s, remotePeer, WebRtcUtil.getOfferTypeFromCallMediaType(callMediaType)); return p.handleStartOutgoingCall(s, remotePeer, WebRtcUtil.getOfferTypeFromCallMediaType(callMediaType));
} else { } else {
return p.handleStartIncomingCall(s, remotePeer); return p.handleStartIncomingCall(s, remotePeer, WebRtcUtil.getOfferTypeFromCallMediaType(callMediaType));
} }
}); });
} }
@@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper; import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil; import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipant;
@@ -244,7 +243,7 @@ public abstract class WebRtcActionProcessor {
return terminate(currentState, remotePeer); return terminate(currentState, remotePeer);
} }
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) {
Log.i(tag, "handleStartIncomingCall not processed"); Log.i(tag, "handleStartIncomingCall not processed");
return currentState; return currentState;
} }
@@ -254,6 +253,45 @@ public abstract class WebRtcActionProcessor {
return currentState; return currentState;
} }
protected @NonNull WebRtcServiceState handleSetTelecomApproved(@NonNull WebRtcServiceState currentState, long callId) {
Log.i(tag, "handleSetTelecomApproved(): call_id: " + callId);
currentState = currentState.builder()
.changeCallSetupState(new CallId(callId))
.telecomApproved(true)
.build();
return currentState;
}
protected @NonNull WebRtcServiceState handleDropCall(@NonNull WebRtcServiceState currentState, long callId) {
Log.i(tag, "handleDropCall(): call_id: " + callId);
CallId id = new CallId(callId);
RemotePeer callIdPeer = currentState.getCallInfoState().getPeerByCallId(id);
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
boolean isActivePeer = activePeer != null && activePeer.getCallId().equals(id);
try {
if (callIdPeer != null && currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_INCOMING) {
webRtcInteractor.insertMissedCall(callIdPeer, callIdPeer.getCallStartTimestamp(), currentState.getCallSetupState(id).isRemoteVideoOffer());
}
webRtcInteractor.getCallManager().hangup();
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.postStateUpdate(currentState);
return terminate(currentState, isActivePeer ? activePeer : callIdPeer);
} catch (CallException e) {
return callFailure(currentState, "hangup() failed: ", e);
}
}
protected @NonNull WebRtcServiceState handleLocalRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { protected @NonNull WebRtcServiceState handleLocalRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(tag, "handleLocalRinging not processed"); Log.i(tag, "handleLocalRinging not processed");
return currentState; return currentState;
@@ -592,14 +630,14 @@ public abstract class WebRtcActionProcessor {
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer(); RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
if (activePeer == null) { if (activePeer == null && remotePeer == null) {
Log.i(tag, "skipping with no active peer"); Log.i(tag, "skipping with no active peer");
return currentState; return currentState;
} } else if (activePeer != null && !activePeer.callIdEquals(remotePeer)) {
if (!activePeer.callIdEquals(remotePeer)) {
Log.i(tag, "skipping remotePeer is not active peer"); Log.i(tag, "skipping remotePeer is not active peer");
return currentState; return currentState;
} else {
activePeer = remotePeer;
} }
ApplicationDependencies.getAppForegroundObserver().removeListener(webRtcInteractor.getForegroundListener()); ApplicationDependencies.getAppForegroundObserver().removeListener(webRtcInteractor.getForegroundListener());
@@ -611,6 +649,7 @@ public abstract class WebRtcActionProcessor {
(activePeer.getState() == CallState.CONNECTED); (activePeer.getState() == CallState.CONNECTED);
webRtcInteractor.stopAudio(playDisconnectSound); webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.terminateCall(activePeer.getId());
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE); webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService(); webRtcInteractor.stopForegroundService();
@@ -6,8 +6,6 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.IBinder; import android.os.IBinder;
import android.telephony.PhoneStateListener; import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
@@ -18,6 +16,8 @@ import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.TelephonyUtil; import org.thoughtcrime.securesms.util.TelephonyUtil;
@@ -54,13 +54,14 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
private SignalCallManager callManager; private SignalCallManager callManager;
private NetworkReceiver networkReceiver; private NetworkListener networkListener;
private PowerButtonReceiver powerButtonReceiver; private PowerButtonReceiver powerButtonReceiver;
private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager; private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager;
private PhoneStateListener hangUpRtcOnDeviceCallAnswered; private PhoneStateListener hangUpRtcOnDeviceCallAnswered;
private SignalAudioManager signalAudioManager; private SignalAudioManager signalAudioManager;
private int lastNotificationId; private int lastNotificationId;
private Notification lastNotification; private Notification lastNotification;
private boolean isGroup = true;
public static void update(@NonNull Context context, int type, @NonNull RecipientId recipientId) { public static void update(@NonNull Context context, int type, @NonNull RecipientId recipientId) {
Intent intent = new Intent(context, WebRtcCallService.class); Intent intent = new Intent(context, WebRtcCallService.class);
@@ -71,6 +72,14 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
public static void denyCall(@NonNull Context context) {
ContextCompat.startForegroundService(context, denyCallIntent(context));
}
public static void hangup(@NonNull Context context) {
ContextCompat.startForegroundService(context, hangupIntent(context));
}
public static void stop(@NonNull Context context) { public static void stop(@NonNull Context context) {
Intent intent = new Intent(context, WebRtcCallService.class); Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(ACTION_STOP); intent.setAction(ACTION_STOP);
@@ -106,15 +115,15 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
Log.v(TAG, "onCreate"); Log.v(TAG, "onCreate");
super.onCreate(); super.onCreate();
this.callManager = ApplicationDependencies.getSignalCallManager(); this.callManager = ApplicationDependencies.getSignalCallManager();
this.signalAudioManager = new SignalAudioManager(this, this);
this.hangUpRtcOnDeviceCallAnswered = new HangUpRtcOnPstnCallAnsweredListener(); this.hangUpRtcOnDeviceCallAnswered = new HangUpRtcOnPstnCallAnsweredListener();
this.lastNotificationId = INVALID_NOTIFICATION_ID; this.lastNotificationId = INVALID_NOTIFICATION_ID;
registerUncaughtExceptionHandler(); registerUncaughtExceptionHandler();
registerNetworkReceiver(); registerNetworkReceiver();
TelephonyUtil.getManager(this) if (!AndroidTelecomUtil.getTelecomSupported()) {
.listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_CALL_STATE); TelephonyUtil.getManager(this).listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_CALL_STATE);
}
} }
@Override @Override
@@ -133,8 +142,9 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
unregisterNetworkReceiver(); unregisterNetworkReceiver();
unregisterPowerButtonReceiver(); unregisterPowerButtonReceiver();
TelephonyUtil.getManager(this) if (!AndroidTelecomUtil.getTelecomSupported()) {
.listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_NONE); TelephonyUtil.getManager(this).listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_NONE);
}
} }
@Override @Override
@@ -147,12 +157,19 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
switch (intent.getAction()) { switch (intent.getAction()) {
case ACTION_UPDATE: case ACTION_UPDATE:
RecipientId recipientId = Objects.requireNonNull(intent.getParcelableExtra(EXTRA_RECIPIENT_ID));
isGroup = Recipient.resolved(recipientId).isGroup();
setCallInProgressNotification(intent.getIntExtra(EXTRA_UPDATE_TYPE, 0), setCallInProgressNotification(intent.getIntExtra(EXTRA_UPDATE_TYPE, 0),
Objects.requireNonNull(intent.getParcelableExtra(EXTRA_RECIPIENT_ID))); Objects.requireNonNull(intent.getParcelableExtra(EXTRA_RECIPIENT_ID)));
return START_STICKY; return START_STICKY;
case ACTION_SEND_AUDIO_COMMAND: case ACTION_SEND_AUDIO_COMMAND:
setCallNotification(); setCallNotification();
signalAudioManager.handleCommand(Objects.requireNonNull(intent.getParcelableExtra(EXTRA_AUDIO_COMMAND))); if (signalAudioManager == null) {
signalAudioManager = SignalAudioManager.create(this, this, isGroup);
}
AudioManagerCommand audioCommand = Objects.requireNonNull(intent.getParcelableExtra(EXTRA_AUDIO_COMMAND));
Log.i(TAG, "Sending audio command [" + audioCommand.getClass().getSimpleName() + "] to " + signalAudioManager.getClass().getSimpleName());
signalAudioManager.handleCommand(audioCommand);
return START_STICKY; return START_STICKY;
case ACTION_CHANGE_POWER_BUTTON: case ACTION_CHANGE_POWER_BUTTON:
setCallNotification(); setCallNotification();
@@ -207,23 +224,21 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
} }
private void registerNetworkReceiver() { private void registerNetworkReceiver() {
if (networkReceiver == null) { if (networkListener == null) {
networkReceiver = new NetworkReceiver(); networkListener = new NetworkListener();
NetworkConstraintObserver.getInstance(ApplicationDependencies.getApplication()).addListener(networkListener);
registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
} }
} }
private void unregisterNetworkReceiver() { private void unregisterNetworkReceiver() {
if (networkReceiver != null) { if (networkListener != null) {
unregisterReceiver(networkReceiver); NetworkConstraintObserver.getInstance(ApplicationDependencies.getApplication()).removeListener(networkListener);
networkListener = null;
networkReceiver = null;
} }
} }
public void registerPowerButtonReceiver() { public void registerPowerButtonReceiver() {
if (powerButtonReceiver == null) { if (!AndroidTelecomUtil.getTelecomSupported() && powerButtonReceiver == null) {
powerButtonReceiver = new PowerButtonReceiver(); powerButtonReceiver = new PowerButtonReceiver();
registerReceiver(powerButtonReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); registerReceiver(powerButtonReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
@@ -263,13 +278,10 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
} }
} }
private static class NetworkReceiver extends BroadcastReceiver { private static class NetworkListener implements NetworkConstraintObserver.NetworkListener {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onNetworkChanged() {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); ApplicationDependencies.getSignalCallManager().networkChange(NetworkConstraint.isMet(ApplicationDependencies.getApplication()));
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
ApplicationDependencies.getSignalCallManager().networkChange(activeNetworkInfo != null && activeNetworkInfo.isConnected());
ApplicationDependencies.getSignalCallManager().bandwidthModeUpdate(); ApplicationDependencies.getSignalCallManager().bandwidthModeUpdate();
} }
} }
@@ -29,12 +29,12 @@ import java.util.UUID;
*/ */
public class WebRtcInteractor { public class WebRtcInteractor {
@NonNull private final Context context; private final Context context;
@NonNull private final SignalCallManager signalCallManager; private final SignalCallManager signalCallManager;
@NonNull private final LockManager lockManager; private final LockManager lockManager;
@NonNull private final CameraEventListener cameraEventListener; private final CameraEventListener cameraEventListener;
@NonNull private final GroupCall.Observer groupCallObserver; private final GroupCall.Observer groupCallObserver;
@NonNull private final AppForegroundObserver.Listener foregroundListener; private final AppForegroundObserver.Listener foregroundListener;
public WebRtcInteractor(@NonNull Context context, public WebRtcInteractor(@NonNull Context context,
@NonNull SignalCallManager signalCallManager, @NonNull SignalCallManager signalCallManager,
@@ -151,15 +151,35 @@ public class WebRtcInteractor {
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.Start()); WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.Start());
} }
public void setUserAudioDevice(@NonNull SignalAudioManager.AudioDevice userDevice) { public void setUserAudioDevice(@Nullable RecipientId recipientId, @NonNull SignalAudioManager.AudioDevice userDevice) {
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetUserDevice(userDevice)); WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetUserDevice(recipientId, userDevice));
} }
public void setDefaultAudioDevice(@NonNull SignalAudioManager.AudioDevice userDevice, boolean clearUserEarpieceSelection) { public void setDefaultAudioDevice(@NonNull RecipientId recipientId, @NonNull SignalAudioManager.AudioDevice userDevice, boolean clearUserEarpieceSelection) {
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetDefaultDevice(userDevice, clearUserEarpieceSelection)); WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetDefaultDevice(recipientId, userDevice, clearUserEarpieceSelection));
} }
void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo groupCallRingCheckInfo) { void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo groupCallRingCheckInfo) {
signalCallManager.peekGroupCallForRingingCheck(groupCallRingCheckInfo); signalCallManager.peekGroupCallForRingingCheck(groupCallRingCheckInfo);
} }
public void activateCall(RecipientId recipientId) {
AndroidTelecomUtil.activateCall(recipientId);
}
public void terminateCall(RecipientId recipientId) {
AndroidTelecomUtil.terminateCall(recipientId);
}
public boolean addNewIncomingCall(RecipientId recipientId, long callId, boolean remoteVideoOffer) {
return AndroidTelecomUtil.addIncomingCall(recipientId, callId, remoteVideoOffer);
}
public void rejectIncomingCall(RecipientId recipientId) {
AndroidTelecomUtil.reject(recipientId);
}
public boolean addNewOutgoingCall(RecipientId recipientId, long callId, boolean isVideoCall) {
return AndroidTelecomUtil.addOutgoingCall(recipientId, callId, isVideoCall);
}
} }
@@ -80,9 +80,10 @@ public final class WebRtcUtil {
} }
if (currentState.getLocalDeviceState().getActiveDevice() == SignalAudioManager.AudioDevice.EARPIECE || if (currentState.getLocalDeviceState().getActiveDevice() == SignalAudioManager.AudioDevice.EARPIECE ||
currentState.getLocalDeviceState().getActiveDevice() == SignalAudioManager.AudioDevice.NONE) currentState.getLocalDeviceState().getActiveDevice() == SignalAudioManager.AudioDevice.NONE &&
currentState.getCallInfoState().getActivePeer() != null)
{ {
webRtcInteractor.setDefaultAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE, true); webRtcInteractor.setDefaultAudioDevice(currentState.getCallInfoState().requireActivePeer().getId(), SignalAudioManager.AudioDevice.SPEAKER_PHONE, true);
} }
} }
@@ -1,159 +0,0 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.OptionalLong;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* General state of ongoing calls.
*/
public class CallInfoState {
WebRtcViewModel.State callState;
Recipient callRecipient;
long callConnectedTime;
Map<CallParticipantId, CallParticipant> remoteParticipants;
Map<Integer, RemotePeer> peerMap;
RemotePeer activePeer;
GroupCall groupCall;
WebRtcViewModel.GroupCallState groupState;
Set<RecipientId> identityChangedRecipients;
OptionalLong remoteDevicesCount;
Long participantLimit;
public CallInfoState() {
this(WebRtcViewModel.State.IDLE,
Recipient.UNKNOWN,
-1,
Collections.emptyMap(),
Collections.emptyMap(),
null,
null,
WebRtcViewModel.GroupCallState.IDLE,
Collections.emptySet(),
OptionalLong.empty(),
null);
}
public CallInfoState(@NonNull CallInfoState toCopy) {
this(toCopy.callState,
toCopy.callRecipient,
toCopy.callConnectedTime,
toCopy.remoteParticipants,
toCopy.peerMap,
toCopy.activePeer,
toCopy.groupCall,
toCopy.groupState,
toCopy.identityChangedRecipients,
toCopy.remoteDevicesCount,
toCopy.participantLimit);
}
public CallInfoState(@NonNull WebRtcViewModel.State callState,
@NonNull Recipient callRecipient,
long callConnectedTime,
@NonNull Map<CallParticipantId, CallParticipant> remoteParticipants,
@NonNull Map<Integer, RemotePeer> peerMap,
@Nullable RemotePeer activePeer,
@Nullable GroupCall groupCall,
@NonNull WebRtcViewModel.GroupCallState groupState,
@NonNull Set<RecipientId> identityChangedRecipients,
@NonNull OptionalLong remoteDevicesCount,
@Nullable Long participantLimit)
{
this.callState = callState;
this.callRecipient = callRecipient;
this.callConnectedTime = callConnectedTime;
this.remoteParticipants = new LinkedHashMap<>(remoteParticipants);
this.peerMap = new HashMap<>(peerMap);
this.activePeer = activePeer;
this.groupCall = groupCall;
this.groupState = groupState;
this.identityChangedRecipients = new HashSet<>(identityChangedRecipients);
this.remoteDevicesCount = remoteDevicesCount;
this.participantLimit = participantLimit;
}
public @NonNull Recipient getCallRecipient() {
return callRecipient;
}
public long getCallConnectedTime() {
return callConnectedTime;
}
public @NonNull Map<CallParticipantId, CallParticipant> getRemoteCallParticipantsMap() {
return new LinkedHashMap<>(remoteParticipants);
}
public @Nullable CallParticipant getRemoteCallParticipant(@NonNull Recipient recipient) {
return getRemoteCallParticipant(new CallParticipantId(recipient));
}
public @Nullable CallParticipant getRemoteCallParticipant(@NonNull CallParticipantId callParticipantId) {
return remoteParticipants.get(callParticipantId);
}
public @NonNull List<CallParticipant> getRemoteCallParticipants() {
return new ArrayList<>(remoteParticipants.values());
}
public @NonNull WebRtcViewModel.State getCallState() {
return callState;
}
public @Nullable RemotePeer getPeer(int hashCode) {
return peerMap.get(hashCode);
}
public @Nullable RemotePeer getActivePeer() {
return activePeer;
}
public @NonNull RemotePeer requireActivePeer() {
return Objects.requireNonNull(activePeer);
}
public @Nullable GroupCall getGroupCall() {
return groupCall;
}
public @NonNull GroupCall requireGroupCall() {
return Objects.requireNonNull(groupCall);
}
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
return groupState;
}
public @NonNull Set<RecipientId> getIdentityChangedRecipients() {
return identityChangedRecipients;
}
public OptionalLong getRemoteDevicesCount() {
return remoteDevicesCount;
}
public @Nullable Long getParticipantLimit() {
return participantLimit;
}
}
@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.service.webrtc.state
import com.annimon.stream.OptionalLong
import org.signal.ringrtc.CallId
import org.signal.ringrtc.GroupCall
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.events.CallParticipantId
import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.ringrtc.RemotePeer
import java.util.ArrayList
/**
* General state of ongoing calls.
*/
data class CallInfoState(
var callState: WebRtcViewModel.State = WebRtcViewModel.State.IDLE,
var callRecipient: Recipient = Recipient.UNKNOWN,
var callConnectedTime: Long = -1,
@get:JvmName("getRemoteCallParticipantsMap") var remoteParticipants: MutableMap<CallParticipantId, CallParticipant> = mutableMapOf(),
var peerMap: MutableMap<Int, RemotePeer> = mutableMapOf(),
var activePeer: RemotePeer? = null,
var groupCall: GroupCall? = null,
@get:JvmName("getGroupCallState") var groupState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE,
var identityChangedRecipients: MutableSet<RecipientId> = mutableSetOf(),
var remoteDevicesCount: OptionalLong = OptionalLong.empty(),
var participantLimit: Long? = null
) {
val remoteCallParticipants: List<CallParticipant>
get() = ArrayList(remoteParticipants.values)
fun getRemoteCallParticipant(recipient: Recipient): CallParticipant? {
return getRemoteCallParticipant(CallParticipantId(recipient))
}
fun getRemoteCallParticipant(callParticipantId: CallParticipantId): CallParticipant? {
return remoteParticipants[callParticipantId]
}
fun getPeer(hashCode: Int): RemotePeer? {
return peerMap[hashCode]
}
fun getPeerByCallId(callId: CallId): RemotePeer? {
return peerMap.values.firstOrNull { it.callId == callId }
}
fun requireActivePeer(): RemotePeer {
return activePeer!!
}
fun requireGroupCall(): GroupCall {
return groupCall!!
}
fun duplicate(): CallInfoState = copy(
remoteParticipants = remoteParticipants.toMutableMap(),
peerMap = peerMap.toMutableMap(),
identityChangedRecipients = identityChangedRecipients.toMutableSet()
)
}
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.service.webrtc.state package org.thoughtcrime.securesms.service.webrtc.state
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.webrtc.PeerConnection
/** /**
* Information specific to setting up a call. * Information specific to setting up a call.
@@ -12,7 +13,11 @@ data class CallSetupState(
@get:JvmName("hasSentJoinedMessage") var sentJoinedMessage: Boolean = false, @get:JvmName("hasSentJoinedMessage") var sentJoinedMessage: Boolean = false,
@get:JvmName("shouldRingGroup") var ringGroup: Boolean = true, @get:JvmName("shouldRingGroup") var ringGroup: Boolean = true,
var ringId: Long = NO_RING, var ringId: Long = NO_RING,
var ringerRecipient: Recipient = Recipient.UNKNOWN var ringerRecipient: Recipient = Recipient.UNKNOWN,
@get:JvmName("shouldWaitForTelecomApproval") var waitForTelecom: Boolean = false,
@get:JvmName("isTelecomApproved") var telecomApproved: Boolean = false,
var iceServers: MutableList<PeerConnection.IceServer> = mutableListOf(),
@get:JvmName("isAlwaysTurnServers") var alwaysTurnServers: Boolean = false
) { ) {
fun duplicate(): CallSetupState { fun duplicate(): CallSetupState {
@@ -31,7 +31,7 @@ public final class WebRtcServiceState {
public WebRtcServiceState(@NonNull WebRtcServiceState toCopy) { public WebRtcServiceState(@NonNull WebRtcServiceState toCopy) {
this.actionProcessor = toCopy.actionProcessor; this.actionProcessor = toCopy.actionProcessor;
this.callInfoState = new CallInfoState(toCopy.callInfoState); this.callInfoState = toCopy.callInfoState.duplicate();
this.localDeviceState = toCopy.localDeviceState.duplicate(); this.localDeviceState = toCopy.localDeviceState.duplicate();
this.videoState = new VideoState(toCopy.videoState); this.videoState = new VideoState(toCopy.videoState);
this.callSetupStates = new HashMap<>(); this.callSetupStates = new HashMap<>();
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor; import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.webrtc.PeerConnection;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
@@ -65,7 +66,7 @@ public class WebRtcServiceStateBuilder {
toBuild.videoState = new VideoState(); toBuild.videoState = new VideoState();
CallInfoState newCallInfoState = new CallInfoState(); CallInfoState newCallInfoState = new CallInfoState();
newCallInfoState.peerMap.putAll(toBuild.callInfoState.peerMap); newCallInfoState.getPeerMap().putAll(toBuild.callInfoState.getPeerMap());
toBuild.callInfoState = newCallInfoState; toBuild.callInfoState = newCallInfoState;
toBuild.callSetupStates.remove(callId); toBuild.callSetupStates.remove(callId);
@@ -179,6 +180,27 @@ public class WebRtcServiceStateBuilder {
toBuild.setRingerRecipient(ringerRecipient); toBuild.setRingerRecipient(ringerRecipient);
return this; return this;
} }
public @NonNull CallSetupStateBuilder waitForTelecom(boolean waitForTelecom) {
toBuild.setWaitForTelecom(waitForTelecom);
return this;
}
public @NonNull CallSetupStateBuilder telecomApproved(boolean telecomApproved) {
toBuild.setTelecomApproved(telecomApproved);
return this;
}
public @NonNull CallSetupStateBuilder iceServers(Collection<PeerConnection.IceServer> iceServers) {
toBuild.getIceServers().clear();
toBuild.getIceServers().addAll(iceServers);
return this;
}
public @NonNull CallSetupStateBuilder alwaysTurn(boolean isAlwaysTurn) {
toBuild.setAlwaysTurnServers(isAlwaysTurn);
return this;
}
} }
public class VideoStateBuilder { public class VideoStateBuilder {
@@ -218,7 +240,7 @@ public class WebRtcServiceStateBuilder {
private CallInfoState toBuild; private CallInfoState toBuild;
public CallInfoStateBuilder() { public CallInfoStateBuilder() {
toBuild = new CallInfoState(WebRtcServiceStateBuilder.this.toBuild.callInfoState); toBuild = WebRtcServiceStateBuilder.this.toBuild.callInfoState.duplicate();
} }
public @NonNull WebRtcServiceStateBuilder commit() { public @NonNull WebRtcServiceStateBuilder commit() {
@@ -232,82 +254,82 @@ public class WebRtcServiceStateBuilder {
} }
public @NonNull CallInfoStateBuilder callState(@NonNull WebRtcViewModel.State callState) { public @NonNull CallInfoStateBuilder callState(@NonNull WebRtcViewModel.State callState) {
toBuild.callState = callState; toBuild.setCallState(callState);
return this; return this;
} }
public @NonNull CallInfoStateBuilder callRecipient(@NonNull Recipient callRecipient) { public @NonNull CallInfoStateBuilder callRecipient(@NonNull Recipient callRecipient) {
toBuild.callRecipient = callRecipient; toBuild.setCallRecipient(callRecipient);
return this; return this;
} }
public @NonNull CallInfoStateBuilder callConnectedTime(long callConnectedTime) { public @NonNull CallInfoStateBuilder callConnectedTime(long callConnectedTime) {
toBuild.callConnectedTime = callConnectedTime; toBuild.setCallConnectedTime(callConnectedTime);
return this; return this;
} }
public @NonNull CallInfoStateBuilder putParticipant(@NonNull CallParticipantId callParticipantId, @NonNull CallParticipant callParticipant) { public @NonNull CallInfoStateBuilder putParticipant(@NonNull CallParticipantId callParticipantId, @NonNull CallParticipant callParticipant) {
toBuild.remoteParticipants.put(callParticipantId, callParticipant); toBuild.getRemoteCallParticipantsMap().put(callParticipantId, callParticipant);
return this; return this;
} }
public @NonNull CallInfoStateBuilder putParticipant(@NonNull Recipient recipient, @NonNull CallParticipant callParticipant) { public @NonNull CallInfoStateBuilder putParticipant(@NonNull Recipient recipient, @NonNull CallParticipant callParticipant) {
toBuild.remoteParticipants.put(new CallParticipantId(recipient), callParticipant); toBuild.getRemoteCallParticipantsMap().put(new CallParticipantId(recipient), callParticipant);
return this; return this;
} }
public @NonNull CallInfoStateBuilder clearParticipantMap() { public @NonNull CallInfoStateBuilder clearParticipantMap() {
toBuild.remoteParticipants.clear(); toBuild.getRemoteCallParticipantsMap().clear();
return this; return this;
} }
public @NonNull CallInfoStateBuilder putRemotePeer(@NonNull RemotePeer remotePeer) { public @NonNull CallInfoStateBuilder putRemotePeer(@NonNull RemotePeer remotePeer) {
toBuild.peerMap.put(remotePeer.hashCode(), remotePeer); toBuild.getPeerMap().put(remotePeer.hashCode(), remotePeer);
return this; return this;
} }
public @NonNull CallInfoStateBuilder clearPeerMap() { public @NonNull CallInfoStateBuilder clearPeerMap() {
toBuild.peerMap.clear(); toBuild.getPeerMap().clear();
return this; return this;
} }
public @NonNull CallInfoStateBuilder removeRemotePeer(@NonNull RemotePeer remotePeer) { public @NonNull CallInfoStateBuilder removeRemotePeer(@NonNull RemotePeer remotePeer) {
toBuild.peerMap.remove(remotePeer.hashCode()); toBuild.getPeerMap().remove(remotePeer.hashCode());
return this; return this;
} }
public @NonNull CallInfoStateBuilder activePeer(@Nullable RemotePeer activePeer) { public @NonNull CallInfoStateBuilder activePeer(@Nullable RemotePeer activePeer) {
toBuild.activePeer = activePeer; toBuild.setActivePeer(activePeer);
return this; return this;
} }
public @NonNull CallInfoStateBuilder groupCall(@Nullable GroupCall groupCall) { public @NonNull CallInfoStateBuilder groupCall(@Nullable GroupCall groupCall) {
toBuild.groupCall = groupCall; toBuild.setGroupCall(groupCall);
return this; return this;
} }
public @NonNull CallInfoStateBuilder groupCallState(@Nullable WebRtcViewModel.GroupCallState groupState) { public @NonNull CallInfoStateBuilder groupCallState(@NonNull WebRtcViewModel.GroupCallState groupState) {
toBuild.groupState = groupState; toBuild.setGroupState(groupState);
return this; return this;
} }
public @NonNull CallInfoStateBuilder addIdentityChangedRecipients(@NonNull Collection<RecipientId> id) { public @NonNull CallInfoStateBuilder addIdentityChangedRecipients(@NonNull Collection<RecipientId> id) {
toBuild.identityChangedRecipients.addAll(id); toBuild.getIdentityChangedRecipients().addAll(id);
return this; return this;
} }
public @NonNull CallInfoStateBuilder removeIdentityChangedRecipients(@NonNull Collection<RecipientId> ids) { public @NonNull CallInfoStateBuilder removeIdentityChangedRecipients(@NonNull Collection<RecipientId> ids) {
toBuild.identityChangedRecipients.removeAll(ids); toBuild.getIdentityChangedRecipients().removeAll(ids);
return this; return this;
} }
public @NonNull CallInfoStateBuilder remoteDevicesCount(long remoteDevicesCount) { public @NonNull CallInfoStateBuilder remoteDevicesCount(long remoteDevicesCount) {
toBuild.remoteDevicesCount = OptionalLong.of(remoteDevicesCount); toBuild.setRemoteDevicesCount(OptionalLong.of(remoteDevicesCount));
return this; return this;
} }
public @NonNull CallInfoStateBuilder participantLimit(@Nullable Long participantLimit) { public @NonNull CallInfoStateBuilder participantLimit(@Nullable Long participantLimit) {
toBuild.participantLimit = participantLimit; toBuild.setParticipantLimit(participantLimit);
return this; return this;
} }
} }
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.webrtc.audio
import android.net.Uri import android.net.Uri
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.ParcelUtil import org.thoughtcrime.securesms.util.ParcelUtil
/** /**
@@ -74,19 +75,21 @@ sealed class AudioManagerCommand : Parcelable {
} }
} }
class SetUserDevice(val device: SignalAudioManager.AudioDevice) : AudioManagerCommand() { class SetUserDevice(val recipientId: RecipientId?, val device: SignalAudioManager.AudioDevice) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(recipientId, flags)
parcel.writeSerializable(device) parcel.writeSerializable(device)
} }
companion object { companion object {
@JvmField @JvmField
val CREATOR: Parcelable.Creator<SetUserDevice> = ParcelCheat { SetUserDevice(it.readSerializable() as SignalAudioManager.AudioDevice) } val CREATOR: Parcelable.Creator<SetUserDevice> = ParcelCheat { SetUserDevice(it.readParcelable(RecipientId::class.java.classLoader), it.readSerializable() as SignalAudioManager.AudioDevice) }
} }
} }
class SetDefaultDevice(val device: SignalAudioManager.AudioDevice, val clearUserEarpieceSelection: Boolean) : AudioManagerCommand() { class SetDefaultDevice(val recipientId: RecipientId?, val device: SignalAudioManager.AudioDevice, val clearUserEarpieceSelection: Boolean) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(recipientId, flags)
parcel.writeSerializable(device) parcel.writeSerializable(device)
ParcelUtil.writeBoolean(parcel, clearUserEarpieceSelection) ParcelUtil.writeBoolean(parcel, clearUserEarpieceSelection)
} }
@@ -95,6 +98,7 @@ sealed class AudioManagerCommand : Parcelable {
@JvmField @JvmField
val CREATOR: Parcelable.Creator<SetDefaultDevice> = ParcelCheat { parcel -> val CREATOR: Parcelable.Creator<SetDefaultDevice> = ParcelCheat { parcel ->
SetDefaultDevice( SetDefaultDevice(
recipientId = parcel.readParcelable(RecipientId::class.java.classLoader),
device = parcel.readSerializable() as SignalAudioManager.AudioDevice, device = parcel.readSerializable() as SignalAudioManager.AudioDevice,
clearUserEarpieceSelection = ParcelUtil.readBoolean(parcel) clearUserEarpieceSelection = ParcelUtil.readBoolean(parcel)
) )
@@ -69,7 +69,7 @@ public class IncomingRinger {
player = null; player = null;
} }
} else { } else {
Log.w(TAG, "Not ringing, player: " + (player != null ? "available" : "null") + " mode: " + ringerMode); Log.w(TAG, "Not ringing, player: " + (player != null ? "available" : "null") + " modeInt: " + ringerMode + " mode: " + (ringerMode == AudioManager.RINGER_MODE_SILENT ? "silent" : "vibrate only"));
} }
} }
@@ -12,11 +12,95 @@ import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil
import org.thoughtcrime.securesms.util.safeUnregisterReceiver import org.thoughtcrime.securesms.util.safeUnregisterReceiver
import org.whispersystems.libsignal.util.guava.Preconditions import org.whispersystems.libsignal.util.guava.Preconditions
private val TAG = Log.tag(SignalAudioManager::class.java) private val TAG = Log.tag(SignalAudioManager::class.java)
sealed class SignalAudioManager(protected val context: Context, protected val eventListener: EventListener?) {
private var commandAndControlThread = SignalExecutors.getAndStartHandlerThread("call-audio")
protected val handler = SignalAudioHandler(commandAndControlThread.looper)
protected var state: State = State.UNINITIALIZED
protected val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
protected var selectedAudioDevice: AudioDevice = AudioDevice.NONE
protected val soundPool: SoundPool = androidAudioManager.createSoundPool()
protected val connectedSoundId = soundPool.load(context, R.raw.webrtc_completed, 1)
protected val disconnectedSoundId = soundPool.load(context, R.raw.webrtc_disconnected, 1)
protected val incomingRinger = IncomingRinger(context)
protected val outgoingRinger = OutgoingRinger(context)
companion object {
@JvmStatic
fun create(context: Context, eventListener: EventListener?, isGroup: Boolean): SignalAudioManager {
return if (AndroidTelecomUtil.telecomSupported && !isGroup) {
TelecomAwareSignalAudioManager(context, eventListener)
} else {
FullSignalAudioManager(context, eventListener)
}
}
}
fun handleCommand(command: AudioManagerCommand) {
handler.post {
when (command) {
is AudioManagerCommand.Initialize -> initialize()
is AudioManagerCommand.Start -> start()
is AudioManagerCommand.Stop -> stop(command.playDisconnect)
is AudioManagerCommand.SetDefaultDevice -> setDefaultAudioDevice(command.recipientId, command.device, command.clearUserEarpieceSelection)
is AudioManagerCommand.SetUserDevice -> selectAudioDevice(command.recipientId, command.device)
is AudioManagerCommand.StartIncomingRinger -> startIncomingRinger(command.ringtoneUri, command.vibrate)
is AudioManagerCommand.SilenceIncomingRinger -> silenceIncomingRinger()
is AudioManagerCommand.StartOutgoingRinger -> startOutgoingRinger()
}
}
}
fun shutdown() {
handler.post {
stop(false)
if (commandAndControlThread != null) {
Log.i(TAG, "Shutting down command and control")
commandAndControlThread.quitSafely()
commandAndControlThread = null
}
}
}
protected abstract fun initialize()
protected abstract fun start()
protected abstract fun stop(playDisconnect: Boolean)
protected abstract fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean)
protected abstract fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice)
protected abstract fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean)
protected abstract fun startOutgoingRinger()
protected open fun silenceIncomingRinger() {
Log.i(TAG, "silenceIncomingRinger():")
incomingRinger.stop()
}
enum class AudioDevice {
SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
}
enum class State {
UNINITIALIZED, PREINITIALIZED, RUNNING
}
interface EventListener {
@JvmSuppressWildcards
fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>)
}
}
/** /**
* Manage all audio and bluetooth routing for calling. Primarily, operates by maintaining a list * Manage all audio and bluetooth routing for calling. Primarily, operates by maintaining a list
* of available devices (wired, speaker, bluetooth, earpiece) and then using a state machine to determine * of available devices (wired, speaker, bluetooth, earpiece) and then using a state machine to determine
@@ -31,15 +115,12 @@ private val TAG = Log.tag(SignalAudioManager::class.java)
* bluetooth headset is then disconnected, and reconnected, the audio will again automatically switch to * bluetooth headset is then disconnected, and reconnected, the audio will again automatically switch to
* the bluetooth headset. * the bluetooth headset.
*/ */
class SignalAudioManager(private val context: Context, private val eventListener: EventListener?) { class FullSignalAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
private var commandAndControlThread = SignalExecutors.getAndStartHandlerThread("call-audio")
private val handler = SignalAudioHandler(commandAndControlThread.looper)
private val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
private val signalBluetoothManager = SignalBluetoothManager(context, this, handler) private val signalBluetoothManager = SignalBluetoothManager(context, this, handler)
private var state: State = State.UNINITIALIZED private var audioDevices: MutableSet<AudioDevice> = mutableSetOf()
private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE
private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE
private var savedAudioMode = AudioManager.MODE_INVALID private var savedAudioMode = AudioManager.MODE_INVALID
private var savedIsSpeakerPhoneOn = false private var savedIsSpeakerPhoneOn = false
@@ -48,37 +129,9 @@ class SignalAudioManager(private val context: Context, private val eventListener
private var autoSwitchToWiredHeadset = true private var autoSwitchToWiredHeadset = true
private var autoSwitchToBluetooth = true private var autoSwitchToBluetooth = true
private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE
private var selectedAudioDevice: AudioDevice = AudioDevice.NONE
private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE
private var audioDevices: MutableSet<AudioDevice> = mutableSetOf()
private val soundPool: SoundPool = androidAudioManager.createSoundPool()
private val connectedSoundId = soundPool.load(context, R.raw.webrtc_completed, 1)
private val disconnectedSoundId = soundPool.load(context, R.raw.webrtc_disconnected, 1)
private val incomingRinger = IncomingRinger(context)
private val outgoingRinger = OutgoingRinger(context)
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
fun handleCommand(command: AudioManagerCommand) { override fun initialize() {
handler.post {
when (command) {
is AudioManagerCommand.Initialize -> initialize()
is AudioManagerCommand.Start -> start()
is AudioManagerCommand.Stop -> stop(command.playDisconnect)
is AudioManagerCommand.SetDefaultDevice -> setDefaultAudioDevice(command.device, command.clearUserEarpieceSelection)
is AudioManagerCommand.SetUserDevice -> selectAudioDevice(command.device)
is AudioManagerCommand.StartIncomingRinger -> startIncomingRinger(command.ringtoneUri, command.vibrate)
is AudioManagerCommand.SilenceIncomingRinger -> silenceIncomingRinger()
is AudioManagerCommand.StartOutgoingRinger -> startOutgoingRinger()
}
}
}
private fun initialize() {
Log.i(TAG, "Initializing audio manager state: $state") Log.i(TAG, "Initializing audio manager state: $state")
if (state == State.UNINITIALIZED) { if (state == State.UNINITIALIZED) {
@@ -109,7 +162,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
} }
} }
private fun start() { override fun start() {
Log.d(TAG, "Starting. state: $state") Log.d(TAG, "Starting. state: $state")
if (state == State.RUNNING) { if (state == State.RUNNING) {
Log.w(TAG, "Skipping, already active") Log.w(TAG, "Skipping, already active")
@@ -134,7 +187,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
Log.d(TAG, "Started") Log.d(TAG, "Started")
} }
private fun stop(playDisconnect: Boolean) { override fun stop(playDisconnect: Boolean) {
Log.d(TAG, "Stopping. state: $state") Log.d(TAG, "Stopping. state: $state")
incomingRinger.stop() incomingRinger.stop()
@@ -162,17 +215,6 @@ class SignalAudioManager(private val context: Context, private val eventListener
Log.d(TAG, "Stopped") Log.d(TAG, "Stopped")
} }
fun shutdown() {
handler.post {
stop(false)
if (commandAndControlThread != null) {
Log.i(TAG, "Shutting down command and control")
commandAndControlThread.quitSafely()
commandAndControlThread = null
}
}
}
fun updateAudioDeviceState() { fun updateAudioDeviceState() {
handler.assertHandlerThread() handler.assertHandlerThread()
@@ -265,7 +307,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
} }
} }
private fun setDefaultAudioDevice(newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) { override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
Log.d(TAG, "setDefaultAudioDevice(): currentDefault: $defaultAudioDevice device: $newDefaultDevice clearUser: $clearUserEarpieceSelection") Log.d(TAG, "setDefaultAudioDevice(): currentDefault: $defaultAudioDevice device: $newDefaultDevice clearUser: $clearUserEarpieceSelection")
defaultAudioDevice = when (newDefaultDevice) { defaultAudioDevice = when (newDefaultDevice) {
AudioDevice.SPEAKER_PHONE -> newDefaultDevice AudioDevice.SPEAKER_PHONE -> newDefaultDevice
@@ -288,7 +330,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
updateAudioDeviceState() updateAudioDeviceState()
} }
private fun selectAudioDevice(device: AudioDevice) { override fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice) {
val actualDevice = if (device == AudioDevice.EARPIECE && audioDevices.contains(AudioDevice.WIRED_HEADSET)) AudioDevice.WIRED_HEADSET else device val actualDevice = if (device == AudioDevice.EARPIECE && audioDevices.contains(AudioDevice.WIRED_HEADSET)) AudioDevice.WIRED_HEADSET else device
Log.d(TAG, "selectAudioDevice(): device: $device actualDevice: $actualDevice") Log.d(TAG, "selectAudioDevice(): device: $device actualDevice: $actualDevice")
@@ -324,21 +366,16 @@ class SignalAudioManager(private val context: Context, private val eventListener
} }
} }
private fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) { override fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate") Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate")
androidAudioManager.mode = AudioManager.MODE_RINGTONE androidAudioManager.mode = AudioManager.MODE_RINGTONE
setMicrophoneMute(false) setMicrophoneMute(false)
setDefaultAudioDevice(AudioDevice.SPEAKER_PHONE, false) setDefaultAudioDevice(null, AudioDevice.SPEAKER_PHONE, false)
incomingRinger.start(ringtoneUri, vibrate) incomingRinger.start(ringtoneUri, vibrate)
} }
private fun silenceIncomingRinger() { override fun startOutgoingRinger() {
Log.i(TAG, "silenceIncomingRinger():")
incomingRinger.stop()
}
private fun startOutgoingRinger() {
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice") Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice")
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
@@ -361,17 +398,52 @@ class SignalAudioManager(private val context: Context, private val eventListener
handler.post { onWiredHeadsetChange(pluggedIn, hasMic) } handler.post { onWiredHeadsetChange(pluggedIn, hasMic) }
} }
} }
}
enum class AudioDevice { class TelecomAwareSignalAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
if (recipientId != null && AndroidTelecomUtil.getSelectedAudioDevice(recipientId) == AudioDevice.EARPIECE) {
selectAudioDevice(recipientId, newDefaultDevice)
}
} }
enum class State { override fun initialize() {
UNINITIALIZED, PREINITIALIZED, RUNNING val focusedGained = androidAudioManager.requestCallAudioFocus()
if (!focusedGained) {
handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
}
} }
interface EventListener { override fun start() {
@JvmSuppressWildcards incomingRinger.stop()
fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>) outgoingRinger.stop()
val focusedGained = androidAudioManager.requestCallAudioFocus()
if (!focusedGained) {
handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
}
}
override fun stop(playDisconnect: Boolean) {
incomingRinger.stop()
outgoingRinger.stop()
androidAudioManager.abandonCallAudioFocus()
}
override fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice) {
if (recipientId != null) {
selectedAudioDevice = device
AndroidTelecomUtil.selectAudioDevice(recipientId, device)
handler.postDelayed({ AndroidTelecomUtil.selectAudioDevice(recipientId, selectedAudioDevice) }, 1000)
}
}
override fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
incomingRinger.start(ringtoneUri, vibrate)
}
override fun startOutgoingRinger() {
outgoingRinger.start(OutgoingRinger.Type.RINGING)
} }
} }
@@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit
*/ */
class SignalBluetoothManager( class SignalBluetoothManager(
private val context: Context, private val context: Context,
private val audioManager: SignalAudioManager, private val audioManager: FullSignalAudioManager,
private val handler: SignalAudioHandler private val handler: SignalAudioHandler
) { ) {
@@ -110,8 +110,7 @@ object MessageUtil {
isChangeNumber:${type == CHANGE_NUMBER_TYPE} isChangeNumber:${type == CHANGE_NUMBER_TYPE}
isBoostRequest:${type == BOOST_REQUEST_TYPE} isBoostRequest:${type == BOOST_REQUEST_TYPE}
isGroupV2LeaveOnly:${type and GROUP_V2_LEAVE_BITS == GROUP_V2_LEAVE_BITS} isGroupV2LeaveOnly:${type and GROUP_V2_LEAVE_BITS == GROUP_V2_LEAVE_BITS}
""".trimIndent() """.trimIndent()
return describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "<br>") return describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "<br>")
} }