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", "
") }