feat: implementing more WebRtcCallService.kt functions and handlers for actions as well as lifecycle

This commit is contained in:
jubb
2021-11-03 17:09:21 +11:00
parent 1af9b8ba46
commit 2e3f46ff9f
10 changed files with 450 additions and 115 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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