Integrate calling with Android Telecom system.
This commit is contained in:
parent
2ed39e4448
commit
d6b6884c69
31 changed files with 920 additions and 332 deletions
|
@ -91,6 +91,8 @@
|
|||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||
|
||||
<application android:name=".ApplicationContext"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
@ -628,6 +630,13 @@
|
|||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||
<service android:name=".service.webrtc.AndroidCallConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".components.voice.VoiceNotePlaybackService">
|
||||
<intent-filter>
|
||||
|
|
|
@ -35,30 +35,28 @@ import org.signal.core.util.logging.AndroidLogger;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.telecom.CallAudioState
|
||||
import android.telecom.Connection
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder
|
||||
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCommand
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
|
||||
/**
|
||||
* Signal implementation for the telecom system connection. Provides an interaction point for the system to
|
||||
* inform us about changes in the telecom system. Created and returned by [AndroidCallConnectionService].
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
class AndroidCallConnection(private val context: Context, val recipientId: RecipientId, val isOutgoing: Boolean = false) : Connection() {
|
||||
|
||||
init {
|
||||
connectionProperties = PROPERTY_SELF_MANAGED
|
||||
connectionCapabilities = CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL or
|
||||
CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL or
|
||||
CAPABILITY_MUTE
|
||||
}
|
||||
|
||||
override fun onShowIncomingCallUi() {
|
||||
Log.i(TAG, "onShowIncomingCallUi()")
|
||||
WebRtcCallService.update(context, CallNotificationBuilder.TYPE_INCOMING_RINGING, recipientId)
|
||||
setRinging()
|
||||
}
|
||||
|
||||
override fun onCallAudioStateChanged(state: CallAudioState) {
|
||||
Log.i(TAG, "onCallAudioStateChanged($state)")
|
||||
|
||||
val activeDevice = state.route.toDevices().firstOrNull() ?: SignalAudioManager.AudioDevice.EARPIECE
|
||||
val availableDevices = state.supportedRouteMask.toDevices()
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().onAudioDeviceChanged(activeDevice, availableDevices)
|
||||
}
|
||||
|
||||
override fun onAnswer(videoState: Int) {
|
||||
Log.i(TAG, "onAnswer($videoState)")
|
||||
if (Permissions.hasAll(context, android.Manifest.permission.RECORD_AUDIO)) {
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(false)
|
||||
} else {
|
||||
val intent = Intent(context, WebRtcCallActivity::class.java)
|
||||
intent.action = WebRtcCallActivity.ANSWER_ACTION
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSilence() {
|
||||
WebRtcCallService.sendAudioManagerCommand(context, AudioManagerCommand.SilenceIncomingRinger())
|
||||
}
|
||||
|
||||
override fun onReject() {
|
||||
Log.i(TAG, "onReject()")
|
||||
WebRtcCallService.denyCall(context)
|
||||
}
|
||||
|
||||
override fun onDisconnect() {
|
||||
Log.i(TAG, "onDisconnect()")
|
||||
WebRtcCallService.hangup(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(AndroidCallConnection::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toDevices(): Set<SignalAudioManager.AudioDevice> {
|
||||
val devices = mutableSetOf<SignalAudioManager.AudioDevice>()
|
||||
|
||||
if (this and CallAudioState.ROUTE_BLUETOOTH != 0) {
|
||||
devices += SignalAudioManager.AudioDevice.BLUETOOTH
|
||||
}
|
||||
|
||||
if (this and CallAudioState.ROUTE_EARPIECE != 0) {
|
||||
devices += SignalAudioManager.AudioDevice.EARPIECE
|
||||
}
|
||||
|
||||
if (this and CallAudioState.ROUTE_WIRED_HEADSET != 0) {
|
||||
devices += SignalAudioManager.AudioDevice.WIRED_HEADSET
|
||||
}
|
||||
|
||||
if (this and CallAudioState.ROUTE_SPEAKER != 0) {
|
||||
devices += SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.telecom.Connection
|
||||
import android.telecom.ConnectionRequest
|
||||
import android.telecom.ConnectionService
|
||||
import android.telecom.PhoneAccountHandle
|
||||
import android.telecom.TelecomManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Signal implementation of the Android telecom [ConnectionService]. The system binds to this service
|
||||
* when we inform the [TelecomManager] of a new incoming or outgoing call. It'll then call the appropriate
|
||||
* create/failure method to let us know how to proceed.
|
||||
*/
|
||||
@RequiresApi(26)
|
||||
class AndroidCallConnectionService : ConnectionService() {
|
||||
|
||||
override fun onCreateIncomingConnection(
|
||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
request: ConnectionRequest
|
||||
): Connection {
|
||||
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
|
||||
|
||||
Log.i(TAG, "onCreateIncomingConnection($recipientId)")
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val displayName = recipient.getDisplayName(this)
|
||||
val connection = AndroidCallConnection(applicationContext, recipientId).apply {
|
||||
setInitializing()
|
||||
if (SignalStore.settings().messageNotificationsPrivacy.isDisplayContact && recipient.e164.isPresent) {
|
||||
setAddress(Uri.fromParts("tel", recipient.e164.get(), null), TelecomManager.PRESENTATION_ALLOWED)
|
||||
setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
|
||||
}
|
||||
videoState = request.videoState
|
||||
extras = request.extras
|
||||
setRinging()
|
||||
}
|
||||
AndroidTelecomUtil.connections[recipientId] = connection
|
||||
ApplicationDependencies.getSignalCallManager().setTelecomApproved(callId)
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
override fun onCreateIncomingConnectionFailed(
|
||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
request: ConnectionRequest
|
||||
) {
|
||||
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
|
||||
|
||||
Log.i(TAG, "onCreateIncomingConnectionFailed($recipientId)")
|
||||
ApplicationDependencies.getSignalCallManager().dropCall(callId)
|
||||
}
|
||||
|
||||
override fun onCreateOutgoingConnection(
|
||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
request: ConnectionRequest
|
||||
): Connection {
|
||||
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
|
||||
|
||||
Log.i(TAG, "onCreateOutgoingConnection($recipientId)")
|
||||
val connection = AndroidCallConnection(applicationContext, recipientId, true).apply {
|
||||
videoState = request.videoState
|
||||
extras = request.extras
|
||||
setDialing()
|
||||
}
|
||||
AndroidTelecomUtil.connections[recipientId] = connection
|
||||
ApplicationDependencies.getSignalCallManager().setTelecomApproved(callId)
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
override fun onCreateOutgoingConnectionFailed(
|
||||
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
request: ConnectionRequest
|
||||
) {
|
||||
val (recipientId: RecipientId, callId: Long) = request.getOurExtras()
|
||||
|
||||
Log.i(TAG, "onCreateOutgoingConnectionFailed($recipientId)")
|
||||
ApplicationDependencies.getSignalCallManager().dropCall(callId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(AndroidCallConnectionService::class.java)
|
||||
const val KEY_RECIPIENT_ID = "org.thoughtcrime.securesms.RECIPIENT_ID"
|
||||
const val KEY_CALL_ID = "org.thoughtcrime.securesms.CALL_ID"
|
||||
}
|
||||
|
||||
private fun ConnectionRequest.getOurExtras(): ServiceExtras {
|
||||
val ourExtras: Bundle = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS) ?: extras
|
||||
|
||||
val recipientId: RecipientId = RecipientId.from(ourExtras.getString(KEY_RECIPIENT_ID)!!)
|
||||
val callId: Long = ourExtras.getLong(KEY_CALL_ID)
|
||||
|
||||
return ServiceExtras(recipientId, callId)
|
||||
}
|
||||
|
||||
private data class ServiceExtras(val recipientId: RecipientId, val callId: Long)
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import android.telecom.CallAudioState
|
||||
import android.telecom.Connection
|
||||
import android.telecom.DisconnectCause
|
||||
import android.telecom.DisconnectCause.REJECTED
|
||||
import android.telecom.DisconnectCause.UNKNOWN
|
||||
import android.telecom.PhoneAccount
|
||||
import android.telecom.PhoneAccountHandle
|
||||
import android.telecom.TelecomManager
|
||||
import android.telecom.VideoProfile
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
|
||||
/**
|
||||
* Wrapper around various [TelecomManager] methods to make dealing with SDK versions easier. Also
|
||||
* maintains a global list of all Signal [AndroidCallConnection]s associated with their [RecipientId].
|
||||
* There should really only be one ever, but there may be times when dealing with glare or a busy that two
|
||||
* may kick off.
|
||||
*/
|
||||
@SuppressLint("NewApi", "InlinedApi")
|
||||
object AndroidTelecomUtil {
|
||||
|
||||
private val TAG = Log.tag(AndroidTelecomUtil::class.java)
|
||||
private val context = ApplicationDependencies.getApplication()
|
||||
private var systemRejected = false
|
||||
private var accountRegistered = false
|
||||
|
||||
@JvmStatic
|
||||
val telecomSupported: Boolean
|
||||
get() {
|
||||
if (Build.VERSION.SDK_INT >= 26 && !systemRejected) {
|
||||
if (!accountRegistered) {
|
||||
registerPhoneAccount()
|
||||
}
|
||||
|
||||
if (accountRegistered) {
|
||||
val phoneAccount = ContextCompat.getSystemService(context, TelecomManager::class.java)!!.getPhoneAccount(getPhoneAccountHandle())
|
||||
if (phoneAccount != null && phoneAccount.isEnabled) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val connections: MutableMap<RecipientId, AndroidCallConnection> = mutableMapOf()
|
||||
|
||||
@JvmStatic
|
||||
fun registerPhoneAccount() {
|
||||
if (Build.VERSION.SDK_INT >= 26 && !systemRejected) {
|
||||
Log.i(TAG, "Registering phone account")
|
||||
val phoneAccount = PhoneAccount.Builder(getPhoneAccountHandle(), "Signal")
|
||||
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED or PhoneAccount.CAPABILITY_VIDEO_CALLING)
|
||||
.build()
|
||||
|
||||
try {
|
||||
ContextCompat.getSystemService(context, TelecomManager::class.java)!!.registerPhoneAccount(phoneAccount)
|
||||
Log.i(TAG, "Phone account registered successfully")
|
||||
accountRegistered = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to register telecom account", e)
|
||||
systemRejected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@RequiresApi(26)
|
||||
fun getPhoneAccountHandle(): PhoneAccountHandle {
|
||||
return PhoneAccountHandle(ComponentName(context, AndroidCallConnectionService::class.java), context.packageName, Process.myUserHandle())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun addIncomingCall(recipientId: RecipientId, callId: Long, remoteVideoOffer: Boolean): Boolean {
|
||||
if (telecomSupported) {
|
||||
val telecomBundle = bundleOf(
|
||||
TelecomManager.EXTRA_INCOMING_CALL_EXTRAS to bundleOf(
|
||||
AndroidCallConnectionService.KEY_RECIPIENT_ID to recipientId.serialize(),
|
||||
AndroidCallConnectionService.KEY_CALL_ID to callId,
|
||||
TelecomManager.EXTRA_INCOMING_VIDEO_STATE to if (remoteVideoOffer) VideoProfile.STATE_BIDIRECTIONAL else VideoProfile.STATE_AUDIO_ONLY
|
||||
),
|
||||
TelecomManager.EXTRA_INCOMING_VIDEO_STATE to if (remoteVideoOffer) VideoProfile.STATE_BIDIRECTIONAL else VideoProfile.STATE_AUDIO_ONLY
|
||||
)
|
||||
try {
|
||||
Log.i(TAG, "Adding incoming call $telecomBundle")
|
||||
ContextCompat.getSystemService(context, TelecomManager::class.java)!!.addNewIncomingCall(getPhoneAccountHandle(), telecomBundle)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Unable to add incoming call", e)
|
||||
systemRejected = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun reject(recipientId: RecipientId) {
|
||||
if (telecomSupported) {
|
||||
connections[recipientId]?.setDisconnected(DisconnectCause(REJECTED))
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun activateCall(recipientId: RecipientId) {
|
||||
if (telecomSupported) {
|
||||
connections[recipientId]?.setActive()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun terminateCall(recipientId: RecipientId) {
|
||||
if (telecomSupported) {
|
||||
connections[recipientId]?.let { connection ->
|
||||
if (connection.disconnectCause == null) {
|
||||
connection.setDisconnected(DisconnectCause(UNKNOWN))
|
||||
}
|
||||
connection.destroy()
|
||||
connections.remove(recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun addOutgoingCall(recipientId: RecipientId, callId: Long, isVideoCall: Boolean): Boolean {
|
||||
if (telecomSupported) {
|
||||
val telecomBundle = bundleOf(
|
||||
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE to getPhoneAccountHandle(),
|
||||
TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE to if (isVideoCall) VideoProfile.STATE_BIDIRECTIONAL else VideoProfile.STATE_AUDIO_ONLY,
|
||||
TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS to bundleOf(
|
||||
AndroidCallConnectionService.KEY_RECIPIENT_ID to recipientId.serialize(),
|
||||
AndroidCallConnectionService.KEY_CALL_ID to callId
|
||||
),
|
||||
)
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Adding outgoing call $telecomBundle")
|
||||
ContextCompat.getSystemService(context, TelecomManager::class.java)!!.placeCall(recipientId.generateTelecomE164(), telecomBundle)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Unable to add outgoing call", e)
|
||||
systemRejected = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun selectAudioDevice(recipientId: RecipientId, device: SignalAudioManager.AudioDevice) {
|
||||
if (telecomSupported) {
|
||||
val connection: AndroidCallConnection? = connections[recipientId]
|
||||
Log.i(TAG, "Selecting audio route: $device connection: ${connection != null}")
|
||||
if (connection != null) {
|
||||
when (device) {
|
||||
SignalAudioManager.AudioDevice.SPEAKER_PHONE -> connection.setAudioRouteIfDifferent(CallAudioState.ROUTE_SPEAKER)
|
||||
SignalAudioManager.AudioDevice.BLUETOOTH -> connection.setAudioRouteIfDifferent(CallAudioState.ROUTE_BLUETOOTH)
|
||||
SignalAudioManager.AudioDevice.WIRED_HEADSET -> connection.setAudioRouteIfDifferent(CallAudioState.ROUTE_WIRED_HEADSET)
|
||||
else -> connection.setAudioRouteIfDifferent(CallAudioState.ROUTE_EARPIECE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedAudioDevice(recipientId: RecipientId): SignalAudioManager.AudioDevice {
|
||||
if (telecomSupported) {
|
||||
val connection: AndroidCallConnection? = connections[recipientId]
|
||||
if (connection != null) {
|
||||
return when (connection.callAudioState.route) {
|
||||
CallAudioState.ROUTE_SPEAKER -> SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
CallAudioState.ROUTE_BLUETOOTH -> SignalAudioManager.AudioDevice.BLUETOOTH
|
||||
CallAudioState.ROUTE_WIRED_HEADSET -> SignalAudioManager.AudioDevice.WIRED_HEADSET
|
||||
else -> SignalAudioManager.AudioDevice.EARPIECE
|
||||
}
|
||||
}
|
||||
}
|
||||
return SignalAudioManager.AudioDevice.NONE
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
private fun Connection.setAudioRouteIfDifferent(newRoute: Int) {
|
||||
if (callAudioState.route != newRoute) {
|
||||
setAudioRoute(newRoute)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RecipientId.generateTelecomE164(): Uri {
|
||||
val pseudoNumber = toLong().toString().padEnd(10, '0').replaceRange(3..5, "555")
|
||||
return Uri.fromParts("tel", "+1$pseudoNumber", null)
|
||||
}
|
|
@ -69,7 +69,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<PeerConnection.IceServer> 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<PeerConnection.IceServer> 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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
package org.thoughtcrime.securesms.service.webrtc.state;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.OptionalLong;
|
||||
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* General state of ongoing calls.
|
||||
*/
|
||||
public class CallInfoState {
|
||||
|
||||
WebRtcViewModel.State callState;
|
||||
Recipient callRecipient;
|
||||
long callConnectedTime;
|
||||
Map<CallParticipantId, CallParticipant> remoteParticipants;
|
||||
Map<Integer, RemotePeer> peerMap;
|
||||
RemotePeer activePeer;
|
||||
GroupCall groupCall;
|
||||
WebRtcViewModel.GroupCallState groupState;
|
||||
Set<RecipientId> identityChangedRecipients;
|
||||
OptionalLong remoteDevicesCount;
|
||||
Long participantLimit;
|
||||
|
||||
public CallInfoState() {
|
||||
this(WebRtcViewModel.State.IDLE,
|
||||
Recipient.UNKNOWN,
|
||||
-1,
|
||||
Collections.emptyMap(),
|
||||
Collections.emptyMap(),
|
||||
null,
|
||||
null,
|
||||
WebRtcViewModel.GroupCallState.IDLE,
|
||||
Collections.emptySet(),
|
||||
OptionalLong.empty(),
|
||||
null);
|
||||
}
|
||||
|
||||
public CallInfoState(@NonNull CallInfoState toCopy) {
|
||||
this(toCopy.callState,
|
||||
toCopy.callRecipient,
|
||||
toCopy.callConnectedTime,
|
||||
toCopy.remoteParticipants,
|
||||
toCopy.peerMap,
|
||||
toCopy.activePeer,
|
||||
toCopy.groupCall,
|
||||
toCopy.groupState,
|
||||
toCopy.identityChangedRecipients,
|
||||
toCopy.remoteDevicesCount,
|
||||
toCopy.participantLimit);
|
||||
}
|
||||
|
||||
public CallInfoState(@NonNull WebRtcViewModel.State callState,
|
||||
@NonNull Recipient callRecipient,
|
||||
long callConnectedTime,
|
||||
@NonNull Map<CallParticipantId, CallParticipant> remoteParticipants,
|
||||
@NonNull Map<Integer, RemotePeer> peerMap,
|
||||
@Nullable RemotePeer activePeer,
|
||||
@Nullable GroupCall groupCall,
|
||||
@NonNull WebRtcViewModel.GroupCallState groupState,
|
||||
@NonNull Set<RecipientId> identityChangedRecipients,
|
||||
@NonNull OptionalLong remoteDevicesCount,
|
||||
@Nullable Long participantLimit)
|
||||
{
|
||||
this.callState = callState;
|
||||
this.callRecipient = callRecipient;
|
||||
this.callConnectedTime = callConnectedTime;
|
||||
this.remoteParticipants = new LinkedHashMap<>(remoteParticipants);
|
||||
this.peerMap = new HashMap<>(peerMap);
|
||||
this.activePeer = activePeer;
|
||||
this.groupCall = groupCall;
|
||||
this.groupState = groupState;
|
||||
this.identityChangedRecipients = new HashSet<>(identityChangedRecipients);
|
||||
this.remoteDevicesCount = remoteDevicesCount;
|
||||
this.participantLimit = participantLimit;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getCallRecipient() {
|
||||
return callRecipient;
|
||||
}
|
||||
|
||||
public long getCallConnectedTime() {
|
||||
return callConnectedTime;
|
||||
}
|
||||
|
||||
public @NonNull Map<CallParticipantId, CallParticipant> getRemoteCallParticipantsMap() {
|
||||
return new LinkedHashMap<>(remoteParticipants);
|
||||
}
|
||||
|
||||
public @Nullable CallParticipant getRemoteCallParticipant(@NonNull Recipient recipient) {
|
||||
return getRemoteCallParticipant(new CallParticipantId(recipient));
|
||||
}
|
||||
|
||||
public @Nullable CallParticipant getRemoteCallParticipant(@NonNull CallParticipantId callParticipantId) {
|
||||
return remoteParticipants.get(callParticipantId);
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getRemoteCallParticipants() {
|
||||
return new ArrayList<>(remoteParticipants.values());
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.State getCallState() {
|
||||
return callState;
|
||||
}
|
||||
|
||||
public @Nullable RemotePeer getPeer(int hashCode) {
|
||||
return peerMap.get(hashCode);
|
||||
}
|
||||
|
||||
public @Nullable RemotePeer getActivePeer() {
|
||||
return activePeer;
|
||||
}
|
||||
|
||||
public @NonNull RemotePeer requireActivePeer() {
|
||||
return Objects.requireNonNull(activePeer);
|
||||
}
|
||||
|
||||
public @Nullable GroupCall getGroupCall() {
|
||||
return groupCall;
|
||||
}
|
||||
|
||||
public @NonNull GroupCall requireGroupCall() {
|
||||
return Objects.requireNonNull(groupCall);
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
|
||||
return groupState;
|
||||
}
|
||||
|
||||
public @NonNull Set<RecipientId> getIdentityChangedRecipients() {
|
||||
return identityChangedRecipients;
|
||||
}
|
||||
|
||||
public OptionalLong getRemoteDevicesCount() {
|
||||
return remoteDevicesCount;
|
||||
}
|
||||
|
||||
public @Nullable Long getParticipantLimit() {
|
||||
return participantLimit;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package org.thoughtcrime.securesms.service.webrtc.state
|
||||
|
||||
import com.annimon.stream.OptionalLong
|
||||
import org.signal.ringrtc.CallId
|
||||
import org.signal.ringrtc.GroupCall
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer
|
||||
import java.util.ArrayList
|
||||
|
||||
/**
|
||||
* General state of ongoing calls.
|
||||
*/
|
||||
data class CallInfoState(
|
||||
var callState: WebRtcViewModel.State = WebRtcViewModel.State.IDLE,
|
||||
var callRecipient: Recipient = Recipient.UNKNOWN,
|
||||
var callConnectedTime: Long = -1,
|
||||
@get:JvmName("getRemoteCallParticipantsMap") var remoteParticipants: MutableMap<CallParticipantId, CallParticipant> = mutableMapOf(),
|
||||
var peerMap: MutableMap<Int, RemotePeer> = mutableMapOf(),
|
||||
var activePeer: RemotePeer? = null,
|
||||
var groupCall: GroupCall? = null,
|
||||
@get:JvmName("getGroupCallState") var groupState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE,
|
||||
var identityChangedRecipients: MutableSet<RecipientId> = mutableSetOf(),
|
||||
var remoteDevicesCount: OptionalLong = OptionalLong.empty(),
|
||||
var participantLimit: Long? = null
|
||||
) {
|
||||
|
||||
val remoteCallParticipants: List<CallParticipant>
|
||||
get() = ArrayList(remoteParticipants.values)
|
||||
|
||||
fun getRemoteCallParticipant(recipient: Recipient): CallParticipant? {
|
||||
return getRemoteCallParticipant(CallParticipantId(recipient))
|
||||
}
|
||||
|
||||
fun getRemoteCallParticipant(callParticipantId: CallParticipantId): CallParticipant? {
|
||||
return remoteParticipants[callParticipantId]
|
||||
}
|
||||
|
||||
fun getPeer(hashCode: Int): RemotePeer? {
|
||||
return peerMap[hashCode]
|
||||
}
|
||||
|
||||
fun getPeerByCallId(callId: CallId): RemotePeer? {
|
||||
return peerMap.values.firstOrNull { it.callId == callId }
|
||||
}
|
||||
|
||||
fun requireActivePeer(): RemotePeer {
|
||||
return activePeer!!
|
||||
}
|
||||
|
||||
fun requireGroupCall(): GroupCall {
|
||||
return groupCall!!
|
||||
}
|
||||
|
||||
fun duplicate(): CallInfoState = copy(
|
||||
remoteParticipants = remoteParticipants.toMutableMap(),
|
||||
peerMap = peerMap.toMutableMap(),
|
||||
identityChangedRecipients = identityChangedRecipients.toMutableSet()
|
||||
)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.service.webrtc.state
|
||||
|
||||
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<PeerConnection.IceServer> = mutableListOf(),
|
||||
@get:JvmName("isAlwaysTurnServers") var alwaysTurnServers: Boolean = false
|
||||
) {
|
||||
|
||||
fun duplicate(): CallSetupState {
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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<PeerConnection.IceServer> iceServers) {
|
||||
toBuild.getIceServers().clear();
|
||||
toBuild.getIceServers().addAll(iceServers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull CallSetupStateBuilder alwaysTurn(boolean isAlwaysTurn) {
|
||||
toBuild.setAlwaysTurnServers(isAlwaysTurn);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class VideoStateBuilder {
|
||||
|
@ -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<RecipientId> id) {
|
||||
toBuild.identityChangedRecipients.addAll(id);
|
||||
toBuild.getIdentityChangedRecipients().addAll(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull CallInfoStateBuilder removeIdentityChangedRecipients(@NonNull Collection<RecipientId> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SetUserDevice> = ParcelCheat { SetUserDevice(it.readSerializable() as SignalAudioManager.AudioDevice) }
|
||||
val CREATOR: Parcelable.Creator<SetUserDevice> = ParcelCheat { SetUserDevice(it.readParcelable(RecipientId::class.java.classLoader), it.readSerializable() as SignalAudioManager.AudioDevice) }
|
||||
}
|
||||
}
|
||||
|
||||
class SetDefaultDevice(val device: SignalAudioManager.AudioDevice, val clearUserEarpieceSelection: Boolean) : AudioManagerCommand() {
|
||||
class SetDefaultDevice(val recipientId: RecipientId?, val device: SignalAudioManager.AudioDevice, val clearUserEarpieceSelection: Boolean) : AudioManagerCommand() {
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(recipientId, flags)
|
||||
parcel.writeSerializable(device)
|
||||
ParcelUtil.writeBoolean(parcel, clearUserEarpieceSelection)
|
||||
}
|
||||
|
@ -95,6 +98,7 @@ sealed class AudioManagerCommand : Parcelable {
|
|||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<SetDefaultDevice> = ParcelCheat { parcel ->
|
||||
SetDefaultDevice(
|
||||
recipientId = parcel.readParcelable(RecipientId::class.java.classLoader),
|
||||
device = parcel.readSerializable() as SignalAudioManager.AudioDevice,
|
||||
clearUserEarpieceSelection = ParcelUtil.readBoolean(parcel)
|
||||
)
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<AudioDevice>)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<AudioDevice> = 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<AudioDevice> = mutableSetOf()
|
||||
|
||||
private val soundPool: SoundPool = androidAudioManager.createSoundPool()
|
||||
private val connectedSoundId = soundPool.load(context, R.raw.webrtc_completed, 1)
|
||||
private val disconnectedSoundId = soundPool.load(context, R.raw.webrtc_disconnected, 1)
|
||||
|
||||
private val incomingRinger = IncomingRinger(context)
|
||||
private val outgoingRinger = OutgoingRinger(context)
|
||||
|
||||
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
|
||||
|
||||
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<AudioDevice>)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
||||
|
|
|
@ -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", "<br>")
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue