feat: call establishing and displaying

This commit is contained in:
jubb
2021-11-10 11:57:03 +11:00
parent 2e973146a3
commit 99b6a38b90
9 changed files with 153 additions and 61 deletions

View File

@@ -11,8 +11,13 @@ import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.jakewharton.rxbinding3.view.clicks
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_webrtc_tests.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
@@ -39,13 +44,13 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
private val viewModel by viewModels<CallViewModel>() private val viewModel by viewModels<CallViewModel>()
private val acceptedCallMessageHashes = mutableSetOf<Int>()
private val candidates: MutableList<IceCandidate> = mutableListOf() private val candidates: MutableList<IceCandidate> = mutableListOf()
private lateinit var callAddress: Address private lateinit var callAddress: Address
private lateinit var callId: UUID private lateinit var callId: UUID
private var uiJob: Job? = null
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) { if (item.itemId == android.R.id.home) {
finish() finish()
@@ -70,10 +75,6 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
} }
.execute() .execute()
lifecycleScope.launch {
// repeat on start or something
}
if (intent.action == ACTION_ANSWER) { if (intent.action == ACTION_ANSWER) {
// answer via ViewModel // answer via ViewModel
val answerIntent = WebRtcCallService.acceptCallIntent(this) val answerIntent = WebRtcCallService.acceptCallIntent(this)
@@ -85,6 +86,20 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
finish() finish()
} }
},IntentFilter(ACTION_END)) },IntentFilter(ACTION_END))
enableCameraButton.setOnClickListener {
startService(WebRtcCallService.cameraEnabled(this, true))
}
switchCameraButton.setOnClickListener {
startService(WebRtcCallService.flipCamera(this))
}
endCallButton.setOnClickListener {
startService(WebRtcCallService.hangupIntent(this))
}
} }
private fun initializeResources() { private fun initializeResources() {
@@ -95,4 +110,29 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
} }
override fun onStart() {
super.onStart()
uiJob = lifecycleScope.launch {
viewModel.callState.collect { state ->
if (state == CallViewModel.State.CALL_CONNECTED) {
// call connected, render the surfaces
remote_renderer.removeAllViews()
local_renderer.removeAllViews()
viewModel.remoteRenderer?.let { remote_renderer.addView(it) }
viewModel.localRenderer?.let { local_renderer.addView(it) }
}
}
viewModel.remoteVideoEnabledState.collect {
}
}
}
override fun onStop() {
super.onStop()
uiJob?.cancel()
}
} }

View File

@@ -427,7 +427,7 @@ public class NotificationChannels {
notificationManager.createNotificationChannelGroup(messagesGroup); notificationManager.createNotificationChannelGroup(messagesGroup);
NotificationChannel messages = new NotificationChannel(getMessagesChannel(context), context.getString(R.string.NotificationChannel_messages), NotificationManager.IMPORTANCE_HIGH); NotificationChannel messages = new NotificationChannel(getMessagesChannel(context), context.getString(R.string.NotificationChannel_messages), NotificationManager.IMPORTANCE_HIGH);
NotificationChannel calls = new NotificationChannel(CALLS, context.getString(R.string.NotificationChannel_calls), NotificationManager.IMPORTANCE_LOW); NotificationChannel calls = new NotificationChannel(CALLS, context.getString(R.string.NotificationChannel_calls), NotificationManager.IMPORTANCE_DEFAULT);
NotificationChannel failures = new NotificationChannel(FAILURES, context.getString(R.string.NotificationChannel_failures), NotificationManager.IMPORTANCE_HIGH); NotificationChannel failures = new NotificationChannel(FAILURES, context.getString(R.string.NotificationChannel_failures), NotificationManager.IMPORTANCE_HIGH);
NotificationChannel backups = new NotificationChannel(BACKUPS, context.getString(R.string.NotificationChannel_backups), NotificationManager.IMPORTANCE_LOW); NotificationChannel backups = new NotificationChannel(BACKUPS, context.getString(R.string.NotificationChannel_backups), NotificationManager.IMPORTANCE_LOW);
NotificationChannel lockedStatus = new NotificationChannel(LOCKED_STATUS, context.getString(R.string.NotificationChannel_locked_status), NotificationManager.IMPORTANCE_LOW); NotificationChannel lockedStatus = new NotificationChannel(LOCKED_STATUS, context.getString(R.string.NotificationChannel_locked_status), NotificationManager.IMPORTANCE_LOW);

View File

@@ -17,6 +17,7 @@ import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.FutureTaskListener import org.session.libsession.utilities.FutureTaskListener
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.Util import org.session.libsession.utilities.Util
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@@ -42,8 +43,6 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class WebRtcCallService: Service(), PeerConnection.Observer { class WebRtcCallService: Service(), PeerConnection.Observer {
@Inject lateinit var callManager: CallManager
companion object { companion object {
private val TAG = Log.tag(WebRtcCallService::class.java) private val TAG = Log.tag(WebRtcCallService::class.java)
@@ -86,6 +85,13 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
const val INVALID_NOTIFICATION_ID = -1 const val INVALID_NOTIFICATION_ID = -1
fun cameraEnabled(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_SET_MUTE_VIDEO)
.putExtra(EXTRA_MUTE, !enabled)
fun flipCamera(context: Context) = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_FLIP_CAMERA)
fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_ANSWER_CALL) fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_ANSWER_CALL)
fun createCall(context: Context, recipient: Recipient) = Intent(context, WebRtcCallService::class.java) fun createCall(context: Context, recipient: Recipient) = Intent(context, WebRtcCallService::class.java)
@@ -139,6 +145,8 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
} }
} }
@Inject lateinit var callManager: CallManager
private var lastNotificationId: Int = INVALID_NOTIFICATION_ID private var lastNotificationId: Int = INVALID_NOTIFICATION_ID
private var lastNotification: Notification? = null private var lastNotification: Notification? = null
@@ -152,6 +160,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
private var callReceiver: IncomingPstnCallReceiver? = null private var callReceiver: IncomingPstnCallReceiver? = null
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
private var uncaughtExceptionHandlerManager: UncaughtExceptionHandlerManager? = null private var uncaughtExceptionHandlerManager: UncaughtExceptionHandlerManager? = null
private var powerButtonReceiver: PowerButtonReceiver? = null
@Synchronized @Synchronized
private fun terminate() { private fun terminate() {
@@ -277,6 +286,10 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
callManager.onIncomingRing(offer, callId, recipient, timestamp) callManager.onIncomingRing(offer, callId, recipient, timestamp)
callManager.clearPendingIceUpdates() callManager.clearPendingIceUpdates()
callManager.postConnectionEvent(STATE_LOCAL_RINGING) callManager.postConnectionEvent(STATE_LOCAL_RINGING)
if (TextSecurePreferences.isCallNotificationsEnabled(this)) {
callManager.startIncomingRinger()
}
registerPowerButtonReceiver()
} }
private fun handleOutgoingCall(intent: Intent) { private fun handleOutgoingCall(intent: Intent) {
@@ -344,6 +357,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
timeoutExecutor.schedule(TimeoutRunnable(callId, this), 5, TimeUnit.MINUTES) timeoutExecutor.schedule(TimeoutRunnable(callId, this), 5, TimeUnit.MINUTES)
callManager.initializeAudioForCall()
callManager.initializeVideo(this) callManager.initializeVideo(this)
val expectedState = callManager.currentConnectionState val expectedState = callManager.currentConnectionState
@@ -373,12 +387,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
return return
} }
if (callManager.callNotSetup()) {
throw AssertionError("assert")
}
callManager.handleDenyCall() callManager.handleDenyCall()
// DatabaseComponent.get(this).smsDatabase().insertMissedCall(recipient) // DatabaseComponent.get(this).smsDatabase().insertMissedCall(recipient)
terminate() terminate()
} }
@@ -471,10 +480,17 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
} }
private fun handleIceConnected(intent: Intent) { private fun handleIceConnected(intent: Intent) {
if (callManager.currentConnectionState == STATE_ANSWERING) { val recipient = callManager.recipient ?: return
val recipient = callManager.recipient ?: return if (callManager.currentConnectionState in arrayOf(STATE_ANSWERING, STATE_LOCAL_RINGING)) {
callManager.postConnectionEvent(STATE_CONNECTED) callManager.postConnectionEvent(STATE_CONNECTED)
callManager.postViewModelState(CallViewModel.State.CALL_CONNECTED)
} else {
Log.w(TAG, "Got ice connected out of state")
} }
setCallInProgressNotification(TYPE_ESTABLISHED, recipient)
callManager.startCommunication(lockManager)
} }
private fun handleCallConnected(intent: Intent) { private fun handleCallConnected(intent: Intent) {
@@ -485,6 +501,14 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
} }
private fun registerPowerButtonReceiver() {
if (powerButtonReceiver == null) {
powerButtonReceiver = PowerButtonReceiver()
registerReceiver(powerButtonReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
}
}
@@ -643,7 +667,9 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
override fun onDataChannel(p0: DataChannel?) {} override fun onDataChannel(p0: DataChannel?) {}
override fun onRenegotiationNeeded() {} override fun onRenegotiationNeeded() {
Log.w(TAG,"onRenegotiationNeeded was called!")
}
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {} override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {}
} }

View File

@@ -58,6 +58,7 @@ class CallNotificationBuilder {
R.drawable.ic_phone_grey600_32dp, R.drawable.ic_phone_grey600_32dp,
R.string.NotificationBarManager__answer_call R.string.NotificationBarManager__answer_call
)) ))
builder.priority = NotificationCompat.PRIORITY_HIGH
} }
TYPE_OUTGOING_RINGING -> { TYPE_OUTGOING_RINGING -> {
builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call)) builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call))

View File

@@ -110,8 +110,8 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
private val outgoingIceDebouncer = Debouncer(2_000L) private val outgoingIceDebouncer = Debouncer(2_000L)
private var localRenderer: SurfaceViewRenderer? = null var localRenderer: SurfaceViewRenderer? = null
private var remoteRenderer: SurfaceViewRenderer? = null var remoteRenderer: SurfaceViewRenderer? = null
private var peerConnectionFactory: PeerConnectionFactory? = null private var peerConnectionFactory: PeerConnectionFactory? = null
fun clearPendingIceUpdates() { fun clearPendingIceUpdates() {
@@ -175,7 +175,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
peerConnectionFactory = PeerConnectionFactory.builder() peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(object: PeerConnectionFactory.Options() { .setOptions(object: PeerConnectionFactory.Options() {
init { init {
networkIgnoreMask = 1 shl 4 // networkIgnoreMask = 1 shl 4
} }
}) })
.setVideoEncoderFactory(encoderFactory) .setVideoEncoderFactory(encoderFactory)
@@ -199,7 +199,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
fun setVideoEnabled(isEnabled: Boolean) { fun setVideoEnabled(isEnabled: Boolean) {
currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) { currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
peerConnection?.setVideoEnabled(isEnabled) peerConnection?.setVideoEnabled(isEnabled)
_audioEvents.value = StateEvent.AudioEnabled(true) _videoEvents.value = StateEvent.VideoEnabled(true)
} }
} }
@@ -290,7 +290,9 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
override fun onMessage(buffer: DataChannel.Buffer?) { override fun onMessage(buffer: DataChannel.Buffer?) {
Log.i(TAG,"onMessage...") Log.i(TAG,"onMessage...")
TODO("interpret the data channel buffer and check for signals") buffer ?: return
Log.i(TAG,"received: ${buffer.data}")
} }
override fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>) { override fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>) {
@@ -329,6 +331,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
localCameraState = newCameraState localCameraState = newCameraState
} }
fun enableLocalCamera() {
setVideoEnabled(true)
}
fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long) { fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long) {
if (currentConnectionState != CallState.STATE_IDLE) return if (currentConnectionState != CallState.STATE_IDLE) return
@@ -357,6 +363,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
peerConnection = connection peerConnection = connection
localCameraState = connection.getCameraState() localCameraState = connection.getCameraState()
val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME) val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME)
this.dataChannel = dataChannel
dataChannel.registerObserver(this) dataChannel.registerObserver(this)
connection.setRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, offer)) connection.setRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, offer))
val answer = connection.createAnswer(MediaConstraints()) val answer = connection.createAnswer(MediaConstraints())
@@ -401,6 +408,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
localCameraState = connection.getCameraState() localCameraState = connection.getCameraState()
val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME) val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME)
dataChannel.registerObserver(this) dataChannel.registerObserver(this)
this.dataChannel = dataChannel
val offer = connection.createOffer(MediaConstraints()) val offer = connection.createOffer(MediaConstraints())
connection.setLocalDescription(offer) connection.setLocalDescription(offer)
@@ -536,4 +544,18 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
signalAudioManager.handleCommand(AudioManagerCommand.Shutdown) signalAudioManager.handleCommand(AudioManagerCommand.Shutdown)
} }
fun startIncomingRinger() {
signalAudioManager.handleCommand(AudioManagerCommand.StartIncomingRinger(true))
}
fun startCommunication(lockManager: LockManager) {
signalAudioManager.handleCommand(AudioManagerCommand.Start)
val connection = peerConnection ?: return
if (localCameraState.enabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO)
else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
connection.setCommunicationMode()
connection.setAudioEnabled(_audioEvents.value.isEnabled)
connection.setVideoEnabled(localCameraState.enabled)
}
} }

View File

@@ -3,11 +3,18 @@ package org.thoughtcrime.securesms.webrtc
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.webrtc.SurfaceViewRenderer
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class CallViewModel @Inject constructor(private val callManager: CallManager): ViewModel() { class CallViewModel @Inject constructor(private val callManager: CallManager): ViewModel() {
val localRenderer: SurfaceViewRenderer?
get() = callManager.localRenderer
val remoteRenderer: SurfaceViewRenderer?
get() = callManager.remoteRenderer
enum class State { enum class State {
CALL_PENDING, CALL_PENDING,

View File

@@ -83,6 +83,8 @@ class PeerConnectionWrapper(context: Context,
fun createDataChannel(channelName: String): DataChannel { fun createDataChannel(channelName: String): DataChannel {
val dataChannelConfiguration = DataChannel.Init().apply { val dataChannelConfiguration = DataChannel.Init().apply {
ordered = true ordered = true
negotiated = true
id = 548
} }
return peerConnection.createDataChannel(channelName, dataChannelConfiguration) return peerConnection.createDataChannel(channelName, dataChannelConfiguration)
} }
@@ -220,6 +222,11 @@ class PeerConnectionWrapper(context: Context,
} }
} }
fun setCommunicationMode() {
peerConnection.setAudioPlayout(true)
peerConnection.setAudioRecording(true)
}
fun setAudioEnabled(isEnabled: Boolean) { fun setAudioEnabled(isEnabled: Boolean) {
audioTrack.setEnabled(isEnabled) audioTrack.setEnabled(isEnabled)
} }

View File

@@ -6,44 +6,10 @@
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<TextView
android:padding="@dimen/small_spacing"
android:elevation="8dp"
tools:visibility="visible"
android:textColor="@color/white"
tools:text="relay"
android:visibility="gone"
android:background="@drawable/pill"
android:backgroundTint="@color/black"
android:layout_marginTop="@dimen/medium_spacing"
android:id="@+id/local_candidate_info"
app:layout_constraintEnd_toStartOf="@id/remote_candidate_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:padding="@dimen/small_spacing"
android:elevation="8dp"
tools:visibility="visible"
tools:text="relay"
android:visibility="gone"
android:textColor="@color/white"
android:background="@drawable/pill"
android:backgroundTint="@color/black"
android:layout_marginTop="@dimen/medium_spacing"
android:id="@+id/remote_candidate_info"
app:layout_constraintStart_toEndOf="@id/local_candidate_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.webrtc.SurfaceViewRenderer <FrameLayout
android:id="@+id/remote_renderer" android:id="@+id/remote_renderer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -69,7 +35,8 @@
app:layout_constraintWidth_percent="0.2" app:layout_constraintWidth_percent="0.2"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_width="0dp"> android:layout_width="0dp">
<org.webrtc.SurfaceViewRenderer <FrameLayout
android:elevation="8dp"
android:id="@+id/local_renderer" android:id="@+id/local_renderer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
@@ -85,7 +52,7 @@
</FrameLayout> </FrameLayout>
<ImageView <ImageView
android:id="@+id/end_call_button" android:id="@+id/endCallButton"
android:background="@drawable/circle_tintable" android:background="@drawable/circle_tintable"
android:src="@drawable/ic_baseline_call_end_24" android:src="@drawable/ic_baseline_call_end_24"
android:padding="@dimen/small_spacing" android:padding="@dimen/small_spacing"
@@ -100,7 +67,7 @@
/> />
<ImageView <ImageView
android:id="@+id/switch_camera_button" android:id="@+id/switchCameraButton"
android:background="@drawable/circle_tintable" android:background="@drawable/circle_tintable"
android:src="@drawable/ic_baseline_flip_camera_android_24" android:src="@drawable/ic_baseline_flip_camera_android_24"
android:padding="@dimen/small_spacing" android:padding="@dimen/small_spacing"
@@ -116,7 +83,23 @@
/> />
<ImageView <ImageView
android:id="@+id/switch_audio_button" android:id="@+id/enableCameraButton"
android:background="@drawable/circle_tintable"
android:src="@drawable/ic_baseline_photo_camera_48"
android:padding="@dimen/small_spacing"
app:tint="@color/unimportant"
android:backgroundTint="@color/unimportant_button_background"
android:layout_width="@dimen/large_button_height"
android:layout_height="@dimen/large_button_height"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="@dimen/large_spacing"
app:layout_constraintHorizontal_bias="0.2"
/>
<ImageView
android:id="@+id/speakerPhoneButton"
android:background="@drawable/circle_tintable" android:background="@drawable/circle_tintable"
android:src="@drawable/ic_audio_light" android:src="@drawable/ic_audio_light"
android:padding="@dimen/small_spacing" android:padding="@dimen/small_spacing"

View File

@@ -89,6 +89,7 @@ object TextSecurePreferences {
const val CONFIGURATION_SYNCED = "pref_configuration_synced" const val CONFIGURATION_SYNCED = "pref_configuration_synced"
private const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time" 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 LAST_OPEN_DATE = "pref_last_open_date"
private const val CALL_NOTIFICATIONS_ENABLED = "pref_call_notifications_enabled"
@JvmStatic @JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long { fun getLastConfigurationSyncTime(context: Context): Long {
@@ -736,4 +737,9 @@ object TextSecurePreferences {
fun clearAll(context: Context) { fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit() getDefaultSharedPreferences(context).edit().clear().commit()
} }
@JvmStatic
fun isCallNotificationsEnabled(context: Context): Boolean {
return getBooleanPreference(context, CALL_NOTIFICATIONS_ENABLED, false)
}
} }