diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fb1732b622..a04dd3c83c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -91,6 +91,8 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
index 5240eddfec..0f35c4c9a9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
@@ -35,30 +35,28 @@ import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
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.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.LogDatabase;
-import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.database.SignalDatabase;
+import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource;
+import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
-import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
+import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
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.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
+import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
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.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
+import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
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.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
-import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.Security;
@@ -196,6 +195,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
.addPostRender(RetrieveReleaseChannelJob::enqueue)
+ .addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt
index 44d174b9f0..9fe68331e7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt
@@ -82,7 +82,7 @@ class WebRtcViewModel(state: WebRtcServiceState) {
}
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 isRemoteVideoOffer: Boolean = state.getCallSetupState(state.callInfoState.activePeer?.callId).isRemoteVideoOffer
val callConnectedTime: Long = state.callInfoState.callConnectedTime
diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java
index 553504e5c1..10fbd938cf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.emoji.EmojiFiles;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.util.AppSignatureUtil;
import org.thoughtcrime.securesms.util.ByteUnit;
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("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("Telecom : ").append(AndroidTelecomUtil.getTelecomSupported()).append("\n");
builder.append("User-Agent : ").append(StandardUserAgentInterceptor.USER_AGENT).append("\n");
builder.append("App : ");
try {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidCallConnection.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidCallConnection.kt
new file mode 100644
index 0000000000..29ee89492f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidCallConnection.kt
@@ -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 {
+ val devices = mutableSetOf()
+
+ 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
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidCallConnectionService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidCallConnectionService.kt
new file mode 100644
index 0000000000..9ce9720847
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidCallConnectionService.kt
@@ -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)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidTelecomUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidTelecomUtil.kt
new file mode 100644
index 0000000000..a2a71380a1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidTelecomUtil.kt
@@ -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 = 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)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java
index 829674f0dd..b2bdd36347 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java
@@ -69,7 +69,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
}
@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();
Log.i(tag, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
@@ -78,8 +78,17 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
webRtcInteractor.retrieveTurnServers(remotePeer);
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()
.actionProcessor(new IncomingCallActionProcessor(webRtcInteractor))
+ .changeCallSetupState(remotePeer.getCallId())
+ .waitForTelecom(AndroidTelecomUtil.getTelecomSupported())
+ .telecomApproved(false)
+ .commit()
.changeCallInfoState()
.callRecipient(remotePeer.getRecipient())
.activePeer(remotePeer)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java
index 67e74dfe62..90720826bd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java
@@ -10,7 +10,6 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
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.locks.LockManager;
@@ -40,6 +39,7 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
ApplicationDependencies.getAppForegroundObserver().removeListener(webRtcInteractor.getForegroundListener());
webRtcInteractor.startAudioCommunication();
+ webRtcInteractor.activateCall(activePeer.getId());
activePeer.connected();
@@ -75,9 +75,9 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
}
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 {
- webRtcInteractor.setDefaultAudioDevice(SignalAudioManager.AudioDevice.EARPIECE, false);
+ webRtcInteractor.setDefaultAudioDevice(activePeer.getId(), SignalAudioManager.AudioDevice.EARPIECE, false);
}
return currentState;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java
index e8dcba2c83..39589edfec 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java
@@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.ringrtc.CameraState;
+import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
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) {
Log.i(tag, "handleSetUserAudioDevice(): userDevice: " + userDevice);
- webRtcInteractor.setUserAudioDevice(userDevice);
+ RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
+ webRtcInteractor.setUserAudioDevice(activePeer != null ? activePeer.getId() : null, userDevice);
return currentState;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java
index 3dff178ca2..d6eea555ea 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java
@@ -26,11 +26,8 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
private static final String TAG = Log.tag(GroupJoiningActionProcessor.class);
- private final CallSetupActionProcessorDelegate callSetupDelegate;
-
public GroupJoiningActionProcessor(@NonNull WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
- callSetupDelegate = new CallSetupActionProcessorDelegate(webRtcInteractor, TAG);
}
@Override
@@ -97,6 +94,7 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
.changeLocalDeviceState()
.commit()
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor));
+
} else if (device.getJoinState() == GroupCall.JoinState.JOINING) {
builder.changeCallInfoState()
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java
index f2025ec5d4..3e4dcf5f6e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IdleActionProcessor.java
@@ -34,11 +34,11 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
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():");
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState, remotePeer.getCallId().longValue());
- return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer);
+ return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer, offerType);
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java
index 5f3a4f3d8f..6688375131 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java
@@ -20,9 +20,12 @@ import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CallState;
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.WebRtcServiceState;
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.webrtc.PeerConnection;
@@ -59,8 +62,37 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
@NonNull List iceServers,
boolean isAlwaysTurn)
{
- RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
- boolean hideIp = !activePeer.getRecipient().isSystemContact() || isAlwaysTurn;
+ RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
+
+ 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();
CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
@@ -72,7 +104,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
videoState.requireLocalSink(),
callParticipant.getVideoSink(),
videoState.requireCamera(),
- iceServers,
+ callSetupState.getIceServers(),
hideIp,
NetworkUtil.getCallingBandwidthMode(context),
null,
@@ -87,6 +119,11 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
return currentState;
}
+ @Override
+ protected @NonNull WebRtcServiceState handleDropCall(@NonNull WebRtcServiceState currentState, long callId) {
+ return callSetupDelegate.handleDropCall(currentState, callId);
+ }
+
@Override
protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
@@ -120,10 +157,11 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
Log.i(TAG, "handleDenyCall():");
try {
+ webRtcInteractor.rejectIncomingCall(activePeer.getId());
webRtcInteractor.getCallManager().hangup();
SignalDatabase.sms().insertMissedCall(activePeer.getId(), System.currentTimeMillis(), currentState.getCallSetupState(activePeer).isRemoteVideoOffer());
return terminate(currentState, activePeer);
- } catch (CallException e) {
+ } catch (CallException e) {
return callFailure(currentState, "hangup() failed: ", e);
}
}
@@ -174,7 +212,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
}
@Override
- protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
+ protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
return activeCallDelegate.handleRemoteVideoEnable(currentState, enable);
}
@@ -199,7 +237,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
}
@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);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java
index aaba5efaa2..40a394b45b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java
@@ -227,8 +227,8 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
long ringId = currentState.getCallSetupState(RemotePeer.GROUP_CALL_ID).getRingId();
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId,
- System.currentTimeMillis(),
- CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE);
+ System.currentTimeMillis(),
+ CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE);
try {
webRtcInteractor.getCallManager().cancelGroupRing(groupId.get().getDecodedId(),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java
index aa344285c6..01987379ac 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java
@@ -10,7 +10,6 @@ import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
-import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.events.CallParticipant;
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.ringrtc.RemotePeer;
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.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.NetworkUtil;
+import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -68,13 +69,18 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
boolean isVideoCall = offerType == OfferMessage.Type.VIDEO_CALL;
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer);
- webRtcInteractor.setDefaultAudioDevice(isVideoCall ? SignalAudioManager.AudioDevice.SPEAKER_PHONE
- : SignalAudioManager.AudioDevice.EARPIECE,
+ webRtcInteractor.setDefaultAudioDevice(remotePeer.getId(),
+ isVideoCall ? SignalAudioManager.AudioDevice.SPEAKER_PHONE : SignalAudioManager.AudioDevice.EARPIECE,
false);
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
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()));
SignalDatabase.sms().insertOutgoingCall(remotePeer.getId(), isVideoCall);
@@ -84,6 +90,8 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
return builder.changeCallSetupState(remotePeer.getCallId())
.enableVideoOnCreate(isVideoCall)
+ .waitForTelecom(AndroidTelecomUtil.getTelecomSupported())
+ .telecomApproved(false)
.commit()
.changeCallInfoState()
.activePeer(remotePeer)
@@ -98,11 +106,40 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
@NonNull List iceServers,
boolean isAlwaysTurn)
{
- try {
- VideoState videoState = currentState.getVideoState();
- RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
- CallParticipant callParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
+ RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
+ 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(),
context,
videoState.getLockableEglBase().require(),
@@ -110,8 +147,8 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
videoState.requireLocalSink(),
callParticipant.getVideoSink(),
videoState.requireCamera(),
- iceServers,
- isAlwaysTurn,
+ callSetupState.getIceServers(),
+ callSetupState.isAlwaysTurnServers(),
NetworkUtil.getCallingBandwidthMode(context),
null,
currentState.getCallSetupState(activePeer).isEnableVideoOnCreate());
@@ -125,6 +162,11 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
.build();
}
+ @Override
+ protected @NonNull WebRtcServiceState handleDropCall(@NonNull WebRtcServiceState currentState, long callId) {
+ return callSetupDelegate.handleDropCall(currentState, callId);
+ }
+
@Override
protected @NonNull WebRtcServiceState handleRemoteRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java
index 78cf489a7d..c3d3dd2b6d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java
@@ -34,7 +34,7 @@ public class PreJoinActionProcessor extends DeviceAwareActionProcessor {
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():");
EglBaseWrapper.replaceHolder(EglBaseWrapper.OUTGOING_PLACEHOLDER, remotePeer.getCallId().longValue());
@@ -45,7 +45,7 @@ public class PreJoinActionProcessor extends DeviceAwareActionProcessor {
.build();
webRtcInteractor.postStateUpdate(currentState);
- return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer);
+ return beginCallDelegate.handleStartIncomingCall(currentState, remotePeer, offerType);
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java
index 8d29cdfe8f..7fa1b7d809 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java
@@ -295,6 +295,14 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
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) {
if (callManager == 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) {
return p.handleStartOutgoingCall(s, remotePeer, WebRtcUtil.getOfferTypeFromCallMediaType(callMediaType));
} else {
- return p.handleStartIncomingCall(s, remotePeer);
+ return p.handleStartIncomingCall(s, remotePeer, WebRtcUtil.getOfferTypeFromCallMediaType(callMediaType));
}
});
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java
index 2bbdd3cad5..3bf3dc2d2f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java
@@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
-import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
@@ -244,7 +243,7 @@ public abstract class WebRtcActionProcessor {
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");
return currentState;
}
@@ -254,6 +253,45 @@ public abstract class WebRtcActionProcessor {
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) {
Log.i(tag, "handleLocalRinging not processed");
return currentState;
@@ -592,14 +630,14 @@ public abstract class WebRtcActionProcessor {
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
- if (activePeer == null) {
+ if (activePeer == null && remotePeer == null) {
Log.i(tag, "skipping with no active peer");
return currentState;
- }
-
- if (!activePeer.callIdEquals(remotePeer)) {
+ } else if (activePeer != null && !activePeer.callIdEquals(remotePeer)) {
Log.i(tag, "skipping remotePeer is not active peer");
return currentState;
+ } else {
+ activePeer = remotePeer;
}
ApplicationDependencies.getAppForegroundObserver().removeListener(webRtcInteractor.getForegroundListener());
@@ -611,6 +649,7 @@ public abstract class WebRtcActionProcessor {
(activePeer.getState() == CallState.CONNECTED);
webRtcInteractor.stopAudio(playDisconnectSound);
+ webRtcInteractor.terminateCall(activePeer.getId());
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java
index a975d977ae..aeff780fa3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java
@@ -6,8 +6,6 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
@@ -18,6 +16,8 @@ import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log;
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.RecipientId;
import org.thoughtcrime.securesms.util.TelephonyUtil;
@@ -54,13 +54,14 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
private SignalCallManager callManager;
- private NetworkReceiver networkReceiver;
+ private NetworkListener networkListener;
private PowerButtonReceiver powerButtonReceiver;
private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager;
private PhoneStateListener hangUpRtcOnDeviceCallAnswered;
private SignalAudioManager signalAudioManager;
private int lastNotificationId;
private Notification lastNotification;
+ private boolean isGroup = true;
public static void update(@NonNull Context context, int type, @NonNull RecipientId recipientId) {
Intent intent = new Intent(context, WebRtcCallService.class);
@@ -71,6 +72,14 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
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) {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(ACTION_STOP);
@@ -106,15 +115,15 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
Log.v(TAG, "onCreate");
super.onCreate();
this.callManager = ApplicationDependencies.getSignalCallManager();
- this.signalAudioManager = new SignalAudioManager(this, this);
this.hangUpRtcOnDeviceCallAnswered = new HangUpRtcOnPstnCallAnsweredListener();
this.lastNotificationId = INVALID_NOTIFICATION_ID;
registerUncaughtExceptionHandler();
registerNetworkReceiver();
- TelephonyUtil.getManager(this)
- .listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_CALL_STATE);
+ if (!AndroidTelecomUtil.getTelecomSupported()) {
+ TelephonyUtil.getManager(this).listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_CALL_STATE);
+ }
}
@Override
@@ -133,8 +142,9 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
unregisterNetworkReceiver();
unregisterPowerButtonReceiver();
- TelephonyUtil.getManager(this)
- .listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_NONE);
+ if (!AndroidTelecomUtil.getTelecomSupported()) {
+ TelephonyUtil.getManager(this).listen(hangUpRtcOnDeviceCallAnswered, PhoneStateListener.LISTEN_NONE);
+ }
}
@Override
@@ -147,12 +157,19 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
switch (intent.getAction()) {
case ACTION_UPDATE:
+ RecipientId recipientId = Objects.requireNonNull(intent.getParcelableExtra(EXTRA_RECIPIENT_ID));
+ isGroup = Recipient.resolved(recipientId).isGroup();
setCallInProgressNotification(intent.getIntExtra(EXTRA_UPDATE_TYPE, 0),
Objects.requireNonNull(intent.getParcelableExtra(EXTRA_RECIPIENT_ID)));
return START_STICKY;
case ACTION_SEND_AUDIO_COMMAND:
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;
case ACTION_CHANGE_POWER_BUTTON:
setCallNotification();
@@ -207,23 +224,21 @@ public final class WebRtcCallService extends Service implements SignalAudioManag
}
private void registerNetworkReceiver() {
- if (networkReceiver == null) {
- networkReceiver = new NetworkReceiver();
-
- registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+ if (networkListener == null) {
+ networkListener = new NetworkListener();
+ NetworkConstraintObserver.getInstance(ApplicationDependencies.getApplication()).addListener(networkListener);
}
}
private void unregisterNetworkReceiver() {
- if (networkReceiver != null) {
- unregisterReceiver(networkReceiver);
-
- networkReceiver = null;
+ if (networkListener != null) {
+ NetworkConstraintObserver.getInstance(ApplicationDependencies.getApplication()).removeListener(networkListener);
+ networkListener = null;
}
}
public void registerPowerButtonReceiver() {
- if (powerButtonReceiver == null) {
+ if (!AndroidTelecomUtil.getTelecomSupported() && powerButtonReceiver == null) {
powerButtonReceiver = new PowerButtonReceiver();
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
- public void onReceive(Context context, Intent intent) {
- ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
-
- ApplicationDependencies.getSignalCallManager().networkChange(activeNetworkInfo != null && activeNetworkInfo.isConnected());
+ public void onNetworkChanged() {
+ ApplicationDependencies.getSignalCallManager().networkChange(NetworkConstraint.isMet(ApplicationDependencies.getApplication()));
ApplicationDependencies.getSignalCallManager().bandwidthModeUpdate();
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java
index 0f83670307..02f9eb059e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java
@@ -29,12 +29,12 @@ import java.util.UUID;
*/
public class WebRtcInteractor {
- @NonNull private final Context context;
- @NonNull private final SignalCallManager signalCallManager;
- @NonNull private final LockManager lockManager;
- @NonNull private final CameraEventListener cameraEventListener;
- @NonNull private final GroupCall.Observer groupCallObserver;
- @NonNull private final AppForegroundObserver.Listener foregroundListener;
+ private final Context context;
+ private final SignalCallManager signalCallManager;
+ private final LockManager lockManager;
+ private final CameraEventListener cameraEventListener;
+ private final GroupCall.Observer groupCallObserver;
+ private final AppForegroundObserver.Listener foregroundListener;
public WebRtcInteractor(@NonNull Context context,
@NonNull SignalCallManager signalCallManager,
@@ -151,15 +151,35 @@ public class WebRtcInteractor {
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.Start());
}
- public void setUserAudioDevice(@NonNull SignalAudioManager.AudioDevice userDevice) {
- WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetUserDevice(userDevice));
+ public void setUserAudioDevice(@Nullable RecipientId recipientId, @NonNull SignalAudioManager.AudioDevice userDevice) {
+ WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetUserDevice(recipientId, userDevice));
}
- public void setDefaultAudioDevice(@NonNull SignalAudioManager.AudioDevice userDevice, boolean clearUserEarpieceSelection) {
- WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetDefaultDevice(userDevice, clearUserEarpieceSelection));
+ public void setDefaultAudioDevice(@NonNull RecipientId recipientId, @NonNull SignalAudioManager.AudioDevice userDevice, boolean clearUserEarpieceSelection) {
+ WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetDefaultDevice(recipientId, userDevice, clearUserEarpieceSelection));
}
void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo 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);
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java
index 0125c8ffbd..8213cf1219 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcUtil.java
@@ -80,9 +80,10 @@ public final class WebRtcUtil {
}
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);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java
deleted file mode 100644
index addf90db55..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java
+++ /dev/null
@@ -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 remoteParticipants;
- Map peerMap;
- RemotePeer activePeer;
- GroupCall groupCall;
- WebRtcViewModel.GroupCallState groupState;
- Set 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 remoteParticipants,
- @NonNull Map peerMap,
- @Nullable RemotePeer activePeer,
- @Nullable GroupCall groupCall,
- @NonNull WebRtcViewModel.GroupCallState groupState,
- @NonNull Set 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 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 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 getIdentityChangedRecipients() {
- return identityChangedRecipients;
- }
-
- public OptionalLong getRemoteDevicesCount() {
- return remoteDevicesCount;
- }
-
- public @Nullable Long getParticipantLimit() {
- return participantLimit;
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.kt
new file mode 100644
index 0000000000..520c82d124
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.kt
@@ -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 = mutableMapOf(),
+ var peerMap: MutableMap = mutableMapOf(),
+ var activePeer: RemotePeer? = null,
+ var groupCall: GroupCall? = null,
+ @get:JvmName("getGroupCallState") var groupState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE,
+ var identityChangedRecipients: MutableSet = mutableSetOf(),
+ var remoteDevicesCount: OptionalLong = OptionalLong.empty(),
+ var participantLimit: Long? = null
+) {
+
+ val remoteCallParticipants: List
+ 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()
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallSetupState.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallSetupState.kt
index 44a3951905..138666b823 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallSetupState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallSetupState.kt
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.service.webrtc.state
import org.thoughtcrime.securesms.recipients.Recipient
+import org.webrtc.PeerConnection
/**
* Information specific to setting up a call.
@@ -12,7 +13,11 @@ data class CallSetupState(
@get:JvmName("hasSentJoinedMessage") var sentJoinedMessage: Boolean = false,
@get:JvmName("shouldRingGroup") var ringGroup: Boolean = true,
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 = mutableListOf(),
+ @get:JvmName("isAlwaysTurnServers") var alwaysTurnServers: Boolean = false
) {
fun duplicate(): CallSetupState {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceState.java
index ebacc5c574..d7c4e9d5e8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceState.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceState.java
@@ -31,7 +31,7 @@ public final class WebRtcServiceState {
public WebRtcServiceState(@NonNull WebRtcServiceState toCopy) {
this.actionProcessor = toCopy.actionProcessor;
- this.callInfoState = new CallInfoState(toCopy.callInfoState);
+ this.callInfoState = toCopy.callInfoState.duplicate();
this.localDeviceState = toCopy.localDeviceState.duplicate();
this.videoState = new VideoState(toCopy.videoState);
this.callSetupStates = new HashMap<>();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java
index 5c2945b389..7498817037 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
+import org.webrtc.PeerConnection;
import java.util.Collection;
import java.util.Set;
@@ -65,7 +66,7 @@ public class WebRtcServiceStateBuilder {
toBuild.videoState = new VideoState();
CallInfoState newCallInfoState = new CallInfoState();
- newCallInfoState.peerMap.putAll(toBuild.callInfoState.peerMap);
+ newCallInfoState.getPeerMap().putAll(toBuild.callInfoState.getPeerMap());
toBuild.callInfoState = newCallInfoState;
toBuild.callSetupStates.remove(callId);
@@ -179,6 +180,27 @@ public class WebRtcServiceStateBuilder {
toBuild.setRingerRecipient(ringerRecipient);
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 iceServers) {
+ toBuild.getIceServers().clear();
+ toBuild.getIceServers().addAll(iceServers);
+ return this;
+ }
+
+ public @NonNull CallSetupStateBuilder alwaysTurn(boolean isAlwaysTurn) {
+ toBuild.setAlwaysTurnServers(isAlwaysTurn);
+ return this;
+ }
}
public class VideoStateBuilder {
@@ -218,7 +240,7 @@ public class WebRtcServiceStateBuilder {
private CallInfoState toBuild;
public CallInfoStateBuilder() {
- toBuild = new CallInfoState(WebRtcServiceStateBuilder.this.toBuild.callInfoState);
+ toBuild = WebRtcServiceStateBuilder.this.toBuild.callInfoState.duplicate();
}
public @NonNull WebRtcServiceStateBuilder commit() {
@@ -232,82 +254,82 @@ public class WebRtcServiceStateBuilder {
}
public @NonNull CallInfoStateBuilder callState(@NonNull WebRtcViewModel.State callState) {
- toBuild.callState = callState;
+ toBuild.setCallState(callState);
return this;
}
public @NonNull CallInfoStateBuilder callRecipient(@NonNull Recipient callRecipient) {
- toBuild.callRecipient = callRecipient;
+ toBuild.setCallRecipient(callRecipient);
return this;
}
public @NonNull CallInfoStateBuilder callConnectedTime(long callConnectedTime) {
- toBuild.callConnectedTime = callConnectedTime;
+ toBuild.setCallConnectedTime(callConnectedTime);
return this;
}
public @NonNull CallInfoStateBuilder putParticipant(@NonNull CallParticipantId callParticipantId, @NonNull CallParticipant callParticipant) {
- toBuild.remoteParticipants.put(callParticipantId, callParticipant);
+ toBuild.getRemoteCallParticipantsMap().put(callParticipantId, callParticipant);
return this;
}
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;
}
public @NonNull CallInfoStateBuilder clearParticipantMap() {
- toBuild.remoteParticipants.clear();
+ toBuild.getRemoteCallParticipantsMap().clear();
return this;
}
public @NonNull CallInfoStateBuilder putRemotePeer(@NonNull RemotePeer remotePeer) {
- toBuild.peerMap.put(remotePeer.hashCode(), remotePeer);
+ toBuild.getPeerMap().put(remotePeer.hashCode(), remotePeer);
return this;
}
public @NonNull CallInfoStateBuilder clearPeerMap() {
- toBuild.peerMap.clear();
+ toBuild.getPeerMap().clear();
return this;
}
public @NonNull CallInfoStateBuilder removeRemotePeer(@NonNull RemotePeer remotePeer) {
- toBuild.peerMap.remove(remotePeer.hashCode());
+ toBuild.getPeerMap().remove(remotePeer.hashCode());
return this;
}
public @NonNull CallInfoStateBuilder activePeer(@Nullable RemotePeer activePeer) {
- toBuild.activePeer = activePeer;
+ toBuild.setActivePeer(activePeer);
return this;
}
public @NonNull CallInfoStateBuilder groupCall(@Nullable GroupCall groupCall) {
- toBuild.groupCall = groupCall;
+ toBuild.setGroupCall(groupCall);
return this;
}
- public @NonNull CallInfoStateBuilder groupCallState(@Nullable WebRtcViewModel.GroupCallState groupState) {
- toBuild.groupState = groupState;
+ public @NonNull CallInfoStateBuilder groupCallState(@NonNull WebRtcViewModel.GroupCallState groupState) {
+ toBuild.setGroupState(groupState);
return this;
}
public @NonNull CallInfoStateBuilder addIdentityChangedRecipients(@NonNull Collection id) {
- toBuild.identityChangedRecipients.addAll(id);
+ toBuild.getIdentityChangedRecipients().addAll(id);
return this;
}
public @NonNull CallInfoStateBuilder removeIdentityChangedRecipients(@NonNull Collection ids) {
- toBuild.identityChangedRecipients.removeAll(ids);
+ toBuild.getIdentityChangedRecipients().removeAll(ids);
return this;
}
public @NonNull CallInfoStateBuilder remoteDevicesCount(long remoteDevicesCount) {
- toBuild.remoteDevicesCount = OptionalLong.of(remoteDevicesCount);
+ toBuild.setRemoteDevicesCount(OptionalLong.of(remoteDevicesCount));
return this;
}
public @NonNull CallInfoStateBuilder participantLimit(@Nullable Long participantLimit) {
- toBuild.participantLimit = participantLimit;
+ toBuild.setParticipantLimit(participantLimit);
return this;
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCommand.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCommand.kt
index 1dd223f732..bb828303a1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCommand.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCommand.kt
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.webrtc.audio
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
+import org.thoughtcrime.securesms.recipients.RecipientId
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) {
+ parcel.writeParcelable(recipientId, flags)
parcel.writeSerializable(device)
}
companion object {
@JvmField
- val CREATOR: Parcelable.Creator = ParcelCheat { SetUserDevice(it.readSerializable() as SignalAudioManager.AudioDevice) }
+ val CREATOR: Parcelable.Creator = 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) {
+ parcel.writeParcelable(recipientId, flags)
parcel.writeSerializable(device)
ParcelUtil.writeBoolean(parcel, clearUserEarpieceSelection)
}
@@ -95,6 +98,7 @@ sealed class AudioManagerCommand : Parcelable {
@JvmField
val CREATOR: Parcelable.Creator = ParcelCheat { parcel ->
SetDefaultDevice(
+ recipientId = parcel.readParcelable(RecipientId::class.java.classLoader),
device = parcel.readSerializable() as SignalAudioManager.AudioDevice,
clearUserEarpieceSelection = ParcelUtil.readBoolean(parcel)
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java
index 1eea3aeb8e..c2d3311951 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java
@@ -69,7 +69,7 @@ public class IncomingRinger {
player = null;
}
} 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"));
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt
index 3cfc22a645..51496f553a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt
@@ -12,11 +12,95 @@ import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
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.whispersystems.libsignal.util.guava.Preconditions
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)
+ }
+}
+
/**
* 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
@@ -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
* the bluetooth headset.
*/
-class SignalAudioManager(private val context: Context, private val eventListener: EventListener?) {
-
- private var commandAndControlThread = SignalExecutors.getAndStartHandlerThread("call-audio")
- private val handler = SignalAudioHandler(commandAndControlThread.looper)
-
- private val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
+class FullSignalAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
private val signalBluetoothManager = SignalBluetoothManager(context, this, handler)
- private var state: State = State.UNINITIALIZED
+ private var audioDevices: MutableSet = mutableSetOf()
+ private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE
+ private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE
private var savedAudioMode = AudioManager.MODE_INVALID
private var savedIsSpeakerPhoneOn = false
@@ -48,37 +129,9 @@ class SignalAudioManager(private val context: Context, private val eventListener
private var autoSwitchToWiredHeadset = 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 = 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
- 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.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() {
+ override fun initialize() {
Log.i(TAG, "Initializing audio manager state: $state")
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")
if (state == State.RUNNING) {
Log.w(TAG, "Skipping, already active")
@@ -134,7 +187,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
Log.d(TAG, "Started")
}
- private fun stop(playDisconnect: Boolean) {
+ override fun stop(playDisconnect: Boolean) {
Log.d(TAG, "Stopping. state: $state")
incomingRinger.stop()
@@ -162,17 +215,6 @@ class SignalAudioManager(private val context: Context, private val eventListener
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() {
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")
defaultAudioDevice = when (newDefaultDevice) {
AudioDevice.SPEAKER_PHONE -> newDefaultDevice
@@ -288,7 +330,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
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
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")
androidAudioManager.mode = AudioManager.MODE_RINGTONE
setMicrophoneMute(false)
- setDefaultAudioDevice(AudioDevice.SPEAKER_PHONE, false)
+ setDefaultAudioDevice(null, AudioDevice.SPEAKER_PHONE, false)
incomingRinger.start(ringtoneUri, vibrate)
}
- private fun silenceIncomingRinger() {
- Log.i(TAG, "silenceIncomingRinger():")
- incomingRinger.stop()
- }
-
- private fun startOutgoingRinger() {
+ override fun startOutgoingRinger() {
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice")
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
@@ -361,17 +398,52 @@ class SignalAudioManager(private val context: Context, private val eventListener
handler.post { onWiredHeadsetChange(pluggedIn, hasMic) }
}
}
+}
- enum class AudioDevice {
- SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
+class TelecomAwareSignalAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
+
+ override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
+ if (recipientId != null && AndroidTelecomUtil.getSelectedAudioDevice(recipientId) == AudioDevice.EARPIECE) {
+ selectAudioDevice(recipientId, newDefaultDevice)
+ }
}
- enum class State {
- UNINITIALIZED, PREINITIALIZED, RUNNING
+ override fun initialize() {
+ val focusedGained = androidAudioManager.requestCallAudioFocus()
+ if (!focusedGained) {
+ handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
+ }
}
- interface EventListener {
- @JvmSuppressWildcards
- fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set)
+ override fun start() {
+ incomingRinger.stop()
+ 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)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt
index db21ba868f..769b646765 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt
@@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit
*/
class SignalBluetoothManager(
private val context: Context,
- private val audioManager: SignalAudioManager,
+ private val audioManager: FullSignalAudioManager,
private val handler: SignalAudioHandler
) {
diff --git a/spinner/lib/src/main/java/org/signal/spinner/MessageUtil.kt b/spinner/lib/src/main/java/org/signal/spinner/MessageUtil.kt
index 07fe831b23..0e0b369385 100644
--- a/spinner/lib/src/main/java/org/signal/spinner/MessageUtil.kt
+++ b/spinner/lib/src/main/java/org/signal/spinner/MessageUtil.kt
@@ -110,8 +110,7 @@ object MessageUtil {
isChangeNumber:${type == CHANGE_NUMBER_TYPE}
isBoostRequest:${type == BOOST_REQUEST_TYPE}
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", "
")
}