Added voice recording messages in chat
|
@ -25,16 +25,21 @@ import android.text.SpannableString
|
|||
import android.text.Spanned
|
||||
import android.text.style.UnderlineSpan
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.ticker
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.Content
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.ImageUtils
|
||||
import java.lang.Exception
|
||||
|
||||
class ChatMessageContentData(
|
||||
private val chatMessage: ChatMessage,
|
||||
|
@ -42,6 +47,8 @@ class ChatMessageContentData(
|
|||
|
||||
) {
|
||||
var listener: OnContentClickedListener? = null
|
||||
|
||||
val isOutgoing = chatMessage.isOutgoing
|
||||
|
||||
val isImage = MutableLiveData<Boolean>()
|
||||
val isVideo = MutableLiveData<Boolean>()
|
||||
|
@ -49,6 +56,7 @@ class ChatMessageContentData(
|
|||
val videoPreview = MutableLiveData<Bitmap>()
|
||||
val isPdf = MutableLiveData<Boolean>()
|
||||
val isGenericFile = MutableLiveData<Boolean>()
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val fileName = MutableLiveData<String>()
|
||||
val filePath = MutableLiveData<String>()
|
||||
|
@ -60,6 +68,11 @@ class ChatMessageContentData(
|
|||
val downloadProgressString = MutableLiveData<String>()
|
||||
val downloadLabel = MutableLiveData<Spannable>()
|
||||
|
||||
val voiceRecordDuration = MutableLiveData<Int>()
|
||||
val formattedDuration = MutableLiveData<String>()
|
||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||
val isVoiceRecordPlaying = MutableLiveData<Boolean>()
|
||||
|
||||
val isAlone: Boolean
|
||||
get() {
|
||||
var count = 0
|
||||
|
@ -74,6 +87,12 @@ class ChatMessageContentData(
|
|||
|
||||
var isFileEncrypted: Boolean = false
|
||||
|
||||
private lateinit var voiceRecordingPlayer: Player
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("[Voice Recording] End of file reached")
|
||||
stopVoiceRecording()
|
||||
}
|
||||
|
||||
private fun getContent(): Content {
|
||||
return chatMessage.contents[contentIndex]
|
||||
}
|
||||
|
@ -111,9 +130,13 @@ class ChatMessageContentData(
|
|||
}
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
isVoiceRecordPlaying.value = false
|
||||
voiceRecordDuration.value = 0
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
|
||||
updateContent()
|
||||
chatMessage.addListener(chatMessageListener)
|
||||
}
|
||||
|
@ -129,6 +152,12 @@ class ChatMessageContentData(
|
|||
}
|
||||
|
||||
chatMessage.removeListener(chatMessageListener)
|
||||
|
||||
if (this::voiceRecordingPlayer.isInitialized) {
|
||||
Log.i("[Voice Recording] Destroying voice record")
|
||||
stopVoiceRecording()
|
||||
voiceRecordingPlayer.removeListener(playerListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun download() {
|
||||
|
@ -174,17 +203,24 @@ class ChatMessageContentData(
|
|||
|
||||
if (path.isNotEmpty()) {
|
||||
Log.i("[Content] Found displayable content: $path")
|
||||
val isVoiceRecord = content.isVoiceRecording
|
||||
filePath.value = path
|
||||
isImage.value = FileUtils.isExtensionImage(path)
|
||||
isVideo.value = FileUtils.isExtensionVideo(path)
|
||||
isAudio.value = FileUtils.isExtensionAudio(path)
|
||||
isAudio.value = FileUtils.isExtensionAudio(path) && !isVoiceRecord
|
||||
isPdf.value = FileUtils.isExtensionPdf(path)
|
||||
isVoiceRecording.value = isVoiceRecord
|
||||
|
||||
if (isVoiceRecord) {
|
||||
val duration = content.fileDuration// duration is in ms
|
||||
voiceRecordDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)
|
||||
Log.i("[Voice Recording] Duration is ${voiceRecordDuration.value} ($duration)")
|
||||
}
|
||||
|
||||
if (isVideo.value == true) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
videoPreview.postValue(ImageUtils.getVideoPreview(path))
|
||||
}
|
||||
videoPreview.postValue(ImageUtils.getVideoPreview(path))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -193,6 +229,7 @@ class ChatMessageContentData(
|
|||
isVideo.value = false
|
||||
isAudio.value = false
|
||||
isPdf.value = false
|
||||
isVoiceRecording.value = false
|
||||
}
|
||||
} else {
|
||||
downloadable.value = true
|
||||
|
@ -200,13 +237,92 @@ class ChatMessageContentData(
|
|||
isVideo.value = FileUtils.isExtensionVideo(fileName.value!!)
|
||||
isAudio.value = FileUtils.isExtensionAudio(fileName.value!!)
|
||||
isPdf.value = FileUtils.isExtensionPdf(fileName.value!!)
|
||||
isVoiceRecording.value = false
|
||||
}
|
||||
|
||||
isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!!
|
||||
isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!!
|
||||
downloadEnabled.value = !chatMessage.isFileTransferInProgress
|
||||
downloadProgressInt.value = 0
|
||||
downloadProgressString.value = "0%"
|
||||
}
|
||||
|
||||
/** Voice recording specifics */
|
||||
|
||||
fun playVoiceRecording() {
|
||||
Log.i("[Voice Recording] Playing voice record")
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("[Voice Recording] Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
voiceRecordingPlayer.start()
|
||||
isVoiceRecordPlaying.value = true
|
||||
tickerFlow().onEach {
|
||||
voiceRecordPlayingPosition.postValue(voiceRecordingPlayer.currentPosition)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun pauseVoiceRecording() {
|
||||
Log.i("[Voice Recording] Pausing voice record")
|
||||
if (!isPlayerClosed()) {
|
||||
voiceRecordingPlayer.pause()
|
||||
}
|
||||
isVoiceRecordPlaying.value = false
|
||||
}
|
||||
|
||||
private fun tickerFlow() = flow {
|
||||
while (isVoiceRecordPlaying.value == true) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initVoiceRecordPlayer() {
|
||||
Log.i("[Voice Recording] Creating player for voice record")
|
||||
// Use speaker sound card to play recordings, otherwise use earpiece
|
||||
// If none are available, default one will be used
|
||||
var speakerCard: String? = null
|
||||
var earpieceCard: String? = null
|
||||
for (device in coreContext.core.audioDevices) {
|
||||
if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
|
||||
if (device.type == AudioDevice.Type.Speaker) {
|
||||
speakerCard = device.id
|
||||
} else if (device.type == AudioDevice.Type.Earpiece) {
|
||||
earpieceCard = device.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordingPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("[Voice Recording] Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordingPlayer.addListener(playerListener)
|
||||
|
||||
val content = getContent()
|
||||
val path = if (content.isFileEncrypted) content.plainFilePath else content.filePath ?: ""
|
||||
voiceRecordingPlayer.open(path.orEmpty())
|
||||
voiceRecordDuration.value = voiceRecordingPlayer.duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds
|
||||
Log.i("[Voice Recording] Duration is ${voiceRecordDuration.value} (${voiceRecordingPlayer.duration})")
|
||||
}
|
||||
|
||||
private fun stopVoiceRecording() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("[Voice Recording] Stopping voice record")
|
||||
pauseVoiceRecording()
|
||||
voiceRecordingPlayer.seek(0)
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
voiceRecordingPlayer.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !this::voiceRecordingPlayer.isInitialized || voiceRecordingPlayer.state == Player.State.Closed
|
||||
}
|
||||
}
|
||||
|
||||
interface OnContentClickedListener {
|
||||
|
|
|
@ -28,6 +28,7 @@ import android.os.Parcelable
|
|||
import android.provider.MediaStore
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.view.menu.MenuBuilder
|
||||
|
@ -201,6 +202,13 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
|||
chatSendingViewModel.onTextToSendChanged(it)
|
||||
})
|
||||
|
||||
chatSendingViewModel.requestRecordAudioPermissionEvent.observe(viewLifecycleOwner, {
|
||||
it.consume {
|
||||
Log.i("[Chat Room] Asking for RECORD_AUDIO permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
|
||||
}
|
||||
})
|
||||
|
||||
listViewModel.events.observe(viewLifecycleOwner, { events ->
|
||||
adapter.submitList(events)
|
||||
})
|
||||
|
@ -363,16 +371,28 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
|||
}
|
||||
}
|
||||
|
||||
binding.setSendMessageClickListener {
|
||||
chatSendingViewModel.sendMessage()
|
||||
binding.message.text?.clear()
|
||||
}
|
||||
|
||||
binding.setStartCallClickListener {
|
||||
val address = viewModel.addressToCall
|
||||
if (address != null) {
|
||||
coreContext.startCall(address)
|
||||
binding.setVoiceRecordingTouchListener { _, event ->
|
||||
if (corePreferences.holdToRecordVoiceMessage) {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
Log.i("[Chat Room] Start recording voice message as long as recording button is held")
|
||||
chatSendingViewModel.startVoiceRecording()
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val voiceRecordingDuration = chatSendingViewModel.voiceRecordingDuration.value ?: 0
|
||||
if (voiceRecordingDuration < 1000) {
|
||||
Log.w("[Chat Room] Voice recording button has been held for less than a second, considering miss click")
|
||||
chatSendingViewModel.cancelVoiceRecording()
|
||||
(requireActivity() as MainActivity).showSnackBar(R.string.chat_message_voice_recording_hold_to_record)
|
||||
} else {
|
||||
Log.i("[Chat Room] Voice recording button has been released, stop recording")
|
||||
chatSendingViewModel.stopVoiceRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
if (textToShare?.isNotEmpty() == true) {
|
||||
|
@ -408,13 +428,21 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
|||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == 0) {
|
||||
var atLeastOneGranted = false
|
||||
for (result in grantResults) {
|
||||
atLeastOneGranted = atLeastOneGranted || result == PackageManager.PERMISSION_GRANTED
|
||||
var atLeastOneGranted = false
|
||||
for (result in grantResults) {
|
||||
atLeastOneGranted = atLeastOneGranted || result == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
when (requestCode) {
|
||||
0 -> {
|
||||
if (atLeastOneGranted) {
|
||||
pickFile()
|
||||
}
|
||||
}
|
||||
if (atLeastOneGranted) {
|
||||
pickFile()
|
||||
2 -> {
|
||||
if (atLeastOneGranted) {
|
||||
chatSendingViewModel.startVoiceRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,14 +23,22 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.activities.main.chat.data.ChatMessageAttachmentData
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomCapabilities
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatMessageSendingViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
@ -58,21 +66,64 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
|||
|
||||
var pendingChatMessageToReplyTo = MutableLiveData<ChatMessageData>()
|
||||
|
||||
val requestRecordAudioPermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val voiceRecordingProgressBarMax = 10000
|
||||
|
||||
val isPendingVoiceRecord = MutableLiveData<Boolean>()
|
||||
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val voiceRecordingDuration = MutableLiveData<Int>()
|
||||
|
||||
val formattedDuration = MutableLiveData<String>()
|
||||
|
||||
val isPlayingVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val recorder: Recorder
|
||||
|
||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||
|
||||
private lateinit var voiceRecordingPlayer: Player
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("[Chat Message Sending] End of file reached")
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
attachments.value = arrayListOf()
|
||||
|
||||
attachFileEnabled.value = true
|
||||
sendMessageEnabled.value = false
|
||||
isReadOnly.value = chatRoom.hasBeenLeft()
|
||||
|
||||
val recorderParams = coreContext.core.createRecorderParams()
|
||||
recorderParams.fileFormat = RecorderFileFormat.Wav
|
||||
recorder = coreContext.core.createRecorder(recorderParams)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
attachments.value.orEmpty().forEach(ChatMessageAttachmentData::destroy)
|
||||
|
||||
if (recorder.state != RecorderState.Closed) {
|
||||
recorder.close()
|
||||
}
|
||||
|
||||
if (this::voiceRecordingPlayer.isInitialized) {
|
||||
stopVoiceRecordPlayer()
|
||||
voiceRecordingPlayer.removeListener(playerListener)
|
||||
}
|
||||
|
||||
scope.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun onTextToSendChanged(value: String) {
|
||||
sendMessageEnabled.value = value.isNotEmpty() || attachments.value?.isNotEmpty() ?: false
|
||||
sendMessageEnabled.value = value.isNotEmpty() || attachments.value?.isNotEmpty() == true || isPendingVoiceRecord.value == true
|
||||
if (value.isNotEmpty()) {
|
||||
if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
|
@ -93,7 +144,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
|||
})
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().isNotEmpty() || list.isNotEmpty()
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().isNotEmpty() || list.isNotEmpty() || isPendingVoiceRecord.value == true
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
|
@ -106,7 +157,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
|||
attachment.destroy()
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().isNotEmpty() || list.isNotEmpty()
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().isNotEmpty() || list.isNotEmpty() || isPendingVoiceRecord.value == true
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = list.isEmpty()
|
||||
}
|
||||
|
@ -118,13 +169,33 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
|||
chatRoom.createReplyMessage(pendingMessageToReplyTo.chatMessage)
|
||||
else
|
||||
chatRoom.createEmptyMessage()
|
||||
val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
|
||||
var voiceRecord = false
|
||||
if (isPendingVoiceRecord.value == true && recorder.file != null) {
|
||||
val content = recorder.createContent()
|
||||
if (content != null) {
|
||||
Log.i("[Chat Message Sending] Voice recording content created, file name is ${content.name} and duration is ${content.fileDuration}")
|
||||
message.addContent(content)
|
||||
voiceRecord = true
|
||||
} else {
|
||||
Log.e("[Chat Message Sending] Voice recording content couldn't be created!")
|
||||
}
|
||||
|
||||
isPendingVoiceRecord.value = false
|
||||
isVoiceRecording.value = false
|
||||
}
|
||||
|
||||
val toSend = textToSend.value
|
||||
if (toSend != null && toSend.isNotEmpty()) {
|
||||
message.addUtf8TextContent(toSend.trim())
|
||||
if (voiceRecord && isBasicChatRoom) {
|
||||
val textMessage: ChatMessage = chatRoom.createMessageFromUtf8(toSend.trim())
|
||||
textMessage.send()
|
||||
} else {
|
||||
message.addUtf8TextContent(toSend.trim())
|
||||
}
|
||||
}
|
||||
|
||||
val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
var fileContent = false
|
||||
for (attachment in attachments.value.orEmpty()) {
|
||||
val content = Factory.instance().createContent()
|
||||
|
@ -140,7 +211,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
|||
|
||||
// Do not send file in the same message as the text in a BasicChatRoom
|
||||
// and don't send multiple files in the same message if setting says so
|
||||
if (isBasicChatRoom or (corePreferences.preventMoreThanOneFilePerMessage and fileContent)) {
|
||||
if (isBasicChatRoom or (corePreferences.preventMoreThanOneFilePerMessage and (fileContent or voiceRecord))) {
|
||||
val fileMessage: ChatMessage = chatRoom.createFileTransferMessage(content)
|
||||
fileMessage.send()
|
||||
} else {
|
||||
|
@ -156,6 +227,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
|||
cancelReply()
|
||||
attachments.value.orEmpty().forEach(ChatMessageAttachmentData::destroy)
|
||||
attachments.value = arrayListOf()
|
||||
textToSend.value = ""
|
||||
}
|
||||
|
||||
fun transferMessage(chatMessage: ChatMessage) {
|
||||
|
@ -167,4 +239,178 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
|||
pendingChatMessageToReplyTo.value?.destroy()
|
||||
isPendingAnswer.value = false
|
||||
}
|
||||
|
||||
private fun tickerFlowRecording() = flow {
|
||||
while (recorder.state == RecorderState.Running) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tickerFlowPlaying() = flow {
|
||||
while (voiceRecordingPlayer.state == Player.State.Playing) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleVoiceRecording() {
|
||||
if (corePreferences.holdToRecordVoiceMessage) {
|
||||
// Disables click listener just in case, touch listener will be used instead
|
||||
return
|
||||
}
|
||||
|
||||
if (isVoiceRecording.value == true) {
|
||||
stopVoiceRecording()
|
||||
} else {
|
||||
startVoiceRecording()
|
||||
}
|
||||
}
|
||||
|
||||
fun startVoiceRecording() {
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
requestRecordAudioPermissionEvent.value = Event(true)
|
||||
return
|
||||
}
|
||||
|
||||
when (recorder.state) {
|
||||
RecorderState.Running -> Log.w("[Chat Message Sending] Recorder is already recording")
|
||||
RecorderState.Paused -> {
|
||||
Log.w("[Chat Message Sending] Recorder isn't closed, resuming recording")
|
||||
recorder.start()
|
||||
}
|
||||
RecorderState.Closed -> {
|
||||
val tempFileName = "voice-recording-${System.currentTimeMillis()}.wav"
|
||||
val file = FileUtils.getFileStoragePath(tempFileName)
|
||||
Log.w("[Chat Message Sending] Recorder is closed, starting recording in ${file.absoluteFile}")
|
||||
recorder.open(file.absolutePath)
|
||||
recorder.start()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val duration = recorder.duration
|
||||
voiceRecordingDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) // duration is in ms
|
||||
|
||||
isPendingVoiceRecord.value = true
|
||||
isVoiceRecording.value = true
|
||||
sendMessageEnabled.value = true
|
||||
|
||||
tickerFlowRecording().onEach {
|
||||
val duration = recorder.duration
|
||||
voiceRecordingDuration.postValue(recorder.duration % voiceRecordingProgressBarMax)
|
||||
formattedDuration.postValue(SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)) // duration is in ms
|
||||
|
||||
if (duration >= corePreferences.voiceRecordingMaxDuration) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Log.w("[Chat Message Sending] Max duration for voice recording exceeded (${corePreferences.voiceRecordingMaxDuration}ms), stopping.")
|
||||
stopVoiceRecording()
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun cancelVoiceRecording() {
|
||||
if (recorder.state != RecorderState.Closed) {
|
||||
Log.i("[Chat Message Sending] Closing voice recorder")
|
||||
recorder.close()
|
||||
|
||||
val path = recorder.file
|
||||
if (path != null) {
|
||||
Log.i("[Chat Message Sending] Deleting voice recording file: $path")
|
||||
FileUtils.deleteFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
isPendingVoiceRecord.value = false
|
||||
isVoiceRecording.value = false
|
||||
sendMessageEnabled.value = textToSend.value?.isNotEmpty() == true || attachments.value?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
fun stopVoiceRecording() {
|
||||
if (recorder.state == RecorderState.Running) {
|
||||
Log.i("[Chat Message Sending] Pausing / closing voice recorder")
|
||||
recorder.pause()
|
||||
recorder.close()
|
||||
voiceRecordingDuration.value = recorder.duration
|
||||
}
|
||||
|
||||
isVoiceRecording.value = false
|
||||
if (corePreferences.sendVoiceRecordingRightAway) {
|
||||
Log.i("[Chat Message Sending] Sending voice recording right away")
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
fun playRecordedMessage() {
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("[Chat Message Sending] Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
voiceRecordingPlayer.start()
|
||||
isPlayingVoiceRecording.value = true
|
||||
|
||||
tickerFlowPlaying().onEach {
|
||||
voiceRecordPlayingPosition.postValue(voiceRecordingPlayer.currentPosition)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun pauseRecordedMessage() {
|
||||
Log.i("[Chat Message Sending] Pausing voice record")
|
||||
if (!isPlayerClosed()) {
|
||||
voiceRecordingPlayer.pause()
|
||||
}
|
||||
|
||||
isPlayingVoiceRecording.value = false
|
||||
}
|
||||
|
||||
private fun initVoiceRecordPlayer() {
|
||||
Log.i("[Chat Message Sending] Creating player for voice record")
|
||||
// Use speaker sound card to play recordings, otherwise use earpiece
|
||||
// If none are available, default one will be used
|
||||
var speakerCard: String? = null
|
||||
var earpieceCard: String? = null
|
||||
for (device in coreContext.core.audioDevices) {
|
||||
if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
|
||||
if (device.type == AudioDevice.Type.Speaker) {
|
||||
speakerCard = device.id
|
||||
} else if (device.type == AudioDevice.Type.Earpiece) {
|
||||
earpieceCard = device.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordingPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("[Chat Message Sending] Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordingPlayer.addListener(playerListener)
|
||||
|
||||
val path = recorder.file
|
||||
if (path != null) {
|
||||
voiceRecordingPlayer.open(path)
|
||||
// Update recording duration using player value to ensure proper progress bar animation
|
||||
voiceRecordingDuration.value = voiceRecordingPlayer.duration
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVoiceRecordPlayer() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("[Chat Message Sending] Stopping voice record")
|
||||
voiceRecordingPlayer.pause()
|
||||
voiceRecordingPlayer.seek(0)
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
voiceRecordingPlayer.close()
|
||||
}
|
||||
isPlayingVoiceRecording.value = false
|
||||
}
|
||||
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !this::voiceRecordingPlayer.isInitialized || voiceRecordingPlayer.state == Player.State.Closed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -225,6 +225,13 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
|||
}
|
||||
}
|
||||
|
||||
fun startCall() {
|
||||
val address = addressToCall
|
||||
if (address != null) {
|
||||
coreContext.startCall(address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLastMessage(msg: ChatMessage?): String {
|
||||
if (msg == null) return ""
|
||||
|
||||
|
|
|
@ -72,6 +72,14 @@ class ChatSettingsViewModel : GenericSettingsViewModel() {
|
|||
}
|
||||
val autoDownloadMaxSize = MutableLiveData<Int>()
|
||||
|
||||
val autoDownloadVoiceRecordingsListener = object : SettingListenerStub() {
|
||||
override fun onBoolValueChanged(newValue: Boolean) {
|
||||
core.isAutoDownloadVoiceRecordingsEnabled = newValue
|
||||
autoDownloadVoiceRecordings.value = newValue
|
||||
}
|
||||
}
|
||||
val autoDownloadVoiceRecordings = MutableLiveData<Boolean>()
|
||||
|
||||
val downloadedMediaPublicListener = object : SettingListenerStub() {
|
||||
override fun onBoolValueChanged(newValue: Boolean) {
|
||||
prefs.makePublicMediaFilesDownloaded = newValue
|
||||
|
@ -141,6 +149,7 @@ class ChatSettingsViewModel : GenericSettingsViewModel() {
|
|||
useInAppFileViewer.value = prefs.useInAppFileViewerForNonEncryptedFiles || prefs.vfsEnabled
|
||||
hideNotificationContent.value = prefs.hideChatMessageContentInNotification
|
||||
initAutoDownloadList()
|
||||
autoDownloadVoiceRecordings.value = core.isAutoDownloadVoiceRecordingsEnabled
|
||||
launcherShortcuts.value = prefs.chatRoomShortcuts
|
||||
hideEmptyRooms.value = prefs.hideEmptyRooms
|
||||
hideRoomsRemovedProxies.value = prefs.hideRoomsFromRemovedProxies
|
||||
|
|
|
@ -201,6 +201,20 @@ class CorePreferences constructor(private val context: Context) {
|
|||
config.setBool("app", "chat_room_shortcuts", value)
|
||||
}
|
||||
|
||||
/* Voice Recordings */
|
||||
|
||||
var voiceRecordingMaxDuration: Int
|
||||
get() = config.getInt("app", "voice_recording_max_duration", 600000) // in ms
|
||||
set(value) = config.setInt("app", "voice_recording_max_duration", value)
|
||||
|
||||
var holdToRecordVoiceMessage: Boolean
|
||||
get() = config.getBool("app", "voice_recording_hold_and_release_mode", false)
|
||||
set(value) = config.setBool("app", "voice_recording_hold_and_release_mode", value)
|
||||
|
||||
var sendVoiceRecordingRightAway: Boolean
|
||||
get() = config.getBool("app", "voice_recording_send_right_away", false)
|
||||
set(value) = config.setBool("app", "voice_recording_send_right_away", value)
|
||||
|
||||
/* Contacts */
|
||||
|
||||
var storePresenceInNativeContact: Boolean
|
||||
|
|
|
@ -20,7 +20,9 @@
|
|||
package org.linphone.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter.formatShortFileSize
|
||||
import android.util.TypedValue
|
||||
|
@ -85,6 +87,10 @@ class AppUtils {
|
|||
)
|
||||
}
|
||||
|
||||
fun dpToPixels(context: Context, dp: Float): Float {
|
||||
return dp * context.resources.displayMetrics.density
|
||||
}
|
||||
|
||||
fun bytesToDisplayableSize(bytes: Long): String {
|
||||
return formatShortFileSize(coreContext.context, bytes)
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ import org.linphone.activities.GenericActivity
|
|||
import org.linphone.activities.main.settings.SettingListener
|
||||
import org.linphone.contact.ContactAvatarView
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.views.VoiceRecordProgressBar
|
||||
|
||||
/**
|
||||
* This file contains all the data binding necessary for the app
|
||||
|
@ -532,3 +533,23 @@ fun setEditTextErrorListener(editText: EditText, attrChange: InverseBindingListe
|
|||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
}
|
||||
|
||||
@BindingAdapter("app:max")
|
||||
fun VoiceRecordProgressBar.setProgressMax(max: Int) {
|
||||
setMax(max)
|
||||
}
|
||||
|
||||
@BindingAdapter("android:progress")
|
||||
fun VoiceRecordProgressBar.setPrimaryProgress(progress: Int) {
|
||||
setProgress(progress)
|
||||
}
|
||||
|
||||
@BindingAdapter("android:secondaryProgress")
|
||||
fun VoiceRecordProgressBar.setSecProgress(progress: Int) {
|
||||
setSecondaryProgress(progress)
|
||||
}
|
||||
|
||||
@BindingAdapter("app:secondaryProgressTint")
|
||||
fun VoiceRecordProgressBar.setSecProgressTint(color: Int) {
|
||||
setSecondaryProgressTint(color)
|
||||
}
|
||||
|
|
333
app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt
Normal file
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 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.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.*
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import org.linphone.R
|
||||
|
||||
class VoiceRecordProgressBar : View {
|
||||
companion object {
|
||||
const val MAX_LEVEL = 10000
|
||||
}
|
||||
|
||||
private var minWidth = 0
|
||||
private var maxWidth = 0
|
||||
private var minHeight = 0
|
||||
private var maxHeight = 0
|
||||
|
||||
private var progress = 0
|
||||
private var secondaryProgress = 0
|
||||
private var max = 0
|
||||
private var progressDrawable: Drawable? = null
|
||||
|
||||
private var primaryLeftMargin: Float = 0f
|
||||
private var primaryRightMargin: Float = 0f
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
|
||||
max = 100
|
||||
progress = 0
|
||||
secondaryProgress = 0
|
||||
minWidth = 24
|
||||
maxWidth = 48
|
||||
minHeight = 24
|
||||
maxHeight = 48
|
||||
|
||||
context.theme.obtainStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.VoiceRecordProgressBar,
|
||||
0, 0).apply {
|
||||
|
||||
try {
|
||||
val drawable = getDrawable(R.styleable.VoiceRecordProgressBar_progressDrawable)
|
||||
if (drawable != null) {
|
||||
setProgressDrawable(drawable)
|
||||
}
|
||||
setPrimaryLeftMargin(getDimension(R.styleable.VoiceRecordProgressBar_primaryLeftMargin, 0f))
|
||||
setPrimaryRightMargin(getDimension(R.styleable.VoiceRecordProgressBar_primaryRightMargin, 0f))
|
||||
setMax(getInteger(R.styleable.VoiceRecordProgressBar_max, 100))
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(0)
|
||||
setSecondaryProgress(0)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState: Parcelable? = super.onSaveInstanceState()
|
||||
val savedState = SavedState(superState)
|
||||
savedState.max = max
|
||||
savedState.progress = progress
|
||||
savedState.secondaryProgress = secondaryProgress
|
||||
return savedState
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
val savedState = state as SavedState
|
||||
super.onRestoreInstanceState(savedState.superState)
|
||||
setMax(savedState.max)
|
||||
setProgress(savedState.progress)
|
||||
setSecondaryProgress(savedState.secondaryProgress)
|
||||
}
|
||||
|
||||
override fun drawableStateChanged() {
|
||||
super.drawableStateChanged()
|
||||
updateDrawableState()
|
||||
}
|
||||
|
||||
override fun invalidateDrawable(drawable: Drawable) {
|
||||
if (verifyDrawable(drawable)) {
|
||||
val dirty: Rect = drawable.bounds
|
||||
val scrollX: Int = scrollX + paddingLeft
|
||||
val scrollY: Int = scrollY + paddingTop
|
||||
invalidate(
|
||||
dirty.left + scrollX, dirty.top + scrollY,
|
||||
dirty.right + scrollX, dirty.bottom + scrollY
|
||||
)
|
||||
} else {
|
||||
super.invalidateDrawable(drawable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val drawable: Drawable? = progressDrawable
|
||||
var dw = 0
|
||||
var dh = 0
|
||||
|
||||
if (drawable != null) {
|
||||
dw = minWidth.coerceAtLeast(maxWidth.coerceAtMost(drawable.intrinsicWidth))
|
||||
dh = minHeight.coerceAtLeast(maxHeight.coerceAtMost(drawable.intrinsicHeight))
|
||||
}
|
||||
|
||||
updateDrawableState()
|
||||
dw += paddingRight + paddingLeft
|
||||
dh += paddingBottom + paddingTop
|
||||
|
||||
setMeasuredDimension(
|
||||
resolveSizeAndState(dw, widthMeasureSpec, 0),
|
||||
resolveSizeAndState(dh, heightMeasureSpec, 0)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
updateDrawableBounds(w, h)
|
||||
}
|
||||
|
||||
override fun verifyDrawable(who: Drawable): Boolean {
|
||||
return who === progressDrawable || super.verifyDrawable(who)
|
||||
}
|
||||
|
||||
override fun jumpDrawablesToCurrentState() {
|
||||
super.jumpDrawablesToCurrentState()
|
||||
progressDrawable?.jumpToCurrentState()
|
||||
}
|
||||
|
||||
private fun setPrimaryLeftMargin(margin: Float) {
|
||||
primaryLeftMargin = margin
|
||||
}
|
||||
|
||||
private fun setPrimaryRightMargin(margin: Float) {
|
||||
primaryRightMargin = margin
|
||||
}
|
||||
|
||||
fun setProgress(p: Int) {
|
||||
var progress = p
|
||||
if (progress < 0) {
|
||||
progress = 0
|
||||
}
|
||||
if (progress > max) {
|
||||
progress = max
|
||||
}
|
||||
|
||||
if (progress != this.progress) {
|
||||
this.progress = progress
|
||||
refreshProgress(android.R.id.progress, this.progress)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSecondaryProgress(sp: Int) {
|
||||
var secondaryProgress = sp
|
||||
if (secondaryProgress < 0) {
|
||||
secondaryProgress = 0
|
||||
}
|
||||
if (secondaryProgress > max) {
|
||||
secondaryProgress = max
|
||||
}
|
||||
|
||||
if (secondaryProgress != this.secondaryProgress) {
|
||||
this.secondaryProgress = secondaryProgress
|
||||
refreshProgress(android.R.id.secondaryProgress, this.secondaryProgress)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMax(m: Int) {
|
||||
var max = m
|
||||
if (max < 0) {
|
||||
max = 0
|
||||
}
|
||||
if (max != this.max) {
|
||||
this.max = max
|
||||
postInvalidate()
|
||||
if (progress > max) {
|
||||
progress = max
|
||||
}
|
||||
refreshProgress(android.R.id.progress, progress)
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgressDrawable(drawable: Drawable) {
|
||||
val needUpdate: Boolean = if (progressDrawable != null && drawable !== progressDrawable) {
|
||||
progressDrawable?.callback = null
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
drawable.callback = this
|
||||
// Make sure the ProgressBar is always tall enough
|
||||
val drawableHeight = drawable.minimumHeight
|
||||
if (maxHeight < drawableHeight) {
|
||||
maxHeight = drawableHeight
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
progressDrawable = drawable
|
||||
postInvalidate()
|
||||
|
||||
if (needUpdate) {
|
||||
updateDrawableBounds(width, height)
|
||||
updateDrawableState()
|
||||
|
||||
refreshProgress(android.R.id.progress, progress)
|
||||
refreshProgress(android.R.id.secondaryProgress, secondaryProgress)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSecondaryProgressTint(color: Int) {
|
||||
val drawable = progressDrawable
|
||||
if (drawable != null) {
|
||||
if (drawable is LayerDrawable) {
|
||||
val secondaryProgressDrawable = drawable.findDrawableByLayerId(android.R.id.secondaryProgress)
|
||||
secondaryProgressDrawable?.setTint(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshProgress(id: Int, progress: Int) {
|
||||
var scale: Float = if (max > 0) (progress.toFloat() / max) else 0f
|
||||
|
||||
if (id == android.R.id.progress && scale > 0) {
|
||||
if (width > 0) {
|
||||
// Wait for secondaryProgress to have reached primaryLeftMargin to start primaryProgress at 0
|
||||
val leftOffset = primaryLeftMargin / width
|
||||
if (scale < leftOffset) return
|
||||
|
||||
// Prevent primaryProgress to go further than (width - rightMargin)
|
||||
val rightOffset = primaryRightMargin / width
|
||||
if (scale > 1 - rightOffset) {
|
||||
scale = 1 - rightOffset
|
||||
}
|
||||
|
||||
// Remove left margin from primary progress
|
||||
scale -= leftOffset
|
||||
|
||||
// Since we use setBounds() to apply margins to the Bitmaps,
|
||||
// the width of the bitmap is reduced so we have to adapt the level
|
||||
val widthScale = width - (primaryLeftMargin + primaryRightMargin)
|
||||
scale = ((scale * width) / widthScale)
|
||||
}
|
||||
}
|
||||
|
||||
val drawable: Drawable? = progressDrawable
|
||||
if (drawable != null) {
|
||||
var progressDrawable: Drawable? = null
|
||||
if (drawable is LayerDrawable) {
|
||||
progressDrawable = drawable.findDrawableByLayerId(id)
|
||||
}
|
||||
(progressDrawable ?: drawable).level = (scale * MAX_LEVEL).toInt()
|
||||
} else {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDrawableState() {
|
||||
val state = drawableState
|
||||
if (progressDrawable != null && progressDrawable?.isStateful == true) {
|
||||
progressDrawable?.state = state
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDrawableBounds(w: Int, h: Int) {
|
||||
val right: Int = w - paddingRight - paddingLeft
|
||||
val bottom: Int = h - paddingBottom - paddingTop
|
||||
progressDrawable?.setBounds(0, 0, right, bottom)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val drawable = progressDrawable as? LayerDrawable
|
||||
|
||||
if (drawable != null) {
|
||||
canvas.save()
|
||||
canvas.translate(paddingLeft.toFloat(), paddingTop.toFloat())
|
||||
|
||||
for (i in 0 until drawable.numberOfLayers) {
|
||||
val drawableLayer = drawable.getDrawable(i)
|
||||
if (i != 1) {
|
||||
canvas.translate(primaryLeftMargin, 0f)
|
||||
drawableLayer.draw(canvas)
|
||||
drawableLayer.setBounds(0, 0, width - primaryRightMargin.toInt() - primaryLeftMargin.toInt(), height)
|
||||
canvas.translate(-primaryLeftMargin, 0f)
|
||||
} else {
|
||||
drawableLayer.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
internal class SavedState(superState: Parcelable?) : BaseSavedState(superState) {
|
||||
var max = 0
|
||||
var progress = 0
|
||||
var secondaryProgress = 0
|
||||
|
||||
override fun writeToParcel(output: Parcel, flags: Int) {
|
||||
super.writeToParcel(output, flags)
|
||||
|
||||
output.writeInt(max)
|
||||
output.writeInt(progress)
|
||||
output.writeInt(secondaryProgress)
|
||||
}
|
||||
}
|
||||
}
|
BIN
app/src/main/res/drawable-xhdpi/audio_recording_default.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 924 B |
Before Width: | Height: | Size: 670 B After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 9.4 KiB |
BIN
app/src/main/res/drawable-xhdpi/record_audio_message_default.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 5.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/record_pause_default.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 6.5 KiB |
BIN
app/src/main/res/drawable-xhdpi/record_play_default.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/record_stop_default.png
Normal file
After Width: | Height: | Size: 915 B |
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?attr/backgroundColor"/>
|
||||
<solid android:color="@color/white_color"/>
|
||||
<corners android:radius="@dimen/chat_message_round_corner_radius"/>
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background"
|
||||
android:gravity="fill_horizontal">
|
||||
<shape android:shape="rectangle" android:tint="@color/white_color">
|
||||
<corners android:radius="@dimen/chat_message_round_corner_radius"/>
|
||||
<size android:height="40dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/secondaryProgress"
|
||||
android:gravity="center_vertical|fill_horizontal">
|
||||
<scale android:scaleWidth="100%">
|
||||
<shape android:shape="rectangle" android:tint="@color/green_color">
|
||||
<corners android:radius="@dimen/chat_message_round_corner_radius"/>
|
||||
<size android:height="40dp" />
|
||||
</shape>
|
||||
</scale>
|
||||
</item>
|
||||
<item android:id="@android:id/progress"
|
||||
android:gravity="fill_horizontal">
|
||||
<bitmap android:src="@drawable/audio_recording_default"
|
||||
android:gravity="center_vertical"
|
||||
android:tileModeX="repeat"/>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background"
|
||||
android:gravity="center_vertical|fill_horizontal">
|
||||
<bitmap android:src="@drawable/audio_recording_default"
|
||||
android:tint="@color/dark_grey_color"
|
||||
android:tileModeX="repeat"/>
|
||||
</item>
|
||||
<item android:id="@android:id/secondaryProgress"
|
||||
android:gravity="center_vertical|fill_horizontal">
|
||||
<scale android:scaleWidth="100%">
|
||||
<shape android:shape="rectangle" android:tint="@color/chat_bubble_incoming_color_dark">
|
||||
<corners android:radius="@dimen/chat_message_round_corner_radius"/>
|
||||
<size android:height="40dp"/>
|
||||
</shape>
|
||||
</scale>
|
||||
</item>
|
||||
<item android:id="@android:id/progress"
|
||||
android:gravity="center_vertical|fill_horizontal">
|
||||
<scale android:scaleWidth="100%">
|
||||
<bitmap android:src="@drawable/audio_recording_default"
|
||||
android:tint="@color/white_color"
|
||||
android:tileModeX="repeat"/>
|
||||
</scale>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/white_color"/>
|
||||
<corners android:radius="@dimen/chat_message_round_corner_radius"/>
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
20
app/src/main/res/drawable/record_audio_message.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<bitmap android:src="@drawable/record_audio_message_default"
|
||||
android:tint="?attr/drawableTintOverColor"/>
|
||||
</item>
|
||||
<item android:state_selected="true">
|
||||
<bitmap android:src="@drawable/record_audio_message_default"
|
||||
android:tint="?attr/drawableTintOverColor"/>
|
||||
</item>
|
||||
<item android:state_enabled="false">
|
||||
<bitmap android:src="@drawable/record_audio_message_default"
|
||||
android:tint="?attr/drawableTintDisabledColor"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:src="@drawable/record_audio_message_default"
|
||||
android:tint="?attr/drawableTintColor"/>
|
||||
</item>
|
||||
</selector>
|
||||
|
8
app/src/main/res/drawable/record_pause_dark.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:src="@drawable/record_pause_default"
|
||||
android:tint="@color/dark_grey_color"/>
|
||||
</item>
|
||||
</selector>
|
||||
|
8
app/src/main/res/drawable/record_pause_light.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:src="@drawable/record_pause_default"
|
||||
android:tint="@color/toolbar_color"/>
|
||||
</item>
|
||||
</selector>
|
||||
|
8
app/src/main/res/drawable/record_play_dark.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:src="@drawable/record_play_default"
|
||||
android:tint="@color/dark_grey_color"/>
|
||||
</item>
|
||||
</selector>
|
||||
|
8
app/src/main/res/drawable/record_play_light.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:src="@drawable/record_play_default"
|
||||
android:tint="@color/toolbar_color"/>
|
||||
</item>
|
||||
</selector>
|
||||
|
8
app/src/main/res/drawable/record_stop_dark.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:src="@drawable/record_stop_default"
|
||||
android:tint="@color/dark_grey_color"/>
|
||||
</item>
|
||||
</selector>
|
||||
|
8
app/src/main/res/drawable/record_stop_light.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:src="@drawable/record_stop_default"
|
||||
android:tint="@color/toolbar_color"/>
|
||||
</item>
|
||||
</selector>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true">
|
||||
<bitmap android:src="@drawable/record_pause"
|
||||
<bitmap android:src="@drawable/record_pause_default"
|
||||
android:tint="?attr/accentColor"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:src="@drawable/record_play"
|
||||
<bitmap android:src="@drawable/record_play_default"
|
||||
android:tint="?attr/accentColor"/>
|
||||
</item>
|
||||
</selector>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||
<solid android:color="@color/white_color"/>
|
||||
<size android:width="30dp" android:height="30dp"/>
|
||||
</shape>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||
<solid android:color="@color/dark_grey_color"/>
|
||||
<size android:width="30dp" android:height="30dp"/>
|
||||
</shape>
|
|
@ -39,9 +39,11 @@
|
|||
|
||||
<ImageView
|
||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/recording_play_pause"
|
||||
android:layout_width="@dimen/play_pause_button_size"
|
||||
android:layout_height="@dimen/play_pause_button_size"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/record_play_dark"
|
||||
android:background="@drawable/round_recording_button_background_dark"
|
||||
android:contentDescription="@string/content_description_chat_message_video_attachment"
|
||||
android:layout_centerInParent="true"/>
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
android:maxHeight="@dimen/chat_message_bubble_image_height_big"
|
||||
android:layout_size="@{data.alone ? 0f : @dimen/chat_message_bubble_file_size}"
|
||||
app:glidePath="@{data.filePath}"
|
||||
android:visibility="@{data.image ? View.VISIBLE : View.GONE}"
|
||||
android:visibility="@{!data.downloadable && data.image ? View.VISIBLE : View.GONE}"
|
||||
android:scaleType="@{data.alone ? ScaleType.FIT_CENTER : ScaleType.CENTER_CROP}"
|
||||
android:adjustViewBounds="true" />
|
||||
|
||||
|
@ -40,25 +40,32 @@
|
|||
android:maxHeight="@dimen/chat_message_bubble_image_height_big"
|
||||
android:layout_size="@{data.alone ? 0f : @dimen/chat_message_bubble_file_size}"
|
||||
android:src="@{data.videoPreview}"
|
||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE}"
|
||||
android:visibility="@{!data.downloadable && data.video ? View.VISIBLE : View.GONE}"
|
||||
android:scaleType="@{data.alone ? ScaleType.FIT_CENTER : ScaleType.CENTER_CROP}"
|
||||
android:adjustViewBounds="true" />
|
||||
|
||||
<ImageView
|
||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE}"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/recording_play_pause"
|
||||
android:visibility="@{!data.downloadable && data.video ? View.VISIBLE : View.GONE}"
|
||||
android:layout_width="@dimen/play_pause_button_size"
|
||||
android:layout_height="@dimen/play_pause_button_size"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/record_play_dark"
|
||||
android:background="@drawable/round_recording_button_background_dark"
|
||||
android:contentDescription="@string/content_description_chat_message_video_attachment"
|
||||
android:layout_centerInParent="true"/>
|
||||
|
||||
<include layout="@layout/chat_message_voice_record_content_cell"
|
||||
app:data="@{data}"
|
||||
app:longClickListener="@{longClickListener}"
|
||||
android:visibility="@{!data.downloadable && data.voiceRecording ? View.VISIBLE : View.GONE, default=gone}" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="@dimen/chat_message_bubble_file_size"
|
||||
android:layout_height="@dimen/chat_message_bubble_file_size"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:background="?attr/backgroundColor"
|
||||
android:visibility="@{data.downloadable || data.pdf || data.audio || data.genericFile ? View.VISIBLE : View.GONE}">
|
||||
android:visibility="@{data.downloadable || data.audio || data.pdf || data.genericFile ? View.VISIBLE : View.GONE}">
|
||||
|
||||
<TextView
|
||||
style="@style/chat_file_attachment_font"
|
||||
|
@ -71,7 +78,7 @@
|
|||
android:gravity="center"
|
||||
android:textAlignment="center"
|
||||
android:drawablePadding="5dp"
|
||||
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : @drawable/file))), default=@drawable/file}"
|
||||
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
|
||||
android:text="@{data.fileName, default=`test.pdf`}"
|
||||
android:onClick="@{() -> data.downloadable ? data.download() : data.openFile()}"
|
||||
android:onLongClick="@{longClickListener}"/>
|
||||
|
@ -89,7 +96,7 @@
|
|||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{data.downloadProgressInt > 0 ? View.VISIBLE : View.GONE}"
|
||||
android:visibility="@{data.downloadProgressInt > 0 ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:layout_centerInParent="true">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
|
|
|
@ -15,13 +15,14 @@
|
|||
<RelativeLayout
|
||||
android:background="@{data.chatMessage.isOutgoing ? @color/chat_bubble_outgoing_color : @color/chat_bubble_incoming_color, default=@color/chat_bubble_incoming_color}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/reply_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_toLeftOf="@id/clear_reply">
|
||||
|
@ -42,6 +43,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
app:entries="@{data.contents}"
|
||||
app:layout="@{@layout/chat_message_reply_preview_content_cell}"/>
|
||||
|
||||
|
@ -60,6 +62,7 @@
|
|||
<ImageView
|
||||
android:id="@+id/clear_reply"
|
||||
android:onClick="@{cancelClickListener}"
|
||||
android:contentDescription="@string/content_description_cancel_reply"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_alignParentRight="true"
|
||||
|
@ -69,6 +72,12 @@
|
|||
android:padding="5dp"
|
||||
android:src="@drawable/field_clean" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/dividerColor"
|
||||
android:layout_below="@id/reply_layout"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</layout>
|
|
@ -39,12 +39,24 @@
|
|||
|
||||
<ImageView
|
||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE}"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/recording_play_pause"
|
||||
android:layout_width="@dimen/play_pause_button_size"
|
||||
android:layout_height="@dimen/play_pause_button_size"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/record_play_dark"
|
||||
android:background="@drawable/round_recording_button_background_dark"
|
||||
android:contentDescription="@string/content_description_chat_message_video_attachment"
|
||||
android:layout_centerInParent="true"/>
|
||||
|
||||
<TextView
|
||||
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="2dp"
|
||||
android:gravity="center"
|
||||
android:text="@{data.formattedDuration, default=`00:00`}"
|
||||
android:textColor="@color/light_primary_text_color"
|
||||
android:drawableTop="@drawable/audio_recording_reply_preview_default"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -12,13 +12,13 @@
|
|||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
||||
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
||||
android:layout_height="@dimen/chat_message_small_bubble_file_size"
|
||||
android:layout_margin="5dp"
|
||||
app:glidePath="@{data.filePath}"
|
||||
android:visibility="@{data.image ? View.VISIBLE : View.GONE}"
|
||||
android:scaleType="@{ScaleType.CENTER_CROP}"
|
||||
|
@ -28,6 +28,7 @@
|
|||
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
||||
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
||||
android:layout_height="@dimen/chat_message_small_bubble_file_size"
|
||||
android:layout_margin="5dp"
|
||||
android:src="@{data.videoPreview}"
|
||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE}"
|
||||
android:scaleType="@{ScaleType.CENTER_CROP}"
|
||||
|
@ -35,15 +36,29 @@
|
|||
|
||||
<ImageView
|
||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE}"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/recording_play_pause"
|
||||
android:layout_width="@dimen/play_pause_button_size"
|
||||
android:layout_height="@dimen/play_pause_button_size"
|
||||
android:layout_margin="5dp"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/record_play_dark"
|
||||
android:background="@drawable/round_recording_button_background_dark"
|
||||
android:contentDescription="@string/content_description_chat_message_video_attachment"
|
||||
android:layout_centerInParent="true"/>
|
||||
|
||||
<TextView
|
||||
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/progress_bar_height"
|
||||
android:drawablePadding="5dp"
|
||||
android:gravity="center"
|
||||
android:text="@{data.formattedDuration, default=`00:00`}"
|
||||
android:textColor="@color/light_primary_text_color"
|
||||
android:drawableLeft="@drawable/audio_recording_reply_preview_default"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
||||
android:layout_height="@dimen/chat_message_small_bubble_file_size"
|
||||
android:layout_margin="5dp"
|
||||
android:background="@drawable/chat_bubble_reply_file_background"
|
||||
android:padding="10dp"
|
||||
android:contentDescription="@{data.fileName}"
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<import type="android.view.View"/>
|
||||
<import type="android.widget.ImageView.ScaleType"/>
|
||||
<variable
|
||||
name="data"
|
||||
type="org.linphone.activities.main.chat.data.ChatMessageContentData" />
|
||||
<variable
|
||||
name="longClickListener"
|
||||
type="android.view.View.OnLongClickListener"/>
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onLongClick="@{longClickListener}">
|
||||
|
||||
<org.linphone.views.VoiceRecordProgressBar
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/progress_bar_height"
|
||||
android:layout_centerVertical="true"
|
||||
android:progress="@{data.voiceRecordPlayingPosition}"
|
||||
android:secondaryProgress="@{data.voiceRecordPlayingPosition}"
|
||||
app:max="@{data.voiceRecordDuration}"
|
||||
app:progressDrawable="@drawable/chat_message_audio_record_progress"
|
||||
app:secondaryProgressTint="@{data.outgoing ? @color/chat_bubble_outgoing_color_dark : @color/chat_bubble_incoming_color_dark}"
|
||||
app:primaryLeftMargin="40dp"
|
||||
app:primaryRightMargin="50dp"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/play_voice_record"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:contentDescription="@string/content_description_play_voice_recording"
|
||||
android:background="@drawable/round_recording_button_background_dark"
|
||||
android:onClick="@{() -> data.isVoiceRecordPlaying() ? data.pauseVoiceRecording() : data.playVoiceRecording()}"
|
||||
android:padding="9dp"
|
||||
android:src="@{data.isVoiceRecordPlaying() ? @drawable/record_pause_dark : @drawable/record_play_dark, default=@drawable/record_play_dark}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recording_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:text="@{data.formattedDuration, default=`00:00`}"
|
||||
android:textColor="@color/light_primary_text_color"
|
||||
android:textSize="15sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</layout>
|
117
app/src/main/res/layout/chat_message_voice_recording.xml
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<import type="android.view.View"/>
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.activities.main.chat.viewmodels.ChatMessageSendingViewModel" />
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
android:background="?attr/backgroundColor2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cancel_recording"
|
||||
android:onClick="@{() -> viewModel.cancelVoiceRecording()}"
|
||||
android:contentDescription="@string/content_description_cancel_voice_recording"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:src="@drawable/delete" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_centerVertical="true"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_toRightOf="@id/cancel_recording"
|
||||
android:layout_toLeftOf="@id/play_pause_stop"
|
||||
android:background="@drawable/chat_message_voice_recording_background">
|
||||
|
||||
<org.linphone.views.VoiceRecordProgressBar
|
||||
android:visibility="@{viewModel.isVoiceRecording ? View.GONE : View.VISIBLE}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="@dimen/progress_bar_height"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:progress="@{viewModel.voiceRecordPlayingPosition}"
|
||||
android:secondaryProgress="@{viewModel.voiceRecordPlayingPosition}"
|
||||
app:max="@{viewModel.voiceRecordingDuration}"
|
||||
app:progressDrawable="@drawable/chat_message_audio_record_progress"
|
||||
app:secondaryProgressTint="@{@color/green_color}"/>
|
||||
|
||||
<ProgressBar
|
||||
android:visibility="@{viewModel.isVoiceRecording ? View.VISIBLE : View.GONE}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="@dimen/progress_bar_height"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:progressDrawable="@drawable/chat_message_audio_record_preview_progress"
|
||||
android:max="@{viewModel.voiceRecordingProgressBarMax}"
|
||||
android:progress="@{viewModel.voiceRecordingDuration}"/>
|
||||
|
||||
<TextView
|
||||
android:text="@{viewModel.formattedDuration, default=`00:00`}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:textColor="@color/light_primary_text_color"
|
||||
android:textSize="15sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/play_pause_stop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentRight="true">
|
||||
|
||||
<ImageView
|
||||
android:visibility="@{viewModel.isVoiceRecording ? View.VISIBLE : View.GONE}"
|
||||
android:onClick="@{() -> viewModel.stopVoiceRecording()}"
|
||||
android:contentDescription="@string/content_description_stop_voice_recording"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:padding="9dp"
|
||||
android:background="@drawable/round_recording_button_background_light"
|
||||
android:src="@drawable/record_stop_light" />
|
||||
|
||||
<ImageView
|
||||
android:visibility="@{!viewModel.isVoiceRecording && !viewModel.isPlayingVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:onClick="@{() -> viewModel.playRecordedMessage()}"
|
||||
android:contentDescription="@string/content_description_play_voice_recording"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:padding="9dp"
|
||||
android:background="@drawable/round_recording_button_background_light"
|
||||
android:src="@drawable/record_play_light" />
|
||||
|
||||
<ImageView
|
||||
android:visibility="@{!viewModel.isVoiceRecording && viewModel.isPlayingVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:onClick="@{() -> viewModel.pauseRecordedMessage()}"
|
||||
android:contentDescription="@string/content_description_pause_voice_recording_playback"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:padding="9dp"
|
||||
android:background="@drawable/round_recording_button_background_light"
|
||||
android:src="@drawable/record_pause_light" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</layout>
|
|
@ -24,11 +24,8 @@
|
|||
name="attachFileClickListener"
|
||||
type="android.view.View.OnClickListener"/>
|
||||
<variable
|
||||
name="sendMessageClickListener"
|
||||
type="android.view.View.OnClickListener"/>
|
||||
<variable
|
||||
name="startCallClickListener"
|
||||
type="android.view.View.OnClickListener"/>
|
||||
name="voiceRecordingTouchListener"
|
||||
type="android.view.View.OnTouchListener" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.activities.main.chat.viewmodels.ChatRoomViewModel" />
|
||||
|
@ -96,7 +93,7 @@
|
|||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:onClick="@{startCallClickListener}"
|
||||
android:onClick="@{() -> viewModel.startCall()}"
|
||||
android:contentDescription="@string/content_description_start_call"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -153,10 +150,21 @@
|
|||
app:cancelClickListener="@{() -> chatSendingViewModel.cancelReply()}"
|
||||
layout="@layout/chat_message_reply" />
|
||||
|
||||
<include
|
||||
android:visibility="@{chatSendingViewModel.isPendingVoiceRecord ? View.VISIBLE : View.GONE, default=gone}"
|
||||
app:viewModel="@{chatSendingViewModel}"
|
||||
layout="@layout/chat_message_voice_recording" />
|
||||
|
||||
<View
|
||||
android:visibility="@{chatSendingViewModel.isPendingVoiceRecord && chatSendingViewModel.attachments.size() > 0 ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/dividerColor" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/lightToolbarBackgroundColor"
|
||||
android:background="?attr/backgroundColor2"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
|
@ -172,7 +180,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/toolbar_color"
|
||||
android:background="?attr/lightToolbarBackgroundColor"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:orientation="horizontal">
|
||||
|
@ -183,9 +191,23 @@
|
|||
android:contentDescription="@string/content_description_attach_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="5dp"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/chat_file" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/voice_record"
|
||||
android:onTouch="@{voiceRecordingTouchListener}"
|
||||
android:onClick="@{() -> chatSendingViewModel.toggleVoiceRecording()}"
|
||||
android:enabled="@{!chatSendingViewModel.isReadOnly}"
|
||||
android:selected="@{chatSendingViewModel.isVoiceRecording}"
|
||||
android:contentDescription="@string/content_description_voice_recording"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:src="@drawable/record_audio_message" />
|
||||
|
||||
<org.linphone.activities.main.chat.views.RichEditText
|
||||
android:id="@+id/message"
|
||||
android:enabled="@{!chatSendingViewModel.isReadOnly}"
|
||||
|
@ -211,12 +233,12 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/send_message"
|
||||
android:onClick="@{sendMessageClickListener}"
|
||||
android:onClick="@{() -> chatSendingViewModel.sendMessage()}"
|
||||
android:enabled="@{chatSendingViewModel.sendMessageEnabled && !chatSendingViewModel.isReadOnly}"
|
||||
android:contentDescription="@string/content_description_send_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="5dp"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/chat_send_message" />
|
||||
|
||||
<ImageView
|
||||
|
|
|
@ -89,6 +89,12 @@
|
|||
linphone:enabled="@{viewModel.autoDownloadIndex == 2}"
|
||||
android:visibility="@{viewModel.autoDownloadIndex == 2 ? View.VISIBLE : View.GONE}"/>
|
||||
|
||||
<include
|
||||
layout="@layout/settings_widget_switch"
|
||||
linphone:title="@{@string/chat_settings_auto_download_voice_recordings}"
|
||||
linphone:listener="@{viewModel.autoDownloadVoiceRecordingsListener}"
|
||||
linphone:checked="@={viewModel.autoDownloadVoiceRecordings}"/>
|
||||
|
||||
<include
|
||||
layout="@layout/settings_widget_switch"
|
||||
linphone:title="@{@string/chat_settings_downloaded_media_public_title}"
|
||||
|
|
|
@ -41,4 +41,12 @@
|
|||
<attr name="value" format="string" />
|
||||
<attr name="image" format="reference" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="VoiceRecordProgressBar">
|
||||
<attr name="max" format="integer" />
|
||||
<attr name="progressDrawable" format="reference" />
|
||||
<attr name="secondaryProgressTint" format="color" />
|
||||
<attr name="primaryLeftMargin" format="dimension" />
|
||||
<attr name="primaryRightMargin" format="dimension" />
|
||||
</declare-styleable>
|
||||
</resources>
|
|
@ -28,4 +28,6 @@
|
|||
<dimen name="chat_message_popup_width">250dp</dimen>
|
||||
<dimen name="chat_message_popup_item_height">50dp</dimen>
|
||||
<dimen name="chat_message_round_corner_radius">6.7dp</dimen>
|
||||
<dimen name="play_pause_button_size">30dp</dimen>
|
||||
<dimen name="progress_bar_height">40dp</dimen>
|
||||
</resources>
|
|
@ -218,6 +218,7 @@
|
|||
<string name="chat_message_cant_open_file_in_app_dialog_open_as_text_button">Open as text</string>
|
||||
<string name="chat_room_sending_reply_hint">Reply</string>
|
||||
<string name="chat_room_sending_message_hint">Message</string>
|
||||
<string name="chat_message_voice_recording_hold_to_record">Hold button to record voice message</string>
|
||||
|
||||
<!-- Recordings -->
|
||||
<string name="recordings_empty_list">No recordings</string>
|
||||
|
@ -443,6 +444,7 @@
|
|||
<string name="chat_settings_use_in_app_file_viewer_title">Always open files inside this app</string>
|
||||
<string name="chat_settings_use_in_app_file_viewer_summary">You\'ll still be able to export them in third-party apps</string>
|
||||
<string name="chat_settins_enable_ephemeral_messages_beta_title">Enable ephemeral messages (beta)</string>
|
||||
<string name="chat_settings_auto_download_voice_recordings">Auto download incoming voice recordings</string>
|
||||
|
||||
<!-- Network settings -->
|
||||
<string name="network_settings_wifi_only_title">Use WiFi only</string>
|
||||
|
@ -643,6 +645,7 @@
|
|||
<string name="content_description_attach_file">Attach a file to the message</string>
|
||||
<string name="content_description_attached_file">File attached to the message</string>
|
||||
<string name="content_description_send_message">Send message</string>
|
||||
<string name="content_description_record_audio_message">Record audio message</string>
|
||||
<string name="content_description_toggle_participant_devices_list">Show or hide the participant devices</string>
|
||||
<string name="content_description_ephemeral_duration_selected">Ephemeral duration selected</string>
|
||||
<string name="content_description_ephemeral_duration_change">Change ephemeral duration by selected value</string>
|
||||
|
@ -715,4 +718,10 @@
|
|||
<string name="content_description_export">Open file in third-party app</string>
|
||||
<string name="content_description_cancel_forward">Cancel message forward</string>
|
||||
<string name="content_description_cancel_sharing">Cancel sharing</string>
|
||||
<string name="content_description_cancel_reply">Cancel reply</string>
|
||||
<string name="content_description_voice_recording">Record a voice message</string>
|
||||
<string name="content_description_stop_voice_recording">Stop voice recording</string>
|
||||
<string name="content_description_cancel_voice_recording">Cancel voice recording</string>
|
||||
<string name="content_description_pause_voice_recording_playback">Pause voice recording</string>
|
||||
<string name="content_description_play_voice_recording">Play voice recording</string>
|
||||
</resources>
|
||||
|
|
|
@ -114,13 +114,13 @@
|
|||
<!-- Chat related -->
|
||||
|
||||
<style name="chat_message_reply_sender_font">
|
||||
<item name="android:textColor">?attr/primaryTextColor</item>
|
||||
<item name="android:textColor">@color/light_primary_text_color</item>
|
||||
<item name="android:textSize">13sp</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
</style>
|
||||
|
||||
<style name="chat_message_reply_font">
|
||||
<item name="android:textColor">?attr/primaryTextColor</item>
|
||||
<item name="android:textColor">@color/light_primary_text_color</item>
|
||||
<item name="android:textSize">15sp</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
</style>
|
||||
|
|
|
@ -12,7 +12,7 @@ buildscript {
|
|||
maven { url "https://www.jitpack.io" } // for com.github.chrisbanes:PhotoView
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath 'com.google.gms:google-services:4.3.5'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jlleitschuh.gradle:ktlint-gradle:9.1.1"
|
||||
|
|