mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-25 06:37:47 +00:00
feat: implementing more WebRtcCallService.kt functions and handlers for actions as well as lifecycle
This commit is contained in:
@@ -1,21 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.dependencies
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import dagger.hilt.EntryPoint
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
|
||||||
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
|
|
||||||
|
|
||||||
@EntryPoint
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
interface CallComponent {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun get(context: Context) = ApplicationContext.getInstance(context).callComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun audioManagerCompat(): AudioManagerCompat
|
|
||||||
|
|
||||||
}
|
|
@@ -25,8 +25,8 @@ abstract class CallModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideCallManager(@ApplicationContext context: Context, storage: Storage) =
|
fun provideCallManager(@ApplicationContext context: Context, storage: Storage, audioManagerCompat: AudioManagerCompat) =
|
||||||
CallManager(context)
|
CallManager(context, audioManagerCompat)
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@@ -4,18 +4,21 @@ import android.app.Notification
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.media.AudioManager
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.ResultReceiver
|
||||||
|
import android.telephony.PhoneStateListener
|
||||||
|
import android.telephony.TelephonyManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.thoughtcrime.securesms.dependencies.CallComponent
|
import org.session.libsession.utilities.FutureTaskListener
|
||||||
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.webrtc.CallManager
|
import org.thoughtcrime.securesms.webrtc.*
|
||||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
|
||||||
import java.sql.CallableStatement
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.properties.Delegates
|
|
||||||
import kotlin.properties.Delegates.observable
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WebRtcCallService: Service() {
|
class WebRtcCallService: Service() {
|
||||||
@@ -23,37 +26,47 @@ class WebRtcCallService: Service() {
|
|||||||
@Inject lateinit var callManager: CallManager
|
@Inject lateinit var callManager: CallManager
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ACTION_UPDATE = "UPDATE"
|
const val ACTION_INCOMING_CALL = "CALL_INCOMING"
|
||||||
private const val ACTION_STOP = "STOP"
|
const val ACTION_OUTGOING_CALL = "CALL_OUTGOING"
|
||||||
private const val ACTION_DENY_CALL = "DENY_CALL"
|
const val ACTION_ANSWER_CALL = "ANSWER_CALL"
|
||||||
private const val ACTION_LOCAL_HANGUP = "LOCAL_HANGUP"
|
const val ACTION_DENY_CALL = "DENY_CALL"
|
||||||
private const val ACTION_CHANGE_POWER_BUTTON = "CHANGE_POWER_BUTTON"
|
const val ACTION_LOCAL_HANGUP = "LOCAL_HANGUP"
|
||||||
private const val ACTION_SEND_AUDIO_COMMAND = "SEND_AUDIO_COMMAND"
|
const val ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO"
|
||||||
|
const val ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO"
|
||||||
|
const val ACTION_FLIP_CAMERA = "FLIP_CAMERA"
|
||||||
|
const val ACTION_UPDATE_AUDIO = "UPDATE_AUDIO"
|
||||||
|
const val ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE"
|
||||||
|
const val ACTION_SCREEN_OFF = "SCREEN_OFF"
|
||||||
|
const val ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT"
|
||||||
|
const val ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL"
|
||||||
|
|
||||||
private const val EXTRA_UPDATE_TYPE = "UPDATE_TYPE"
|
const val ACTION_RESPONSE_MESSAGE = "RESPONSE_MESSAGE"
|
||||||
private const val EXTRA_RECIPIENT_ID = "RECIPIENT_ID"
|
const val ACTION_ICE_MESSAGE = "ICE_MESSAGE"
|
||||||
private const val EXTRA_ENABLED = "ENABLED"
|
const val ACTION_ICE_CANDIDATE = "ICE_CANDIDATE"
|
||||||
private const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND"
|
const val ACTION_CALL_CONNECTED = "CALL_CONNECTED"
|
||||||
|
const val ACTION_REMOTE_HANGUP = "REMOTE_HANGUP"
|
||||||
|
const val ACTION_REMOTE_BUSY = "REMOTE_BUSY"
|
||||||
|
const val ACTION_REMOTE_VIDEO_MUTE = "REMOTE_VIDEO_MUTE"
|
||||||
|
const val ACTION_ICE_CONNECTED = "ICE_CONNECTED"
|
||||||
|
|
||||||
private const val INVALID_NOTIFICATION_ID = -1
|
const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID"
|
||||||
|
const val EXTRA_ENABLED = "ENABLED"
|
||||||
|
const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND"
|
||||||
|
const val EXTRA_MUTE = "mute_value"
|
||||||
|
const val EXTRA_AVAILABLE = "enabled_value"
|
||||||
|
const val EXTRA_REMOTE_DESCRIPTION = "remote_description"
|
||||||
|
const val EXTRA_TIMESTAMP = "timestamp"
|
||||||
|
const val EXTRA_CALL_ID = "call_id"
|
||||||
|
const val EXTRA_ICE_SDP = "ice_sdp"
|
||||||
|
const val EXTRA_ICE_SDP_MID = "ice_sdp_mid"
|
||||||
|
const val EXTRA_ICE_SDP_LINE_INDEX = "ice_sdp_line_index"
|
||||||
|
const val EXTRA_RESULT_RECEIVER = "result_receiver"
|
||||||
|
|
||||||
private var lastNotificationId: Int = INVALID_NOTIFICATION_ID
|
const val DATA_CHANNEL_NAME = "signaling"
|
||||||
private var lastNotification: Notification? = null
|
|
||||||
|
|
||||||
|
const val INVALID_NOTIFICATION_ID = -1
|
||||||
|
|
||||||
fun update(context: Context, type: Int, callId: UUID) {
|
fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_ANSWER_CALL)
|
||||||
val intent = Intent(context, WebRtcCallService::class.java)
|
|
||||||
.setAction(ACTION_UPDATE)
|
|
||||||
.putExtra(EXTRA_RECIPIENT_ID, callId)
|
|
||||||
.putExtra(EXTRA_UPDATE_TYPE, type)
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop(context: Context) {
|
|
||||||
val intent = Intent(context, WebRtcCallService::class.java)
|
|
||||||
.setAction(ACTION_STOP)
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun denyCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_DENY_CALL)
|
fun denyCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_DENY_CALL)
|
||||||
|
|
||||||
@@ -61,35 +74,157 @@ class WebRtcCallService: Service() {
|
|||||||
|
|
||||||
fun sendAudioManagerCommand(context: Context, command: AudioManagerCommand) {
|
fun sendAudioManagerCommand(context: Context, command: AudioManagerCommand) {
|
||||||
val intent = Intent(context, WebRtcCallService::class.java)
|
val intent = Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_SEND_AUDIO_COMMAND)
|
.setAction(ACTION_UPDATE_AUDIO)
|
||||||
.putExtra(EXTRA_AUDIO_COMMAND, command)
|
.putExtra(EXTRA_AUDIO_COMMAND, command)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changePowerButtonReceiver(context: Context, register: Boolean) {
|
@JvmStatic
|
||||||
|
fun isCallActive(context: Context, resultReceiver: ResultReceiver) {
|
||||||
val intent = Intent(context, WebRtcCallService::class.java)
|
val intent = Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_CHANGE_POWER_BUTTON)
|
.setAction(ACTION_IS_IN_CALL_QUERY)
|
||||||
.putExtra(EXTRA_ENABLED, register)
|
.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var lastNotificationId: Int = INVALID_NOTIFICATION_ID
|
||||||
|
private var lastNotification: Notification? = null
|
||||||
|
|
||||||
|
private val serviceExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
private val hangupOnCallAnswered = HangUpRtcOnPstnCallAnsweredListener {
|
||||||
|
startService(hangupIntent(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var callReceiver: IncomingPstnCallReceiver? = null
|
||||||
|
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun terminate() {
|
||||||
|
stopForeground(true)
|
||||||
|
callManager.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBusy() = callManager.isBusy(this)
|
||||||
|
|
||||||
|
private fun initializeVideo() {
|
||||||
|
callManager.initializeVideo(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null || intent.action == null) return START_NOT_STICKY
|
||||||
|
serviceExecutor.execute {
|
||||||
|
val action = intent.action
|
||||||
|
when {
|
||||||
|
action == ACTION_INCOMING_CALL && isBusy() -> handleBusyCall(intent)
|
||||||
|
action == ACTION_REMOTE_BUSY -> handleBusyMessage(intent)
|
||||||
|
action == ACTION_INCOMING_CALL -> handleIncomingCall(intent)
|
||||||
|
action == ACTION_OUTGOING_CALL -> handleOutgoingCall(intent)
|
||||||
|
action == ACTION_ANSWER_CALL -> handleAnswerCall(intent)
|
||||||
|
action == ACTION_DENY_CALL -> handleDenyCall(intent)
|
||||||
|
action == ACTION_LOCAL_HANGUP -> handleLocalHangup(intent)
|
||||||
|
action == ACTION_REMOTE_HANGUP -> handleRemoteHangup(intent)
|
||||||
|
action == ACTION_SET_MUTE_AUDIO -> handleSetMuteAudio(intent)
|
||||||
|
action == ACTION_SET_MUTE_VIDEO -> handleSetMuteVideo(intent)
|
||||||
|
action == ACTION_FLIP_CAMERA -> handlesetCameraFlip(intent)
|
||||||
|
// action == ACTION_BLUETOOTH_CHANGE -> handleBluetoothChange(intent)
|
||||||
|
// action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChange(intent)
|
||||||
|
action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent)
|
||||||
|
action == ACTION_REMOTE_VIDEO_MUTE -> handleRemoteVideoMute(intent)
|
||||||
|
action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent)
|
||||||
|
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
|
||||||
|
action == ACTION_ICE_CANDIDATE -> handleLocalIceCandidate(intent)
|
||||||
|
action == ACTION_CALL_CONNECTED -> handleCallConnected(intent)
|
||||||
|
action == ACTION_CHECK_TIMEOUT -> handleCheckTimeout(intent)
|
||||||
|
action == ACTION_IS_IN_CALL_QUERY -> handleIsInCallQuery(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
callManager.initializeResources(this)
|
||||||
// create audio manager
|
// create audio manager
|
||||||
|
registerIncomingPstnCallReceiver()
|
||||||
|
registerWiredHeadsetStateReceiver()
|
||||||
|
getSystemService(TelephonyManager::class.java)
|
||||||
|
.listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE)
|
||||||
// reset call notification
|
// reset call notification
|
||||||
// register uncaught exception handler
|
// register uncaught exception handler
|
||||||
// register network receiver
|
// register network receiver
|
||||||
// telephony listen to call state
|
// telephony listen to call state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun registerIncomingPstnCallReceiver() {
|
||||||
|
callReceiver = IncomingPstnCallReceiver()
|
||||||
|
registerReceiver(callReceiver, IntentFilter("android.intent.action.PHONE_STATE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerWiredHeadsetStateReceiver() {
|
||||||
|
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver()
|
||||||
|
registerReceiver(wiredHeadsetStateReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
callReceiver?.let { receiver ->
|
||||||
|
unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
callReceiver = null
|
||||||
// unregister exception handler
|
// unregister exception handler
|
||||||
// shutdown audiomanager
|
// shutdown audiomanager
|
||||||
// unregister network receiver
|
// unregister network receiver
|
||||||
// unregister power button
|
// unregister power button
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TimeoutRunnable(private val callId: UUID, private val context: Context): Runnable {
|
||||||
|
override fun run() {
|
||||||
|
val intent = Intent(context, WebRtcCallService::class.java)
|
||||||
|
.setAction(ACTION_CHECK_TIMEOUT)
|
||||||
|
.putExtra(EXTRA_CALL_ID, callId)
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class StateAwareListener<V>(
|
||||||
|
private val expectedState: CallManager.CallState,
|
||||||
|
private val expectedCallId: UUID,
|
||||||
|
private val getState: ()->Pair<CallManager.CallState, UUID>): FutureTaskListener<V> {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(StateAwareListener::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(result: V) {
|
||||||
|
if (!isConsistentState()) {
|
||||||
|
Log.w(TAG,"State has changed since request, aborting success callback...")
|
||||||
|
} else {
|
||||||
|
onSuccessContinue(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(exception: ExecutionException?) {
|
||||||
|
if (!isConsistentState()) {
|
||||||
|
Log.w(TAG, exception)
|
||||||
|
Log.w(TAG,"State has changed since request, aborting failure callback...")
|
||||||
|
} else {
|
||||||
|
exception?.let {
|
||||||
|
onFailureContinue(it.cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isConsistentState(): Boolean {
|
||||||
|
val (currentState, currentCallId) = getState()
|
||||||
|
return expectedState == currentState && expectedCallId == currentCallId
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun onSuccessContinue(result: V)
|
||||||
|
abstract fun onFailureContinue(throwable: Throwable?)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -1,20 +1,23 @@
|
|||||||
package org.thoughtcrime.securesms.webrtc
|
package org.thoughtcrime.securesms.webrtc
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.android.mms.transaction.MessageSender
|
import android.telephony.TelephonyManager
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import org.session.libsession.messaging.messages.control.CallMessage
|
import org.session.libsession.messaging.messages.control.CallMessage
|
||||||
|
import org.session.libsession.utilities.Util
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
|
||||||
import org.thoughtcrime.securesms.dependencies.CallComponent
|
|
||||||
import org.thoughtcrime.securesms.service.WebRtcCallService
|
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||||
|
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
|
||||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||||
|
import org.thoughtcrime.securesms.webrtc.video.CameraState
|
||||||
import org.webrtc.*
|
import org.webrtc.*
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class CallManager(private val context: Context): PeerConnection.Observer,
|
class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConnection.Observer,
|
||||||
SignalAudioManager.EventListener,
|
SignalAudioManager.EventListener,
|
||||||
CallDataListener {
|
CallDataListener {
|
||||||
|
|
||||||
@@ -22,19 +25,62 @@ class CallManager(private val context: Context): PeerConnection.Observer,
|
|||||||
STATE_IDLE, STATE_DIALING, STATE_ANSWERING, STATE_REMOTE_RINGING, STATE_LOCAL_RINGING, STATE_CONNECTED
|
STATE_IDLE, STATE_DIALING, STATE_ANSWERING, STATE_REMOTE_RINGING, STATE_LOCAL_RINGING, STATE_CONNECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class StateEvent {
|
||||||
val signalAudioManager: SignalAudioManager by lazy {
|
data class AudioEnabled(val isEnabled: Boolean): StateEvent()
|
||||||
SignalAudioManager(context, this, CallComponent.get(context).audioManagerCompat())
|
data class VideoEnabled(val isEnabled: Boolean): StateEvent()
|
||||||
|
data class CallStateUpdate(val state: CallState): StateEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val serviceExecutor = Executors.newSingleThreadExecutor()
|
companion object {
|
||||||
|
private val TAG = Log.tag(CallManager::class.java)
|
||||||
|
val CONNECTED_STATES = arrayOf(CallState.STATE_CONNECTED)
|
||||||
|
val PENDING_CONNECTION_STATES = arrayOf(
|
||||||
|
CallState.STATE_DIALING,
|
||||||
|
CallState.STATE_ANSWERING,
|
||||||
|
CallState.STATE_LOCAL_RINGING,
|
||||||
|
CallState.STATE_REMOTE_RINGING
|
||||||
|
)
|
||||||
|
val OUTGOING_STATES = arrayOf(
|
||||||
|
CallState.STATE_DIALING,
|
||||||
|
CallState.STATE_REMOTE_RINGING,
|
||||||
|
CallState.STATE_CONNECTED
|
||||||
|
)
|
||||||
|
val DISCONNECTED_STATES = arrayOf(CallState.STATE_IDLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val signalAudioManager: SignalAudioManager = SignalAudioManager(context, this, audioManager)
|
||||||
|
|
||||||
|
private val _audioEvents = MutableStateFlow(StateEvent.AudioEnabled(false))
|
||||||
|
val audioEvents = _audioEvents.asSharedFlow()
|
||||||
|
private val _videoEvents = MutableStateFlow(StateEvent.VideoEnabled(false))
|
||||||
|
val videoEvents = _videoEvents.asSharedFlow()
|
||||||
|
private val _remoteVideoEvents = MutableStateFlow(StateEvent.VideoEnabled(false))
|
||||||
|
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow()
|
||||||
|
private val _connectionEvents = MutableStateFlow<StateEvent>(StateEvent.CallStateUpdate(CallState.STATE_IDLE))
|
||||||
|
val connectionEvents = _connectionEvents.asSharedFlow()
|
||||||
|
private var localCameraState: CameraState = CameraState.UNKNOWN
|
||||||
|
private var microphoneEnabled = true
|
||||||
|
private var remoteVideoEnabled = false
|
||||||
|
private var bluetoothAvailable = false
|
||||||
|
|
||||||
|
private val currentCallState = (_connectionEvents.value as StateEvent.CallStateUpdate).state
|
||||||
|
|
||||||
private val networkExecutor = Executors.newSingleThreadExecutor()
|
private val networkExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
private val eglBase: EglBase = EglBase.create()
|
private var eglBase: EglBase? = null
|
||||||
|
|
||||||
|
private var callId: UUID? = null
|
||||||
|
private var recipient: Recipient? = null
|
||||||
private var peerConnectionWrapper: PeerConnectionWrapper? = null
|
private var peerConnectionWrapper: PeerConnectionWrapper? = null
|
||||||
|
private var dataChannel: DataChannel? = null
|
||||||
|
|
||||||
private val currentCallState: MutableStateFlow<CallState> = MutableStateFlow(CallState.STATE_IDLE)
|
private val pendingOutgoingIceUpdates = ArrayDeque<IceCandidate>()
|
||||||
|
private val pendingIncomingIceUpdates = ArrayDeque<IceCandidate>()
|
||||||
|
|
||||||
|
private var localRenderer: SurfaceViewRenderer? = null
|
||||||
|
private var remoteRenderer: SurfaceViewRenderer? = null
|
||||||
|
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||||
|
|
||||||
private fun createCameraCapturer(enumerator: CameraEnumerator): CameraVideoCapturer? {
|
private fun createCameraCapturer(enumerator: CameraEnumerator): CameraVideoCapturer? {
|
||||||
val deviceNames = enumerator.deviceNames
|
val deviceNames = enumerator.deviceNames
|
||||||
@@ -82,67 +128,99 @@ class CallManager(private val context: Context): PeerConnection.Observer,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isBusy(context: Context) = currentCallState != CallState.STATE_IDLE
|
||||||
|
|| context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE
|
||||||
|
|
||||||
|
fun initializeVideo(context: Context) {
|
||||||
|
Util.runOnMainSync {
|
||||||
|
val base = EglBase.create()
|
||||||
|
eglBase = base
|
||||||
|
localRenderer = SurfaceViewRenderer(context)
|
||||||
|
remoteRenderer = SurfaceViewRenderer(context)
|
||||||
|
|
||||||
|
localRenderer?.init(base.eglBaseContext, null)
|
||||||
|
remoteRenderer?.init(base.eglBaseContext, null)
|
||||||
|
|
||||||
|
val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true)
|
||||||
|
val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext)
|
||||||
|
|
||||||
|
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||||
|
.setOptions(object: PeerConnectionFactory.Options() {
|
||||||
|
init {
|
||||||
|
networkIgnoreMask = 1 shl 4
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setVideoEncoderFactory(encoderFactory)
|
||||||
|
.setVideoDecoderFactory(decoderFactory)
|
||||||
|
.createPeerConnectionFactory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun callEnded() {
|
fun callEnded() {
|
||||||
peerConnectionWrapper?.()
|
peerConnectionWrapper?.dispose()
|
||||||
peerConnectionWrapper = null
|
peerConnectionWrapper = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAudioEnabled(isEnabled: Boolean) {
|
fun setAudioEnabled(isEnabled: Boolean) {
|
||||||
|
currentCallState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
|
||||||
|
peerConnectionWrapper?.setAudioEnabled(isEnabled)
|
||||||
|
_audioEvents.value = StateEvent.AudioEnabled(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setVideoEnabled(isEnabled: Boolean) {
|
fun setVideoEnabled(isEnabled: Boolean) {
|
||||||
|
currentCallState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
|
||||||
|
peerConnectionWrapper?.setVideoEnabled(isEnabled)
|
||||||
|
_audioEvents.value = StateEvent.AudioEnabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSignalingChange(newState: PeerConnection.SignalingState) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
|
override fun onIceConnectionReceivingChange(receiving: Boolean) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIceConnectionReceivingChange(p0: Boolean) {
|
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIceCandidate(p0: IceCandidate?) {
|
override fun onIceCandidate(p0: IceCandidate?) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
|
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAddStream(p0: MediaStream?) {
|
override fun onAddStream(p0: MediaStream?) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRemoveStream(p0: MediaStream?) {
|
override fun onRemoveStream(p0: MediaStream?) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDataChannel(p0: DataChannel?) {
|
override fun onDataChannel(p0: DataChannel?) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRenegotiationNeeded() {
|
override fun onRenegotiationNeeded() {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAudioDeviceChanged(activeDevice: SignalAudioManager.AudioDevice, devices: Set<SignalAudioManager.AudioDevice>) {
|
override fun onAudioDeviceChanged(activeDevice: SignalAudioManager.AudioDevice, devices: Set<SignalAudioManager.AudioDevice>) {
|
||||||
TODO("Not yet implemented")
|
signalAudioManager.handleCommand(AudioManagerCommand())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CallMessage.iceCandidates(): List<IceCandidate> {
|
private fun CallMessage.iceCandidates(): List<IceCandidate> {
|
||||||
@@ -152,4 +230,36 @@ class CallManager(private val context: Context): PeerConnection.Observer,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CallState.withState(vararg expected: CallState, transition: ()->Unit) {
|
||||||
|
if (this in expected) transition()
|
||||||
|
else Log.w(TAG,"Tried to transition state $this but expected $expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
signalAudioManager.stop(currentCallState in OUTGOING_STATES)
|
||||||
|
peerConnectionWrapper?.dispose()
|
||||||
|
peerConnectionWrapper = null
|
||||||
|
|
||||||
|
localRenderer?.release()
|
||||||
|
remoteRenderer?.release()
|
||||||
|
eglBase?.release()
|
||||||
|
|
||||||
|
localRenderer = null
|
||||||
|
remoteRenderer = null
|
||||||
|
eglBase = null
|
||||||
|
|
||||||
|
_connectionEvents.value = StateEvent.CallStateUpdate(CallState.STATE_IDLE)
|
||||||
|
localCameraState = CameraState.UNKNOWN
|
||||||
|
recipient = null
|
||||||
|
callId = null
|
||||||
|
microphoneEnabled = true
|
||||||
|
remoteVideoEnabled = false
|
||||||
|
pendingOutgoingIceUpdates.clear()
|
||||||
|
pendingIncomingIceUpdates.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeResources(webRtcCallService: WebRtcCallService) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.session.libsession.messaging.messages.control.CallMessage
|
import org.session.libsession.messaging.messages.control.CallMessage
|
||||||
import org.webrtc.*
|
import org.webrtc.*
|
||||||
@@ -13,28 +14,14 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class CallViewModel @Inject constructor(private val callManager: CallManager): ViewModel() {
|
class CallViewModel @Inject constructor(private val callManager: CallManager): ViewModel() {
|
||||||
|
|
||||||
sealed class StateEvent {
|
val localAudioEnabledState = callManager.audioEvents.map { it.isEnabled }
|
||||||
data class AudioEnabled(val isEnabled: Boolean): StateEvent()
|
val localVideoEnabledState = callManager.videoEvents.map { it.isEnabled }
|
||||||
data class VideoEnabled(val isEnabled: Boolean): StateEvent()
|
val remoteVideoEnabledState = callManager.remoteVideoEvents.map { it.isEnabled }
|
||||||
}
|
|
||||||
|
|
||||||
val audioEnabledState = MutableStateFlow(
|
|
||||||
callManager.audioEnabled.let { isEnabled ->
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val videoEnabledState = MutableStateFlow(
|
|
||||||
callManager.videoEnabled.let { isEnabled ->
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
// set up listeners for establishing connection toggling video / audio
|
// set up listeners for establishing connection toggling video / audio
|
||||||
init {
|
init {
|
||||||
audioEnabledState.onEach { (enabled) -> callManager.setAudioEnabled(enabled) }
|
callManager.audioEvents.onEach { (enabled) -> callManager.setAudioEnabled(enabled) }
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
videoEnabledState.onEach { (enabled) -> callManager.setVideoEnabled(enabled) }
|
callManager.videoEvents.onEach { (enabled) -> callManager.setVideoEnabled(enabled) }
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,74 @@
|
|||||||
|
package org.thoughtcrime.securesms.webrtc;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
import android.telephony.TelephonyManager;
|
||||||
|
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for incoming PSTN calls and rejects them if a RedPhone call is already in progress.
|
||||||
|
*
|
||||||
|
* Unstable use of reflection employed to gain access to ITelephony.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class IncomingPstnCallReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
private static final String TAG = IncomingPstnCallReceiver.class.getSimpleName();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
Log.i(TAG, "Checking incoming call...");
|
||||||
|
|
||||||
|
if (intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) == null) {
|
||||||
|
Log.w(TAG, "Telephony event does not contain number...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intent.getStringExtra(TelephonyManager.EXTRA_STATE).equals(TelephonyManager.EXTRA_STATE_RINGING)) {
|
||||||
|
Log.w(TAG, "Telephony event is not state ringing...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InCallListener listener = new InCallListener(context, new Handler());
|
||||||
|
|
||||||
|
WebRtcCallService.isCallActive(context, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InCallListener extends ResultReceiver {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
InCallListener(Context context, Handler handler) {
|
||||||
|
super(handler);
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||||
|
if (resultCode == 1) {
|
||||||
|
Log.i(TAG, "Attempting to deny incoming PSTN call.");
|
||||||
|
|
||||||
|
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Method getTelephony = tm.getClass().getDeclaredMethod("getITelephony");
|
||||||
|
getTelephony.setAccessible(true);
|
||||||
|
Object telephonyService = getTelephony.invoke(tm);
|
||||||
|
Method endCall = telephonyService.getClass().getDeclaredMethod("endCall");
|
||||||
|
endCall.invoke(telephonyService);
|
||||||
|
Log.i(TAG, "Denied Incoming Call.");
|
||||||
|
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||||
|
Log.w(TAG, "Unable to access ITelephony API", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -72,4 +72,30 @@ class PeerConnectionWrapper(context: Context,
|
|||||||
peerConnection.addStream(mediaStream)
|
peerConnection.addStream(mediaStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addIceCandidate(candidate: IceCandidate) {
|
||||||
|
// TODO: filter logic based on known servers
|
||||||
|
peerConnection.addIceCandidate(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
camera.dispose()
|
||||||
|
|
||||||
|
videoSource?.dispose()
|
||||||
|
|
||||||
|
audioSource.dispose()
|
||||||
|
peerConnection.close()
|
||||||
|
peerConnection.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAudioEnabled(isEnabled: Boolean) {
|
||||||
|
audioTrack.setEnabled(isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVideoEnabled(isEnabled: Boolean) {
|
||||||
|
videoTrack?.let { track ->
|
||||||
|
track.setEnabled(isEnabled)
|
||||||
|
camera.enabled = isEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -7,6 +7,7 @@ import android.telephony.PhoneStateListener
|
|||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@@ -31,19 +32,38 @@ class NetworkReceiver: BroadcastReceiver() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var callManager: CallManager
|
lateinit var callManager: CallManager
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PowerButtonReceiver : BroadcastReceiver() {
|
class PowerButtonReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
TODO("Not yet implemented")
|
if (Intent.ACTION_SCREEN_OFF == intent.action) {
|
||||||
|
val serviceIntent = Intent(context,WebRtcCallService::class.java)
|
||||||
|
.setAction(WebRtcCallService.ACTION_SCREEN_OFF)
|
||||||
|
context.startService(serviceIntent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProximityLockRelease: Thread.UncaughtExceptionHandler {
|
class ProximityLockRelease: Thread.UncaughtExceptionHandler {
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(ProximityLockRelease::class.java)
|
||||||
|
}
|
||||||
override fun uncaughtException(t: Thread, e: Throwable) {
|
override fun uncaughtException(t: Thread, e: Throwable) {
|
||||||
TODO("Not yet implemented")
|
Log.e(TAG,"Uncaught exception - releasing proximity lock", e)
|
||||||
|
// lockManager update phone state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WiredHeadsetStateReceiver: BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val state = intent.getIntExtra("state", -1)
|
||||||
|
val serviceIntent = Intent(context, WebRtcCallService::class.java)
|
||||||
|
.setAction(WebRtcCallService.ACTION_WIRED_HEADSET_CHANGE)
|
||||||
|
.putExtra(WebRtcCallService.EXTRA_AVAILABLE, state != 0)
|
||||||
|
|
||||||
|
context.startService(serviceIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -126,7 +126,7 @@ class SignalAudioManager(private val context: Context,
|
|||||||
Log.d(TAG, "Started")
|
Log.d(TAG, "Started")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stop(playDisconnect: Boolean) {
|
fun stop(playDisconnect: Boolean) {
|
||||||
Log.d(TAG, "Stopping. state: $state")
|
Log.d(TAG, "Stopping. state: $state")
|
||||||
if (state == State.UNINITIALIZED) {
|
if (state == State.UNINITIALIZED) {
|
||||||
Log.i(TAG, "Trying to stop AudioManager in incorrect state: $state")
|
Log.i(TAG, "Trying to stop AudioManager in incorrect state: $state")
|
||||||
|
@@ -45,6 +45,10 @@ class Camera(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
capturer?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
fun flip() {
|
fun flip() {
|
||||||
if (capturer == null || cameraCount < 2) {
|
if (capturer == null || cameraCount < 2) {
|
||||||
Log.w(TAG, "Tried to flip camera without capturer or less than 2 cameras")
|
Log.w(TAG, "Tried to flip camera without capturer or less than 2 cameras")
|
||||||
|
Reference in New Issue
Block a user