diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallComponent.kt deleted file mode 100644 index b1cc600c84..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallComponent.kt +++ /dev/null @@ -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 - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallModule.kt index a5b9dead84..bf0cd73bb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/CallModule.kt @@ -25,8 +25,8 @@ abstract class CallModule { @Provides @Singleton - fun provideCallManager(@ApplicationContext context: Context, storage: Storage) = - CallManager(context) + fun provideCallManager(@ApplicationContext context: Context, storage: Storage, audioManagerCompat: AudioManagerCompat) = + CallManager(context, audioManagerCompat) @Binds @Singleton diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt index f909de1903..8019692ebd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -4,18 +4,21 @@ import android.app.Notification import android.app.Service import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.media.AudioManager import android.os.IBinder +import android.os.ResultReceiver +import android.telephony.PhoneStateListener +import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint -import org.thoughtcrime.securesms.dependencies.CallComponent -import org.thoughtcrime.securesms.webrtc.AudioManagerCommand -import org.thoughtcrime.securesms.webrtc.CallManager -import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager -import java.sql.CallableStatement +import org.session.libsession.utilities.FutureTaskListener +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.webrtc.* import java.util.* +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors import javax.inject.Inject -import kotlin.properties.Delegates -import kotlin.properties.Delegates.observable @AndroidEntryPoint class WebRtcCallService: Service() { @@ -23,37 +26,47 @@ class WebRtcCallService: Service() { @Inject lateinit var callManager: CallManager companion object { - private const val ACTION_UPDATE = "UPDATE" - private const val ACTION_STOP = "STOP" - private const val ACTION_DENY_CALL = "DENY_CALL" - private const val ACTION_LOCAL_HANGUP = "LOCAL_HANGUP" - private const val ACTION_CHANGE_POWER_BUTTON = "CHANGE_POWER_BUTTON" - private const val ACTION_SEND_AUDIO_COMMAND = "SEND_AUDIO_COMMAND" + const val ACTION_INCOMING_CALL = "CALL_INCOMING" + const val ACTION_OUTGOING_CALL = "CALL_OUTGOING" + const val ACTION_ANSWER_CALL = "ANSWER_CALL" + const val ACTION_DENY_CALL = "DENY_CALL" + const val ACTION_LOCAL_HANGUP = "LOCAL_HANGUP" + 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" - private const val EXTRA_RECIPIENT_ID = "RECIPIENT_ID" - private const val EXTRA_ENABLED = "ENABLED" - private const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND" + const val ACTION_RESPONSE_MESSAGE = "RESPONSE_MESSAGE" + const val ACTION_ICE_MESSAGE = "ICE_MESSAGE" + const val ACTION_ICE_CANDIDATE = "ICE_CANDIDATE" + 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 - private var lastNotification: Notification? = null + const val DATA_CHANNEL_NAME = "signaling" + const val INVALID_NOTIFICATION_ID = -1 - fun update(context: Context, type: Int, callId: UUID) { - 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 acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_ANSWER_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) { val intent = Intent(context, WebRtcCallService::class.java) - .setAction(ACTION_SEND_AUDIO_COMMAND) + .setAction(ACTION_UPDATE_AUDIO) .putExtra(EXTRA_AUDIO_COMMAND, command) ContextCompat.startForegroundService(context, intent) } - fun changePowerButtonReceiver(context: Context, register: Boolean) { + @JvmStatic + fun isCallActive(context: Context, resultReceiver: ResultReceiver) { val intent = Intent(context, WebRtcCallService::class.java) - .setAction(ACTION_CHANGE_POWER_BUTTON) - .putExtra(EXTRA_ENABLED, register) - ContextCompat.startForegroundService(context, intent) + .setAction(ACTION_IS_IN_CALL_QUERY) + .putExtra(EXTRA_RESULT_RECEIVER, resultReceiver) + 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 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() { super.onCreate() + callManager.initializeResources(this) // create audio manager + registerIncomingPstnCallReceiver() + registerWiredHeadsetStateReceiver() + getSystemService(TelephonyManager::class.java) + .listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE) // reset call notification // register uncaught exception handler // register network receiver // 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() { super.onDestroy() + callReceiver?.let { receiver -> + unregisterReceiver(receiver) + } + callReceiver = null // unregister exception handler // shutdown audiomanager // unregister network receiver // 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( + private val expectedState: CallManager.CallState, + private val expectedCallId: UUID, + private val getState: ()->Pair): FutureTaskListener { + + 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?) + + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index 47477c4cdb..6552398e2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -1,20 +1,23 @@ package org.thoughtcrime.securesms.webrtc import android.content.Context -import com.android.mms.transaction.MessageSender +import android.telephony.TelephonyManager import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow 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.utilities.Log -import org.thoughtcrime.securesms.database.Storage -import org.thoughtcrime.securesms.dependencies.CallComponent 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.video.CameraState import org.webrtc.* +import java.util.* 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, 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 } - - val signalAudioManager: SignalAudioManager by lazy { - SignalAudioManager(context, this, CallComponent.get(context).audioManagerCompat()) + sealed class StateEvent { + data class AudioEnabled(val isEnabled: Boolean): StateEvent() + 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.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 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 dataChannel: DataChannel? = null - private val currentCallState: MutableStateFlow = MutableStateFlow(CallState.STATE_IDLE) + private val pendingOutgoingIceUpdates = ArrayDeque() + private val pendingIncomingIceUpdates = ArrayDeque() + + private var localRenderer: SurfaceViewRenderer? = null + private var remoteRenderer: SurfaceViewRenderer? = null + private var peerConnectionFactory: PeerConnectionFactory? = null private fun createCameraCapturer(enumerator: CameraEnumerator): CameraVideoCapturer? { 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() { - peerConnectionWrapper?.() + peerConnectionWrapper?.dispose() peerConnectionWrapper = null } fun setAudioEnabled(isEnabled: Boolean) { - + currentCallState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) { + peerConnectionWrapper?.setAudioEnabled(isEnabled) + _audioEvents.value = StateEvent.AudioEnabled(true) + } } 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?) { - TODO("Not yet implemented") + override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) { + } - override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) { - TODO("Not yet implemented") + override fun onIceConnectionReceivingChange(receiving: Boolean) { + } - override fun onIceConnectionReceivingChange(p0: Boolean) { - TODO("Not yet implemented") - } + override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) { - override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) { - TODO("Not yet implemented") } override fun onIceCandidate(p0: IceCandidate?) { - TODO("Not yet implemented") + } override fun onIceCandidatesRemoved(p0: Array?) { - TODO("Not yet implemented") + } override fun onAddStream(p0: MediaStream?) { - TODO("Not yet implemented") + } override fun onRemoveStream(p0: MediaStream?) { - TODO("Not yet implemented") + } override fun onDataChannel(p0: DataChannel?) { - TODO("Not yet implemented") + } override fun onRenegotiationNeeded() { - TODO("Not yet implemented") + } override fun onAddTrack(p0: RtpReceiver?, p1: Array?) { - TODO("Not yet implemented") + } override fun onAudioDeviceChanged(activeDevice: SignalAudioManager.AudioDevice, devices: Set) { - TODO("Not yet implemented") + signalAudioManager.handleCommand(AudioManagerCommand()) } private fun CallMessage.iceCandidates(): List { @@ -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") + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt index 9ddf032729..d3866bf1ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import org.session.libsession.messaging.messages.control.CallMessage import org.webrtc.* @@ -13,28 +14,14 @@ import javax.inject.Inject @HiltViewModel class CallViewModel @Inject constructor(private val callManager: CallManager): ViewModel() { - sealed class StateEvent { - data class AudioEnabled(val isEnabled: Boolean): StateEvent() - data class VideoEnabled(val isEnabled: Boolean): StateEvent() - } - - val audioEnabledState = MutableStateFlow( - callManager.audioEnabled.let { isEnabled -> - - } - ) - val videoEnabledState = MutableStateFlow( - callManager.videoEnabled.let { isEnabled -> - - } - ) - - + val localAudioEnabledState = callManager.audioEvents.map { it.isEnabled } + val localVideoEnabledState = callManager.videoEvents.map { it.isEnabled } + val remoteVideoEnabledState = callManager.remoteVideoEvents.map { it.isEnabled } // set up listeners for establishing connection toggling video / audio init { - audioEnabledState.onEach { (enabled) -> callManager.setAudioEnabled(enabled) } + callManager.audioEvents.onEach { (enabled) -> callManager.setAudioEnabled(enabled) } .launchIn(viewModelScope) - videoEnabledState.onEach { (enabled) -> callManager.setVideoEnabled(enabled) } + callManager.videoEvents.onEach { (enabled) -> callManager.setVideoEnabled(enabled) } .launchIn(viewModelScope) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/IncomingPstnCallReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/IncomingPstnCallReceiver.java new file mode 100644 index 0000000000..01b161e088 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/IncomingPstnCallReceiver.java @@ -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); + } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt index c2b644f7dc..2abda1071e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -72,4 +72,30 @@ class PeerConnectionWrapper(context: Context, 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 + } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt index 614eff09df..694d343ed6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt @@ -7,6 +7,7 @@ import android.telephony.PhoneStateListener import android.telephony.TelephonyManager import dagger.hilt.android.AndroidEntryPoint import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.service.WebRtcCallService import javax.inject.Inject @@ -31,19 +32,38 @@ class NetworkReceiver: BroadcastReceiver() { @Inject lateinit var callManager: CallManager - override fun onReceive(context: Context?, intent: Intent?) { + override fun onReceive(context: Context, intent: Intent) { TODO("Not yet implemented") } } class PowerButtonReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - TODO("Not yet implemented") + override fun onReceive(context: Context, intent: Intent) { + 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 { + companion object { + private val TAG = Log.tag(ProximityLockRelease::class.java) + } 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) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt index f0ab170830..2c6136e39f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt @@ -126,7 +126,7 @@ class SignalAudioManager(private val context: Context, Log.d(TAG, "Started") } - private fun stop(playDisconnect: Boolean) { + fun stop(playDisconnect: Boolean) { Log.d(TAG, "Stopping. state: $state") if (state == State.UNINITIALIZED) { Log.i(TAG, "Trying to stop AudioManager in incorrect state: $state") diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/Camera.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/Camera.kt index fcb74456d6..27f97120d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/Camera.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/Camera.kt @@ -45,6 +45,10 @@ class Camera(context: Context, } } + fun dispose() { + capturer?.dispose() + } + fun flip() { if (capturer == null || cameraCount < 2) { Log.w(TAG, "Tried to flip camera without capturer or less than 2 cameras")