diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 000000000..cdb2ce998
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt
index e6ca53974..b7620a76c 100644
--- a/app/src/main/java/org/linphone/core/CoreContext.kt
+++ b/app/src/main/java/org/linphone/core/CoreContext.kt
@@ -132,6 +132,7 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
forceZRTP: Boolean = false,
localAddress: Address? = null
) {
+ // Core thread
if (!core.isNetworkReachable) {
Log.e("[Context] Network unreachable, abort outgoing call")
return
diff --git a/app/src/main/java/org/linphone/ui/main/calls/adapter/CallsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/calls/adapter/CallsListAdapter.kt
new file mode 100644
index 000000000..a74a1eee1
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/calls/adapter/CallsListAdapter.kt
@@ -0,0 +1,92 @@
+package org.linphone.ui.main.calls.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+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.main.calls.model.CallLogModel
+import org.linphone.utils.Event
+
+class CallsListAdapter(
+ private val viewLifecycleOwner: LifecycleOwner
+) : ListAdapter(CallLogDiffCallback()) {
+ var selectedAdapterPosition = -1
+
+ val callLogClickedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val callLogLongClickedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val callLogCallBackClickedEvent: 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) {
+ fun bind(callLogModel: CallLogModel) {
+ with(binding) {
+ model = callLogModel
+
+ lifecycleOwner = viewLifecycleOwner
+
+ binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
+
+ binding.setOnClickListener {
+ callLogClickedEvent.value = Event(callLogModel)
+ }
+
+ binding.setOnLongClickListener {
+ selectedAdapterPosition = bindingAdapterPosition
+ binding.root.isSelected = true
+ callLogLongClickedEvent.value = Event(callLogModel)
+ true
+ }
+
+ binding.setOnCallClickListener {
+ callLogCallBackClickedEvent.value = Event(callLogModel)
+ }
+
+ executePendingBindings()
+ }
+ }
+ }
+}
+
+private class CallLogDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: CallLogModel, newItem: CallLogModel): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: CallLogModel, newItem: CallLogModel): Boolean {
+ return oldItem.avatarModel.id == newItem.avatarModel.id
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/calls/fragment/CallFragment.kt b/app/src/main/java/org/linphone/ui/main/calls/fragment/CallFragment.kt
new file mode 100644
index 000000000..0c65e7638
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/calls/fragment/CallFragment.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.main.calls.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.doOnPreDraw
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.navArgs
+import org.linphone.core.tools.Log
+import org.linphone.databinding.CallFragmentBinding
+import org.linphone.ui.main.calls.viewmodel.CallLogViewModel
+import org.linphone.ui.main.fragment.GenericFragment
+import org.linphone.utils.Event
+
+class CallFragment : GenericFragment() {
+ private lateinit var binding: CallFragmentBinding
+
+ private lateinit var viewModel: CallLogViewModel
+
+ private val args: CallFragmentArgs by navArgs()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = CallFragmentBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun goBack() {
+ sharedViewModel.closeSlidingPaneEvent.value = Event(true)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ postponeEnterTransition()
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ viewModel = ViewModelProvider(this)[CallLogViewModel::class.java]
+ binding.viewModel = viewModel
+
+ val callId = args.callId
+ Log.i("[Call Fragment] Looking up for call log with call id [$callId]")
+ viewModel.findCallLogByCallId(callId)
+
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable ->
+ viewModel.showBackButton.value = slideable
+ }
+
+ viewModel.callLogFoundEvent.observe(viewLifecycleOwner) {
+ (view.parent as? ViewGroup)?.doOnPreDraw {
+ startPostponedEnterTransition()
+ }
+ sharedViewModel.openSlidingPaneEvent.value = Event(true)
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsFragment.kt b/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsFragment.kt
index 7a27e00d3..c4adce19e 100644
--- a/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsFragment.kt
@@ -28,6 +28,7 @@ import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.slidingpanelayout.widget.SlidingPaneLayout
import org.linphone.R
+import org.linphone.core.tools.Log
import org.linphone.databinding.CallsFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.SlidingPaneBackPressedCallback
@@ -77,6 +78,19 @@ class CallsFragment : GenericFragment() {
}
}
+ sharedViewModel.showCallLogEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { callId ->
+ Log.i("[Calls Fragment] Displaying call log with call ID [$callId]")
+ val navController = binding.callsRightNavContainer.findNavController()
+ val action = CallFragmentDirections.actionGlobalCallFragment(
+ callId
+ )
+ navController.navigate(action)
+ }
+ }
+
sharedViewModel.navigateToConversationsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.callsFragment) {
diff --git a/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsListFragment.kt b/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsListFragment.kt
index 0a7c50544..f6a2c7689 100644
--- a/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsListFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsListFragment.kt
@@ -28,11 +28,15 @@ import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.databinding.CallsListFragmentBinding
import org.linphone.ui.main.MainActivity
+import org.linphone.ui.main.calls.adapter.CallsListAdapter
import org.linphone.ui.main.calls.viewmodel.CallsListViewModel
import org.linphone.ui.main.fragment.GenericFragment
+import org.linphone.utils.Event
import org.linphone.utils.setKeyboardInsetListener
class CallsListFragment : GenericFragment() {
@@ -43,6 +47,8 @@ class CallsListFragment : GenericFragment() {
R.id.callsListFragment
)
+ private lateinit var adapter: CallsListAdapter
+
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (findNavController().currentDestination?.id == R.id.newContactFragment) {
// Holds fragment in place while new contact fragment slides over it
@@ -65,13 +71,50 @@ class CallsListFragment : GenericFragment() {
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = listViewModel
+ postponeEnterTransition()
+
binding.root.setKeyboardInsetListener { keyboardVisible ->
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
listViewModel.bottomNavBarVisible.value = !portraitOrientation || !keyboardVisible
}
+ adapter = CallsListAdapter(viewLifecycleOwner)
+ binding.callsList.setHasFixedSize(true)
+ binding.callsList.adapter = adapter
+
+ adapter.callLogLongClickedEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ val modalBottomSheet = CallsListMenuDialogFragment(model.callLog) {
+ adapter.resetSelection()
+ }
+ modalBottomSheet.show(parentFragmentManager, CallsListMenuDialogFragment.TAG)
+ }
+ }
+
+ adapter.callLogClickedEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ sharedViewModel.showCallLogEvent.value = Event(model.id ?: "")
+ }
+ }
+
+ adapter.callLogCallBackClickedEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ coreContext.postOnCoreThread {
+ coreContext.startCall(model.address)
+ }
+ }
+ }
+
+ val layoutManager = LinearLayoutManager(requireContext())
+ binding.callsList.layoutManager = layoutManager
+
binding.setOnAvatarClickListener {
(requireActivity() as MainActivity).toggleDrawerMenu()
}
+
+ listViewModel.callLogs.observe(viewLifecycleOwner) {
+ adapter.submitList(it)
+ startPostponedEnterTransition()
+ }
}
}
diff --git a/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsListMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsListMenuDialogFragment.kt
index 58dd4a650..6614e2173 100644
--- a/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsListMenuDialogFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/calls/fragment/CallsListMenuDialogFragment.kt
@@ -19,6 +19,39 @@
*/
package org.linphone.ui.main.calls.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.core.CallLog
+import org.linphone.databinding.CallsListLongPressMenuBinding
-class CallsListMenuDialogFragment : BottomSheetDialogFragment()
+class CallsListMenuDialogFragment(
+ private val calLog: CallLog,
+ private val onDismiss: (() -> Unit)? = null
+) : BottomSheetDialogFragment() {
+ companion object {
+ const val TAG = "CallsListMenuDialogFragment"
+ }
+
+ 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)
+ return view.root
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/calls/model/CallLogModel.kt b/app/src/main/java/org/linphone/ui/main/calls/model/CallLogModel.kt
new file mode 100644
index 000000000..27da0d50f
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/calls/model/CallLogModel.kt
@@ -0,0 +1,38 @@
+package org.linphone.ui.main.calls.model
+
+import androidx.lifecycle.MutableLiveData
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.Call.Dir
+import org.linphone.core.CallLog
+import org.linphone.ui.main.contacts.model.ContactAvatarModel
+import org.linphone.utils.TimestampUtils
+
+class CallLogModel(val callLog: CallLog) {
+ val id = callLog.callId ?: callLog.refKey
+
+ val address = if (callLog.dir == Dir.Outgoing) callLog.remoteAddress else callLog.fromAddress
+
+ val avatarModel: ContactAvatarModel
+
+ val isOutgoing = MutableLiveData()
+
+ val dateTime = MutableLiveData()
+
+ init {
+ // Core thread
+ isOutgoing.postValue(callLog.dir == Dir.Outgoing)
+
+ dateTime.postValue(
+ TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false)
+ )
+
+ val friend = coreContext.core.findFriend(address)
+ if (friend != null) {
+ avatarModel = ContactAvatarModel(friend)
+ } else {
+ val fakeFriend = coreContext.core.createFriend()
+ fakeFriend.address = address
+ avatarModel = ContactAvatarModel(fakeFriend)
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/calls/viewmodel/CallLogViewModel.kt b/app/src/main/java/org/linphone/ui/main/calls/viewmodel/CallLogViewModel.kt
new file mode 100644
index 000000000..66b8f0a2f
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/calls/viewmodel/CallLogViewModel.kt
@@ -0,0 +1,38 @@
+package org.linphone.ui.main.calls.viewmodel
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.ui.main.calls.model.CallLogModel
+import org.linphone.utils.Event
+
+class CallLogViewModel : ViewModel() {
+ val callLogModel = MutableLiveData()
+
+ val showBackButton = MutableLiveData()
+
+ val callLogFoundEvent = MutableLiveData>()
+
+ fun findCallLogByCallId(callId: String) {
+ // UI thread
+ coreContext.postOnCoreThread { core ->
+ val callLog = core.findCallLogFromCallId(callId)
+ if (callLog != null) {
+ callLogModel.postValue(CallLogModel(callLog))
+ callLogFoundEvent.postValue(Event(true))
+ }
+ }
+ }
+
+ fun startAudioCall() {
+ // TODO
+ }
+
+ fun startVideoCall() {
+ // TODO
+ }
+
+ fun sendMessage() {
+ // TODO
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/calls/viewmodel/CallsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/calls/viewmodel/CallsListViewModel.kt
index cb43d1c6f..b5d864ea9 100644
--- a/app/src/main/java/org/linphone/ui/main/calls/viewmodel/CallsListViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/calls/viewmodel/CallsListViewModel.kt
@@ -19,11 +19,28 @@
*/
package org.linphone.ui.main.calls.viewmodel
+import androidx.lifecycle.MutableLiveData
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.ui.main.calls.model.CallLogModel
import org.linphone.ui.main.viewmodel.TopBarViewModel
class CallsListViewModel : TopBarViewModel() {
+ val callLogs = MutableLiveData>()
+
init {
title.value = "Calls"
bottomNavBarVisible.value = true
+
+ coreContext.postOnCoreThread { core ->
+ val list = arrayListOf()
+
+ // TODO : filter depending on currently selected account
+ for (callLog in core.callLogs) {
+ val model = CallLogModel(callLog)
+ list.add(model)
+ }
+
+ callLogs.postValue(list)
+ }
}
}
diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt
index 7aeec5c71..ed9231be9 100644
--- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt
@@ -30,7 +30,6 @@ import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
-import androidx.transition.ChangeBounds
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ContactFragmentBinding
@@ -48,11 +47,6 @@ class ContactFragment : GenericFragment() {
private val args: ContactFragmentArgs by navArgs()
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- sharedElementEnterTransition = ChangeBounds()
- }
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
index 043b6cd58..37f6468fc 100644
--- a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt
@@ -43,4 +43,8 @@ class SharedMainViewModel : ViewModel() {
/* Contacts related */
val showContactEvent = MutableLiveData>()
+
+ /* Call logs related */
+
+ val showCallLogEvent = MutableLiveData>()
}
diff --git a/app/src/main/res/drawable/incoming_call_bounced.xml b/app/src/main/res/drawable/incoming_call_bounced.xml
new file mode 100644
index 000000000..10a82b3a8
--- /dev/null
+++ b/app/src/main/res/drawable/incoming_call_bounced.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/new_contact.xml b/app/src/main/res/drawable/new_contact.xml
new file mode 100644
index 000000000..b35fbfda1
--- /dev/null
+++ b/app/src/main/res/drawable/new_contact.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/outgoing_call_bounced.xml b/app/src/main/res/drawable/outgoing_call_bounced.xml
new file mode 100644
index 000000000..c89423e4c
--- /dev/null
+++ b/app/src/main/res/drawable/outgoing_call_bounced.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/layout-land/calls_list_fragment.xml b/app/src/main/res/layout-land/calls_list_fragment.xml
index 26be7c7f8..d78fef779 100644
--- a/app/src/main/res/layout-land/calls_list_fragment.xml
+++ b/app/src/main/res/layout-land/calls_list_fragment.xml
@@ -45,6 +45,17 @@
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/top_bar" />
+
+
+ android:background="@drawable/shape_conversation_cell_background">
+ android:layout_height="match_parent"
+ android:background="@color/white">
+ android:layout_height="match_parent"
+ android:background="@color/white">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/call_list_cell.xml b/app/src/main/res/layout/call_list_cell.xml
new file mode 100644
index 000000000..14f27e112
--- /dev/null
+++ b/app/src/main/res/layout/call_list_cell.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/calls_list_fragment.xml b/app/src/main/res/layout/calls_list_fragment.xml
index 2d5ae4a30..919dc2778 100644
--- a/app/src/main/res/layout/calls_list_fragment.xml
+++ b/app/src/main/res/layout/calls_list_fragment.xml
@@ -40,6 +40,17 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/contact_list_cell.xml b/app/src/main/res/layout/contact_list_cell.xml
index 3c292c343..d89ceb5f2 100644
--- a/app/src/main/res/layout/contact_list_cell.xml
+++ b/app/src/main/res/layout/contact_list_cell.xml
@@ -22,8 +22,6 @@
android:onLongClick="@{onLongClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="5dp"
- android:layout_marginBottom="5dp"
android:background="@drawable/cell_background">
+
+
+
+
+
+
\ No newline at end of file