diff --git a/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt b/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt new file mode 100644 index 000000000..71917372d --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt @@ -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 . + */ +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(CallDiffCallback()) { + var selectedAdapterPosition = -1 + + val callClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val callLongClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + 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() { + override fun areItemsTheSame(oldItem: CallModel, newItem: CallModel): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: CallModel, newItem: CallModel): Boolean { + return false + } + } +} diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt index 0726d1aac..1aba0b716 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt @@ -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 diff --git a/app/src/main/java/org/linphone/ui/call/fragment/CallMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/CallMenuDialogFragment.kt new file mode 100644 index 000000000..00f469b8c --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/fragment/CallMenuDialogFragment.kt @@ -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 . + */ +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 + } +} diff --git a/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt index cfeb4839c..2cabff9ea 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt @@ -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) + } } } diff --git a/app/src/main/java/org/linphone/ui/call/model/CallModel.kt b/app/src/main/java/org/linphone/ui/call/model/CallModel.kt index ead266f46..c4de2c22b 100644 --- a/app/src/main/java/org/linphone/ui/call/model/CallModel.kt +++ b/app/src/main/java/org/linphone/ui/call/model/CallModel.kt @@ -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() val state = MutableLiveData() @@ -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() + } + } } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt index 5afd5644c..8027a3dd7 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt @@ -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>() + val callsCount = MutableLiveData() + val goToActiveCallEvent = MutableLiveData>() val showIncomingCallEvent = MutableLiveData>() @@ -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) } } diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryMenuDialogFragment.kt index c3af0c466..bb8d993b0 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryMenuDialogFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryMenuDialogFragment.kt @@ -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) { diff --git a/app/src/main/res/layout-land/call_extra_actions.xml b/app/src/main/res/layout-land/call_extra_actions.xml index 825bae7f5..ae8aea999 100644 --- a/app/src/main/res/layout-land/call_extra_actions.xml +++ b/app/src/main/res/layout-land/call_extra_actions.xml @@ -17,6 +17,9 @@ + + + + app:layout_constraintTop_toBottomOf="@id/calls_list"/> + diff --git a/app/src/main/res/layout/call_extra_actions.xml b/app/src/main/res/layout/call_extra_actions.xml index 9f9f8e954..7e9b41c5e 100644 --- a/app/src/main/res/layout/call_extra_actions.xml +++ b/app/src/main/res/layout/call_extra_actions.xml @@ -17,6 +17,9 @@ + + + + app:layout_constraintStart_toStartOf="@id/calls_list" + app:layout_constraintEnd_toEndOf="@id/calls_list" /> diff --git a/app/src/main/res/layout/call_list_cell.xml b/app/src/main/res/layout/call_list_cell.xml index a3dda8d77..eafff4c77 100644 --- a/app/src/main/res/layout/call_list_cell.xml +++ b/app/src/main/res/layout/call_list_cell.xml @@ -5,27 +5,25 @@ + + - - + android:layout_marginEnd="16dp" + android:background="@drawable/primary_cell_background"> - + app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app/src/main/res/layout/calls_list_long_press_menu.xml b/app/src/main/res/layout/calls_list_long_press_menu.xml new file mode 100644 index 000000000..f847f9c40 --- /dev/null +++ b/app/src/main/res/layout/calls_list_long_press_menu.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/history_fragment.xml b/app/src/main/res/layout/history_fragment.xml index f6342b074..b31826fac 100644 --- a/app/src/main/res/layout/history_fragment.xml +++ b/app/src/main/res/layout/history_fragment.xml @@ -12,7 +12,7 @@ android:layout_height="match_parent"> Pause Pause Record + Hang up In progress Ringing Incoming