Added first version of ZRTP SAS confirmation dialog

This commit is contained in:
Sylvain Berfini 2023-08-11 14:54:01 +02:00
parent c4b0bf0ee0
commit f368114b88
27 changed files with 720 additions and 237 deletions

View file

@ -11,21 +11,21 @@ import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.ContactFavouriteListCellBinding
import org.linphone.databinding.ContactListCellBinding
import org.linphone.ui.main.contacts.model.ContactModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.Event
class ContactsListAdapter(
private val viewLifecycleOwner: LifecycleOwner,
private val favourites: Boolean
) : ListAdapter<ContactModel, RecyclerView.ViewHolder>(ContactDiffCallback()) {
) : ListAdapter<ContactAvatarModel, RecyclerView.ViewHolder>(ContactDiffCallback()) {
var selectedAdapterPosition = -1
val contactClickedEvent: MutableLiveData<Event<ContactModel>> by lazy {
MutableLiveData<Event<ContactModel>>()
val contactClickedEvent: MutableLiveData<Event<ContactAvatarModel>> by lazy {
MutableLiveData<Event<ContactAvatarModel>>()
}
val contactLongClickedEvent: MutableLiveData<Event<ContactModel>> by lazy {
MutableLiveData<Event<ContactModel>>()
val contactLongClickedEvent: MutableLiveData<Event<ContactAvatarModel>> by lazy {
MutableLiveData<Event<ContactAvatarModel>>()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -64,7 +64,7 @@ class ContactsListAdapter(
inner class ViewHolder(
val binding: ContactListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(contactModel: ContactModel) {
fun bind(contactModel: ContactAvatarModel) {
with(binding) {
model = contactModel
@ -91,7 +91,7 @@ class ContactsListAdapter(
inner class FavouriteViewHolder(
val binding: ContactFavouriteListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(contactModel: ContactModel) {
fun bind(contactModel: ContactAvatarModel) {
with(binding) {
model = contactModel
@ -116,12 +116,12 @@ class ContactsListAdapter(
}
}
private class ContactDiffCallback : DiffUtil.ItemCallback<ContactModel>() {
override fun areItemsTheSame(oldItem: ContactModel, newItem: ContactModel): Boolean {
private class ContactDiffCallback : DiffUtil.ItemCallback<ContactAvatarModel>() {
override fun areItemsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ContactModel, newItem: ContactModel): Boolean {
override fun areContentsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean {
return oldItem.showFirstLetter.value == newItem.showFirstLetter.value
}
}

View file

@ -28,7 +28,7 @@ import org.linphone.core.Friend
import org.linphone.core.FriendListenerStub
import org.linphone.utils.LinphoneUtils
class ContactModel(val friend: Friend) {
class ContactAvatarModel(val friend: Friend) {
val id = friend.refKey
val initials = LinphoneUtils.getInitials(friend.name.orEmpty())

View file

@ -31,7 +31,6 @@ class NumberOrAddressPickerDialogModel(viewModel: ContactViewModel) : ViewModel(
init {
sipAddressesAndPhoneNumbers.value = viewModel.sipAddressesAndPhoneNumbers.value
// TODO: set listener
}
fun dismiss() {

View file

@ -23,14 +23,14 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Address
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.contacts.model.ContactDeviceModel
import org.linphone.ui.main.contacts.model.ContactModel
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.utils.Event
class ContactViewModel : ViewModel() {
val contact = MutableLiveData<ContactModel>()
val contact = MutableLiveData<ContactAvatarModel>()
val sipAddressesAndPhoneNumbers = MutableLiveData<ArrayList<ContactNumberOrAddressModel>>()
@ -83,7 +83,7 @@ class ContactViewModel : ViewModel() {
coreContext.postOnCoreThread { core ->
val friend = coreContext.contactsManager.findContactById(refKey)
if (friend != null) {
contact.postValue(ContactModel(friend))
contact.postValue(ContactAvatarModel(friend))
val organization = friend.organization
if (!organization.isNullOrEmpty()) {
company.postValue(organization)

View file

@ -28,13 +28,13 @@ import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.viewmodel.TopBarViewModel
class ContactsListViewModel : TopBarViewModel() {
val contactsList = MutableLiveData<ArrayList<ContactModel>>()
val contactsList = MutableLiveData<ArrayList<ContactAvatarModel>>()
val favourites = MutableLiveData<ArrayList<ContactModel>>()
val favourites = MutableLiveData<ArrayList<ContactAvatarModel>>()
val showFavourites = MutableLiveData<Boolean>()
@ -88,10 +88,10 @@ class ContactsListViewModel : TopBarViewModel() {
fun processMagicSearchResults(results: Array<SearchResult>) {
// Core thread
Log.i("[Contacts List] Processing ${results.size} results")
contactsList.value.orEmpty().forEach(ContactModel::destroy)
contactsList.value.orEmpty().forEach(ContactAvatarModel::destroy)
val list = arrayListOf<ContactModel>()
val favouritesList = arrayListOf<ContactModel>()
val list = arrayListOf<ContactAvatarModel>()
val favouritesList = arrayListOf<ContactAvatarModel>()
var previousLetter = ""
for (result in results) {
@ -100,13 +100,13 @@ class ContactsListViewModel : TopBarViewModel() {
var currentLetter = ""
val model = if (friend != null) {
currentLetter = friend.name?.get(0).toString()
ContactModel(friend)
ContactAvatarModel(friend)
} else {
Log.w("[Contacts] SearchResult [$result] has no Friend!")
val fakeFriend =
createFriendFromSearchResult(result)
currentLetter = fakeFriend.name?.get(0).toString()
ContactModel(fakeFriend)
ContactAvatarModel(fakeFriend)
}
val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter

View file

@ -20,6 +20,7 @@
package org.linphone.ui.voip.fragment
import android.os.Bundle
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -27,12 +28,15 @@ import androidx.lifecycle.ViewModelProvider
import org.linphone.R
import org.linphone.databinding.VoipActiveCallFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.voip.viewmodel.CallViewModel
import org.linphone.ui.voip.model.ZrtpSasConfirmationDialogModel
import org.linphone.ui.voip.viewmodel.CurrentCallViewModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.slideInToastFromTop
class ActiveCallFragment : GenericFragment() {
private lateinit var binding: VoipActiveCallFragmentBinding
private lateinit var callViewModel: CallViewModel
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
@ -47,7 +51,7 @@ class ActiveCallFragment : GenericFragment() {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CallViewModel::class.java]
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
@ -60,7 +64,43 @@ class ActiveCallFragment : GenericFragment() {
}*/
}
binding.blueToast.icon.setImageResource(R.drawable.trusted)
binding.blueToast.message.text = "This call can be trusted"
callViewModel.isRemoteDeviceTrusted.observe(viewLifecycleOwner) { trusted ->
if (trusted) {
binding.blueToast.message.text = "This call can be trusted"
binding.blueToast.icon.setImageResource(R.drawable.trusted)
binding.blueToast.root.slideInToastFromTop(binding.root as ViewGroup, true)
} else if (binding.blueToast.root.visibility == View.VISIBLE) {
binding.blueToast.root.slideInToastFromTop(binding.root as ViewGroup, false)
}
}
callViewModel.startCallChronometerEvent.observe(viewLifecycleOwner) {
it.consume { duration ->
binding.chronometer.base = SystemClock.elapsedRealtime() - (1000 * duration)
binding.chronometer.start()
}
}
callViewModel.showZrtpSasDialogEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val model = ZrtpSasConfirmationDialogModel(pair.first, pair.second)
val dialog = DialogUtils.getZrtpSasConfirmationDialog(requireActivity(), model)
model.dismissEvent.observe(viewLifecycleOwner) { event ->
event.consume {
dialog.dismiss()
}
}
model.trustVerified.observe(viewLifecycleOwner) { event ->
event.consume { verified ->
callViewModel.updateZrtpSas(verified)
dialog.dismiss()
}
}
dialog.show()
}
}
}
}

View file

@ -20,18 +20,19 @@
package org.linphone.ui.voip.fragment
import android.os.Bundle
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import org.linphone.databinding.VoipIncomingCallFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.voip.viewmodel.CallViewModel
import org.linphone.ui.voip.viewmodel.CurrentCallViewModel
class IncomingCallFragment : GenericFragment() {
private lateinit var binding: VoipIncomingCallFragmentBinding
private lateinit var callViewModel: CallViewModel
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
@ -46,10 +47,17 @@ class IncomingCallFragment : GenericFragment() {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CallViewModel::class.java]
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
callViewModel.startCallChronometerEvent.observe(viewLifecycleOwner) {
it.consume { duration ->
binding.chronometer.base = SystemClock.elapsedRealtime() - (1000 * duration)
binding.chronometer.start()
}
}
}
}

View file

@ -20,18 +20,19 @@
package org.linphone.ui.voip.fragment
import android.os.Bundle
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import org.linphone.databinding.VoipOutgoingCallFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.voip.viewmodel.CallViewModel
import org.linphone.ui.voip.viewmodel.CurrentCallViewModel
class OutgoingCallFragment : GenericFragment() {
private lateinit var binding: VoipOutgoingCallFragmentBinding
private lateinit var callViewModel: CallViewModel
private lateinit var callViewModel: CurrentCallViewModel
override fun onCreateView(
inflater: LayoutInflater,
@ -46,12 +47,17 @@ class OutgoingCallFragment : GenericFragment() {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CallViewModel::class.java]
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
binding.chronometer.start()
callViewModel.startCallChronometerEvent.observe(viewLifecycleOwner) {
it.consume { duration ->
binding.chronometer.base = SystemClock.elapsedRealtime() - (1000 * duration)
binding.chronometer.start()
}
}
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.voip.model
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.Random
import org.linphone.core.tools.Log
import org.linphone.utils.Event
class ZrtpSasConfirmationDialogModel(
private val authTokenToRead: String,
private val authTokenToListen: String
) : ViewModel() {
companion object {
const val TAG = "[ZRTP SAS Confirmation Dialog]"
const val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
}
val message = MutableLiveData<String>()
val letters1 = MutableLiveData<String>()
val letters2 = MutableLiveData<String>()
val letters3 = MutableLiveData<String>()
val letters4 = MutableLiveData<String>()
val trustVerified = MutableLiveData<Event<Boolean>>()
val dismissEvent = MutableLiveData<Event<Boolean>>()
init {
message.value = "Dites $authTokenToRead et cliquez sur les lettres données par votre interlocuteur :"
// TODO: improve algo
val rnd = Random()
val randomLetters1 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[
rnd.nextInt(
alphabet.length
)
]}"
val randomLetters2 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[
rnd.nextInt(
alphabet.length
)
]}"
val randomLetters3 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[
rnd.nextInt(
alphabet.length
)
]}"
val randomLetters4 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[
rnd.nextInt(
alphabet.length
)
]}"
val correctLetters = rnd.nextInt(4)
letters1.value = if (correctLetters == 0) authTokenToListen else randomLetters1
letters2.value = if (correctLetters == 1) authTokenToListen else randomLetters2
letters3.value = if (correctLetters == 2) authTokenToListen else randomLetters3
letters4.value = if (correctLetters == 3) authTokenToListen else randomLetters4
}
fun dismiss() {
dismissEvent.value = Event(true)
}
fun lettersClicked(letters: MutableLiveData<String>) {
val verified = letters.value == authTokenToListen
Log.i(
"$TAG User clicked on ${if (verified) "right" else "wrong"} letters"
)
trustVerified.value = Event(verified)
}
}

View file

@ -1,127 +0,0 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.voip.viewmodel
import android.animation.ValueAnimator
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Call
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class CallViewModel() : ViewModel() {
companion object {
const val TAG = "[Call ViewModel]"
}
val contact = MutableLiveData<ContactModel>()
val displayedName = MutableLiveData<String>()
val displayedAddress = MutableLiveData<String>()
val isVideoEnabled = MutableLiveData<Boolean>()
val isOutgoing = MutableLiveData<Boolean>()
val isActionsMenuExpanded = MutableLiveData<Boolean>()
val extraActionsMenuTranslateY = MutableLiveData<Float>()
private val extraActionsMenuHeight = coreContext.context.resources.getDimension(
R.dimen.in_call_extra_actions_menu_height
)
private val extraButtonsMenuAnimator: ValueAnimator by lazy {
ValueAnimator.ofFloat(
extraActionsMenuHeight,
0f
).apply {
addUpdateListener {
val value = it.animatedValue as Float
extraActionsMenuTranslateY.value = value
}
duration = 500
}
}
val toggleExtraActionMenuVisibilityEvent = MutableLiveData<Event<Boolean>>()
private lateinit var call: Call
init {
isVideoEnabled.value = false
isActionsMenuExpanded.value = false
extraActionsMenuTranslateY.value = extraActionsMenuHeight
coreContext.postOnCoreThread { core ->
val currentCall = core.currentCall ?: core.calls.firstOrNull()
if (currentCall != null) {
call = currentCall
Log.i("$TAG Found call [$call]")
if (call.state == Call.State.StreamsRunning) {
isVideoEnabled.postValue(call.currentParams.isVideoEnabled)
} else {
isVideoEnabled.postValue(call.params.isVideoEnabled)
}
isOutgoing.postValue(call.dir == Call.Dir.Outgoing)
val address = call.remoteAddress
address.clean()
displayedAddress.postValue(address.asStringUriOnly())
val friend = core.findFriend(address)
if (friend != null) {
displayedName.postValue(friend.name)
contact.postValue(ContactModel(friend))
} else {
displayedName.postValue(LinphoneUtils.getDisplayName(address))
}
} else {
Log.e("$TAG Failed to find outgoing call!")
}
}
}
fun hangUp() {
// UI thread
coreContext.postOnCoreThread {
Log.i("$TAG Terminating call [$call]")
call.terminate()
}
}
fun toggleExpandActionsMenu() {
// UI thread
isActionsMenuExpanded.value = isActionsMenuExpanded.value == false
if (isActionsMenuExpanded.value == true) {
extraButtonsMenuAnimator.start()
} else {
extraButtonsMenuAnimator.reverse()
}
// toggleExtraActionMenuVisibilityEvent.value = Event(isActionsMenuExpanded.value == true)
}
}

View file

@ -0,0 +1,246 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.voip.viewmodel
import android.animation.ValueAnimator
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.MediaEncryption
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class CurrentCallViewModel() : ViewModel() {
companion object {
const val TAG = "[Call ViewModel]"
}
val contact = MutableLiveData<ContactAvatarModel>()
val displayedName = MutableLiveData<String>()
val displayedAddress = MutableLiveData<String>()
val isVideoEnabled = MutableLiveData<Boolean>()
val isOutgoing = MutableLiveData<Boolean>()
val isMicrophoneMuted = MutableLiveData<Boolean>()
val isRemoteDeviceTrusted = MutableLiveData<Boolean>()
val showZrtpSasDialogEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
MutableLiveData<Event<Pair<String, String>>>()
}
val startCallChronometerEvent = MutableLiveData<Event<Int>>()
// Extras actions
val isActionsMenuExpanded = MutableLiveData<Boolean>()
val extraActionsMenuTranslateY = MutableLiveData<Float>()
private val extraActionsMenuHeight = coreContext.context.resources.getDimension(
R.dimen.in_call_extra_actions_menu_height
)
private val extraButtonsMenuAnimator: ValueAnimator by lazy {
ValueAnimator.ofFloat(
extraActionsMenuHeight,
0f
).apply {
addUpdateListener {
val value = it.animatedValue as Float
extraActionsMenuTranslateY.value = value
}
duration = 500
}
}
val toggleExtraActionMenuVisibilityEvent = MutableLiveData<Event<Boolean>>()
private lateinit var call: Call
private val callListener = object : CallListenerStub() {
override fun onEncryptionChanged(call: Call, on: Boolean, authenticationToken: String?) {
updateEncryption()
}
}
init {
isVideoEnabled.value = false
isMicrophoneMuted.value = false
isActionsMenuExpanded.value = false
extraActionsMenuTranslateY.value = extraActionsMenuHeight
coreContext.postOnCoreThread { core ->
val currentCall = core.currentCall ?: core.calls.firstOrNull()
if (currentCall != null) {
call = currentCall
Log.i("$TAG Found call [$call]")
configureCall(call)
} else {
Log.e("$TAG Failed to find outgoing call!")
}
}
}
override fun onCleared() {
super.onCleared()
coreContext.postOnCoreThread {
if (::call.isInitialized) {
call.removeListener(callListener)
}
}
}
fun hangUp() {
// UI thread
coreContext.postOnCoreThread {
Log.i("$TAG Terminating call [$call]")
call.terminate()
}
}
fun updateZrtpSas(verified: Boolean) {
// UI thread
coreContext.postOnCoreThread {
if (::call.isInitialized) {
call.authenticationTokenVerified = verified
}
}
}
fun toggleMuteMicrophone() {
// UI thread
// TODO: check record audio permission
coreContext.postOnCoreThread {
call.microphoneMuted = !call.microphoneMuted
isMicrophoneMuted.postValue(call.microphoneMuted)
}
}
fun changeAudioOutputDevice() {
// UI thread
// TODO: display list of all output devices
}
fun toggleVideo() {
// UI thread
// TODO: check video permission
// TODO
}
fun toggleExpandActionsMenu() {
// UI thread
isActionsMenuExpanded.value = isActionsMenuExpanded.value == false
if (isActionsMenuExpanded.value == true) {
extraButtonsMenuAnimator.start()
} else {
extraButtonsMenuAnimator.reverse()
}
// toggleExtraActionMenuVisibilityEvent.value = Event(isActionsMenuExpanded.value == true)
}
fun forceShowZrtpSasDialog() {
val authToken = call.authenticationToken
if (authToken.orEmpty().isNotEmpty()) {
showZrtpSasDialog(authToken!!.uppercase(Locale.getDefault()))
}
}
private fun showZrtpSasDialog(authToken: String) {
val toRead: String
val toListen: String
when (call.dir) {
Call.Dir.Incoming -> {
toRead = authToken.substring(0, 2)
toListen = authToken.substring(2)
}
else -> {
toRead = authToken.substring(2)
toListen = authToken.substring(0, 2)
}
}
showZrtpSasDialogEvent.postValue(Event(Pair(toRead, toListen)))
}
private fun updateEncryption() {
// Core thread
when (call.currentParams.mediaEncryption) {
MediaEncryption.ZRTP -> {
val authToken = call.authenticationToken
val deviceIsTrusted = call.authenticationTokenVerified && authToken != null
Log.i(
"$TAG Current call media encryption is ZRTP, auth token is ${if (deviceIsTrusted) "trusted" else "not trusted yet"}"
)
isRemoteDeviceTrusted.postValue(deviceIsTrusted)
if (!deviceIsTrusted && authToken.orEmpty().isNotEmpty()) {
Log.i("$TAG Showing ZRTP SAS confirmation dialog")
showZrtpSasDialog(authToken!!.uppercase(Locale.getDefault()))
}
}
MediaEncryption.SRTP, MediaEncryption.DTLS -> {
}
else -> {
}
}
}
private fun configureCall(call: Call) {
// Core thread
call.addListener(callListener)
if (LinphoneUtils.isCallOutgoing(call.state)) {
isVideoEnabled.postValue(call.params.isVideoEnabled)
} else {
isVideoEnabled.postValue(call.currentParams.isVideoEnabled)
}
isMicrophoneMuted.postValue(call.microphoneMuted)
isOutgoing.postValue(call.dir == Call.Dir.Outgoing)
val address = call.remoteAddress
address.clean()
displayedAddress.postValue(address.asStringUriOnly())
val friend = call.core.findFriend(address)
if (friend != null) {
displayedName.postValue(friend.name)
contact.postValue(ContactAvatarModel(friend))
} else {
displayedName.postValue(LinphoneUtils.getDisplayName(address))
}
updateEncryption()
startCallChronometerEvent.postValue(Event(call.duration))
}
}

View file

@ -31,6 +31,20 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
fun View.slideInToastFromTop(
root: ViewGroup,
visible: Boolean
) {
val view = this
val transition: Transition = Slide(Gravity.TOP)
transition.duration = 600
transition.addTarget(view)
TransitionManager.beginDelayedTransition(root, transition)
view.visibility = if (visible) View.VISIBLE else View.GONE
}
fun View.slideInToastFromTopForDuration(
root: ViewGroup,
lifecycleScope: LifecycleCoroutineScope,
@ -55,15 +69,3 @@ fun View.slideInToastFromTopForDuration(
}
}
}
fun View.slideInExtraActionsMenu(
root: ViewGroup,
visibility: Int
) {
val transition: Transition = Slide(Gravity.BOTTOM)
transition.duration = 600
transition.addTarget(this)
TransitionManager.beginDelayedTransition(root, transition)
this.translationY = 0f
}

View file

@ -43,7 +43,7 @@ import org.linphone.R
import org.linphone.contacts.ContactData
import org.linphone.core.ConsolidatedPresence
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.contacts.model.ContactModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
/**
* This file contains all the data binding necessary for the app
@ -142,7 +142,7 @@ fun loadContactPictureWithCoil2(imageView: ImageView, contact: ContactData?) {
}
@BindingAdapter("contactAvatar")
fun AvatarView.loadContactPicture(contact: ContactModel?) {
fun AvatarView.loadContactPicture(contact: ContactAvatarModel?) {
if (contact == null) {
loadImage(R.drawable.contact_avatar)
} else {

View file

@ -29,8 +29,10 @@ import android.view.WindowManager
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import org.linphone.R
import org.linphone.databinding.DialogConfirmZrtpSasBinding
import org.linphone.databinding.DialogPickNumberOrAddressBinding
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.voip.model.ZrtpSasConfirmationDialogModel
class DialogUtils {
companion object {
@ -50,6 +52,34 @@ class DialogUtils {
binding.viewModel = viewModel
dialog.setContentView(binding.root)
val d: Drawable = ColorDrawable(
ContextCompat.getColor(dialog.context, R.color.dialog_background)
)
d.alpha = 100
dialog.window
?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
dialog.window?.setBackgroundDrawable(d)
return dialog
}
fun getZrtpSasConfirmationDialog(
context: Context,
viewModel: ZrtpSasConfirmationDialogModel
): Dialog {
val dialog = Dialog(context)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
val binding: DialogConfirmZrtpSasBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.dialog_confirm_zrtp_sas,
null,
false
)
binding.viewModel = viewModel
dialog.setContentView(binding.root)
val d: Drawable = ColorDrawable(
ContextCompat.getColor(dialog.context, R.color.dialog_background)
)

View file

@ -19,16 +19,12 @@
*/
package org.linphone.utils
import android.content.ContentUris
import android.net.Uri
import android.provider.ContactsContract
import androidx.emoji2.text.EmojiCompat
import java.io.IOException
import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Address
import org.linphone.core.Call
import org.linphone.core.ChatRoom
import org.linphone.core.Friend
import org.linphone.core.tools.Log
class LinphoneUtils {
@ -74,18 +70,6 @@ class LinphoneUtils {
return initials
}
private fun getChatRoomId(localAddress: Address, remoteAddress: Address): String {
val localSipUri = localAddress.clone()
localSipUri.clean()
val remoteSipUri = remoteAddress.clone()
remoteSipUri.clean()
return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}"
}
fun getChatRoomId(chatRoom: ChatRoom): String {
return getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
}
fun getDisplayName(address: Address?): String {
if (address == null) return "[null]"
if (address.displayName == null) {
@ -102,41 +86,23 @@ class LinphoneUtils {
return address.displayName ?: address.username ?: address.asString()
}
fun Friend.getPictureUri(thumbnailPreferred: Boolean = false): Uri? {
val refKey = refKey
if (refKey != null) {
try {
val lookupUri = ContentUris.withAppendedId(
ContactsContract.Contacts.CONTENT_URI,
refKey.toLong()
)
if (!thumbnailPreferred) {
val pictureUri = Uri.withAppendedPath(
lookupUri,
ContactsContract.Contacts.Photo.DISPLAY_PHOTO
)
// Check that the URI points to a real file
val contentResolver = coreContext.context.contentResolver
try {
if (contentResolver.openAssetFileDescriptor(pictureUri, "r") != null) {
return pictureUri
}
} catch (ioe: IOException) { }
}
// Fallback to thumbnail if high res picture isn't available
return Uri.withAppendedPath(
lookupUri,
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
)
} catch (e: Exception) { }
} else if (photo != null) {
try {
return Uri.parse(photo)
} catch (e: Exception) { }
fun isCallOutgoing(callState: Call.State): Boolean {
return when (callState) {
Call.State.OutgoingInit, Call.State.OutgoingProgress, Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> true
else -> false
}
return null
}
private fun getChatRoomId(localAddress: Address, remoteAddress: Address): String {
val localSipUri = localAddress.clone()
localSipUri.clean()
val remoteSipUri = remoteAddress.clone()
remoteSipUri.clean()
return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}"
}
fun getChatRoomId(chatRoom: ChatRoom): String {
return getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
}
}
}

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 10.8 4.9 C 10.8 4.24 11.34 3.7 12 3.7 C 12.66 3.7 13.2 4.24 13.2 4.9 L 13.19 8.81 L 15 10.6 L 15 5 C 15 3.34 13.66 2 12 2 C 10.46 2 9.21 3.16 9.04 4.65 L 10.8 6.41 L 10.8 4.9 Z M 19 11 L 17.3 11 C 17.3 11.58 17.2 12.13 17.03 12.64 L 18.3 13.91 C 18.74 13.03 19 12.04 19 11 Z M 4.41 2.86 L 3 4.27 L 9 10.27 L 9 11 C 9 12.66 10.34 14 12 14 C 12.23 14 12.44 13.97 12.65 13.92 L 14.31 15.58 C 13.6 15.91 12.81 16.1 12 16.1 C 9.24 16.1 6.7 14 6.7 11 L 5 11 C 5 14.41 7.72 17.23 11 17.72 L 11 21 L 13 21 L 13 17.72 C 13.91 17.59 14.77 17.27 15.55 16.82 L 19.75 21.02 L 21.16 19.61 L 4.41 2.86 Z"
android:fillColor="#000000"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<size android:width="70dp" android:height="70dp" />
<solid android:color="@color/white"/>
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="48dp" />
<stroke android:color="@color/red_danger" android:width="1dp" />
<solid android:color="@color/white"/>
</shape>

View file

@ -7,7 +7,7 @@
<import type="android.view.View" />
<variable
name="model"
type="org.linphone.ui.main.contacts.model.ContactModel" />
type="org.linphone.ui.main.contacts.model.ContactAvatarModel" />
<variable
name="onClickListener"
type="View.OnClickListener" />

View file

@ -8,7 +8,7 @@
<import type="android.graphics.Typeface" />
<variable
name="model"
type="org.linphone.ui.main.contacts.model.ContactModel" />
type="org.linphone.ui.main.contacts.model.ContactAvatarModel" />
<variable
name="onClickListener"
type="View.OnClickListener" />

View file

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="android.graphics.Typeface" />
<variable
name="viewModel"
type="org.linphone.ui.voip.model.ZrtpSasConfirmationDialogModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> viewModel.dismiss()}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/in_call_black"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/dialog_background_shadow"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/shape_dialog_orange_shadow"
app:layout_constraintBottom_toBottomOf="@id/anchor"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintTop_toTopOf="@id/dialog_background" />
<ImageView
android:id="@+id/dialog_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="2dp"
android:src="@drawable/shape_dialog_background"
app:layout_constraintBottom_toBottomOf="@id/anchor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintVertical_bias="1.0" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:paddingTop="25dp"
android:text="Vérifiez l'appareil"
android:textSize="16sp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/message"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:layout_marginTop="10dp"
android:text="@{viewModel.message}"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/letters_1"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.lettersClicked(viewModel.letters1)}"
style="@style/default_text_style"
android:id="@+id/letters_1"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:text="@{viewModel.letters1, default=`RV`}"
android:textSize="32sp"
android:gravity="center"
android:background="@drawable/shape_circle_white_background"
android:elevation="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/letters_2"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintBottom_toTopOf="@id/letters_3"
app:layout_constraintHorizontal_chainStyle="packed"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.lettersClicked(viewModel.letters2)}"
style="@style/default_text_style"
android:id="@+id/letters_2"
android:layout_width="70dp"
android:layout_height="70dp"
android:text="@{viewModel.letters2, default=`PT`}"
android:textSize="32sp"
android:gravity="center"
android:background="@drawable/shape_circle_white_background"
android:elevation="5dp"
app:layout_constraintStart_toEndOf="@id/letters_1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/letters_1"
app:layout_constraintBottom_toBottomOf="@id/letters_1"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.lettersClicked(viewModel.letters3)}"
style="@style/default_text_style"
android:id="@+id/letters_3"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:text="@{viewModel.letters3, default=`BB`}"
android:textSize="32sp"
android:gravity="center"
android:background="@drawable/shape_circle_white_background"
android:elevation="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/letters_4"
app:layout_constraintTop_toBottomOf="@id/letters_1"
app:layout_constraintBottom_toTopOf="@id/skip"
app:layout_constraintHorizontal_chainStyle="packed"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.lettersClicked(viewModel.letters4)}"
style="@style/default_text_style"
android:id="@+id/letters_4"
android:layout_width="70dp"
android:layout_height="70dp"
android:text="@{viewModel.letters4, default=`NM`}"
android:textSize="32sp"
android:gravity="center"
android:background="@drawable/shape_circle_white_background"
android:elevation="5dp"
app:layout_constraintStart_toEndOf="@id/letters_3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/letters_3"
app:layout_constraintBottom_toBottomOf="@id/letters_3"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
style="@style/default_text_style_600"
android:id="@+id/skip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="45dp"
android:text="Skip"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/letters_3"
app:layout_constraintBottom_toTopOf="@id/nothing_matches"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
style="@style/default_text_style_600"
android:id="@+id/nothing_matches"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="45dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:paddingBottom="13dp"
android:paddingTop="13dp"
android:gravity="center"
android:background="@drawable/shape_dialog_red_button_background"
android:text="Letters don't match!"
android:textSize="18sp"
android:textColor="@color/red_danger"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/skip"
app:layout_constraintBottom_toTopOf="@id/anchor"/>
<View
android:id="@+id/anchor"
android:layout_width="wrap_content"
android:layout_height="20dp"
app:layout_constraintTop_toBottomOf="@id/nothing_matches"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -7,7 +7,7 @@
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CallViewModel" />
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -92,7 +92,9 @@
app:layout_constraintEnd_toEndOf="parent" />
<include
android:onClick="@{() -> viewModel.forceShowZrtpSasDialog()}"
android:id="@+id/blue_toast"
android:visibility="gone"
layout="@layout/toast_blue"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -7,7 +7,7 @@
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CallViewModel" />
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View file

@ -7,7 +7,7 @@
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CallViewModel" />
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
<variable
name="showExpandToggle"
type="Boolean" />
@ -48,6 +48,7 @@
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:onClick="@{() -> viewModel.toggleVideo()}"
android:id="@+id/toggle_video"
android:enabled="@{viewModel.isVideoEnabled}"
android:layout_width="wrap_content"
@ -60,12 +61,13 @@
app:layout_constraintEnd_toStartOf="@id/toggle_mute_mic" />
<ImageView
android:onClick="@{() -> viewModel.toggleMuteMicrophone()}"
android:id="@+id/toggle_mute_mic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:padding="15dp"
android:src="@drawable/microphone"
android:src="@{viewModel.isMicrophoneMuted ? @drawable/microphone_muted : @drawable/microphone, default=@drawable/microphone}"
android:background="@drawable/in_call_button_background"
app:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"

View file

@ -7,7 +7,7 @@
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CallViewModel" />
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View file

@ -7,7 +7,7 @@
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CallViewModel" />
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View file

@ -7,7 +7,7 @@
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.voip.viewmodel.CallViewModel" />
type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout