diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 2e9e6b846e..14113975c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -1,21 +1,27 @@ package org.thoughtcrime.securesms.preferences; +import android.Manifest; import android.app.Activity; +import android.app.AlertDialog; import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; +import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.service.KeyCachingService; import java.util.concurrent.TimeUnit; +import kotlin.jvm.functions.Function1; import mobi.upod.timedurationpicker.TimeDurationPickerDialog; import network.loki.messenger.R; @@ -36,10 +42,22 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment this.findPreference(TextSecurePreferences.READ_RECEIPTS_PREF).setOnPreferenceChangeListener(new ReadReceiptToggleListener()); this.findPreference(TextSecurePreferences.TYPING_INDICATORS).setOnPreferenceChangeListener(new TypingIndicatorsToggleListener()); this.findPreference(TextSecurePreferences.LINK_PREVIEWS).setOnPreferenceChangeListener(new LinkPreviewToggleListener()); + this.findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED).setOnPreferenceChangeListener(new CallToggleListener(this, this::setCall)); initializeVisibility(); } + private Void setCall(boolean isEnabled) { + ((SwitchPreferenceCompat)findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)).setChecked(isEnabled); + return null; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); + } + @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.preferences_app_protection); @@ -136,4 +154,52 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment return true; } } + + private class CallToggleListener implements Preference.OnPreferenceChangeListener { + + private final Fragment context; + private final Function1 setCallback; + + private CallToggleListener(Fragment context, Function1 setCallback) { + this.context = context; + this.setCallback = setCallback; + } + + private void requestMicrophonePermission() { + Permissions.with(context) + .request(Manifest.permission.RECORD_AUDIO) + .onAllGranted(() -> { + TextSecurePreferences.setBooleanPreference(context.requireContext(), TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED, true); + setCallback.invoke(true); + }) + .onAnyDenied(() -> setCallback.invoke(false)) + .execute(); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean val = (boolean) newValue; + if (val) { + // check if we've shown the info dialog and check for microphone permissions + if (TextSecurePreferences.setShownCallWarning(context.requireContext())) { + new AlertDialog.Builder(context.requireContext()) + .setTitle(R.string.dialog_voice_video_title) + .setMessage(R.string.dialog_voice_video_message) + .setPositiveButton(R.string.dialog_link_preview_enable_button_title, (d, w) -> { + requestMicrophonePermission(); + }) + .setNegativeButton(R.string.cancel, (d, w) -> { + + }) + .show(); + } else { + requestMicrophonePermission(); + } + return false; + } else { + return true; + } + } + } + } 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 00a8aefa57..ff162494d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -6,13 +6,12 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.media.AudioManager -import android.net.ConnectivityManager import android.os.Build import android.os.IBinder import android.os.ResultReceiver import android.telephony.PhoneStateListener import android.telephony.TelephonyManager -import androidx.core.content.ContextCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.utilities.Address import org.session.libsession.utilities.FutureTaskListener @@ -32,7 +31,6 @@ import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.locks.LockManager import org.webrtc.* import org.webrtc.PeerConnection.IceConnectionState.* -import java.lang.AssertionError import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.Executors @@ -228,10 +226,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer { networkChangedReceiver = NetworkChangeReceiver { available -> networkChange(available) } - registerReceiver(networkChangedReceiver, IntentFilter().apply { - addAction("android.net.conn.CONNECTIVITY_CHANGE") - addAction("android.net.wifi.WIFI_STATE_CHANGED") - }) + LocalBroadcastManager.getInstance(this).registerReceiver(networkChangedReceiver!!, IntentFilter("pathsBuilt")) } private fun registerUncaughtExceptionHandler() { @@ -298,7 +293,6 @@ class WebRtcCallService: Service(), PeerConnection.Observer { val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return val callId = getCallId(intent) val recipient = getRemoteRecipient(intent) - callManager.clearPendingIceUpdates() callManager.onNewOffer(offer, callId, recipient) } @@ -338,7 +332,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer { callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING) setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient) // TODO: DatabaseComponent.get(this).insertOutgoingCall(callManager.recipient!!.address) - timeoutExecutor.schedule(TimeoutRunnable(callId, this), 5, TimeUnit.MINUTES) + timeoutExecutor.schedule(TimeoutRunnable(callId, this), 2, TimeUnit.MINUTES) val expectedState = callManager.currentConnectionState val expectedCallId = callManager.callId @@ -533,7 +527,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer { if (callId == getCallId(intent) && callState != STATE_CONNECTED) { Log.w(TAG, "Timing out call: $callId") - callManager.postViewModelState(CallViewModel.State.CALL_DISCONNECTED) + handleLocalHangup(intent) } } @@ -571,7 +565,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer { unregisterReceiver(receiver) } networkChangedReceiver?.let { receiver -> - unregisterReceiver(receiver) + LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } networkChangedReceiver = null callReceiver = null @@ -584,7 +578,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer { } fun networkChange(networkAvailable: Boolean) { - if (networkAvailable && callManager.currentConnectionState in arrayOf(STATE_CONNECTED, STATE_ANSWERING, STATE_DIALING)) { + if (!callManager.isReestablishing && callManager.currentConnectionState in arrayOf(STATE_CONNECTED)) { callManager.networkReestablished() } } @@ -676,6 +670,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer { startService(intent) } + Log.d(TAG, "onIceConnectionChange: $newState") } override fun onIceConnectionReceivingChange(p0: Boolean) {} 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 ca98007566..8c344fd3fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -10,7 +10,6 @@ import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.task import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.utilities.Debouncer @@ -116,12 +115,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne var recipient: Recipient? = null set(value) { field = value - _recipientEvents.value = StateEvent.RecipientUpdate(value) + _recipientEvents.value = RecipientUpdate(value) } var isReestablishing: Boolean = false - fun getCurrentCallState(): Pair = currentConnectionState to callId - private var peerConnection: PeerConnectionWrapper? = null private var dataChannel: DataChannel? = null @@ -537,7 +534,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne } fun handleResponseMessage(recipient: Recipient, callId: UUID, answer: SessionDescription) { - if (currentConnectionState != CallState.STATE_DIALING || recipient != this.recipient || callId != this.callId) { + if (currentConnectionState !in arrayOf(CallState.STATE_DIALING, CallState.STATE_CONNECTED) || recipient != this.recipient || callId != this.callId) { Log.w(TAG,"Got answer for recipient and call ID we're not currently dialing") return } @@ -603,14 +600,16 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne val recipient = recipient ?: return if (isReestablishing) return + isReestablishing = true val offer = connection.createOffer(MediaConstraints().apply { mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true")) }) + connection.setLocalDescription(offer) - isReestablishing = true - - MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId), recipient.address) + MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId), recipient.address).success { + isReestablishing = false + } } @Serializable diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt index 4ed07aced5..19196383a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.launch import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.utilities.WebRtcUtils import org.session.libsession.utilities.Address +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.* import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.service.WebRtcCallService @@ -22,6 +23,10 @@ class CallMessageProcessor(private val context: Context, lifecycle: Lifecycle) { while (isActive) { val nextMessage = WebRtcUtils.SIGNAL_QUEUE.receive() Log.d("Loki", nextMessage.type?.name ?: "CALL MESSAGE RECEIVED") + if (!TextSecurePreferences.isCallNotificationsEnabled(context)) { + Log.d("Loki","Dropping call message if call notifications disabled") + // TODO: maybe insert a message here saying you missed a call due to permissions + } when (nextMessage.type) { OFFER -> incomingCall(nextMessage) ANSWER -> incomingAnswer(nextMessage) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/NetworkChangeReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/NetworkChangeReceiver.kt index 5ad5b480a6..e2ac35d05c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/NetworkChangeReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/NetworkChangeReceiver.kt @@ -4,10 +4,12 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.net.ConnectivityManager +import org.session.libsignal.utilities.Log class NetworkChangeReceiver(private val onNetworkChangedCallback: (Boolean)->Unit): BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { + Log.d("Loki", intent.toString()) onNetworkChangedCallback(context.isConnected()) } 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 c4cbbd3f84..bd328eddda 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.webrtc import android.content.Context -import kotlinx.coroutines.runBlocking import org.session.libsignal.utilities.SettableFuture import org.thoughtcrime.securesms.webrtc.video.Camera import org.thoughtcrime.securesms.webrtc.video.CameraEventListener @@ -32,7 +31,6 @@ class PeerConnectionWrapper(context: Context, val iceServers = listOf(turn) val constraints = MediaConstraints().apply { - optional.add(MediaConstraints.KeyValuePair("IceRestart", "true")) optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) } val audioConstraints = MediaConstraints().apply { 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 0c1ba5bcda..955356c7d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt @@ -5,11 +5,9 @@ import android.content.Context import android.content.Intent 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 org.thoughtcrime.securesms.webrtc.locks.LockManager -import javax.inject.Inject class HangUpRtcOnPstnCallAnsweredListener(private val hangupListener: ()->Unit): PhoneStateListener() { @@ -27,17 +25,6 @@ class HangUpRtcOnPstnCallAnsweredListener(private val hangupListener: ()->Unit): } } -//@AndroidEntryPoint -class NetworkReceiver: BroadcastReceiver() { - -// @Inject -// lateinit var callManager: CallManager - - override fun onReceive(context: Context, intent: Intent) { - TODO("Not yet implemented") - } -} - class PowerButtonReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_SCREEN_OFF == intent.action) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e7b208220d..59db0054fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -913,5 +913,9 @@ End call Accept Call Decline call + Voice and video calls + Allow access to accept voice and video calls from other users + Voice / video calls + The current implementation of voice / video calls will expose your IP address to the Oxen Foundation servers and the calling / called user diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml index 35f9028a5b..513a2ee0f6 100644 --- a/app/src/main/res/xml/preferences_app_protection.xml +++ b/app/src/main/res/xml/preferences_app_protection.xml @@ -84,6 +84,11 @@ +