feat: add call related permissions and more network handover tests

This commit is contained in:
Harris
2021-11-17 12:51:15 +11:00
parent bf74483b9f
commit 98a50cbf69
12 changed files with 118 additions and 42 deletions

View File

@@ -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<Boolean, Void> setCallback;
private CallToggleListener(Fragment context, Function1<Boolean,Void> 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;
}
}
}
}

View File

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

View File

@@ -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<CallState, UUID?> = 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -913,5 +913,9 @@
<string name="NotificationBarManager__end_call">End call</string>
<string name="accept_call">Accept Call</string>
<string name="decline_call">Decline call</string>
<string name="preferences__voice_video_calls">Voice and video calls</string>
<string name="preferences__allow_access_voice_video">Allow access to accept voice and video calls from other users</string>
<string name="dialog_voice_video_title">Voice / video calls</string>
<string name="dialog_voice_video_message">The current implementation of voice / video calls will expose your IP address to the Oxen Foundation servers and the calling / called user</string>
</resources>

View File

@@ -84,6 +84,11 @@
<!-- <Preference android:key="preference_category_blocked"
android:title="@string/preferences_app_protection__blocked_contacts" /> -->
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_call_notifications_enabled"
android:title="@string/preferences__voice_video_calls"
android:summary="@string/preferences__allow_access_voice_video"/>
</PreferenceCategory>
<!-- <PreferenceCategory android:layout="@layout/preference_divider"/>