Added long press menu to calls list

This commit is contained in:
Sylvain Berfini 2023-09-22 17:25:12 +02:00
parent ad06f989b7
commit 46e06f2c9d
16 changed files with 348 additions and 32 deletions

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.call.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.CallListCellBinding
import org.linphone.ui.call.model.CallModel
import org.linphone.utils.Event
class CallsListAdapter(private val viewLifecycleOwner: LifecycleOwner) :
ListAdapter<CallModel, RecyclerView.ViewHolder>(CallDiffCallback()) {
var selectedAdapterPosition = -1
val callClickedEvent: MutableLiveData<Event<CallModel>> by lazy {
MutableLiveData<Event<CallModel>>()
}
val callLongClickedEvent: MutableLiveData<Event<CallModel>> by lazy {
MutableLiveData<Event<CallModel>>()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: CallListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.call_list_cell,
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(getItem(position))
}
fun resetSelection() {
notifyItemChanged(selectedAdapterPosition)
selectedAdapterPosition = -1
}
inner class ViewHolder(
val binding: CallListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
@UiThread
fun bind(callModel: CallModel) {
with(binding) {
model = callModel
lifecycleOwner = viewLifecycleOwner
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
binding.setOnClickListener {
callClickedEvent.value = Event(callModel)
}
binding.setOnLongClickListener {
selectedAdapterPosition = bindingAdapterPosition
binding.root.isSelected = true
callLongClickedEvent.value = Event(callModel)
true
}
executePendingBindings()
}
}
}
private class CallDiffCallback : DiffUtil.ItemCallback<CallModel>() {
override fun areItemsTheSame(oldItem: CallModel, newItem: CallModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: CallModel, newItem: CallModel): Boolean {
return false
}
}
}

View file

@ -40,6 +40,7 @@ import org.linphone.core.tools.Log
import org.linphone.databinding.CallActiveFragmentBinding
import org.linphone.ui.call.CallActivity
import org.linphone.ui.call.model.ZrtpSasConfirmationDialogModel
import org.linphone.ui.call.viewmodel.CallsViewModel
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.ui.call.viewmodel.SharedCallViewModel
import org.linphone.utils.AppUtils
@ -56,6 +57,8 @@ class ActiveCallFragment : GenericCallFragment() {
private lateinit var callViewModel: CurrentCallViewModel
private lateinit var callsViewModel: CallsViewModel
// For moving video preview purposes
private var previewX: Float = 0f
@ -107,8 +110,13 @@ class ActiveCallFragment : GenericCallFragment() {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
callsViewModel = requireActivity().run {
ViewModelProvider(this)[CallsViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
binding.callsViewModel = callsViewModel
val bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomBar.root)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED

View file

@ -0,0 +1,63 @@
/*
* 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.call.fragment
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.databinding.CallsListLongPressMenuBinding
import org.linphone.ui.call.model.CallModel
class CallMenuDialogFragment(
private val callModel: CallModel,
private val onDismiss: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "CallMenuDialogFragment"
}
override fun onCancel(dialog: DialogInterface) {
onDismiss?.invoke()
super.onCancel(dialog)
}
override fun onDismiss(dialog: DialogInterface) {
onDismiss?.invoke()
super.onDismiss(dialog)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = CallsListLongPressMenuBinding.inflate(layoutInflater)
view.setHangUpClickListener {
callModel.hangUp()
dismiss()
}
return view.root
}
}

View file

@ -23,10 +23,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.linphone.R
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.core.tools.Log
import org.linphone.databinding.CallsListFragmentBinding
import org.linphone.ui.call.adapter.CallsListAdapter
import org.linphone.ui.call.viewmodel.CallsViewModel
class CallsListFragment : GenericCallFragment() {
@ -36,9 +38,9 @@ class CallsListFragment : GenericCallFragment() {
private lateinit var binding: CallsListFragmentBinding
private val viewModel: CallsViewModel by navGraphViewModels(
R.id.call_nav_graph
)
private lateinit var viewModel: CallsViewModel
private lateinit var adapter: CallsListAdapter
override fun onCreateView(
inflater: LayoutInflater,
@ -52,11 +54,43 @@ class CallsListFragment : GenericCallFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = requireActivity().run {
ViewModelProvider(this)[CallsViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
adapter = CallsListAdapter(viewLifecycleOwner)
binding.callsList.setHasFixedSize(true)
binding.callsList.adapter = adapter
val layoutManager = LinearLayoutManager(requireContext())
binding.callsList.layoutManager = layoutManager
adapter.callLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val modalBottomSheet = CallMenuDialogFragment(model) {
// onDismiss
adapter.resetSelection()
}
modalBottomSheet.show(parentFragmentManager, CallMenuDialogFragment.TAG)
}
}
adapter.callClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
model.togglePauseResume()
}
}
binding.setBackClickListener {
findNavController().popBackStack()
}
viewModel.calls.observe(viewLifecycleOwner) {
Log.i("$TAG Calls list updated with [${it.size}] items")
adapter.submitList(it)
}
}
}

View file

@ -19,15 +19,23 @@
*/
package org.linphone.ui.call.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.LinphoneUtils
class CallModel @WorkerThread constructor(val call: Call) {
companion object {
private const val TAG = "[Call Model]"
}
val id = call.callLog.callId
val displayName = MutableLiveData<String>()
val state = MutableLiveData<String>()
@ -61,4 +69,26 @@ class CallModel @WorkerThread constructor(val call: Call) {
fun destroy() {
call.removeListener(callListener)
}
@WorkerThread
fun togglePauseResume() {
when (call.state) {
Call.State.Paused -> {
Log.i("$TAG Trying to resume call [${call.remoteAddress.asStringUriOnly()}]")
call.resume()
}
else -> {
Log.i("$TAG Trying to resume call [${call.remoteAddress.asStringUriOnly()}]")
call.pause()
}
}
}
@UiThread
fun hangUp() {
coreContext.postOnCoreThread {
Log.i("$TAG Terminating call [${call.remoteAddress.asStringUriOnly()}]")
call.terminate()
}
}
}

View file

@ -37,6 +37,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Calls ViewModel]"
// Keys are hardcoded in SDK
private const val ALERT_NETWORK_TYPE_KEY = "network-type"
private const val ALERT_NETWORK_TYPE_WIFI = "wifi"
private const val ALERT_NETWORK_TYPE_CELLULAR = "mobile"
@ -44,6 +45,8 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
val calls = MutableLiveData<ArrayList<CallModel>>()
val callsCount = MutableLiveData<Int>()
val goToActiveCallEvent = MutableLiveData<Event<Boolean>>()
val showIncomingCallEvent = MutableLiveData<Event<Boolean>>()
@ -111,6 +114,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
val model = CallModel(call)
list.add(model)
calls.postValue(list)
callsCount.postValue(list.size)
}
} else {
if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
@ -176,6 +180,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
list.add(model)
}
calls.postValue(list)
callsCount.postValue(list.size)
val currentCall = core.currentCall ?: core.calls.first()
@ -204,6 +209,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
coreContext.postOnCoreThread { core ->
calls.value.orEmpty().forEach(CallModel::destroy)
callsCount.postValue(0)
core.removeListener(coreListener)
}
}

View file

@ -38,7 +38,7 @@ class HistoryMenuDialogFragment(
private val onDeleteCallLog: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "CallsListMenuDialogFragment"
const val TAG = "HistoryMenuDialogFragment"
}
override fun onCancel(dialog: DialogInterface) {

View file

@ -17,6 +17,9 @@
<variable
name="viewModel"
type="org.linphone.ui.call.viewmodel.CurrentCallViewModel" />
<variable
name="callsViewModel"
type="org.linphone.ui.call.viewmodel.CallsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -68,7 +71,7 @@
app:tint="@color/white" />
<ImageView
android:id="@+id/history_list"
android:id="@+id/calls_list"
android:onClick="@{callsListClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/call_button_size"
@ -82,6 +85,22 @@
app:layout_constraintTop_toBottomOf="@id/main_actions"
app:tint="@color/white" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/missed_calls"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="50dp"
android:gravity="center"
android:background="@drawable/shape_orange_round"
android:text="@{String.valueOf(callsViewModel.callsCount), default=`1`}"
android:textColor="@color/white"
android:textSize="13sp"
android:visibility="@{callsViewModel.callsCount > 1 ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="@id/calls_list"
app:layout_constraintStart_toStartOf="@id/calls_list"
app:layout_constraintEnd_toEndOf="@id/calls_list"/>
<ImageView
android:id="@+id/dialer"
android:layout_width="0dp"
@ -172,7 +191,7 @@
android:text="@string/call_action_go_to_calls_list"
app:layout_constraintEnd_toStartOf="@id/dialer_label"
app:layout_constraintStart_toEndOf="@id/new_call_label"
app:layout_constraintTop_toBottomOf="@id/history_list"/>
app:layout_constraintTop_toBottomOf="@id/calls_list"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/in_call_extra_action_label_style"

View file

@ -20,7 +20,7 @@
app:layout_constraintEnd_toEndOf="parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/history_list"
android:id="@+id/history"
android:name="org.linphone.ui.main.history.fragment.HistoryListFragment"
android:layout_width="@dimen/sliding_pane_left_fragment_with_nav_width"
android:layout_height="match_parent"

View file

@ -17,6 +17,9 @@
<variable
name="viewModel"
type="org.linphone.ui.call.viewmodel.CurrentCallViewModel" />
<variable
name="callsViewModel"
type="org.linphone.ui.call.viewmodel.CallsViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
@ -215,6 +218,7 @@
android:visibility="@{viewModel.fullScreenMode || viewModel.pipMode ? View.INVISIBLE : View.VISIBLE}"
layout="@layout/call_extra_actions"
bind:viewModel="@{viewModel}"
bind:callsViewModel="@{callsViewModel}"
bind:transferClickListener="@{transferClickListener}"
bind:newCallClickListener="@{newCallClickListener}"
bind:callsListClickListener="@{callsListClickListener}"/>

View file

@ -17,6 +17,9 @@
<variable
name="viewModel"
type="org.linphone.ui.call.viewmodel.CurrentCallViewModel" />
<variable
name="callsViewModel"
type="org.linphone.ui.call.viewmodel.CallsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -68,7 +71,7 @@
app:layout_constraintTop_toBottomOf="@id/main_actions" />
<ImageView
android:id="@+id/history_list"
android:id="@+id/calls_list"
android:onClick="@{callsListClickListener}"
android:layout_width="0dp"
android:layout_height="@dimen/call_button_size"
@ -82,6 +85,22 @@
app:layout_constraintEnd_toEndOf="@id/calls_list_label"
app:layout_constraintTop_toBottomOf="@id/main_actions" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/missed_calls"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="50dp"
android:gravity="center"
android:background="@drawable/shape_orange_round"
android:text="@{String.valueOf(callsViewModel.callsCount), default=`1`}"
android:textColor="@color/white"
android:textSize="13sp"
android:visibility="@{callsViewModel.callsCount > 1 ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="@id/calls_list"
app:layout_constraintStart_toStartOf="@id/calls_list"
app:layout_constraintEnd_toEndOf="@id/calls_list"/>
<ImageView
android:id="@+id/dialer"
android:layout_width="0dp"
@ -135,8 +154,8 @@
app:tint="@color/white"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/calls_list_label"
app:layout_constraintStart_toStartOf="@id/history_list"
app:layout_constraintEnd_toEndOf="@id/history_list" />
app:layout_constraintStart_toStartOf="@id/calls_list"
app:layout_constraintEnd_toEndOf="@id/calls_list" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/in_call_extra_action_label_style"
@ -167,7 +186,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/call_action_go_to_calls_list"
app:layout_constraintTop_toBottomOf="@id/history_list"
app:layout_constraintTop_toBottomOf="@id/calls_list"
app:layout_constraintStart_toEndOf="@id/new_call_label"
app:layout_constraintEnd_toStartOf="@id/dialer_label" />

View file

@ -5,27 +5,25 @@
<data>
<import type="android.view.View" />
<variable
name="onClickListener"
type="View.OnClickListener" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="model"
type="org.linphone.ui.call.model.CallModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{onClickListener}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp">
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="5dp"
android:src="@drawable/primary_cell_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
android:layout_marginEnd="16dp"
android:background="@drawable/primary_cell_background">
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"

View file

@ -43,18 +43,15 @@
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
<LinearLayout
android:id="@+id/history_list"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/calls_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_marginTop="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
entries="@{viewModel.calls}"
layout="@{@layout/call_list_cell}"/>
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="hangUpClickListener"
type="View.OnClickListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/gray_main2_200">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/delete"
android:onClick="@{hangUpClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/call_action_hang_up"
style="@style/context_menu_danger_action_label_style"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/phone_disconnect"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -12,7 +12,7 @@
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/history_list"
android:id="@+id/history"
android:name="org.linphone.ui.main.history.fragment.HistoryListFragment"
android:layout_width="@dimen/sliding_pane_left_fragment_width"
android:layout_height="match_parent"

View file

@ -282,6 +282,7 @@
<string name="call_action_pause_call">Pause</string>
<string name="call_action_resume_call">Pause</string>
<string name="call_action_record_call">Record</string>
<string name="call_action_hang_up">Hang up</string>
<string name="call_state_outgoing_progress">In progress</string>
<string name="call_state_outgoing_ringing">Ringing</string>
<string name="call_state_incoming_received">Incoming</string>