Added orange border to chat bubble to show selected bubble when opening bottom sheet menu + small refactoring

This commit is contained in:
Sylvain Berfini 2023-11-23 14:03:54 +01:00
parent 62fa1d532c
commit c273fc451a
25 changed files with 331 additions and 220 deletions

View file

@ -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)

View file

@ -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(

View file

@ -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(

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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

View file

@ -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<FileModel>()
val isSelected = MutableLiveData<Boolean>()
// Below are for conferences info
val meetingFound = MutableLiveData<Boolean>()
@ -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
)

View file

@ -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
)

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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)
}
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@drawable/shape_chat_bubble_incoming_first_with_border" />
<item
android:drawable="@drawable/shape_chat_bubble_incoming_first" />
</selector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@drawable/shape_chat_bubble_incoming_full_with_border" />
<item
android:drawable="@drawable/shape_chat_bubble_incoming_full" />
</selector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@drawable/shape_chat_bubble_outgoing_first_with_border" />
<item
android:drawable="@drawable/shape_chat_bubble_outgoing_first" />
</selector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@drawable/shape_chat_bubble_outgoing_full_with_border" />
<item
android:drawable="@drawable/shape_chat_bubble_outgoing_full" />
</selector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topRightRadius="16dp" android:bottomRightRadius="16dp" android:bottomLeftRadius="16dp" />
<solid android:color="@color/gray_main2_100"/>
<stroke android:color="@color/orange_main_500" android:width="1dp" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="16dp" />
<solid android:color="@color/gray_main2_100"/>
<stroke android:color="@color/orange_main_500" android:width="1dp" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topLeftRadius="16dp" android:topRightRadius="16dp" android:bottomLeftRadius="16dp" />
<solid android:color="@color/orange_main_100"/>
<stroke android:color="@color/orange_main_500" android:width="1dp" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="16dp" />
<solid android:color="@color/orange_main_100"/>
<stroke android:color="@color/orange_main_500" android:width="1dp" />
</shape>

View file

@ -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"

View file

@ -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"

View file

@ -151,6 +151,7 @@
<string name="toast_conversation_deleted">Conversation was successfully deleted</string>
<string name="toast_conversation_history_deleted">History has been successfully deleted</string>
<string name="toast_group_conversation_left">You have left the group</string>
<string name="toast_low_media_volume">Media volume is low, you may not hear anything!</string>
<string name="assistant_account_login">Login</string>
<string name="assistant_scan_qr_code">Scan QR code</string>