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