mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Added orange border to chat bubble to show selected bubble when opening bottom sheet menu + small refactoring
This commit is contained in:
parent
62fa1d532c
commit
c273fc451a
25 changed files with 331 additions and 220 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
151
app/src/main/java/org/linphone/utils/ToastUtils.kt
Normal file
151
app/src/main/java/org/linphone/utils/ToastUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue