From bac8d8e4e8be2498ccd2c0f6ab3f675316bf5ff2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 9 Jun 2021 14:09:40 +0200 Subject: [PATCH] Added voice recording messages in chat --- .../main/chat/data/ChatMessageContentData.kt | 134 ++++++- .../chat/fragments/DetailChatRoomFragment.kt | 58 ++- .../viewmodels/ChatMessageSendingViewModel.kt | 266 +++++++++++++- .../main/chat/viewmodels/ChatRoomViewModel.kt | 7 + .../viewmodels/ChatSettingsViewModel.kt | 9 + .../java/org/linphone/core/CorePreferences.kt | 14 + .../main/java/org/linphone/utils/AppUtils.kt | 8 +- .../org/linphone/utils/DataBindingUtils.kt | 21 ++ .../linphone/views/VoiceRecordProgressBar.kt | 333 ++++++++++++++++++ .../audio_recording_default.png | Bin 0 -> 8867 bytes .../audio_recording_reply_preview_default.png | Bin 0 -> 924 bytes .../res/drawable-xhdpi/chat_file_over.png | Bin 670 -> 9032 bytes .../res/drawable-xhdpi/chat_send_over.png | Bin 628 -> 9673 bytes .../record_audio_message_default.png | Bin 0 -> 12846 bytes .../main/res/drawable-xhdpi/record_pause.png | Bin 5179 -> 0 bytes .../drawable-xhdpi/record_pause_default.png | Bin 0 -> 7268 bytes .../main/res/drawable-xhdpi/record_play.png | Bin 6663 -> 0 bytes .../drawable-xhdpi/record_play_default.png | Bin 0 -> 8311 bytes .../drawable-xhdpi/record_stop_default.png | Bin 0 -> 915 bytes .../drawable/chat_bubble_reply_background.xml | 2 +- ..._message_audio_record_preview_progress.xml | 25 ++ .../chat_message_audio_record_progress.xml | 26 ++ ...hat_message_voice_recording_background.xml | 6 + .../res/drawable/record_audio_message.xml | 20 ++ .../main/res/drawable/record_pause_dark.xml | 8 + .../main/res/drawable/record_pause_light.xml | 8 + .../main/res/drawable/record_play_dark.xml | 8 + .../main/res/drawable/record_play_light.xml | 8 + .../main/res/drawable/record_stop_dark.xml | 8 + .../main/res/drawable/record_stop_light.xml | 8 + .../res/drawable/recording_play_pause.xml | 4 +- ...round_recording_button_background_dark.xml | 5 + ...ound_recording_button_background_light.xml | 5 + .../layout/chat_message_attachment_cell.xml | 8 +- .../res/layout/chat_message_content_cell.xml | 25 +- .../main/res/layout/chat_message_reply.xml | 15 +- .../chat_message_reply_content_cell.xml | 18 +- ...hat_message_reply_preview_content_cell.xml | 25 +- ...chat_message_voice_record_content_cell.xml | 62 ++++ .../layout/chat_message_voice_recording.xml | 117 ++++++ .../res/layout/chat_room_detail_fragment.xml | 44 ++- .../res/layout/settings_chat_fragment.xml | 6 + app/src/main/res/values/attrs.xml | 8 + app/src/main/res/values/dimen.xml | 2 + app/src/main/res/values/strings.xml | 9 + app/src/main/res/values/styles.xml | 4 +- build.gradle | 2 +- 47 files changed, 1261 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt create mode 100644 app/src/main/res/drawable-xhdpi/audio_recording_default.png create mode 100644 app/src/main/res/drawable-xhdpi/audio_recording_reply_preview_default.png create mode 100644 app/src/main/res/drawable-xhdpi/record_audio_message_default.png delete mode 100644 app/src/main/res/drawable-xhdpi/record_pause.png create mode 100644 app/src/main/res/drawable-xhdpi/record_pause_default.png delete mode 100644 app/src/main/res/drawable-xhdpi/record_play.png create mode 100644 app/src/main/res/drawable-xhdpi/record_play_default.png create mode 100644 app/src/main/res/drawable-xhdpi/record_stop_default.png create mode 100644 app/src/main/res/drawable/chat_message_audio_record_preview_progress.xml create mode 100644 app/src/main/res/drawable/chat_message_audio_record_progress.xml create mode 100644 app/src/main/res/drawable/chat_message_voice_recording_background.xml create mode 100644 app/src/main/res/drawable/record_audio_message.xml create mode 100644 app/src/main/res/drawable/record_pause_dark.xml create mode 100644 app/src/main/res/drawable/record_pause_light.xml create mode 100644 app/src/main/res/drawable/record_play_dark.xml create mode 100644 app/src/main/res/drawable/record_play_light.xml create mode 100644 app/src/main/res/drawable/record_stop_dark.xml create mode 100644 app/src/main/res/drawable/record_stop_light.xml create mode 100644 app/src/main/res/drawable/round_recording_button_background_dark.xml create mode 100644 app/src/main/res/drawable/round_recording_button_background_light.xml create mode 100644 app/src/main/res/layout/chat_message_voice_record_content_cell.xml create mode 100644 app/src/main/res/layout/chat_message_voice_recording.xml diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt index 333d4cf72..a5447207e 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt @@ -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() val isVideo = MutableLiveData() @@ -49,6 +56,7 @@ class ChatMessageContentData( val videoPreview = MutableLiveData() val isPdf = MutableLiveData() val isGenericFile = MutableLiveData() + val isVoiceRecording = MutableLiveData() val fileName = MutableLiveData() val filePath = MutableLiveData() @@ -60,6 +68,11 @@ class ChatMessageContentData( val downloadProgressString = MutableLiveData() val downloadLabel = MutableLiveData() + val voiceRecordDuration = MutableLiveData() + val formattedDuration = MutableLiveData() + val voiceRecordPlayingPosition = MutableLiveData() + val isVoiceRecordPlaying = MutableLiveData() + 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 { diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt index d4c564946..14d433001 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt @@ -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 adapter.submitList(events) }) @@ -363,16 +371,28 @@ class DetailChatRoomFragment : MasterFragment + 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, 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() + } } } } diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt index 6d26ac077..e50088784 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt @@ -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() + val requestRecordAudioPermissionEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val voiceRecordingProgressBarMax = 10000 + + val isPendingVoiceRecord = MutableLiveData() + + val isVoiceRecording = MutableLiveData() + + val voiceRecordingDuration = MutableLiveData() + + val formattedDuration = MutableLiveData() + + val isPlayingVoiceRecording = MutableLiveData() + + val recorder: Recorder + + val voiceRecordPlayingPosition = MutableLiveData() + + 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 + } } diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt index eb186efa2..5b2a1d5ac 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt @@ -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 "" diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt index 1967c38a1..d599278a0 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt @@ -72,6 +72,14 @@ class ChatSettingsViewModel : GenericSettingsViewModel() { } val autoDownloadMaxSize = MutableLiveData() + val autoDownloadVoiceRecordingsListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.isAutoDownloadVoiceRecordingsEnabled = newValue + autoDownloadVoiceRecordings.value = newValue + } + } + val autoDownloadVoiceRecordings = MutableLiveData() + 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 diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 8fdd03525..49345c7da 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -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 diff --git a/app/src/main/java/org/linphone/utils/AppUtils.kt b/app/src/main/java/org/linphone/utils/AppUtils.kt index 8e58e751b..0875e5139 100644 --- a/app/src/main/java/org/linphone/utils/AppUtils.kt +++ b/app/src/main/java/org/linphone/utils/AppUtils.kt @@ -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) } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index db8b28770..3be9be1be 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -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) +} diff --git a/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt b/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt new file mode 100644 index 000000000..099c4461a --- /dev/null +++ b/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt @@ -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 . + */ +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) + } + } +} diff --git a/app/src/main/res/drawable-xhdpi/audio_recording_default.png b/app/src/main/res/drawable-xhdpi/audio_recording_default.png new file mode 100644 index 0000000000000000000000000000000000000000..6e800a6bc3f8baa854dd1b8d61c29e58012280c8 GIT binary patch literal 8867 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 Ca%<}V literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8Y)RhkE)4%caKYZ?lYt_f1s;*b zKpodXn9)gNb_Gz7y~NYkmHiQyB)h)M=haD83=B-#o-U3d8Ta1K+V33}C~7; zf?``mGb~!Xn;(jqS#m8BV$pS+_|@JeIHca8xx>;zB*okNhLB!h^MWgn7VssqG&Sz! zySjW~@#B|AuYZ@l_x;zr?az15DW3m%PVxKa^Wvv}jyd_XXlIPx^wkQjZl*jMEeZ;g zP1IJ#U-I|VR%{8sre4X&)W=jDa_#Y*?V<_i8Sk%H=es~VGUM{*-E3K67Wp-LjNZ%s z1i!NVbLBwKbpG2m{5FPl+k;xq@*Mf%sI0h5`eOOrDZEVi4ciZVOk08>btmrs{l9uGXQj5yNuB&&cTw;w-aC?)Qye^WNZNTZAvao6_LT_QIp3O58X0KuVn4`8D!0UT>WtYw$ML{OccvHs31=ms&!+ zj{Re*V5u?NR=upZzcR(J({-VZmV|N_Gkc3qy!3v>{ z2eW3LXWBPo*Idm5&sO&G``_5a_4QUmXz=qqqu((yv&2uey)}MN!utR9f%3<@Cp=-} zk722s+PI$a^8ua>JI>e4TWcmYYwz;ijnhs4y#6REp~+-sB4x#P<($VZ#g|=6>^x;B yZByAh{fkY@h8srv4X*CG0E)QN!1xQgo-aRp$1d0MQ@+5I#Ng@b=d#Wzp$P!j^)Vv= diff --git a/app/src/main/res/drawable-xhdpi/chat_send_over.png b/app/src/main/res/drawable-xhdpi/chat_send_over.png index cd1f6926867bb29d113cce074baa3367dd5f018c..cce92a4b91ced6ccdbe877c2dea68eb758cd5e62 100644 GIT binary patch literal 9673 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 delta 612 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�EBh&tZ?uXLZMf-q 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%0Dg^-q z0l}at8;pwjC8UiV(eZDOWeWUg!ORseknCS9IW*sY$oeI>Wz9-CKPLjZ|Bd^L^U~uRNlB0je(+je%nbBr+U>!;;}-j2;P2!l6-E zG8sul2N7j!UE-A1E_a{p1d(1L*- z9MUqID4Z@@7mdQ9bTL>o4vYH7XeYp8gPpjHi9+h=VpcTE%c2h!0}@MG?o$w8MGn@Y zZ@~gc90tpU!3Z)^T}}zM?77n3FvIVQVoPI#77@!G|D)%303qMszHfmb+KLJWTWMQ; z66JdkHYp6Ct^@-8zN;wyB)T5}?(ZKB^_QIX9~KM1>QN|oJsg}u#(?Whq9EZUlr9pk zho!bvQ^%nz&-tOjhLm1>^sfzefFk*RE|5dRe;(u{sxMJ|rF#!5~mx0F%cv>O;KCXUnwoK!H z@$+LX{)-Vn=-)y96~Djf`c2orV&GpH|L(5ebp0y^{+03X?)pEYOZ=Y;4?qW}pm6Xy zW9iiT6}$=wlkIIRS1v`6FBVlX;FB2B)}0OBpE{SneC8UNTu>;&u_IcEycHG`(~wj{ zSd>8^f@ke4&0M%W6W!bWl>z1b(|*OhZcC2lc3W%hBce2{&BWXUn>RepEbz%wz)sYv zc3hnvs-1mm@d@UgoWFHOR^}q?*3)?62Z^YzT-D?Dht8+p6qLJp@#NEv_WI=gXDKo* zEln*i$LC^=CaV|jPWk!G0%}`FyiA~rcC82e?6PL=y9DiCSD9W}O2GIj<#|qXn{XQ38Kb194kEWq#nsb!bzMS5^>MXzUqv_@)lRYOZpnEZl z*>FI#dX53<=RXqazi(IUT+{e4@7^K;(hu1l>i?)puDZm3c3+iZ!Q0d04K-`d_0?k6 zLsG}XCt1o3fqeX2vC|^i;{!KBeHsb3F2PhRR-S*;|962)`yMY`k*wjl{>q?Njj{=bgDE;|Z$DjZjPoH3h#|td z%M&Metrko%RgXf%B_4X{4Ve_G)Jf5m^%SHFIQ&W+ zDfZj|)8;xrL@%J~(eC2s`Km=FIsRQ$HFD^ME4>oWSq_n*_t+&h>j%0!Uf(>~5!P?yxn{NWSA%6 z8+La8mZ?PuQ}i^yb^E85lA+x-3Iki1eN9EHXXeJmMf6K0KK~#x9 j>}}W8OJ@|m#XN{H>r}zWR^5BQe6-nFIan5%`^5bZV8^#h literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/record_play.png b/app/src/main/res/drawable-xhdpi/record_play.png deleted file mode 100644 index f3095fd98814a39cef926ddcd3425a1b13c2e5e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6663 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 C8dyyL diff --git a/app/src/main/res/drawable-xhdpi/record_play_default.png b/app/src/main/res/drawable-xhdpi/record_play_default.png new file mode 100644 index 0000000000000000000000000000000000000000..134279fc09606a867110b535df514e143e0ebf52 GIT binary patch literal 8311 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 NTAMqVm7DmU{2y@~(c}OC literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/record_stop_default.png b/app/src/main/res/drawable-xhdpi/record_stop_default.png new file mode 100644 index 0000000000000000000000000000000000000000..14f700c3dc3acf8ccb4e205555e3100bcf182796 GIT binary patch literal 915 zcmeAS@N?(olHy`uVBq!ia0vp^jUddy1|**^xEa8}z}TAU>>S|f?5t2wl%JNFlghxL zF|l@{t;b;piMIROvet&kyis^GVPV-O1+7ObLN%OP1GKliN+^0MZ<6T|6eGiWy{Xy0 zJCt2KJyte`)$s%Sk+Ww#FS>gD(bL@0@v-l*+`ZrTrtf~wP`~(4$mVRB0MpYR>cUQD z+qGEYFE$IDm2eU={%RTMbFjGP?&0I#cUzqQwC;ED7wyW)jCPF`(_Xf@?s2r)oB49i z_fF+CDw;>0A4z75o$zDg>rJQg=XCS^u1q^}V#)JPW*mQU^L3oSj@y-c*``%XUR*OhY#h;{wVEC zDVlol#Ks1#5RX3jKm2vyuj%;s?eKiaa^Y{%ZTZCx1>zaka)1B6v2C`x!vo13mu~&b zVxI9+W$(Rfi`)+!Fe@m$d+mwsgCcf`s!OWQMN1fT>lZTYnSaszX~V(j1^H*+M!%L> z&hOAxHFXywA75j{#7)<2@7=w9b9;Su)ZgEXa_U8`#_#s!0ON%%$=lrpNCUx7=iT){ zinG8YvKSbJ*Fl)kNn>^eP>{XE)7O>#DH|uJI+yR2U;eU8h$J6IckeCqqU zyUN|xx3>MA_xz_~#ME8u$`2dlOn-j!t@~vY@x@X5H|f}a`~COxd=8cg^9p4)cXY%s zynOZh-%}~Z%vsA`>MALH=bF%|kn0*)6Up9bE!V%4<3n->hXA9J14{>kpaPRi!_eXU zNS}T7SujhdwG5yCPm8{pff4K~tJb}~-qG<|`+~{YqY;-r?PmS-l<{?V=+)nEYwz#< tvF`P`<(FSx+!6NuPkwvi4c+tg48dLI-)`RPz5|Rc22WQ%mvv4FO#t9YYmWc` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/chat_bubble_reply_background.xml b/app/src/main/res/drawable/chat_bubble_reply_background.xml index 5e5b435c7..3725abfdf 100644 --- a/app/src/main/res/drawable/chat_bubble_reply_background.xml +++ b/app/src/main/res/drawable/chat_bubble_reply_background.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/res/drawable/chat_message_audio_record_preview_progress.xml b/app/src/main/res/drawable/chat_message_audio_record_preview_progress.xml new file mode 100644 index 000000000..f6419f6e9 --- /dev/null +++ b/app/src/main/res/drawable/chat_message_audio_record_preview_progress.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_message_audio_record_progress.xml b/app/src/main/res/drawable/chat_message_audio_record_progress.xml new file mode 100644 index 000000000..c004038dc --- /dev/null +++ b/app/src/main/res/drawable/chat_message_audio_record_progress.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_message_voice_recording_background.xml b/app/src/main/res/drawable/chat_message_voice_recording_background.xml new file mode 100644 index 000000000..3725abfdf --- /dev/null +++ b/app/src/main/res/drawable/chat_message_voice_recording_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/record_audio_message.xml b/app/src/main/res/drawable/record_audio_message.xml new file mode 100644 index 000000000..a3b70050b --- /dev/null +++ b/app/src/main/res/drawable/record_audio_message.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/record_pause_dark.xml b/app/src/main/res/drawable/record_pause_dark.xml new file mode 100644 index 000000000..7e7a59878 --- /dev/null +++ b/app/src/main/res/drawable/record_pause_dark.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/record_pause_light.xml b/app/src/main/res/drawable/record_pause_light.xml new file mode 100644 index 000000000..5af5e7552 --- /dev/null +++ b/app/src/main/res/drawable/record_pause_light.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/record_play_dark.xml b/app/src/main/res/drawable/record_play_dark.xml new file mode 100644 index 000000000..33721dc80 --- /dev/null +++ b/app/src/main/res/drawable/record_play_dark.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/record_play_light.xml b/app/src/main/res/drawable/record_play_light.xml new file mode 100644 index 000000000..f17480c37 --- /dev/null +++ b/app/src/main/res/drawable/record_play_light.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/record_stop_dark.xml b/app/src/main/res/drawable/record_stop_dark.xml new file mode 100644 index 000000000..c6d3ef924 --- /dev/null +++ b/app/src/main/res/drawable/record_stop_dark.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/record_stop_light.xml b/app/src/main/res/drawable/record_stop_light.xml new file mode 100644 index 000000000..1d80da938 --- /dev/null +++ b/app/src/main/res/drawable/record_stop_light.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/recording_play_pause.xml b/app/src/main/res/drawable/recording_play_pause.xml index 4de1934d3..cb4e8130a 100644 --- a/app/src/main/res/drawable/recording_play_pause.xml +++ b/app/src/main/res/drawable/recording_play_pause.xml @@ -1,11 +1,11 @@ - - diff --git a/app/src/main/res/drawable/round_recording_button_background_dark.xml b/app/src/main/res/drawable/round_recording_button_background_dark.xml new file mode 100644 index 000000000..70fdb4900 --- /dev/null +++ b/app/src/main/res/drawable/round_recording_button_background_dark.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_recording_button_background_light.xml b/app/src/main/res/drawable/round_recording_button_background_light.xml new file mode 100644 index 000000000..d6194f718 --- /dev/null +++ b/app/src/main/res/drawable/round_recording_button_background_light.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/chat_message_attachment_cell.xml b/app/src/main/res/layout/chat_message_attachment_cell.xml index e5c0d31de..46a1a90db 100644 --- a/app/src/main/res/layout/chat_message_attachment_cell.xml +++ b/app/src/main/res/layout/chat_message_attachment_cell.xml @@ -39,9 +39,11 @@ diff --git a/app/src/main/res/layout/chat_message_content_cell.xml b/app/src/main/res/layout/chat_message_content_cell.xml index ab447e257..d723906ca 100644 --- a/app/src/main/res/layout/chat_message_content_cell.xml +++ b/app/src/main/res/layout/chat_message_content_cell.xml @@ -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" /> + + + android:visibility="@{data.downloadable || data.audio || data.pdf || data.genericFile ? View.VISIBLE : View.GONE}"> @@ -89,7 +96,7 @@ + android:layout_height="wrap_content"> @@ -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 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_message_reply_content_cell.xml b/app/src/main/res/layout/chat_message_reply_content_cell.xml index 9da6290dd..5e4acae7a 100644 --- a/app/src/main/res/layout/chat_message_reply_content_cell.xml +++ b/app/src/main/res/layout/chat_message_reply_content_cell.xml @@ -39,12 +39,24 @@ + + + android:layout_height="wrap_content"> + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_message_voice_recording.xml b/app/src/main/res/layout/chat_message_voice_recording.xml new file mode 100644 index 000000000..31444841d --- /dev/null +++ b/app/src/main/res/layout/chat_message_voice_recording.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_room_detail_fragment.xml b/app/src/main/res/layout/chat_room_detail_fragment.xml index 12db0e4be..f13d6ed1f 100644 --- a/app/src/main/res/layout/chat_room_detail_fragment.xml +++ b/app/src/main/res/layout/chat_room_detail_fragment.xml @@ -24,11 +24,8 @@ name="attachFileClickListener" type="android.view.View.OnClickListener"/> - + name="voiceRecordingTouchListener" + type="android.view.View.OnTouchListener" /> @@ -96,7 +93,7 @@ + + + + @@ -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" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 3f8ea7826..05457d1d4 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -28,4 +28,6 @@ 250dp 50dp 6.7dp + 30dp + 40dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4aa86075..9dc80758b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -218,6 +218,7 @@ Open as text Reply Message + Hold button to record voice message No recordings @@ -443,6 +444,7 @@ Always open files inside this app You\'ll still be able to export them in third-party apps Enable ephemeral messages (beta) + Auto download incoming voice recordings Use WiFi only @@ -643,6 +645,7 @@ Attach a file to the message File attached to the message Send message + Record audio message Show or hide the participant devices Ephemeral duration selected Change ephemeral duration by selected value @@ -715,4 +718,10 @@ Open file in third-party app Cancel message forward Cancel sharing + Cancel reply + Record a voice message + Stop voice recording + Cancel voice recording + Pause voice recording + Play voice recording diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ab7875f81..68a0c9e5c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -114,13 +114,13 @@ diff --git a/build.gradle b/build.gradle index 4f1406228..a49aca70b 100644 --- a/build.gradle +++ b/build.gradle @@ -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"