Integrate calling with Android Telecom system.

This commit is contained in:
Cody Henthorne 2022-02-23 13:16:25 -05:00 committed by Alex Hart
parent 2ed39e4448
commit d6b6884c69
31 changed files with 920 additions and 332 deletions

View file

@ -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>

View file

@ -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");

View file

@ -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

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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;

View file

@ -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;
}

View file

@ -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)

View file

@ -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

View file

@ -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);
}

View file

@ -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(),

View file

@ -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());

View file

@ -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

View file

@ -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));
}
});
}

View file

@ -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();

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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()
)
}

View file

@ -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 {

View file

@ -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<>();

View file

@ -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;
}
}

View file

@ -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)
)

View file

@ -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"));
}
}

View file

@ -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)
}
}

View file

@ -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
) {

View file

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