mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-24 15:58:22 +00:00
feat: add call related permissions and more network handover tests
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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) {}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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())
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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) {
|
||||
|
@@ -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>
|
||||
|
@@ -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"/>
|
||||
|
@@ -8,10 +8,7 @@ import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.jobs.NotifyPNServerJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.control.*
|
||||
import org.session.libsession.messaging.messages.visible.*
|
||||
import org.session.libsession.messaging.open_groups.*
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
@@ -165,7 +162,7 @@ object MessageSender {
|
||||
val hash = it["hash"] as? String
|
||||
message.serverHash = hash
|
||||
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
||||
var shouldNotify = ((message is VisibleMessage || message is UnsendRequest) && !isSyncMessage)
|
||||
var shouldNotify = ((message is VisibleMessage || message is UnsendRequest || message is CallMessage) && !isSyncMessage)
|
||||
/*
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
||||
shouldNotify = true
|
||||
|
@@ -89,7 +89,8 @@ object TextSecurePreferences {
|
||||
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
||||
private const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
|
||||
private const val LAST_OPEN_DATE = "pref_last_open_date"
|
||||
private const val CALL_NOTIFICATIONS_ENABLED = "pref_call_notifications_enabled"
|
||||
const val CALL_NOTIFICATIONS_ENABLED = "pref_call_notifications_enabled"
|
||||
private const val SHOWN_CALL_WARNING = "pref_shown_call_warning"
|
||||
|
||||
@JvmStatic
|
||||
fun getLastConfigurationSyncTime(context: Context): Long {
|
||||
@@ -742,4 +743,21 @@ object TextSecurePreferences {
|
||||
fun isCallNotificationsEnabled(context: Context): Boolean {
|
||||
return getBooleanPreference(context, CALL_NOTIFICATIONS_ENABLED, false)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the SHOWN_CALL_WARNING preference to `true`
|
||||
* Return `true` if the value did update (it was previously unset)
|
||||
*/
|
||||
@JvmStatic
|
||||
fun setShownCallWarning(context: Context) : Boolean {
|
||||
val previousValue = getBooleanPreference(context, SHOWN_CALL_WARNING, false)
|
||||
if (previousValue) {
|
||||
return false
|
||||
}
|
||||
val setValue = true
|
||||
setBooleanPreference(context, SHOWN_CALL_WARNING, setValue)
|
||||
return previousValue != setValue
|
||||
}
|
||||
|
||||
}
|
@@ -4,7 +4,7 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import java.util.*
|
||||
|
||||
fun <V, T : Promise<V, Exception>> retryIfNeeded(maxRetryCount: Int, retryInterval: Long = 1 * 1000, body: () -> T): Promise<V, Exception> {
|
||||
fun <V, T : Promise<V, Exception>> retryIfNeeded(maxRetryCount: Int, retryInterval: Long = 1000L, body: () -> T): Promise<V, Exception> {
|
||||
var retryCount = 0
|
||||
val deferred = deferred<V, Exception>()
|
||||
val thread = Thread.currentThread()
|
||||
|
Reference in New Issue
Block a user