diff --git a/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt index e0fc2f9fd..5ddb0fb30 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt @@ -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(ContactDiffCallback()) { +) : ListAdapter(ContactDiffCallback()) { var selectedAdapterPosition = -1 - val contactClickedEvent: MutableLiveData> by lazy { - MutableLiveData>() + val contactClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() } - val contactLongClickedEvent: MutableLiveData> by lazy { - MutableLiveData>() + val contactLongClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() } 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() { - override fun areItemsTheSame(oldItem: ContactModel, newItem: ContactModel): Boolean { +private class ContactDiffCallback : DiffUtil.ItemCallback() { + 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 } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt similarity index 98% rename from app/src/main/java/org/linphone/ui/main/contacts/model/ContactModel.kt rename to app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index 08c144d8a..3d2dc6168 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -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()) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/NumberOrAddressPickerDialogModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/NumberOrAddressPickerDialogModel.kt index 46e129a14..dd7f6d175 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/NumberOrAddressPickerDialogModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/NumberOrAddressPickerDialogModel.kt @@ -31,7 +31,6 @@ class NumberOrAddressPickerDialogModel(viewModel: ContactViewModel) : ViewModel( init { sipAddressesAndPhoneNumbers.value = viewModel.sipAddressesAndPhoneNumbers.value - // TODO: set listener } fun dismiss() { diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index a274cdcee..5c3bb8346 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -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() + val contact = MutableLiveData() val sipAddressesAndPhoneNumbers = MutableLiveData>() @@ -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) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index ae70de333..2615bb876 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -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>() + val contactsList = MutableLiveData>() - val favourites = MutableLiveData>() + val favourites = MutableLiveData>() val showFavourites = MutableLiveData() @@ -88,10 +88,10 @@ class ContactsListViewModel : TopBarViewModel() { fun processMagicSearchResults(results: Array) { // 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() - val favouritesList = arrayListOf() + val list = arrayListOf() + val favouritesList = arrayListOf() 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 diff --git a/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt index 0a7712324..bd93241e7 100644 --- a/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt @@ -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() + } + } } } diff --git a/app/src/main/java/org/linphone/ui/voip/fragment/IncomingCallFragment.kt b/app/src/main/java/org/linphone/ui/voip/fragment/IncomingCallFragment.kt index d88e75342..32d1ff35e 100644 --- a/app/src/main/java/org/linphone/ui/voip/fragment/IncomingCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/voip/fragment/IncomingCallFragment.kt @@ -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() + } + } } } diff --git a/app/src/main/java/org/linphone/ui/voip/fragment/OutgoingCallFragment.kt b/app/src/main/java/org/linphone/ui/voip/fragment/OutgoingCallFragment.kt index 5f4fd8713..25c6f8e6f 100644 --- a/app/src/main/java/org/linphone/ui/voip/fragment/OutgoingCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/voip/fragment/OutgoingCallFragment.kt @@ -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() + } + } } } diff --git a/app/src/main/java/org/linphone/ui/voip/model/ZrtpSasConfirmationDialogModel.kt b/app/src/main/java/org/linphone/ui/voip/model/ZrtpSasConfirmationDialogModel.kt new file mode 100644 index 000000000..ef488486c --- /dev/null +++ b/app/src/main/java/org/linphone/ui/voip/model/ZrtpSasConfirmationDialogModel.kt @@ -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 . + */ +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() + val letters1 = MutableLiveData() + val letters2 = MutableLiveData() + val letters3 = MutableLiveData() + val letters4 = MutableLiveData() + + val trustVerified = MutableLiveData>() + + val dismissEvent = MutableLiveData>() + + 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) { + val verified = letters.value == authTokenToListen + Log.i( + "$TAG User clicked on ${if (verified) "right" else "wrong"} letters" + ) + trustVerified.value = Event(verified) + } +} diff --git a/app/src/main/java/org/linphone/ui/voip/viewmodel/CallViewModel.kt b/app/src/main/java/org/linphone/ui/voip/viewmodel/CallViewModel.kt deleted file mode 100644 index d241b489a..000000000 --- a/app/src/main/java/org/linphone/ui/voip/viewmodel/CallViewModel.kt +++ /dev/null @@ -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 . - */ -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() - - val displayedName = MutableLiveData() - - val displayedAddress = MutableLiveData() - - val isVideoEnabled = MutableLiveData() - - val isOutgoing = MutableLiveData() - - val isActionsMenuExpanded = MutableLiveData() - - val extraActionsMenuTranslateY = MutableLiveData() - - 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>() - - 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) - } -} diff --git a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt new file mode 100644 index 000000000..4ded649a2 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt @@ -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 . + */ +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() + + val displayedName = MutableLiveData() + + val displayedAddress = MutableLiveData() + + val isVideoEnabled = MutableLiveData() + + val isOutgoing = MutableLiveData() + + val isMicrophoneMuted = MutableLiveData() + + val isRemoteDeviceTrusted = MutableLiveData() + + val showZrtpSasDialogEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + + val startCallChronometerEvent = MutableLiveData>() + + // Extras actions + + val isActionsMenuExpanded = MutableLiveData() + + val extraActionsMenuTranslateY = MutableLiveData() + + 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>() + + 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)) + } +} diff --git a/app/src/main/java/org/linphone/utils/AnimationsUtils.kt b/app/src/main/java/org/linphone/utils/AnimationsUtils.kt index 52c4c8bcd..ed56be858 100644 --- a/app/src/main/java/org/linphone/utils/AnimationsUtils.kt +++ b/app/src/main/java/org/linphone/utils/AnimationsUtils.kt @@ -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 -} diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 65720cbe8..03155a714 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -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 { diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index 4ed77c9e4..e194494f8 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -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) ) diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 9f9430d21..26c796fe1 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -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) } } } diff --git a/app/src/main/res/drawable/microphone_muted.xml b/app/src/main/res/drawable/microphone_muted.xml new file mode 100644 index 000000000..3327915a6 --- /dev/null +++ b/app/src/main/res/drawable/microphone_muted.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/shape_circle_white_background.xml b/app/src/main/res/drawable/shape_circle_white_background.xml new file mode 100644 index 000000000..b8f2da426 --- /dev/null +++ b/app/src/main/res/drawable/shape_circle_white_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_dialog_red_button_background.xml b/app/src/main/res/drawable/shape_dialog_red_button_background.xml new file mode 100644 index 000000000..7a9c9f98a --- /dev/null +++ b/app/src/main/res/drawable/shape_dialog_red_button_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/contact_favourite_list_cell.xml b/app/src/main/res/layout/contact_favourite_list_cell.xml index c0d2e9f76..2c7653c94 100644 --- a/app/src/main/res/layout/contact_favourite_list_cell.xml +++ b/app/src/main/res/layout/contact_favourite_list_cell.xml @@ -7,7 +7,7 @@ + type="org.linphone.ui.main.contacts.model.ContactAvatarModel" /> diff --git a/app/src/main/res/layout/contact_list_cell.xml b/app/src/main/res/layout/contact_list_cell.xml index 26de41ec0..3c292c343 100644 --- a/app/src/main/res/layout/contact_list_cell.xml +++ b/app/src/main/res/layout/contact_list_cell.xml @@ -8,7 +8,7 @@ + type="org.linphone.ui.main.contacts.model.ContactAvatarModel" /> diff --git a/app/src/main/res/layout/dialog_confirm_zrtp_sas.xml b/app/src/main/res/layout/dialog_confirm_zrtp_sas.xml new file mode 100644 index 000000000..e7e6b0d1e --- /dev/null +++ b/app/src/main/res/layout/dialog_confirm_zrtp_sas.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_active_call_fragment.xml b/app/src/main/res/layout/voip_active_call_fragment.xml index f623919a8..c34a834e6 100644 --- a/app/src/main/res/layout/voip_active_call_fragment.xml +++ b/app/src/main/res/layout/voip_active_call_fragment.xml @@ -7,7 +7,7 @@ + type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" /> + type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" /> + type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" /> @@ -48,6 +48,7 @@ app:layout_constraintStart_toStartOf="parent" /> + type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" /> + type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" /> + type="org.linphone.ui.voip.viewmodel.CurrentCallViewModel" />