From c273fc451ac56c37efb19dfddb45ac526dadca44 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 23 Nov 2023 14:03:54 +0100 Subject: [PATCH] Added orange border to chat bubble to show selected bubble when opening bottom sheet menu + small refactoring --- .../telecom/TelecomCallControlCallback.kt | 4 +- .../ui/assistant/AssistantActivity.kt | 6 +- .../java/org/linphone/ui/call/CallActivity.kt | 15 +- .../ui/call/viewmodel/CurrentCallViewModel.kt | 16 +- .../java/org/linphone/ui/main/MainActivity.kt | 7 +- .../chat/fragment/ConversationFragment.kt | 33 +++- .../ui/main/chat/model/ChatMessageModel.kt | 15 +- .../SendMessageInConversationViewModel.kt | 25 +-- .../ui/main/help/fragment/DebugFragment.kt | 31 +++- .../settings/viewmodel/SettingsViewModel.kt | 4 +- .../utils/{AppUtils.kt => AndroidUtils.kt} | 101 ------------ .../org/linphone/utils/AnimationsUtils.kt | 71 -------- .../{AudioRouteUtils.kt => AudioUtils.kt} | 13 +- .../java/org/linphone/utils/ToastUtils.kt | 151 ++++++++++++++++++ .../chat_bubble_incoming_first_background.xml | 7 + .../chat_bubble_incoming_full_background.xml | 7 + .../chat_bubble_outgoing_first_background.xml | 7 + .../chat_bubble_outgoing_full_background.xml | 7 + ...chat_bubble_incoming_first_with_border.xml | 6 + ..._chat_bubble_incoming_full_with_border.xml | 6 + ...chat_bubble_outgoing_first_with_border.xml | 6 + ..._chat_bubble_outgoing_full_with_border.xml | 6 + .../main/res/layout/chat_bubble_incoming.xml | 3 +- .../main/res/layout/chat_bubble_outgoing.xml | 3 +- app/src/main/res/values/strings.xml | 1 + 25 files changed, 331 insertions(+), 220 deletions(-) rename app/src/main/java/org/linphone/utils/{AppUtils.kt => AndroidUtils.kt} (64%) delete mode 100644 app/src/main/java/org/linphone/utils/AnimationsUtils.kt rename app/src/main/java/org/linphone/utils/{AudioRouteUtils.kt => AudioUtils.kt} (95%) create mode 100644 app/src/main/java/org/linphone/utils/ToastUtils.kt create mode 100644 app/src/main/res/drawable/chat_bubble_incoming_first_background.xml create mode 100644 app/src/main/res/drawable/chat_bubble_incoming_full_background.xml create mode 100644 app/src/main/res/drawable/chat_bubble_outgoing_first_background.xml create mode 100644 app/src/main/res/drawable/chat_bubble_outgoing_full_background.xml create mode 100644 app/src/main/res/drawable/shape_chat_bubble_incoming_first_with_border.xml create mode 100644 app/src/main/res/drawable/shape_chat_bubble_incoming_full_with_border.xml create mode 100644 app/src/main/res/drawable/shape_chat_bubble_outgoing_first_with_border.xml create mode 100644 app/src/main/res/drawable/shape_chat_bubble_outgoing_full_with_border.xml diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index a69da95d5..014ac856f 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -35,7 +35,7 @@ import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.CallListenerStub import org.linphone.core.tools.Log -import org.linphone.utils.AudioRouteUtils +import org.linphone.utils.AudioUtils class TelecomCallControlCallback constructor( private val call: Call, @@ -143,7 +143,7 @@ class TelecomCallControlCallback constructor( } if (route.isNotEmpty()) { coreContext.postOnCoreThread { - AudioRouteUtils.applyAudioRouteChangeInLinphone(call, route) + AudioUtils.applyAudioRouteChangeInLinphone(call, route) } } }.launchIn(scope) diff --git a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt index 62a7ebf50..2b0d5ed77 100644 --- a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt +++ b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt @@ -38,7 +38,7 @@ import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.AssistantActivityBinding import org.linphone.ui.assistant.fragment.PermissionsFragmentDirections -import org.linphone.utils.AppUtils +import org.linphone.utils.ToastUtils import org.linphone.utils.slideInToastFromTopForDuration @UiThread @@ -89,7 +89,7 @@ class AssistantActivity : AppCompatActivity() { } fun showGreenToast(message: String, @DrawableRes icon: Int) { - val greenToast = AppUtils.getGreenToast(this, binding.toastsArea, message, icon) + val greenToast = ToastUtils.getGreenToast(this, binding.toastsArea, message, icon) binding.toastsArea.addView(greenToast.root) greenToast.root.slideInToastFromTopForDuration( @@ -99,7 +99,7 @@ class AssistantActivity : AppCompatActivity() { } fun showRedToast(message: String, @DrawableRes icon: Int) { - val redToast = AppUtils.getRedToast(this, binding.toastsArea, message, icon) + val redToast = ToastUtils.getRedToast(this, binding.toastsArea, message, icon) binding.toastsArea.addView(redToast.root) redToast.root.slideInToastFromTopForDuration( diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index e5b37544a..938fa6869 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -53,6 +53,7 @@ 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 +import org.linphone.utils.ToastUtils import org.linphone.utils.slideInToastFromTop import org.linphone.utils.slideInToastFromTopForDuration @@ -322,7 +323,7 @@ class CallActivity : AppCompatActivity() { } fun showBlueToast(message: String, @DrawableRes icon: Int, doNotTint: Boolean = false) { - val blueToast = AppUtils.getBlueToast(this, binding.toastsArea, message, icon, doNotTint) + val blueToast = ToastUtils.getBlueToast(this, binding.toastsArea, message, icon, doNotTint) binding.toastsArea.addView(blueToast.root) blueToast.root.slideInToastFromTopForDuration( @@ -337,7 +338,7 @@ class CallActivity : AppCompatActivity() { duration: Long = 4000, doNotTint: Boolean = false ) { - val redToast = AppUtils.getRedToast(this, binding.toastsArea, message, icon, doNotTint) + val redToast = ToastUtils.getRedToast(this, binding.toastsArea, message, icon, doNotTint) binding.toastsArea.addView(redToast.root) redToast.root.slideInToastFromTopForDuration( @@ -353,7 +354,7 @@ class CallActivity : AppCompatActivity() { tag: String, doNotTint: Boolean = false ) { - val redToast = AppUtils.getRedToast(this, binding.toastsArea, message, icon, doNotTint) + val redToast = ToastUtils.getRedToast(this, binding.toastsArea, message, icon, doNotTint) redToast.root.tag = tag binding.toastsArea.addView(redToast.root) @@ -377,7 +378,13 @@ class CallActivity : AppCompatActivity() { duration: Long = 4000, doNotTint: Boolean = false ) { - val greenToast = AppUtils.getGreenToast(this, binding.toastsArea, message, icon, doNotTint) + val greenToast = ToastUtils.getGreenToast( + this, + binding.toastsArea, + message, + icon, + doNotTint + ) binding.toastsArea.addView(greenToast.root) greenToast.root.slideInToastFromTopForDuration( diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index acad23a5d..9c1cebe99 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -47,7 +47,7 @@ import org.linphone.ui.call.model.ConferenceModel import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.history.model.NumpadModel import org.linphone.utils.AppUtils -import org.linphone.utils.AudioRouteUtils +import org.linphone.utils.AudioUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils @@ -223,7 +223,7 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { if (videoEnabled && isVideoEnabled.value == false) { if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) { Log.i("$TAG Video is now enabled, routing audio to speaker") - AudioRouteUtils.routeAudioToSpeaker(call) + AudioUtils.routeAudioToSpeaker(call) } } isVideoEnabled.postValue(videoEnabled) @@ -491,10 +491,10 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { Log.i("$TAG Selected audio device with ID [${device.id}]") if (::currentCall.isInitialized) { when { - isHeadset -> AudioRouteUtils.routeAudioToHeadset(currentCall) - isBluetooth -> AudioRouteUtils.routeAudioToBluetooth(currentCall) - isSpeaker -> AudioRouteUtils.routeAudioToSpeaker(currentCall) - else -> AudioRouteUtils.routeAudioToEarpiece(currentCall) + isHeadset -> AudioUtils.routeAudioToHeadset(currentCall) + isBluetooth -> AudioUtils.routeAudioToBluetooth(currentCall) + isSpeaker -> AudioUtils.routeAudioToSpeaker(currentCall) + else -> AudioUtils.routeAudioToEarpiece(currentCall) } } } @@ -512,9 +512,9 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { ) if (::currentCall.isInitialized) { if (routeAudioToSpeaker) { - AudioRouteUtils.routeAudioToSpeaker(currentCall) + AudioUtils.routeAudioToSpeaker(currentCall) } else { - AudioRouteUtils.routeAudioToEarpiece(currentCall) + AudioUtils.routeAudioToEarpiece(currentCall) } } } diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index d9571f8d9..de2caf0eb 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -58,6 +58,7 @@ import org.linphone.ui.welcome.WelcomeActivity import org.linphone.utils.AppUtils import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils +import org.linphone.utils.ToastUtils import org.linphone.utils.slideInToastFromTop import org.linphone.utils.slideInToastFromTopForDuration @@ -293,7 +294,7 @@ class MainActivity : AppCompatActivity() { } fun showGreenToast(message: String, @DrawableRes icon: Int) { - val greenToast = AppUtils.getGreenToast(this, binding.toastsArea, message, icon) + val greenToast = ToastUtils.getGreenToast(this, binding.toastsArea, message, icon) binding.toastsArea.addView(greenToast.root) greenToast.root.slideInToastFromTopForDuration( @@ -303,7 +304,7 @@ class MainActivity : AppCompatActivity() { } fun showRedToast(message: String, @DrawableRes icon: Int) { - val redToast = AppUtils.getRedToast(this, binding.toastsArea, message, icon) + val redToast = ToastUtils.getRedToast(this, binding.toastsArea, message, icon) binding.toastsArea.addView(redToast.root) redToast.root.slideInToastFromTopForDuration( @@ -318,7 +319,7 @@ class MainActivity : AppCompatActivity() { tag: String, doNotTint: Boolean = false ) { - val redToast = AppUtils.getRedToast(this, binding.toastsArea, message, icon, doNotTint) + val redToast = ToastUtils.getRedToast(this, binding.toastsArea, message, icon, doNotTint) redToast.root.tag = tag binding.toastsArea.addView(redToast.root) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 0d646a613..109fc42ca 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -48,6 +48,7 @@ import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import kotlinx.coroutines.Dispatchers @@ -156,6 +157,18 @@ class ConversationFragment : GenericFragment() { } } + private var currentChatMessageModelForBottomSheet: ChatMessageModel? = null + private val bottomSheetCallback = object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + Log.i("$TAG Bottom sheet state is [$newState]") + if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + currentChatMessageModelForBottomSheet?.isSelected?.value = false + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { } + } + private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> @@ -295,13 +308,13 @@ class ConversationFragment : GenericFragment() { adapter.showDeliveryForChatMessageModelEvent.observe(viewLifecycleOwner) { it.consume { model -> - showDeliveryBottomSheetDialog(model, showDelivery = true) + showBottomSheetDialog(model, showDelivery = true) } } adapter.showReactionForChatMessageModelEvent.observe(viewLifecycleOwner) { it.consume { model -> - showDeliveryBottomSheetDialog(model, showReactions = true) + showBottomSheetDialog(model, showReactions = true) } } @@ -517,6 +530,9 @@ class ConversationFragment : GenericFragment() { } catch (e: IllegalStateException) { Log.e("$TAG Failed to register data observer to adapter: $e") } + + val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root) + bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback) } override fun onPause() { @@ -539,6 +555,10 @@ class ConversationFragment : GenericFragment() { val layoutManager = binding.eventsList.layoutManager as LinearLayoutManager viewModel.scrollingPosition = layoutManager.findFirstVisibleItemPosition() + val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root) + bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback) + currentChatMessageModelForBottomSheet = null + super.onPause() } @@ -620,21 +640,26 @@ class ConversationFragment : GenericFragment() { } @UiThread - private fun showDeliveryBottomSheetDialog( + private fun showBottomSheetDialog( chatMessageModel: ChatMessageModel, showDelivery: Boolean = false, showReactions: Boolean = false ) { val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root) - bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED binding.messageBottomSheet.setHandleClickedListener { bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } + if (binding.messageBottomSheet.bottomSheetList.adapter != bottomSheetAdapter) { binding.messageBottomSheet.bottomSheetList.adapter = bottomSheetAdapter } + currentChatMessageModelForBottomSheet?.isSelected?.value = false + currentChatMessageModelForBottomSheet = chatMessageModel + chatMessageModel.isSelected.value = true + lifecycleScope.launch { withContext(Dispatchers.IO) { // Wait for previous bottom sheet to go away diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt index 14d1367e8..075a5b547 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt @@ -50,7 +50,7 @@ import org.linphone.core.PlayerListener import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.utils.AppUtils -import org.linphone.utils.AudioRouteUtils +import org.linphone.utils.AudioUtils import org.linphone.utils.Event import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils @@ -102,6 +102,8 @@ class ChatMessageModel @WorkerThread constructor( val firstImage = MutableLiveData() + val isSelected = MutableLiveData() + // Below are for conferences info val meetingFound = MutableLiveData() @@ -439,7 +441,7 @@ class ChatMessageModel @WorkerThread constructor( SpannableClickedListener { override fun onSpanClicked(text: String) { Log.i("$TAG Clicked on [$text] span") - // TODO + // TODO: go to contact page if not ourselves } } ) @@ -541,7 +543,7 @@ class ChatMessageModel @WorkerThread constructor( Log.i("$TAG Creating player for voice record") - val playbackSoundCard = AudioRouteUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() + val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() Log.i( "$TAG Using device $playbackSoundCard to make the voice message playback" ) @@ -574,9 +576,10 @@ class ChatMessageModel @WorkerThread constructor( } // TODO: check media volume + val lowMediaVolume = AudioUtils.isMediaVolumeLow(coreContext.context) if (voiceRecordAudioFocusRequest == null) { - voiceRecordAudioFocusRequest = AudioRouteUtils.acquireAudioFocusForVoiceRecordingOrPlayback( + voiceRecordAudioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback( coreContext.context ) } @@ -601,7 +604,7 @@ class ChatMessageModel @WorkerThread constructor( val request = voiceRecordAudioFocusRequest if (request != null) { - AudioRouteUtils.releaseAudioFocusForVoiceRecordingOrPlayback( + AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback( coreContext.context, request ) @@ -631,7 +634,7 @@ class ChatMessageModel @WorkerThread constructor( val request = voiceRecordAudioFocusRequest if (request != null) { - AudioRouteUtils.releaseAudioFocusForVoiceRecordingOrPlayback( + AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback( coreContext.context, request ) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index bf39eec09..1fea03723 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -51,7 +51,7 @@ import org.linphone.ui.main.chat.model.ChatMessageModel import org.linphone.ui.main.chat.model.FileModel import org.linphone.ui.main.chat.model.ParticipantModel import org.linphone.utils.AppUtils -import org.linphone.utils.AudioRouteUtils +import org.linphone.utils.AudioUtils import org.linphone.utils.Event import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils @@ -430,7 +430,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { val recorderParams = core.createRecorderParams() recorderParams.fileFormat = Recorder.FileFormat.Mkv - val recordingAudioDevice = AudioRouteUtils.getAudioRecordingDeviceIdForVoiceMessage() + val recordingAudioDevice = AudioUtils.getAudioRecordingDeviceIdForVoiceMessage() recorderParams.audioDevice = recordingAudioDevice Log.i( "$TAG Using device ${recorderParams.audioDevice?.id} to make the voice message recording" @@ -444,7 +444,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { private fun startVoiceRecorder() { if (voiceRecordAudioFocusRequest == null) { Log.i("$TAG Requesting audio focus for voice message recording") - voiceRecordAudioFocusRequest = AudioRouteUtils.acquireAudioFocusForVoiceRecordingOrPlayback( + voiceRecordAudioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback( coreContext.context ) } @@ -509,7 +509,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { val request = voiceRecordAudioFocusRequest if (request != null) { Log.i("$TAG Releasing voice recording audio focus request") - AudioRouteUtils.releaseAudioFocusForVoiceRecordingOrPlayback( + AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback( coreContext.context, request ) @@ -523,7 +523,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { private fun initVoiceRecordPlayer() { Log.i("$TAG Creating player for voice record") - val playbackSoundCard = AudioRouteUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() + val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() Log.i( "$TAG Using device $playbackSoundCard to make the voice message playback" ) @@ -553,11 +553,16 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { initVoiceRecordPlayer() } - // TODO: check media volume + val context = coreContext.context + val lowMediaVolume = AudioUtils.isMediaVolumeLow(context) + if (lowMediaVolume) { + val message = AppUtils.getString(R.string.toast_low_media_volume) + showRedToastEvent.postValue(Event(Pair(message, R.drawable.speaker_slash))) + } if (voiceRecordAudioFocusRequest == null) { - voiceRecordAudioFocusRequest = AudioRouteUtils.acquireAudioFocusForVoiceRecordingOrPlayback( - coreContext.context + voiceRecordAudioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback( + context ) } @@ -581,7 +586,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { val request = voiceRecordAudioFocusRequest if (request != null) { - AudioRouteUtils.releaseAudioFocusForVoiceRecordingOrPlayback( + AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback( coreContext.context, request ) @@ -606,7 +611,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { val request = voiceRecordAudioFocusRequest if (request != null) { - AudioRouteUtils.releaseAudioFocusForVoiceRecordingOrPlayback( + AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback( coreContext.context, request ) diff --git a/app/src/main/java/org/linphone/ui/main/help/fragment/DebugFragment.kt b/app/src/main/java/org/linphone/ui/main/help/fragment/DebugFragment.kt index 955af3e9b..95915380d 100644 --- a/app/src/main/java/org/linphone/ui/main/help/fragment/DebugFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/help/fragment/DebugFragment.kt @@ -19,17 +19,19 @@ */ package org.linphone.ui.main.help.fragment +import android.content.ActivityNotFoundException +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.navigation.navGraphViewModels import org.linphone.R +import org.linphone.core.tools.Log import org.linphone.databinding.HelpDebugFragmentBinding import org.linphone.ui.main.MainActivity import org.linphone.ui.main.fragment.GenericFragment import org.linphone.ui.main.help.viewmodel.HelpViewModel -import org.linphone.utils.AppUtils class DebugFragment : GenericFragment() { companion object { @@ -78,7 +80,32 @@ class DebugFragment : GenericFragment() { R.drawable.info ) - AppUtils.shareUploadedLogsUrl(requireActivity(), url) + val appName = requireContext().getString(R.string.app_name) + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra( + Intent.EXTRA_EMAIL, + arrayOf( + requireContext().getString( + R.string.help_advanced_send_debug_logs_email_address + ) + ) + ) + intent.putExtra(Intent.EXTRA_SUBJECT, "$appName Logs") + intent.putExtra(Intent.EXTRA_TEXT, url) + intent.type = "text/plain" + + try { + requireContext().startActivity( + Intent.createChooser( + intent, + requireContext().getString( + R.string.help_troubleshooting_share_logs_dialog_title + ) + ) + ) + } catch (ex: ActivityNotFoundException) { + Log.e(ex) + } } } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 7b41e0483..972c8900c 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -38,7 +38,7 @@ import org.linphone.core.Player import org.linphone.core.PlayerListener import org.linphone.core.tools.Log import org.linphone.utils.AppUtils -import org.linphone.utils.AudioRouteUtils +import org.linphone.utils.AudioUtils class SettingsViewModel @UiThread constructor() : ViewModel() { companion object { @@ -208,7 +208,7 @@ class SettingsViewModel @UiThread constructor() : ViewModel() { coreContext.postOnCoreThread { core -> if (!::ringtonePlayer.isInitialized) { // Also works for ringtone - val playbackDevice = AudioRouteUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() + val playbackDevice = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() val player = core.createLocalPlayer(playbackDevice, null, null) ringtonePlayer = player ?: return@postOnCoreThread ringtonePlayer.addListener(playerListener) diff --git a/app/src/main/java/org/linphone/utils/AppUtils.kt b/app/src/main/java/org/linphone/utils/AndroidUtils.kt similarity index 64% rename from app/src/main/java/org/linphone/utils/AppUtils.kt rename to app/src/main/java/org/linphone/utils/AndroidUtils.kt index a46473345..a540b21ea 100644 --- a/app/src/main/java/org/linphone/utils/AppUtils.kt +++ b/app/src/main/java/org/linphone/utils/AndroidUtils.kt @@ -20,34 +20,26 @@ package org.linphone.utils import android.app.Activity -import android.content.ActivityNotFoundException import android.content.Context -import android.content.Intent import android.os.Build import android.provider.Settings import android.util.DisplayMetrics import android.util.Rational -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.annotation.AnyThread import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DimenRes -import androidx.annotation.DrawableRes import androidx.annotation.MainThread import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.annotation.UiThread import androidx.core.content.ContextCompat import androidx.core.view.SoftwareKeyboardControllerCompat -import androidx.databinding.DataBindingUtil import androidx.emoji2.text.EmojiCompat import java.util.Locale import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R import org.linphone.core.tools.Log -import org.linphone.databinding.ToastBinding @UiThread fun View.showKeyboard() { @@ -195,98 +187,5 @@ class AppUtils { } return name } - - @MainThread - fun getRedToast( - context: Context, - parent: ViewGroup, - message: String, - @DrawableRes icon: Int, - doNotTint: Boolean = false - ): ToastBinding { - val redToast: ToastBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.toast, - parent, - false - ) - redToast.doNotTint = doNotTint - redToast.message = message - redToast.icon = icon - redToast.shadowColor = R.drawable.shape_toast_red_background - redToast.textColor = R.color.red_danger_500 - redToast.root.visibility = View.GONE - return redToast - } - - @MainThread - fun getGreenToast( - context: Context, - parent: ViewGroup, - message: String, - @DrawableRes icon: Int, - doNotTint: Boolean = false - ): ToastBinding { - val greenToast: ToastBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.toast, - parent, - false - ) - greenToast.doNotTint = doNotTint - greenToast.message = message - greenToast.icon = icon - greenToast.shadowColor = R.drawable.shape_toast_green_background - greenToast.textColor = R.color.green_success_500 - greenToast.root.visibility = View.GONE - return greenToast - } - - @MainThread - fun getBlueToast( - context: Context, - parent: ViewGroup, - message: String, - @DrawableRes icon: Int, - doNotTint: Boolean = false - ): ToastBinding { - val blueToast: ToastBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.toast, - parent, - false - ) - blueToast.doNotTint = doNotTint - blueToast.message = message - blueToast.icon = icon - blueToast.shadowColor = R.drawable.shape_toast_blue_background - blueToast.textColor = R.color.blue_info_500 - blueToast.root.visibility = View.GONE - return blueToast - } - - @AnyThread - fun shareUploadedLogsUrl(activity: Activity, info: String) { - val appName = activity.getString(R.string.app_name) - val intent = Intent(Intent.ACTION_SEND) - intent.putExtra( - Intent.EXTRA_EMAIL, - arrayOf(activity.getString(R.string.help_advanced_send_debug_logs_email_address)) - ) - intent.putExtra(Intent.EXTRA_SUBJECT, "$appName Logs") - intent.putExtra(Intent.EXTRA_TEXT, info) - intent.type = "text/plain" - - try { - activity.startActivity( - Intent.createChooser( - intent, - activity.getString(R.string.help_troubleshooting_share_logs_dialog_title) - ) - ) - } catch (ex: ActivityNotFoundException) { - Log.e(ex) - } - } } } diff --git a/app/src/main/java/org/linphone/utils/AnimationsUtils.kt b/app/src/main/java/org/linphone/utils/AnimationsUtils.kt deleted file mode 100644 index 16a53ad9c..000000000 --- a/app/src/main/java/org/linphone/utils/AnimationsUtils.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.utils - -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import androidx.annotation.UiThread -import androidx.lifecycle.LifecycleCoroutineScope -import androidx.transition.Slide -import androidx.transition.Transition -import androidx.transition.TransitionManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@UiThread -fun View.slideInToastFromTop( - root: ViewGroup, - visible: Boolean -) { - val view = this - val transition: Transition = Slide(Gravity.TOP) - transition.duration = 600 - transition.addTarget(view) - - TransitionManager.beginDelayedTransition(root, transition) - view.visibility = if (visible) View.VISIBLE else View.GONE -} - -@UiThread -fun View.slideInToastFromTopForDuration( - root: ViewGroup, - lifecycleScope: LifecycleCoroutineScope, - duration: Long = 4000 -) { - val view = this - val transition: Transition = Slide(Gravity.TOP) - transition.duration = 600 - transition.addTarget(view) - - TransitionManager.beginDelayedTransition(root, transition) - view.visibility = View.VISIBLE - - lifecycleScope.launch { - withContext(Dispatchers.IO) { - delay(duration) - withContext(Dispatchers.Main) { - root.removeView(view) - } - } - } -} diff --git a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt b/app/src/main/java/org/linphone/utils/AudioUtils.kt similarity index 95% rename from app/src/main/java/org/linphone/utils/AudioRouteUtils.kt rename to app/src/main/java/org/linphone/utils/AudioUtils.kt index cc3dc5880..2c839c015 100644 --- a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioUtils.kt @@ -31,9 +31,9 @@ import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.tools.Log -class AudioRouteUtils { +class AudioUtils { companion object { - private const val TAG = "[Audio Route Utils]" + private const val TAG = "[Audio Utils]" @WorkerThread fun routeAudioToEarpiece(call: Call? = null) { @@ -267,5 +267,14 @@ class AudioRouteUtils { AudioManagerCompat.abandonAudioFocusRequest(audioManager, request) Log.i("$TAG Voice recording/playback audio focus request abandoned") } + + @AnyThread + fun isMediaVolumeLow(context: Context): Boolean { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + Log.i("$TAG Current media volume value is $currentVolume, max value is $maxVolume") + return currentVolume <= maxVolume * 0.5 + } } } diff --git a/app/src/main/java/org/linphone/utils/ToastUtils.kt b/app/src/main/java/org/linphone/utils/ToastUtils.kt new file mode 100644 index 000000000..5541750a5 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/ToastUtils.kt @@ -0,0 +1,151 @@ +/* + * 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.utils + +import android.content.Context +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.DrawableRes +import androidx.annotation.MainThread +import androidx.annotation.UiThread +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.transition.Slide +import androidx.transition.Transition +import androidx.transition.TransitionManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.linphone.R +import org.linphone.databinding.ToastBinding + +@UiThread +fun View.slideInToastFromTop( + root: ViewGroup, + visible: Boolean +) { + val view = this + val transition: Transition = Slide(Gravity.TOP) + transition.duration = 600 + transition.addTarget(view) + + TransitionManager.beginDelayedTransition(root, transition) + view.visibility = if (visible) View.VISIBLE else View.GONE +} + +@UiThread +fun View.slideInToastFromTopForDuration( + root: ViewGroup, + lifecycleScope: LifecycleCoroutineScope, + duration: Long = 4000 +) { + val view = this + val transition: Transition = Slide(Gravity.TOP) + transition.duration = 600 + transition.addTarget(view) + + TransitionManager.beginDelayedTransition(root, transition) + view.visibility = View.VISIBLE + + lifecycleScope.launch { + withContext(Dispatchers.IO) { + delay(duration) + withContext(Dispatchers.Main) { + root.removeView(view) + } + } + } +} + +class ToastUtils { + companion object { + @MainThread + fun getRedToast( + context: Context, + parent: ViewGroup, + message: String, + @DrawableRes icon: Int, + doNotTint: Boolean = false + ): ToastBinding { + val redToast: ToastBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.toast, + parent, + false + ) + redToast.doNotTint = doNotTint + redToast.message = message + redToast.icon = icon + redToast.shadowColor = R.drawable.shape_toast_red_background + redToast.textColor = R.color.red_danger_500 + redToast.root.visibility = View.GONE + return redToast + } + + @MainThread + fun getGreenToast( + context: Context, + parent: ViewGroup, + message: String, + @DrawableRes icon: Int, + doNotTint: Boolean = false + ): ToastBinding { + val greenToast: ToastBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.toast, + parent, + false + ) + greenToast.doNotTint = doNotTint + greenToast.message = message + greenToast.icon = icon + greenToast.shadowColor = R.drawable.shape_toast_green_background + greenToast.textColor = R.color.green_success_500 + greenToast.root.visibility = View.GONE + return greenToast + } + + @MainThread + fun getBlueToast( + context: Context, + parent: ViewGroup, + message: String, + @DrawableRes icon: Int, + doNotTint: Boolean = false + ): ToastBinding { + val blueToast: ToastBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.toast, + parent, + false + ) + blueToast.doNotTint = doNotTint + blueToast.message = message + blueToast.icon = icon + blueToast.shadowColor = R.drawable.shape_toast_blue_background + blueToast.textColor = R.color.blue_info_500 + blueToast.root.visibility = View.GONE + return blueToast + } + } +} diff --git a/app/src/main/res/drawable/chat_bubble_incoming_first_background.xml b/app/src/main/res/drawable/chat_bubble_incoming_first_background.xml new file mode 100644 index 000000000..1b4596986 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_incoming_first_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_bubble_incoming_full_background.xml b/app/src/main/res/drawable/chat_bubble_incoming_full_background.xml new file mode 100644 index 000000000..734a35492 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_incoming_full_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_first_background.xml b/app/src/main/res/drawable/chat_bubble_outgoing_first_background.xml new file mode 100644 index 000000000..c98f37474 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_outgoing_first_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_full_background.xml b/app/src/main/res/drawable/chat_bubble_outgoing_full_background.xml new file mode 100644 index 000000000..0d2eccaec --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_outgoing_full_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_first_with_border.xml b/app/src/main/res/drawable/shape_chat_bubble_incoming_first_with_border.xml new file mode 100644 index 000000000..96c7ddcc0 --- /dev/null +++ b/app/src/main/res/drawable/shape_chat_bubble_incoming_first_with_border.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_full_with_border.xml b/app/src/main/res/drawable/shape_chat_bubble_incoming_full_with_border.xml new file mode 100644 index 000000000..7808f65fe --- /dev/null +++ b/app/src/main/res/drawable/shape_chat_bubble_incoming_full_with_border.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chat_bubble_outgoing_first_with_border.xml b/app/src/main/res/drawable/shape_chat_bubble_outgoing_first_with_border.xml new file mode 100644 index 000000000..c1123eb28 --- /dev/null +++ b/app/src/main/res/drawable/shape_chat_bubble_outgoing_first_with_border.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chat_bubble_outgoing_full_with_border.xml b/app/src/main/res/drawable/shape_chat_bubble_outgoing_full_with_border.xml new file mode 100644 index 000000000..1df1843b4 --- /dev/null +++ b/app/src/main/res/drawable/shape_chat_bubble_outgoing_full_with_border.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml index 1e92e5a22..1e2b2d06a 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -138,7 +138,8 @@ android:layout_marginEnd="16dp" android:padding="10dp" android:orientation="vertical" - android:background="@{model.isGroupedWithPreviousOne ? @drawable/shape_chat_bubble_incoming_full : @drawable/shape_chat_bubble_incoming_first, default=@drawable/shape_chat_bubble_incoming_first}" + android:selected="@{model.isSelected}" + android:background="@{model.isGroupedWithPreviousOne ? @drawable/chat_bubble_incoming_full_background : @drawable/chat_bubble_incoming_first_background, default=@drawable/chat_bubble_incoming_first_background}" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_max="@dimen/chat_bubble_max_width" app:layout_constraintTop_toBottomOf="@id/reply" diff --git a/app/src/main/res/layout/chat_bubble_outgoing.xml b/app/src/main/res/layout/chat_bubble_outgoing.xml index 54c61036c..7d928f43e 100644 --- a/app/src/main/res/layout/chat_bubble_outgoing.xml +++ b/app/src/main/res/layout/chat_bubble_outgoing.xml @@ -100,7 +100,8 @@ android:padding="10dp" android:orientation="vertical" android:gravity="end" - android:background="@{model.isGroupedWithPreviousOne ? @drawable/shape_chat_bubble_outgoing_full : @drawable/shape_chat_bubble_outgoing_first, default=@drawable/shape_chat_bubble_outgoing_first}" + android:selected="@{model.isSelected}" + android:background="@{model.isGroupedWithPreviousOne ? @drawable/chat_bubble_outgoing_full_background : @drawable/chat_bubble_outgoing_first_background, default=@drawable/chat_bubble_outgoing_first_background}" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_max="@dimen/chat_bubble_max_width" app:layout_constraintTop_toBottomOf="@id/reply" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 24f092e41..88b018919 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -151,6 +151,7 @@ Conversation was successfully deleted History has been successfully deleted You have left the group + Media volume is low, you may not hear anything! Login Scan QR code