Started multiple contact selection in chat room creation

This commit is contained in:
Sylvain Berfini 2023-10-17 13:31:34 +02:00
parent 4fb4c7c85d
commit ee46722a3d
14 changed files with 459 additions and 200 deletions

View file

@ -354,6 +354,7 @@ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddress
for (address in addresses) { for (address in addresses) {
val data = ContactNumberOrAddressModel( val data = ContactNumberOrAddressModel(
this,
address, address,
address.asStringUriOnly(), address.asStringUriOnly(),
true, // SIP addresses are always enabled true, // SIP addresses are always enabled
@ -385,6 +386,7 @@ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddress
address.clean() // To remove ;user=phone address.clean() // To remove ;user=phone
presenceAddress = address presenceAddress = address
val data = ContactNumberOrAddressModel( val data = ContactNumberOrAddressModel(
this,
address, address,
address.asStringUriOnly(), address.asStringUriOnly(),
true, // SIP addresses are always enabled true, // SIP addresses are always enabled
@ -412,6 +414,7 @@ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddress
number.label ?: "" number.label ?: ""
) )
val data = ContactNumberOrAddressModel( val data = ContactNumberOrAddressModel(
this,
address, address,
number.phoneNumber, number.phoneNumber,
enablePhoneNumbers, enablePhoneNumbers,

View file

@ -19,34 +19,30 @@
*/ */
package org.linphone.ui.main.chat.fragment package org.linphone.ui.main.chat.fragment
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.view.doOnPreDraw import androidx.core.view.doOnPreDraw
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R import org.linphone.R
import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers import org.linphone.core.Address
import org.linphone.core.Friend
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.StartChatFragmentBinding import org.linphone.databinding.StartChatFragmentBinding
import org.linphone.ui.main.MainActivity import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.chat.viewmodel.StartConversationViewModel import org.linphone.ui.main.chat.viewmodel.StartConversationViewModel
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.fragment.GenericAddressPickerFragment
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter
import org.linphone.ui.main.history.model.ContactOrSuggestionModel import org.linphone.ui.main.model.SelectedAddressModel
import org.linphone.ui.main.model.isInSecureMode
import org.linphone.utils.DialogUtils
import org.linphone.utils.Event import org.linphone.utils.Event
@UiThread @UiThread
class StartConversationFragment : GenericFragment() { class StartConversationFragment : GenericAddressPickerFragment() {
companion object { companion object {
private const val TAG = "[Start Conversation Fragment]" private const val TAG = "[Start Conversation Fragment]"
} }
@ -59,27 +55,6 @@ class StartConversationFragment : GenericFragment() {
private lateinit var adapter: ContactsAndSuggestionsListAdapter private lateinit var adapter: ContactsAndSuggestionsListAdapter
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
override fun onClicked(model: ContactNumberOrAddressModel) {
val address = model.address
coreContext.postOnCoreThread {
if (address != null) {
Log.i(
"$TAG Creating a 1-1 conversation with [${model.address.asStringUriOnly()}]"
)
viewModel.createOneToOneChatRoomWith(model.address)
}
}
}
@UiThread
override fun onLongPress(model: ContactNumberOrAddressModel) {
}
}
private var numberOrAddressPickerDialog: Dialog? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -106,7 +81,7 @@ class StartConversationFragment : GenericFragment() {
adapter.contactClickedEvent.observe(viewLifecycleOwner) { adapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model -> it.consume { model ->
createChatRoom(model) handleClickOnContactModel(model)
} }
} }
@ -154,69 +129,16 @@ class StartConversationFragment : GenericFragment() {
} }
} }
override fun onPause() { @WorkerThread
super.onPause() override fun onAddressSelected(address: Address, friend: Friend) {
if (viewModel.multipleSelectionMode.value == true) {
numberOrAddressPickerDialog?.dismiss() val avatarModel = ContactAvatarModel(friend)
numberOrAddressPickerDialog = null val model = SelectedAddressModel(address, avatarModel) {
} viewModel.removeAddressModelFromSelection(it)
private fun createChatRoom(model: ContactOrSuggestionModel) {
coreContext.postOnCoreThread { core ->
val friend = model.friend
if (friend == null) {
Log.i("$TAG Friend is null, creating conversation with [${model.address}]")
viewModel.createOneToOneChatRoomWith(model.address)
return@postOnCoreThread
}
val addressesCount = friend.addresses.size
val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = core.defaultAccount?.isInSecureMode() != true
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
Log.i(
"$TAG Only 1 SIP address found for contact [${friend.name}], creating conversation directly"
)
val address = friend.addresses.first()
viewModel.createOneToOneChatRoomWith(address)
} else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) {
val number = friend.phoneNumbers.first()
val address = core.interpretUrl(number, true)
if (address != null) {
Log.i(
"$TAG Only 1 phone number found for contact [${friend.name}], creating conversation directly"
)
viewModel.createOneToOneChatRoomWith(address)
} else {
Log.e("$TAG Failed to interpret phone number [$number] as SIP address")
}
} else {
val list = friend.getListOfSipAddressesAndPhoneNumbers(listener)
Log.i(
"$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog"
)
coreContext.postOnMainThread {
val numberOrAddressModel = NumberOrAddressPickerDialogModel(list)
val dialog =
DialogUtils.getNumberOrAddressPickerDialog(
requireActivity(),
numberOrAddressModel
)
numberOrAddressPickerDialog = dialog
numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event ->
event.consume {
dialog.dismiss()
}
}
dialog.show()
}
} }
viewModel.addAddressModelToSelection(model)
} else {
viewModel.createOneToOneChatRoomWith(address)
} }
} }
} }

View file

@ -22,8 +22,7 @@ package org.linphone.ui.main.chat.viewmodel
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import kotlin.collections.ArrayList
import java.util.ArrayList
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
@ -39,11 +38,12 @@ import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.history.model.ContactOrSuggestionModel import org.linphone.ui.main.history.model.ContactOrSuggestionModel
import org.linphone.ui.main.model.isInSecureMode import org.linphone.ui.main.model.isInSecureMode
import org.linphone.ui.main.viewmodel.AddressSelectionViewModel
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils import org.linphone.utils.LinphoneUtils
class StartConversationViewModel @UiThread constructor() : ViewModel() { class StartConversationViewModel @UiThread constructor() : AddressSelectionViewModel() {
companion object { companion object {
private const val TAG = "[Start Conversation ViewModel]" private const val TAG = "[Start Conversation ViewModel]"
} }

View file

@ -23,8 +23,10 @@ import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import org.linphone.core.Address import org.linphone.core.Address
import org.linphone.core.Friend
class ContactNumberOrAddressModel @WorkerThread constructor( class ContactNumberOrAddressModel @WorkerThread constructor(
val friend: Friend,
val address: Address?, val address: Address?,
val displayedValue: String, val displayedValue: String,
val isEnabled: Boolean, val isEnabled: Boolean,

View file

@ -0,0 +1,133 @@
/*
* 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.main.fragment
import android.app.Dialog
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers
import org.linphone.core.Address
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.history.model.ContactOrSuggestionModel
import org.linphone.ui.main.model.isInSecureMode
import org.linphone.utils.DialogUtils
@UiThread
abstract class GenericAddressPickerFragment : GenericFragment() {
companion object {
private const val TAG = "[Generic Address Picker Fragment]"
}
private var numberOrAddressPickerDialog: Dialog? = null
@WorkerThread
abstract fun onAddressSelected(address: Address, friend: Friend)
override fun onPause() {
super.onPause()
numberOrAddressPickerDialog?.dismiss()
numberOrAddressPickerDialog = null
}
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
override fun onClicked(model: ContactNumberOrAddressModel) {
val address = model.address
coreContext.postOnCoreThread {
if (address != null) {
Log.i(
"$TAG Selected address [${model.address.asStringUriOnly()}] from friend [${model.friend.name}]"
)
onAddressSelected(model.address, model.friend)
}
}
numberOrAddressPickerDialog?.dismiss()
numberOrAddressPickerDialog = null
}
@UiThread
override fun onLongPress(model: ContactNumberOrAddressModel) {
}
}
protected fun handleClickOnContactModel(model: ContactOrSuggestionModel) {
coreContext.postOnCoreThread { core ->
val friend = model.friend
if (friend == null) {
Log.i("$TAG Friend is null, using address [${model.address}]")
val fakeFriend = core.createFriend()
fakeFriend.addAddress(model.address)
onAddressSelected(model.address, fakeFriend)
return@postOnCoreThread
}
val addressesCount = friend.addresses.size
val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = core.defaultAccount?.isInSecureMode() != true
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
val address = friend.addresses.first()
Log.i("$TAG Only 1 SIP address found for contact [${friend.name}], using it")
onAddressSelected(address, friend)
} else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) {
val number = friend.phoneNumbers.first()
val address = core.interpretUrl(number, true)
if (address != null) {
Log.i("$TAG Only 1 phone number found for contact [${friend.name}], using it")
onAddressSelected(address, friend)
} else {
Log.e("$TAG Failed to interpret phone number [$number] as SIP address")
}
} else {
val list = friend.getListOfSipAddressesAndPhoneNumbers(listener)
Log.i(
"$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog"
)
coreContext.postOnMainThread {
val numberOrAddressModel = NumberOrAddressPickerDialogModel(list)
val dialog =
DialogUtils.getNumberOrAddressPickerDialog(
requireActivity(),
numberOrAddressModel
)
numberOrAddressPickerDialog = dialog
numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event ->
event.consume {
dialog.dismiss()
}
}
dialog.show()
}
}
}
}
}

View file

@ -31,8 +31,6 @@ class ContactsAndSuggestionsListAdapter(
private const val SUGGESTION_TYPE = 1 private const val SUGGESTION_TYPE = 1
} }
var selectedAdapterPosition = -1
val contactClickedEvent: MutableLiveData<Event<ContactOrSuggestionModel>> by lazy { val contactClickedEvent: MutableLiveData<Event<ContactOrSuggestionModel>> by lazy {
MutableLiveData<Event<ContactOrSuggestionModel>>() MutableLiveData<Event<ContactOrSuggestionModel>>()
} }
@ -108,8 +106,6 @@ class ContactsAndSuggestionsListAdapter(
lifecycleOwner = viewLifecycleOwner lifecycleOwner = viewLifecycleOwner
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
binding.setOnClickListener { binding.setOnClickListener {
contactClickedEvent.value = Event(contactOrSuggestionModel) contactClickedEvent.value = Event(contactOrSuggestionModel)
} }
@ -129,8 +125,6 @@ class ContactsAndSuggestionsListAdapter(
lifecycleOwner = viewLifecycleOwner lifecycleOwner = viewLifecycleOwner
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
binding.setOnClickListener { binding.setOnClickListener {
contactClickedEvent.value = Event(contactOrSuggestionModel) contactClickedEvent.value = Event(contactOrSuggestionModel)
} }

View file

@ -19,30 +19,25 @@
*/ */
package org.linphone.ui.main.history.fragment package org.linphone.ui.main.history.fragment
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.view.doOnPreDraw import androidx.core.view.doOnPreDraw
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R import org.linphone.R
import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers import org.linphone.core.Address
import org.linphone.core.Friend
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.StartCallFragmentBinding import org.linphone.databinding.StartCallFragmentBinding
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.fragment.GenericAddressPickerFragment
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter import org.linphone.ui.main.history.adapter.ContactsAndSuggestionsListAdapter
import org.linphone.ui.main.history.model.ContactOrSuggestionModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.ui.main.model.isInSecureMode
import org.linphone.utils.DialogUtils
import org.linphone.utils.RecyclerViewHeaderDecoration import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.addCharacterAtPosition import org.linphone.utils.addCharacterAtPosition
import org.linphone.utils.hideKeyboard import org.linphone.utils.hideKeyboard
@ -51,7 +46,7 @@ import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard import org.linphone.utils.showKeyboard
@UiThread @UiThread
class StartCallFragment : GenericFragment() { class StartCallFragment : GenericAddressPickerFragment() {
companion object { companion object {
private const val TAG = "[Start Call Fragment]" private const val TAG = "[Start Call Fragment]"
} }
@ -64,24 +59,6 @@ class StartCallFragment : GenericFragment() {
private lateinit var adapter: ContactsAndSuggestionsListAdapter private lateinit var adapter: ContactsAndSuggestionsListAdapter
private val listener = object : ContactNumberOrAddressClickListener {
@UiThread
override fun onClicked(model: ContactNumberOrAddressModel) {
val address = model.address
if (address != null) {
coreContext.postOnCoreThread {
coreContext.startCall(address)
}
}
}
@UiThread
override fun onLongPress(model: ContactNumberOrAddressModel) {
}
}
private var numberOrAddressPickerDialog: Dialog? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -117,7 +94,7 @@ class StartCallFragment : GenericFragment() {
adapter.contactClickedEvent.observe(viewLifecycleOwner) { adapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model -> it.consume { model ->
startCall(model) handleClickOnContactModel(model)
} }
} }
@ -187,71 +164,14 @@ class StartCallFragment : GenericFragment() {
} }
} }
@WorkerThread
override fun onAddressSelected(address: Address, friend: Friend) {
coreContext.startCall(address)
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
viewModel.isNumpadVisible.value = false viewModel.isNumpadVisible.value = false
numberOrAddressPickerDialog?.dismiss()
numberOrAddressPickerDialog = null
}
private fun startCall(model: ContactOrSuggestionModel) {
coreContext.postOnCoreThread { core ->
val friend = model.friend
if (friend == null) {
Log.i("$TAG Friend is null, starting call with [${model.address}]")
coreContext.startCall(model.address)
return@postOnCoreThread
}
val addressesCount = friend.addresses.size
val numbersCount = friend.phoneNumbers.size
// Do not consider phone numbers if default account is in secure mode
val enablePhoneNumbers = core.defaultAccount?.isInSecureMode() != true
if (addressesCount == 1 && (numbersCount == 0 || !enablePhoneNumbers)) {
Log.i(
"$TAG Only 1 SIP address found for contact [${friend.name}], starting call directly"
)
val address = friend.addresses.first()
coreContext.startCall(address)
} else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) {
val number = friend.phoneNumbers.first()
val address = core.interpretUrl(number, true)
if (address != null) {
Log.i(
"$TAG Only 1 phone number found for contact [${friend.name}], starting call directly"
)
coreContext.startCall(address)
} else {
Log.e("$TAG Failed to interpret phone number [$number] as SIP address")
}
} else {
val list = friend.getListOfSipAddressesAndPhoneNumbers(listener)
Log.i(
"$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog"
)
coreContext.postOnMainThread {
val numberOrAddressModel = NumberOrAddressPickerDialogModel(list)
val dialog =
DialogUtils.getNumberOrAddressPickerDialog(
requireActivity(),
numberOrAddressModel
)
numberOrAddressPickerDialog = dialog
numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event ->
event.consume {
dialog.dismiss()
}
}
dialog.show()
}
}
}
} }
} }

View file

@ -0,0 +1,40 @@
/*
* 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.main.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import org.linphone.core.Address
import org.linphone.ui.main.contacts.model.ContactAvatarModel
class SelectedAddressModel @WorkerThread constructor(
val address: Address,
val avatarModel: ContactAvatarModel?,
private val onRemovedFromSelection: ((model: SelectedAddressModel) -> Unit)? = null
) {
companion object {
private const val TAG = "[Selected Address Model]"
}
@UiThread
fun removeFromSelection() {
onRemovedFromSelection?.invoke(this)
}
}

View file

@ -0,0 +1,104 @@
/*
* 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.main.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.R
import org.linphone.mediastream.Log
import org.linphone.ui.main.model.SelectedAddressModel
import org.linphone.utils.AppUtils
abstract class AddressSelectionViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Address Selection ViewModel]"
}
val multipleSelectionMode = MutableLiveData<Boolean>()
val selection = MutableLiveData<ArrayList<SelectedAddressModel>>()
val selectionCount = MutableLiveData<String>()
init {
multipleSelectionMode.value = false
}
@UiThread
fun switchToMultipleSelectionMode() {
Log.i("$$TAG Multiple selection mode ON")
multipleSelectionMode.value = true
}
@WorkerThread
fun addAddressModelToSelection(model: SelectedAddressModel) {
val actual = selection.value.orEmpty()
if (actual.find {
it.address.weakEqual(model.address)
} == null
) {
Log.i("$TAG Adding [${model.address.asStringUriOnly()}] address to selection")
val list = arrayListOf<SelectedAddressModel>()
list.addAll(actual)
list.add(model)
selectionCount.postValue(
AppUtils.getStringWithPlural(
R.plurals.selection_count_label,
list.size,
list.size.toString()
)
)
selection.postValue(list)
} else {
Log.w("$TAG Address is already in selection, doing nothing")
}
}
@WorkerThread
fun removeAddressModelFromSelection(model: SelectedAddressModel) {
val actual = selection.value.orEmpty()
if (actual.find {
it.address.weakEqual(model.address)
} != null
) {
Log.i("$TAG Removing [${model.address.asStringUriOnly()}] address from selection")
val list = arrayListOf<SelectedAddressModel>()
list.addAll(actual)
model.avatarModel?.destroy()
list.remove(model)
selectionCount.postValue(
AppUtils.getStringWithPlural(
R.plurals.selection_count_label,
list.size,
list.size.toString()
)
)
selection.postValue(list)
} else {
Log.w("$TAG Address isn't in selection, doing nothing")
}
}
}

View file

@ -80,8 +80,8 @@ fun <T> setEntries(
entries: List<T>?, entries: List<T>?,
layoutId: Int layoutId: Int
) { ) {
viewGroup.removeAllViews()
if (!entries.isNullOrEmpty()) { if (!entries.isNullOrEmpty()) {
viewGroup.removeAllViews()
val inflater = viewGroup.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val inflater = viewGroup.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
for (entry in entries) { for (entry in entries) {
val binding = DataBindingUtil.inflate<ViewDataBinding>( val binding = DataBindingUtil.inflate<ViewDataBinding>(

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="12dp" android:height="12dp" />
<solid android:color="@color/gray_100"/>
</shape>

View file

@ -0,0 +1,81 @@
<?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="org.linphone.core.ConsolidatedPresence" />
<import type="org.linphone.core.ChatRoom.SecurityLevel" />
<variable
name="model"
type="org.linphone.ui.main.model.SelectedAddressModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="75dp"
android:layout_height="wrap_content"
android:padding="5dp"
android:background="@drawable/primary_cell_background">
<com.google.android.material.imageview.ShapeableImageView
style="@style/avatar_imageview"
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_favorite_list_cell_size"
android:layout_height="@dimen/avatar_favorite_list_cell_size"
coilAvatar="@{model.avatarModel}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/presence_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:layout_marginEnd="@dimen/avatar_presence_badge_end_margin"
android:background="@drawable/led_background"
android:padding="@dimen/avatar_presence_badge_padding"
app:presenceIcon="@{model.avatarModel.presenceStatus}"
android:visibility="@{model.avatarModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<ImageView
android:id="@+id/trust_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:src="@{model.avatarModel.trust == SecurityLevel.Safe ? @drawable/trusted : @drawable/not_trusted, default=@drawable/trusted}"
android:visibility="@{model.avatarModel.trust == SecurityLevel.Safe || model.avatarModel.trust == SecurityLevel.Unsafe ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<ImageView
android:id="@+id/remove_from_selection"
android:onClick="@{() -> model.removeFromSelection()}"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:src="@drawable/x"
android:padding="5dp"
android:background="@drawable/shape_circle_gray_100_background"
app:tint="@color/gray_main2_500"
app:layout_constraintTop_toTopOf="@id/avatar"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@{model.avatarModel.name, default=`John Doe`}"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/avatar"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -43,10 +43,23 @@
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/new_conversation_title" android:text="@string/new_conversation_title"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@id/create_group"
app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/create_group"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:padding="15dp"
android:adjustViewBounds="true"
android:src="@drawable/check"
android:visibility="@{viewModel.multipleSelectionMode ? View.VISIBLE : View.GONE, default=gone}"
app:tint="@color/orange_main_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/title" />
<View <View
android:id="@+id/background" android:id="@+id/background"
android:layout_width="0dp" android:layout_width="0dp"
@ -56,13 +69,44 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" /> app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/multiple_selection_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:text="@{viewModel.selectionCount, default=`0 selected`}"
android:textSize="12sp"
android:textColor="@color/black"
android:visibility="@{viewModel.multipleSelectionMode ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/background" />
<HorizontalScrollView
android:id="@+id/multiple_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="@{viewModel.multipleSelectionMode ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/multiple_selection_count">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
entries="@{viewModel.selection}"
layout="@{@layout/address_selected_list_cell}"/>
</HorizontalScrollView>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style" style="@style/default_text_style"
android:id="@+id/search_bar" android:id="@+id/search_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:layout_marginTop="30dp" android:layout_marginTop="16dp"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:drawableStart="@drawable/magnifying_glass" android:drawableStart="@drawable/magnifying_glass"
@ -80,7 +124,7 @@
app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintWidth_max="@dimen/text_input_max_width"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" /> app:layout_constraintTop_toBottomOf="@id/multiple_selection" />
<ImageView <ImageView
android:id="@+id/clear_field" android:id="@+id/clear_field"
@ -100,7 +144,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:constraint_referenced_ids="group_chat_icon, gradient_background, group_chat_label" app:constraint_referenced_ids="group_chat_icon, gradient_background, group_chat_label"
android:visibility="@{viewModel.hideGroupChatButton || viewModel.searchFilter.length() > 0 ? View.GONE : View.VISIBLE}" /> android:visibility="@{viewModel.hideGroupChatButton || viewModel.multipleSelectionMode || viewModel.searchFilter.length() > 0 ? View.GONE : View.VISIBLE}" />
<!-- margin start must be half the size of the group_call_icon below --> <!-- margin start must be half the size of the group_call_icon below -->
<View <View
@ -116,6 +160,7 @@
<ImageView <ImageView
android:id="@+id/group_chat_icon" android:id="@+id/group_chat_icon"
android:onClick="@{() -> viewModel.switchToMultipleSelectionMode()}"
android:layout_width="44dp" android:layout_width="44dp"
android:layout_height="44dp" android:layout_height="44dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -130,6 +175,7 @@
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800" style="@style/default_text_style_800"
android:id="@+id/group_chat_label" android:id="@+id/group_chat_label"
android:onClick="@{() -> viewModel.switchToMultipleSelectionMode()}"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -198,6 +244,10 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>

View file

@ -47,6 +47,11 @@
<string name="next">next</string> <string name="next">next</string>
<string name="start">Start</string> <string name="start">Start</string>
<plurals name="selection_count_label">
<item quantity="one">%s selected</item>
<item quantity="other">%s selected</item>
</plurals>
<string name="notification_channel_call_name">&appName; active calls notifications</string> <string name="notification_channel_call_name">&appName; active calls notifications</string>
<string name="notification_channel_incoming_call_name">&appName; incoming calls notifications</string> <string name="notification_channel_incoming_call_name">&appName; incoming calls notifications</string>
<string name="notification_channel_service_name">&appName; service notification</string> <string name="notification_channel_service_name">&appName; service notification</string>