Added start group call feature from start call fragment

This commit is contained in:
Sylvain Berfini 2024-01-29 13:57:19 +01:00
parent 260ad798ed
commit 44af8bb340
13 changed files with 230 additions and 39 deletions

View file

@ -38,9 +38,9 @@ import org.linphone.databinding.ChatInfoFragmentBinding
import org.linphone.databinding.ChatParticipantAdminPopupMenuBinding
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.chat.adapter.ConversationParticipantsAdapter
import org.linphone.ui.main.chat.model.ConversationSetOrEditSubjectDialogModel
import org.linphone.ui.main.chat.model.ParticipantModel
import org.linphone.ui.main.chat.viewmodel.ConversationInfoViewModel
import org.linphone.ui.main.fragment.GroupSetOrEditSubjectDialogModel
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
import org.linphone.utils.DialogUtils
import org.linphone.utils.Event
@ -217,9 +217,9 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
binding.setEditSubjectClickListener {
val currentSubject = viewModel.subject.value.orEmpty()
val model = ConversationSetOrEditSubjectDialogModel(currentSubject)
val model = GroupSetOrEditSubjectDialogModel(currentSubject, isGroupConversation = true)
val dialog = DialogUtils.getSetOrEditConversationSubjectDialog(
val dialog = DialogUtils.getSetOrEditGroupSubjectDialog(
requireContext(),
model
)

View file

@ -33,9 +33,9 @@ import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.databinding.StartChatFragmentBinding
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.chat.model.ConversationSetOrEditSubjectDialogModel
import org.linphone.ui.main.chat.viewmodel.StartConversationViewModel
import org.linphone.ui.main.fragment.GenericAddressPickerFragment
import org.linphone.ui.main.fragment.GroupSetOrEditSubjectDialogModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.Event
import org.linphone.utils.hideKeyboard
@ -122,9 +122,9 @@ class StartConversationFragment : GenericAddressPickerFragment() {
}
private fun showGroupConversationSubjectDialog() {
val model = ConversationSetOrEditSubjectDialogModel("")
val model = GroupSetOrEditSubjectDialogModel("", isGroupConversation = true)
val dialog = DialogUtils.getSetOrEditConversationSubjectDialog(
val dialog = DialogUtils.getSetOrEditGroupSubjectDialog(
requireContext(),
model
)

View file

@ -46,7 +46,7 @@ class StartConversationViewModel @UiThread constructor() : AddressSelectionViewM
val subject = MutableLiveData<String>()
val groupChatRoomCreateButtonEnabled = MediatorLiveData<Boolean>()
val createGroupConversationButtonEnabled = MediatorLiveData<Boolean>()
val operationInProgress = MutableLiveData<Boolean>()
@ -87,9 +87,9 @@ class StartConversationViewModel @UiThread constructor() : AddressSelectionViewM
}
init {
groupChatRoomCreateButtonEnabled.postValue(false)
groupChatRoomCreateButtonEnabled.addSource(selection) {
groupChatRoomCreateButtonEnabled.postValue(it.isNotEmpty())
createGroupConversationButtonEnabled.value = false
createGroupConversationButtonEnabled.addSource(selection) {
createGroupConversationButtonEnabled.value = it.isNotEmpty()
}
updateGroupChatButtonVisibility()

View file

@ -17,13 +17,16 @@
* 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.chat.model
package org.linphone.ui.main.fragment
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.utils.Event
class ConversationSetOrEditSubjectDialogModel @UiThread constructor(initialSubject: String) {
class GroupSetOrEditSubjectDialogModel @UiThread constructor(
initialSubject: String,
val isGroupConversation: Boolean
) {
val isEdit = initialSubject.isNotEmpty()
val subject = MutableLiveData<String>()

View file

@ -35,7 +35,9 @@ import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.databinding.StartCallFragmentBinding
import org.linphone.ui.main.fragment.GenericAddressPickerFragment
import org.linphone.ui.main.fragment.GroupSetOrEditSubjectDialogModel
import org.linphone.ui.main.history.viewmodel.StartCallViewModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.addCharacterAtPosition
import org.linphone.utils.hideKeyboard
import org.linphone.utils.removeCharacterAtPosition
@ -80,6 +82,11 @@ class StartCallFragment : GenericAddressPickerFragment() {
viewModel.hideNumpad()
}
binding.setAskForGroupCallSubjectClickListener {
viewModel.hideNumpad()
showGroupCallSubjectDialog()
}
setupRecyclerView(binding.contactsAndSuggestionsList)
viewModel.contactsAndSuggestionsList.observe(
@ -151,4 +158,40 @@ class StartCallFragment : GenericAddressPickerFragment() {
viewModel.isNumpadVisible.value = false
}
private fun showGroupCallSubjectDialog() {
val model = GroupSetOrEditSubjectDialogModel("", isGroupConversation = false)
val dialog = DialogUtils.getSetOrEditGroupSubjectDialog(
requireContext(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Set group call subject cancelled")
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume { newSubject ->
if (newSubject.isNotEmpty()) {
Log.i(
"$TAG Group call subject has been set to [$newSubject]"
)
viewModel.subject.value = newSubject
viewModel.createGroupCall()
dialog.currentFocus?.hideKeyboard()
dialog.dismiss()
} else {
// TODO: show error
}
}
}
Log.i("$TAG Showing dialog to set group call subject")
dialog.show()
}
}

View file

@ -20,12 +20,18 @@
package org.linphone.ui.main.history.viewmodel
import androidx.annotation.UiThread
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.Factory
import org.linphone.core.Participant
import org.linphone.core.ParticipantInfo
import org.linphone.core.tools.Log
import org.linphone.ui.main.history.model.NumpadModel
import org.linphone.ui.main.viewmodel.AddressSelectionViewModel
@ -45,7 +51,11 @@ class StartCallViewModel @UiThread constructor() : AddressSelectionViewModel() {
val isNumpadVisible = MutableLiveData<Boolean>()
val isGroupCallAvailable = MutableLiveData<Boolean>()
val startGroupCallButtonEnabled = MediatorLiveData<Boolean>()
val subject = MutableLiveData<String>()
val operationInProgress = MutableLiveData<Boolean>()
val appendDigitToSearchBarEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
@ -59,6 +69,35 @@ class StartCallViewModel @UiThread constructor() : AddressSelectionViewModel() {
MutableLiveData<Event<Boolean>>()
}
private val conferenceSchedulerListener = object : ConferenceSchedulerListenerStub() {
override fun onStateChanged(
conferenceScheduler: ConferenceScheduler,
state: ConferenceScheduler.State
) {
Log.i("$TAG Conference scheduler state is $state")
if (state == ConferenceScheduler.State.Ready) {
conferenceScheduler.removeListener(this)
val conferenceAddress = conferenceScheduler.info?.uri
if (conferenceAddress != null) {
Log.i(
"$TAG Conference info created, address is ${conferenceAddress.asStringUriOnly()}"
)
coreContext.startCall(conferenceAddress)
} else {
Log.e("$TAG Conference info URI is null!")
// TODO: notify error to user
}
operationInProgress.postValue(false)
} else if (state == ConferenceScheduler.State.Error) {
conferenceScheduler.removeListener(this)
Log.e("$TAG Failed to create group call!")
// TODO: notify error to user
operationInProgress.postValue(false)
}
}
}
init {
isNumpadVisible.value = false
numpadModel = NumpadModel(
@ -89,6 +128,11 @@ class StartCallViewModel @UiThread constructor() : AddressSelectionViewModel() {
}
)
startGroupCallButtonEnabled.value = false
startGroupCallButtonEnabled.addSource(selection) {
startGroupCallButtonEnabled.value = it.isNotEmpty()
}
updateGroupCallButtonVisibility()
}
@ -116,4 +160,43 @@ class StartCallViewModel @UiThread constructor() : AddressSelectionViewModel() {
fun hideNumpad() {
isNumpadVisible.value = false
}
@UiThread
fun createGroupCall() {
coreContext.postOnCoreThread { core ->
val account = core.defaultAccount
if (account == null) {
Log.e(
"$TAG No default account found, can't create group call!"
)
return@postOnCoreThread
}
operationInProgress.postValue(true)
val conferenceInfo = Factory.instance().createConferenceInfo()
conferenceInfo.organizer = account.params.identityAddress
conferenceInfo.subject = subject.value
val participants = arrayOfNulls<ParticipantInfo>(selection.value.orEmpty().size)
var index = 0
for (participant in selection.value.orEmpty()) {
val info = Factory.instance().createParticipantInfo(participant.address)
// For meetings, all participants must have Speaker role
info?.role = Participant.Role.Speaker
participants[index] = info
index += 1
}
conferenceInfo.setParticipantInfos(participants)
Log.i(
"$TAG Creating group call with subject ${subject.value} and ${participants.size} participant(s)"
)
val conferenceScheduler = core.createConferenceScheduler()
conferenceScheduler.addListener(conferenceSchedulerListener)
conferenceScheduler.account = account
// Will trigger the conference creation/update automatically
conferenceScheduler.info = conferenceInfo
}
}
}

View file

@ -76,8 +76,7 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
applyFilter(
currentFilter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
magicSearchSourceFlags,
MagicSearch.Aggregation.Friend
magicSearchSourceFlags
)
}
}
@ -109,7 +108,9 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
@UiThread
fun clearFilter() {
searchFilter.value = ""
if (searchFilter.value.orEmpty().isNotEmpty()) {
searchFilter.value = ""
}
}
@UiThread
@ -185,8 +186,7 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
applyFilter(
filter,
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
magicSearchSourceFlags,
MagicSearch.Aggregation.Friend
magicSearchSourceFlags
)
}
}
@ -195,8 +195,7 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
private fun applyFilter(
filter: String,
domain: String,
sources: Int,
aggregation: MagicSearch.Aggregation
sources: Int
) {
if (previousFilter.isNotEmpty() && (
previousFilter.length > filter.length ||
@ -215,7 +214,7 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
filter,
domain,
sources,
aggregation
MagicSearch.Aggregation.Friend
)
}

View file

@ -44,14 +44,14 @@ import org.linphone.databinding.DialogPickNumberOrAddressBinding
import org.linphone.databinding.DialogRemoveAccountBinding
import org.linphone.databinding.DialogRemoveAllCallLogsBinding
import org.linphone.databinding.DialogRemoveCallLogsBinding
import org.linphone.databinding.DialogSetOrEditGroupConversationSubjectBindingImpl
import org.linphone.databinding.DialogSetOrEditGroupSubjectBindingImpl
import org.linphone.databinding.DialogUpdateAvailableBinding
import org.linphone.ui.assistant.model.AcceptConditionsAndPolicyDialogModel
import org.linphone.ui.assistant.model.ConfirmPhoneNumberDialogModel
import org.linphone.ui.call.model.ZrtpSasConfirmationDialogModel
import org.linphone.ui.main.chat.model.ConversationSetOrEditSubjectDialogModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.contacts.model.TrustCallDialogModel
import org.linphone.ui.main.fragment.GroupSetOrEditSubjectDialogModel
import org.linphone.ui.main.history.model.ConfirmationDialogModel
class DialogUtils {
@ -268,13 +268,13 @@ class DialogUtils {
}
@UiThread
fun getSetOrEditConversationSubjectDialog(
fun getSetOrEditGroupSubjectDialog(
context: Context,
viewModel: ConversationSetOrEditSubjectDialogModel
viewModel: GroupSetOrEditSubjectDialogModel
): Dialog {
val binding: DialogSetOrEditGroupConversationSubjectBindingImpl = DataBindingUtil.inflate(
val binding: DialogSetOrEditGroupSubjectBindingImpl = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.dialog_set_or_edit_group_conversation_subject,
R.layout.dialog_set_or_edit_group_subject,
null,
false
)

View file

@ -8,7 +8,7 @@
<import type="android.graphics.Typeface" />
<variable
name="viewModel"
type="org.linphone.ui.main.chat.model.ConversationSetOrEditSubjectDialogModel" />
type="org.linphone.ui.main.fragment.GroupSetOrEditSubjectDialogModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -37,7 +37,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:paddingTop="@dimen/dialog_top_bottom_margin"
android:text="@{viewModel.isEdit ? @string/dialog_group_conversation_edit_subject : @string/dialog_group_conversation_set_subject, default=@string/dialog_group_conversation_set_subject}"
android:text="@{viewModel.isGroupConversation ? viewModel.isEdit ? @string/dialog_group_conversation_edit_subject : @string/dialog_group_conversation_set_subject : @string/dialog_group_call_set_subject, default=@string/dialog_group_conversation_set_subject}"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/subject"
app:layout_constraintStart_toStartOf="@id/dialog_background"
@ -54,7 +54,7 @@
android:layout_marginTop="5dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:hint="@string/dialog_group_conversation_subject_hint"
android:hint="@{viewModel.isGroupConversation ? @string/dialog_group_conversation_subject_hint : @string/dialog_group_call_subject_hint, default=@string/dialog_group_conversation_subject_hint}"
android:text="@={viewModel.subject, default=`Lorem Ipsum`}"
android:textSize="14sp"
android:textColor="?attr/color_main2_600"

View file

@ -109,8 +109,9 @@
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginEnd="6dp"
android:src="@{model.wasConference ? @drawable/meeting : @drawable/phone, default=@drawable/phone}"
android:src="@drawable/phone"
app:tint="?attr/color_main2_500"
android:visibility="@{model.wasConference ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/name"
app:layout_constraintBottom_toBottomOf="@id/date_time" />

View file

@ -11,6 +11,9 @@
<variable
name="hideNumpadClickListener"
type="View.OnClickListener" />
<variable
name="askForGroupCallSubjectClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.history.viewmodel.StartCallViewModel" />
@ -25,27 +28,34 @@
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<androidx.constraintlayout.widget.Group
android:id="@+id/multiple_selection_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="multiple_selection, multiple_selection_count"
android:visibility="@{viewModel.multipleSelectionMode ? View.VISIBLE : View.GONE, default=gone}" />
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_height="@dimen/top_bar_height"
android:adjustViewBounds="true"
android:onClick="@{backClickListener}"
android:padding="15dp"
android:src="@drawable/caret_left"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title" />
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/main_page_title_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@{viewModel.title, default=@string/history_call_start_title}"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent" />
@ -57,15 +67,44 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/back" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/multiple_selection_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.selectionCount, default=`0 selected`}"
android:textSize="12sp"
android:textColor="?attr/color_main2_900"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<HorizontalScrollView
android:id="@+id/multiple_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
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
style="@style/default_text_style"
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="30dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="20dp"
android:background="@drawable/edit_text_background"
android:drawableStart="@drawable/magnifying_glass"
@ -83,7 +122,7 @@
app:layout_constraintWidth_max="@dimen/text_input_max_width"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
app:layout_constraintTop_toBottomOf="@id/multiple_selection" />
<ImageView
android:id="@+id/numpad"
@ -116,7 +155,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="group_call_icon, gradient_background, group_call_label"
android:visibility="@{viewModel.hideGroupCallButton || viewModel.searchFilter.length() > 0 ? View.GONE : View.VISIBLE}" />
android:visibility="@{viewModel.hideGroupCallButton || viewModel.multipleSelectionMode || viewModel.searchFilter.length() > 0 ? View.GONE : View.VISIBLE}" />
<!-- margin start must be half the size of the group_call_icon below -->
<View
@ -132,6 +171,7 @@
<ImageView
android:id="@+id/group_call_icon"
android:onClick="@{() -> viewModel.switchToMultipleSelectionMode()}"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="16dp"
@ -146,6 +186,7 @@
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/group_call_label"
android:onClick="@{() -> viewModel.switchToMultipleSelectionMode()}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -198,6 +239,21 @@
app:layout_constraintTop_toBottomOf="@id/group_call_icon"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/create_group"
android:onClick="@{askForGroupCallSubjectClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:visibility="@{viewModel.multipleSelectionMode &amp;&amp; viewModel.startGroupCallButtonEnabled ? View.VISIBLE : View.GONE, default=gone}"
android:src="@drawable/check"
app:tint="?attr/color_on_main"
app:backgroundTint="?attr/color_main1_500"
app:shapeAppearanceOverlay="@style/rounded"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
@ -206,6 +262,10 @@
bind:model="@{viewModel.numpadModel}"
layout="@layout/start_call_numpad_bottom_sheet" />
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -229,7 +229,7 @@
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:visibility="@{viewModel.multipleSelectionMode &amp;&amp; viewModel.groupChatRoomCreateButtonEnabled ? View.VISIBLE : View.GONE, default=gone}"
android:visibility="@{viewModel.multipleSelectionMode &amp;&amp; viewModel.createGroupConversationButtonEnabled ? View.VISIBLE : View.GONE, default=gone}"
android:src="@drawable/check"
app:tint="?attr/color_on_main"
app:backgroundTint="?attr/color_main1_500"

View file

@ -123,6 +123,8 @@
<string name="dialog_group_conversation_edit_subject">Edit conversation subject</string>
<string name="dialog_group_conversation_subject_hint">Conversation subject</string>
<string name="dialog_group_conversation_edit_subject_confirm_button">Confirm</string>
<string name="dialog_group_call_set_subject">Set group call subject</string>
<string name="dialog_group_call_subject_hint">Group call subject</string>
<string name="toast_assistant_qr_code_invalid">Invalid QR code!</string>
<string name="toast_sip_address_copied_to_clipboard">SIP address copied into clipboard</string>