From 69350ef514400a28deb0bc9ceeaf2899ad312c62 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Jun 2021 16:17:38 +0300 Subject: [PATCH 01/66] Voice message UI initial implementation. --- build.gradle | 3 + vector/build.gradle | 3 + .../home/room/detail/RoomDetailAction.kt | 4 + .../home/room/detail/RoomDetailFragment.kt | 13 +++ .../home/room/detail/RoomDetailViewModel.kt | 23 +++++ .../room/detail/composer/TextComposerView.kt | 39 +++++++- .../composer/VoiceMessageRecorderView.kt | 92 +++++++++++++++++++ .../composer/VoiceMessageRecordingHelper.kt | 87 ++++++++++++++++++ .../res/drawable/bg_voice_message_lock.xml | 14 +++ .../main/res/drawable/ic_voice_lock_arrow.xml | 13 +++ .../src/main/res/drawable/ic_voice_locked.xml | 5 + .../drawable/ic_voice_message_unlocked.xml | 5 + vector/src/main/res/drawable/ic_voice_mic.xml | 12 +++ .../res/drawable/ic_voice_mic_recording.xml | 18 ++++ .../ic_voice_slide_to_cancel_arrow.xml | 8 ++ .../src/main/res/layout/composer_layout.xml | 6 ++ ...composer_layout_constraint_set_compact.xml | 8 ++ .../layout/view_voice_message_recorder.xml | 69 ++++++++++++++ vector/src/main/res/values/colors.xml | 5 + vector/src/main/res/values/strings.xml | 4 + 20 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt create mode 100644 vector/src/main/res/drawable/bg_voice_message_lock.xml create mode 100644 vector/src/main/res/drawable/ic_voice_lock_arrow.xml create mode 100644 vector/src/main/res/drawable/ic_voice_locked.xml create mode 100644 vector/src/main/res/drawable/ic_voice_message_unlocked.xml create mode 100644 vector/src/main/res/drawable/ic_voice_mic.xml create mode 100644 vector/src/main/res/drawable/ic_voice_mic_recording.xml create mode 100644 vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml create mode 100644 vector/src/main/res/layout/view_voice_message_recorder.xml diff --git a/build.gradle b/build.gradle index 881cd340f1..a7acc1c124 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,9 @@ allprojects { // Chat effects includeGroupByRegex 'com\\.github\\.jetradarmobile' includeGroupByRegex 'nl\\.dionsegijn' + + // Voice RecordView + includeGroupByRegex 'com\\.github\\.3llomi' } } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } diff --git a/vector/build.gradle b/vector/build.gradle index a94f796a90..991e483e98 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -144,6 +144,8 @@ android { buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" + buildConfigField "Long", "VOICE_MESSAGE_DURATION_LIMIT_MS", "120000L" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk @@ -391,6 +393,7 @@ dependencies { implementation "androidx.autofill:autofill:$autofill_version" implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' + implementation 'com.github.3llomi:RecordView:3.0.1' // Custom Tab implementation 'androidx.browser:browser:1.3.0' diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 72e614c18c..e63fc9e17b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -109,4 +109,8 @@ sealed class RoomDetailAction : VectorViewModelAction { // Failed messages object RemoveAllFailedMessages : RoomDetailAction() + + // Voice Message + object StartRecordingVoiceMessage : RoomDetailAction() + data class EndRecordingVoiceMessage(val recordTime: Long) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9ed4feebc4..024a37067a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -100,6 +100,7 @@ import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.KeyboardStateUtils +import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.colorizeMatchingText @@ -1192,6 +1193,18 @@ class RoomDetailFragment @Inject constructor( override fun onTextEmptyStateChanged(isEmpty: Boolean) { // No op } + + override fun onVoiceRecordingStarted() { + roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) + } + + override fun onVoiceRecordingEnded(recordTime: Long) { + roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(recordTime)) + } + + override fun checkVoiceRecordingPermission(): Boolean { + return checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, requireActivity(), 0) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index a2041c0a80..55ee63091e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail import android.net.Uri import androidx.annotation.IdRes +import androidx.core.net.toUri import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail @@ -38,6 +39,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.lookup.CallProtocolsChecker @@ -47,6 +49,7 @@ import im.vector.app.features.command.ParsedCommand import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.home.room.detail.composer.VoiceMessageRecordingHelper import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder @@ -56,6 +59,7 @@ import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.multipicker.utils.toMultiPickerAudioType import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers @@ -119,6 +123,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val chatEffectManager: ChatEffectManager, private val directRoomHelper: DirectRoomHelper, private val jitsiService: JitsiService, + private val voiceMessageRecordingHelper: VoiceMessageRecordingHelper, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { @@ -324,6 +329,8 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() RoomDetailAction.ResendAll -> handleResendAll() + RoomDetailAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() + is RoomDetailAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.recordTime) }.exhaustive } @@ -611,6 +618,22 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleStartRecordingVoiceMessage() { + voiceMessageRecordingHelper.startRecording() + } + + private fun handleEndRecordingVoiceMessage(recordTime: Long) { + if (recordTime == 0L) { + voiceMessageRecordingHelper.deleteRecording() + return + } + voiceMessageRecordingHelper.stopRecording(recordTime)?.let { audioType -> + room.sendMedia(audioType.toContentAttachmentData(), false, emptySet()) + room + //voiceMessageRecordingHelper.deleteRecording() + } + } + private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index d5e24dbb6b..6672027133 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -24,6 +24,7 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.transition.ChangeBounds import androidx.transition.Fade @@ -32,6 +33,7 @@ import androidx.transition.TransitionManager import androidx.transition.TransitionSet import im.vector.app.R import im.vector.app.databinding.ComposerLayoutBinding +import org.matrix.android.sdk.api.extensions.orFalse /** * Encapsulate the timeline composer UX. @@ -46,6 +48,9 @@ class TextComposerView @JvmOverloads constructor( fun onCloseRelatedMessage() fun onSendMessage(text: CharSequence) fun onAddAttachment() + fun onVoiceRecordingStarted() + fun onVoiceRecordingEnded(recordTime: Long) + fun checkVoiceRecordingPermission(): Boolean } val views: ComposerLayoutBinding @@ -71,7 +76,9 @@ class TextComposerView @JvmOverloads constructor( } override fun onTextEmptyStateChanged(isEmpty: Boolean) { - views.sendButton.isVisible = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty + val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty + views.sendButton.isInvisible = !shouldShowSendButton + views.voiceMessageRecorderView.isVisible = !shouldShowSendButton } } views.composerRelatedMessageCloseButton.setOnClickListener { @@ -87,6 +94,28 @@ class TextComposerView @JvmOverloads constructor( views.attachmentButton.setOnClickListener { callback?.onAddAttachment() } + + views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback { + override fun onVoiceRecordingStarted() { + views.attachmentButton.isVisible = false + views.composerEditText.isVisible = false + views.composerEmojiButton.isVisible = false + views.composerEditTextOuterBorder.isVisible = false + callback?.onVoiceRecordingStarted() + } + + override fun onVoiceRecordingEnded(recordTime: Long) { + views.attachmentButton.isVisible = true + views.composerEditText.isVisible = true + views.composerEmojiButton.isVisible = true + views.composerEditTextOuterBorder.isVisible = true + callback?.onVoiceRecordingEnded(recordTime) + } + + override fun checkVoiceRecordingPermission(): Boolean { + return callback?.checkVoiceRecordingPermission().orFalse() + } + } } fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -96,7 +125,10 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isVisible = !views.composerEditText.text.isNullOrEmpty() + + val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() + views.sendButton.isInvisible = !shouldShowSendButton + views.voiceMessageRecorderView.isVisible = !shouldShowSendButton } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -106,7 +138,8 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isVisible = true + views.sendButton.isInvisible = false + views.voiceMessageRecorderView.isVisible = false } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt new file mode 100644 index 0000000000..8ae32fcc69 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.composer + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.devlomi.record_view.OnRecordListener +import im.vector.app.BuildConfig +import im.vector.app.R +import im.vector.app.databinding.ViewVoiceMessageRecorderBinding +import org.matrix.android.sdk.api.extensions.orFalse + +/** + * Encapsulates the voice message recording view and animations. + */ +class VoiceMessageRecorderView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + interface Callback { + fun onVoiceRecordingStarted() + fun onVoiceRecordingEnded(recordTime: Long) + fun checkVoiceRecordingPermission(): Boolean + } + + private val views: ViewVoiceMessageRecorderBinding + + var callback: Callback? = null + + init { + inflate(context, R.layout.view_voice_message_recorder, this) + views = ViewVoiceMessageRecorderBinding.bind(this) + + views.voiceMessageButton.setRecordView(views.voiceMessageRecordView) + views.voiceMessageRecordView.timeLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS + + views.voiceMessageRecordView.setRecordPermissionHandler { callback?.checkVoiceRecordingPermission().orFalse() } + + views.voiceMessageRecordView.setOnRecordListener(object : OnRecordListener { + override fun onStart() { + onVoiceRecordingStarted() + } + + override fun onCancel() { + onVoiceRecordingEnded(0) + } + + override fun onFinish(recordTime: Long, limitReached: Boolean) { + onVoiceRecordingEnded(recordTime) + } + + override fun onLessThanSecond() { + onVoiceRecordingEnded(0) + } + }) + } + + private fun onVoiceRecordingStarted() { + views.voiceMessageLockBackground.isVisible = true + views.voiceMessageLockArrow.isVisible = true + views.voiceMessageLockImage.isVisible = true + views.voiceMessageButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_voice_mic_recording)) + callback?.onVoiceRecordingStarted() + } + + private fun onVoiceRecordingEnded(recordTime: Long) { + views.voiceMessageLockBackground.isVisible = false + views.voiceMessageLockArrow.isVisible = false + views.voiceMessageLockImage.isVisible = false + views.voiceMessageButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_voice_mic)) + callback?.onVoiceRecordingEnded(recordTime) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt new file mode 100644 index 0000000000..63cbbe6e79 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.composer + +import android.content.Context +import android.media.MediaRecorder +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import im.vector.app.BuildConfig +import im.vector.lib.multipicker.entity.MultiPickerAudioType +import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.lang.RuntimeException +import java.util.UUID +import javax.inject.Inject + +/** + * Helper class to record audio for voice messages. + */ +class VoiceMessageRecordingHelper @Inject constructor( + private val context: Context +) { + + private lateinit var mediaRecorder: MediaRecorder + private val outputDirectory = File(context.cacheDir, "downloads") + private var outputFile: File? = null + + init { + if (!outputDirectory.exists()) { + outputDirectory.mkdirs() + } + } + + private fun refreshMediaRecorder() { + mediaRecorder = MediaRecorder() + mediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT) + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG) + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) + mediaRecorder.setAudioEncodingBitRate(24000) + mediaRecorder.setAudioSamplingRate(48000) + } + + fun startRecording() { + outputFile = File(outputDirectory, UUID.randomUUID().toString() + ".ogg") + FileOutputStream(outputFile).use { fos -> + refreshMediaRecorder() + mediaRecorder.setOutputFile(fos.fd) + mediaRecorder.prepare() + mediaRecorder.start() + } + } + + fun stopRecording(recordTime: Long): MultiPickerAudioType? { + try { + mediaRecorder.stop() + mediaRecorder.reset() + mediaRecorder.release() + outputFile?.let { + val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it) + return outputFileUri?.toMultiPickerAudioType(context) + } ?: return null + } catch (e: RuntimeException) { // Usually thrown when the record is less than 1 second. + Timber.e(e, "Voice message is not valid. Record time: %s", recordTime) + return null + } + } + + fun deleteRecording() { + outputFile?.delete() + } +} diff --git a/vector/src/main/res/drawable/bg_voice_message_lock.xml b/vector/src/main/res/drawable/bg_voice_message_lock.xml new file mode 100644 index 0000000000..672d7bf80f --- /dev/null +++ b/vector/src/main/res/drawable/bg_voice_message_lock.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_voice_lock_arrow.xml b/vector/src/main/res/drawable/ic_voice_lock_arrow.xml new file mode 100644 index 0000000000..7f9f2403ad --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_lock_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_locked.xml b/vector/src/main/res/drawable/ic_voice_locked.xml new file mode 100644 index 0000000000..2b92d9d5e0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_locked.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_message_unlocked.xml b/vector/src/main/res/drawable/ic_voice_message_unlocked.xml new file mode 100644 index 0000000000..007de349ec --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_message_unlocked.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_mic.xml b/vector/src/main/res/drawable/ic_voice_mic.xml new file mode 100644 index 0000000000..7cb091afaa --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_mic.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_voice_mic_recording.xml b/vector/src/main/res/drawable/ic_voice_mic_recording.xml new file mode 100644 index 0000000000..eb6cea39a5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_mic_recording.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml b/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml new file mode 100644 index 0000000000..1299e82530 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml @@ -0,0 +1,8 @@ + + + diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml index 3816c206b4..7c9c23645d 100644 --- a/vector/src/main/res/layout/composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -131,4 +131,10 @@ android:src="@drawable/ic_send" tools:ignore="MissingConstraints" /> + + diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index 079bc9705a..b51f69302a 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -178,4 +178,12 @@ tools:ignore="MissingPrefix" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml new file mode 100644 index 0000000000..3b124ae7ef --- /dev/null +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/colors.xml b/vector/src/main/res/values/colors.xml index 7158554833..f66476a795 100644 --- a/vector/src/main/res/values/colors.xml +++ b/vector/src/main/res/values/colors.xml @@ -132,4 +132,9 @@ @color/black_alpha @android:color/transparent + + #FFF3F8FD + #22252B + #22252B + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 80120b51bf..b2519f60b2 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3399,4 +3399,8 @@ Some rooms may be hidden because they’re private and you need an invite. Unnamed Room + + Start Voice Message + Slide to cancel + Voice Message Lock From cb96886568e1969fdbe43f069a08ad792951ef32 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Jun 2021 16:18:20 +0300 Subject: [PATCH 02/66] Send voice message. --- .../java/org/matrix/android/sdk/rx/RxRoom.kt | 5 ++++ .../room/model/message/MessageAudioContent.kt | 13 +++++++- .../crypto/model/rest/AudioWaveformInfo.kt | 30 +++++++++++++++++++ .../room/send/LocalEchoEventFactory.kt | 9 +++++- .../multipicker/utils/ContentResolverUtil.kt | 2 +- 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index 21db4e1893..b3495c4493 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -23,6 +23,7 @@ import io.reactivex.Single import kotlinx.coroutines.rx2.rxCompletable import kotlinx.coroutines.rx2.rxSingle import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.Room @@ -146,6 +147,10 @@ class RxRoom(private val room: Room) { fun deleteAvatar(): Completable = rxCompletable { room.deleteAvatar() } + + fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set): Completable = rxCompletable { + room.sendMedia(attachment, compressBeforeSending, roomIds) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt index fcf3d73cbe..3ce4cd6932 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.AudioWaveformInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) @@ -50,7 +51,17 @@ data class MessageAudioContent( /** * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ - @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null, + + /** + * Encapsulates waveform and duration of the audio. + */ + @Json(name = "org.matrix.msc1767.audio") val audioWaveformInfo: AudioWaveformInfo? = null, + + /** + * Indicates that is a voice message. + */ + @Json(name = "org.matrix.msc2516.voice") val voiceMessageIndicator: Any? = null ) : MessageWithAttachmentContent { override val mimeType: String? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt new file mode 100644 index 0000000000..31a356e164 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AudioWaveformInfo( + @Json(name = "duration") + val duration: Long? = null, + + @Json(name = "waveform") + val waveform: List? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index f505b13b33..03afa4beb6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.isReply +import org.matrix.android.sdk.internal.crypto.model.rest.AudioWaveformInfo import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory @@ -289,6 +290,7 @@ internal class LocalEchoEventFactory @Inject constructor( } private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { + val isVoiceMessage = attachment.mimeType == "audio/ogg" val content = MessageAudioContent( msgType = MessageType.MSGTYPE_AUDIO, body = attachment.name ?: "audio", @@ -296,7 +298,12 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, size = attachment.size ), - url = attachment.queryUri.toString() + url = attachment.queryUri.toString(), + audioWaveformInfo = if (!isVoiceMessage) null else AudioWaveformInfo( + duration = attachment.duration, + waveform = null // TODO. + ), + voiceMessageIndicator = if (!isVoiceMessage) null else Any() ) return createMessageEvent(roomId, content) } diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt index a1982b0bbc..6cadfa6ce7 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt @@ -111,7 +111,7 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType? } } -internal fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { +fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { val projection = arrayOf( MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.SIZE From 5676226f4229c42a25367c57d2d60bc6b5544484 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 1 Jul 2021 10:47:41 +0300 Subject: [PATCH 03/66] Voice message recording view implementations. --- build.gradle | 2 +- vector/build.gradle | 4 +- .../room/detail/composer/TextComposerView.kt | 31 +-- .../composer/VoiceMessageRecordingHelper.kt | 87 ------- .../detail/timeline/item/MessageVoiceItem.kt | 134 +++++++++++ .../drawable/bg_voice_play_pause_button.xml | 5 + .../main/res/drawable/bg_voice_playback.xml | 12 + ...locked.xml => ic_voice_message_locked.xml} | 0 .../src/main/res/drawable/ic_voice_pause.xml | 12 + .../src/main/res/drawable/ic_voice_play.xml | 9 + .../src/main/res/layout/composer_layout.xml | 16 +- ...composer_layout_constraint_set_compact.xml | 18 +- .../main/res/layout/fragment_room_detail.xml | 8 + .../res/layout/item_timeline_event_base.xml | 7 + .../item_timeline_event_base_noinfo.xml | 2 +- .../layout/item_timeline_event_voice_stub.xml | 87 +++++++ .../layout/view_voice_message_recorder.xml | 225 +++++++++++++++--- vector/src/main/res/values/colors.xml | 13 +- vector/src/main/res/values/strings.xml | 8 + vector/src/main/res/values/theme_dark.xml | 6 + vector/src/main/res/values/theme_light.xml | 6 + 21 files changed, 523 insertions(+), 169 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt create mode 100644 vector/src/main/res/drawable/bg_voice_play_pause_button.xml create mode 100644 vector/src/main/res/drawable/bg_voice_playback.xml rename vector/src/main/res/drawable/{ic_voice_locked.xml => ic_voice_message_locked.xml} (100%) create mode 100644 vector/src/main/res/drawable/ic_voice_pause.xml create mode 100644 vector/src/main/res/drawable/ic_voice_play.xml create mode 100644 vector/src/main/res/layout/item_timeline_event_voice_stub.xml diff --git a/build.gradle b/build.gradle index a7acc1c124..df412d3fe8 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ allprojects { includeGroupByRegex 'nl\\.dionsegijn' // Voice RecordView - includeGroupByRegex 'com\\.github\\.3llomi' + includeGroupByRegex 'com\\.github\\.Armen101' } } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } diff --git a/vector/build.gradle b/vector/build.gradle index 991e483e98..2d662ea242 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -331,7 +331,7 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.2.1" implementation 'androidx.appcompat:appcompat:1.3.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.0-beta02' implementation "androidx.sharetarget:sharetarget:1.1.0" implementation 'androidx.core:core-ktx:1.5.0' implementation "androidx.media:media:1.3.1" @@ -393,7 +393,7 @@ dependencies { implementation "androidx.autofill:autofill:$autofill_version" implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' - implementation 'com.github.3llomi:RecordView:3.0.1' + implementation 'com.github.Armen101:AudioRecordView:1.0.5' // Custom Tab implementation 'androidx.browser:browser:1.3.0' diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index 6672027133..7833864707 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -20,6 +20,7 @@ import android.content.Context import android.net.Uri import android.text.Editable import android.util.AttributeSet +import android.view.KeyEvent import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet @@ -48,9 +49,7 @@ class TextComposerView @JvmOverloads constructor( fun onCloseRelatedMessage() fun onSendMessage(text: CharSequence) fun onAddAttachment() - fun onVoiceRecordingStarted() - fun onVoiceRecordingEnded(recordTime: Long) - fun checkVoiceRecordingPermission(): Boolean + fun onTouchVoiceRecording() } val views: ComposerLayoutBinding @@ -78,7 +77,7 @@ class TextComposerView @JvmOverloads constructor( override fun onTextEmptyStateChanged(isEmpty: Boolean) { val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty views.sendButton.isInvisible = !shouldShowSendButton - views.voiceMessageRecorderView.isVisible = !shouldShowSendButton + callback?.onTextEmptyStateChanged(isEmpty) } } views.composerRelatedMessageCloseButton.setOnClickListener { @@ -94,28 +93,6 @@ class TextComposerView @JvmOverloads constructor( views.attachmentButton.setOnClickListener { callback?.onAddAttachment() } - - views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback { - override fun onVoiceRecordingStarted() { - views.attachmentButton.isVisible = false - views.composerEditText.isVisible = false - views.composerEmojiButton.isVisible = false - views.composerEditTextOuterBorder.isVisible = false - callback?.onVoiceRecordingStarted() - } - - override fun onVoiceRecordingEnded(recordTime: Long) { - views.attachmentButton.isVisible = true - views.composerEditText.isVisible = true - views.composerEmojiButton.isVisible = true - views.composerEditTextOuterBorder.isVisible = true - callback?.onVoiceRecordingEnded(recordTime) - } - - override fun checkVoiceRecordingPermission(): Boolean { - return callback?.checkVoiceRecordingPermission().orFalse() - } - } } fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -128,7 +105,6 @@ class TextComposerView @JvmOverloads constructor( val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() views.sendButton.isInvisible = !shouldShowSendButton - views.voiceMessageRecorderView.isVisible = !shouldShowSendButton } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -139,7 +115,6 @@ class TextComposerView @JvmOverloads constructor( currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) views.sendButton.isInvisible = false - views.voiceMessageRecorderView.isVisible = false } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt deleted file mode 100644 index 63cbbe6e79..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.detail.composer - -import android.content.Context -import android.media.MediaRecorder -import androidx.core.content.FileProvider -import androidx.core.net.toUri -import im.vector.app.BuildConfig -import im.vector.lib.multipicker.entity.MultiPickerAudioType -import im.vector.lib.multipicker.utils.toMultiPickerAudioType -import timber.log.Timber -import java.io.File -import java.io.FileOutputStream -import java.lang.RuntimeException -import java.util.UUID -import javax.inject.Inject - -/** - * Helper class to record audio for voice messages. - */ -class VoiceMessageRecordingHelper @Inject constructor( - private val context: Context -) { - - private lateinit var mediaRecorder: MediaRecorder - private val outputDirectory = File(context.cacheDir, "downloads") - private var outputFile: File? = null - - init { - if (!outputDirectory.exists()) { - outputDirectory.mkdirs() - } - } - - private fun refreshMediaRecorder() { - mediaRecorder = MediaRecorder() - mediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT) - mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG) - mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) - mediaRecorder.setAudioEncodingBitRate(24000) - mediaRecorder.setAudioSamplingRate(48000) - } - - fun startRecording() { - outputFile = File(outputDirectory, UUID.randomUUID().toString() + ".ogg") - FileOutputStream(outputFile).use { fos -> - refreshMediaRecorder() - mediaRecorder.setOutputFile(fos.fd) - mediaRecorder.prepare() - mediaRecorder.start() - } - } - - fun stopRecording(recordTime: Long): MultiPickerAudioType? { - try { - mediaRecorder.stop() - mediaRecorder.reset() - mediaRecorder.release() - outputFile?.let { - val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it) - return outputFileUri?.toMultiPickerAudioType(context) - } ?: return null - } catch (e: RuntimeException) { // Usually thrown when the record is less than 1 second. - Timber.e(e, "Voice message is not valid. Record time: %s", recordTime) - return null - } - } - - fun deleteRecording() { - outputFile?.delete() - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt new file mode 100644 index 0000000000..cfca70840a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.item + +import android.text.format.DateUtils +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.visualizer.amplitude.AudioRecordView +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessageVoiceItem : AbsMessageItem() { + + @EpoxyAttribute + var mxcUrl: String = "" + + @EpoxyAttribute + var duration: Int = 0 + + @EpoxyAttribute + var waveform: List = emptyList() + + @EpoxyAttribute + var izLocalFile = false + + @EpoxyAttribute + var izDownloaded = false + + @EpoxyAttribute + lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder + + @EpoxyAttribute + lateinit var contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var playbackControlButtonClickListener: ClickListener? = null + + @EpoxyAttribute + lateinit var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker + + + override fun bind(holder: Holder) { + super.bind(holder) + renderSendState(holder.voiceLayout, null) + if (!attributes.informationData.sendState.hasFailed()) { + contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout) + } else { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_cross) + holder.progressLayout.isVisible = false + } + + holder.voicePlaybackTime.text = formatPlaybackTime(duration) + + holder.voicePlaybackWaveform.post { + holder.voicePlaybackWaveform.recreate() + waveform.forEach { amplitude -> + holder.voicePlaybackWaveform.update(amplitude) + } + } + + holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } + + voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener { + override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { + when (state) { + is VoiceMessagePlaybackTracker.Listener.State.Idle -> handleIdleState(holder, state) + is VoiceMessagePlaybackTracker.Listener.State.Playing -> handlePlayingState(holder, state) + } + } + }) + } + + private fun handleIdleState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Idle) { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_voice_play) + if (state.playbackTime > 0) { + holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + } else { + holder.voicePlaybackTime.text = formatPlaybackTime(duration) + } + } + + private fun handlePlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_voice_pause) + if (state.playbackTime > 0) { + holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + } else { + holder.voicePlaybackTime.text = formatPlaybackTime(duration) + } + } + + private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) + + override fun unbind(holder: Holder) { + super.unbind(holder) + contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId) + contentDownloadStateTrackerBinder.unbind(mxcUrl) + } + + override fun getViewType() = STUB_ID + + class Holder : AbsMessageItem.Holder(STUB_ID) { + val voiceLayout by bind(R.id.voiceLayout) + val voicePlaybackControlButton by bind(R.id.voicePlaybackControlButton) + val voicePlaybackTime by bind(R.id.voicePlaybackTime) + val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform) + val progressLayout by bind(R.id.messageFileUploadProgressLayout) + } + + companion object { + private const val STUB_ID = R.id.messageContentVoiceStub + } +} diff --git a/vector/src/main/res/drawable/bg_voice_play_pause_button.xml b/vector/src/main/res/drawable/bg_voice_play_pause_button.xml new file mode 100644 index 0000000000..c0b14c77e9 --- /dev/null +++ b/vector/src/main/res/drawable/bg_voice_play_pause_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_voice_playback.xml b/vector/src/main/res/drawable/bg_voice_playback.xml new file mode 100644 index 0000000000..db31e29bc7 --- /dev/null +++ b/vector/src/main/res/drawable/bg_voice_playback.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_voice_locked.xml b/vector/src/main/res/drawable/ic_voice_message_locked.xml similarity index 100% rename from vector/src/main/res/drawable/ic_voice_locked.xml rename to vector/src/main/res/drawable/ic_voice_message_locked.xml diff --git a/vector/src/main/res/drawable/ic_voice_pause.xml b/vector/src/main/res/drawable/ic_voice_pause.xml new file mode 100644 index 0000000000..af74dec46c --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_pause.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_voice_play.xml b/vector/src/main/res/drawable/ic_voice_play.xml new file mode 100644 index 0000000000..ad90006799 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_play.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml index 7c9c23645d..5e40ab275e 100644 --- a/vector/src/main/res/layout/composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -131,10 +131,16 @@ android:src="@drawable/ic_send" tools:ignore="MissingConstraints" /> - + diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index b51f69302a..e429cf7d16 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -178,12 +178,18 @@ tools:ignore="MissingPrefix" tools:visibility="visible" /> - + app:layout_constraintEnd_toEndOf="parent" /> + --> \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index ae7494894c..6a5cae4452 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -248,4 +248,12 @@ android:background="?vctr_chat_effect_snow_background" android:visibility="invisible" /> + + diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index e4d7aa7d9f..f753bc18a9 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -132,6 +132,13 @@ android:layout_marginEnd="56dp" android:layout="@layout/item_timeline_event_option_buttons_stub" /> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 3b124ae7ef..bf48ee1513 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -2,68 +2,217 @@ + android:layout_height="match_parent"> + app:layout_constraintTop_toBottomOf="@id/voiceMessageMicButton" /> - - - + + + + + + + + + + + tools:ignore="ContentDescription" /> - + tools:visibility="visible" + android:layout_marginBottom="4dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/voiceMessageMicButton" + app:layout_constraintStart_toStartOf="parent" > + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/colors.xml b/vector/src/main/res/values/colors.xml index f66476a795..f756b4f243 100644 --- a/vector/src/main/res/values/colors.xml +++ b/vector/src/main/res/values/colors.xml @@ -135,6 +135,17 @@ #FFF3F8FD #22252B - #22252B + + + #FFE3E8F0 + #FF394049 + + + @android:color/white + #FF8E99A4 + + + #FFE3E8F0 + #FF394049 diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b2519f60b2..0dbb65a138 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2605,6 +2605,7 @@ Video. Image. Audio + Voice File Sticker Poll @@ -3403,4 +3404,11 @@ Start Voice Message Slide to cancel Voice Message Lock + Play Voice Message + Pause Voice Message + Recording voice message + Delete recorded voice message + Release to send + %1$ds left + Tap on the waveform to stop and playback diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 040e73501a..385388d3d4 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -144,6 +144,12 @@ @style/WidgetButtonSocialLogin.Gitlab.Dark @style/ActionModeTheme + + + @color/vctr_voice_message_lock_background_dark + @color/vctr_voice_message_playback_background_dark + @color/vctr_voice_message_play_pause_button_background_dark + @color/vctr_voice_message_recording_playback_background_dark - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_voice_message_lock.xml b/vector/src/main/res/drawable/bg_voice_message_lock.xml index 672d7bf80f..16fcabc75f 100644 --- a/vector/src/main/res/drawable/bg_voice_message_lock.xml +++ b/vector/src/main/res/drawable/bg_voice_message_lock.xml @@ -4,7 +4,7 @@ android:width="56dp" android:height="160dp" /> - + Date: Fri, 9 Jul 2021 18:05:41 +0200 Subject: [PATCH 14/66] Fix issue with play / pause button alignment --- .../room/detail/composer/VoiceMessageRecorderView.kt | 4 ++-- .../room/detail/timeline/item/MessageVoiceItem.kt | 4 ++-- vector/src/main/res/drawable/ic_play_pause_pause.xml | 12 ++++++++++++ vector/src/main/res/drawable/ic_play_pause_play.xml | 9 +++++++++ vector/src/main/res/drawable/ic_voice_pause.xml | 12 ------------ vector/src/main/res/drawable/ic_voice_play.xml | 9 --------- .../res/layout/item_timeline_event_voice_stub.xml | 7 +++---- .../main/res/layout/view_voice_message_recorder.xml | 7 +++---- 8 files changed, 31 insertions(+), 33 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_play_pause_pause.xml create mode 100644 vector/src/main/res/drawable/ic_play_pause_play.xml delete mode 100644 vector/src/main/res/drawable/ic_voice_pause.xml delete mode 100644 vector/src/main/res/drawable/ic_voice_play.xml diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index 13679da52b..25bc41e497 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -359,12 +359,12 @@ class VoiceMessageRecorderView @JvmOverloads constructor( this.amplitudeList = state.amplitudeList } is VoiceMessagePlaybackTracker.Listener.State.Playing -> { - views.voicePlaybackControlButton.setImageResource(R.drawable.ic_voice_pause) + views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong()) views.voicePlaybackTime.setText(formattedTimerText) } is VoiceMessagePlaybackTracker.Listener.State.Idle -> { - views.voicePlaybackControlButton.setImageResource(R.drawable.ic_voice_play) + views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index 9202d7f72a..a369ed1df7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -92,7 +92,7 @@ abstract class MessageVoiceItem : AbsMessageItem() { } private fun handleIdleState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Idle) { - holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_voice_play) + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) if (state.playbackTime > 0) { holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) } else { @@ -101,7 +101,7 @@ abstract class MessageVoiceItem : AbsMessageItem() { } private fun handlePlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) { - holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_voice_pause) + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) if (state.playbackTime > 0) { holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) } else { diff --git a/vector/src/main/res/drawable/ic_play_pause_pause.xml b/vector/src/main/res/drawable/ic_play_pause_pause.xml new file mode 100644 index 0000000000..d8cfafbcf5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_play_pause_pause.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_play_pause_play.xml b/vector/src/main/res/drawable/ic_play_pause_play.xml new file mode 100644 index 0000000000..84056219ec --- /dev/null +++ b/vector/src/main/res/drawable/ic_play_pause_play.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_pause.xml b/vector/src/main/res/drawable/ic_voice_pause.xml deleted file mode 100644 index af74dec46c..0000000000 --- a/vector/src/main/res/drawable/ic_voice_pause.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_voice_play.xml b/vector/src/main/res/drawable/ic_voice_play.xml deleted file mode 100644 index ad90006799..0000000000 --- a/vector/src/main/res/drawable/ic_voice_play.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml index eed7ae4495..2e10ad1550 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml @@ -27,12 +27,11 @@ android:layout_height="32dp" android:background="@drawable/bg_voice_play_pause_button" android:backgroundTint="?vctr_voice_message_play_pause_button_background" - android:paddingStart="3dp" - android:paddingEnd="0dp" - android:src="@drawable/ic_voice_play" + android:src="@drawable/ic_play_pause_play" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + app:tint="?vctr_content_secondary" /> + app:layout_constraintTop_toTopOf="parent" + app:tint="?vctr_content_secondary" /> Date: Fri, 9 Jul 2021 18:22:52 +0200 Subject: [PATCH 15/66] Apply missing tint --- .../main/res/drawable/ic_voice_lock_arrow.xml | 14 +++++++------- .../ic_voice_slide_to_cancel_arrow.xml | 18 ++++++++++++------ .../res/layout/view_voice_message_recorder.xml | 11 +++++++---- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/vector/src/main/res/drawable/ic_voice_lock_arrow.xml b/vector/src/main/res/drawable/ic_voice_lock_arrow.xml index 7f9f2403ad..d23dfaa932 100644 --- a/vector/src/main/res/drawable/ic_voice_lock_arrow.xml +++ b/vector/src/main/res/drawable/ic_voice_lock_arrow.xml @@ -3,11 +3,11 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + diff --git a/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml b/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml index 1299e82530..59beaaf52f 100644 --- a/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml +++ b/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml @@ -1,8 +1,14 @@ - - + + android:strokeWidth="2" + android:strokeColor="#F00" + android:strokeLineCap="round" + android:strokeLineJoin="round" /> diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index d1d812d0a1..7cca6e670a 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -16,7 +16,7 @@ android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/voiceMessageMicButton" - tools:layout_constraintBottom_toBottomOf="parent" + tools:translationY="-148dp" tools:visibility="visible" /> Date: Fri, 9 Jul 2021 18:26:58 +0200 Subject: [PATCH 16/66] Fix touchable area to delete the voice message --- vector/src/main/res/layout/view_voice_message_recorder.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 7cca6e670a..ee8dd677ac 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -121,7 +121,6 @@ android:id="@+id/voiceMessagePlaybackLayout" android:layout_width="0dp" android:layout_height="44dp" - android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_marginBottom="4dp" android:visibility="gone" @@ -133,8 +132,8 @@ Date: Fri, 9 Jul 2021 20:58:56 +0200 Subject: [PATCH 17/66] Theme for Toast --- library/ui-styles/build.gradle | 2 ++ .../main/res/drawable/bg_round_corner_8dp.xml | 8 +++++ .../ui-styles/src/main/res/values/colors.xml | 4 +++ .../main/res/values/styles_voice_message.xml | 11 ++++++ .../src/main/res/values/theme_dark.xml | 1 + .../src/main/res/values/theme_light.xml | 1 + .../home/room/detail/RoomDetailFragment.kt | 8 +++-- .../composer/VoiceMessageRecorderView.kt | 35 ++++++++++++------- .../layout/view_voice_message_recorder.xml | 15 +++++++- 9 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 library/ui-styles/src/main/res/drawable/bg_round_corner_8dp.xml diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index 47c4664636..3a8851784e 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -60,4 +60,6 @@ dependencies { implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // dialpad dimen implementation 'im.dlg:android-dialer:1.2.5' + // AudioRecordView attr + implementation 'com.github.Armen101:AudioRecordView:1.0.5' } \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_round_corner_8dp.xml b/library/ui-styles/src/main/res/drawable/bg_round_corner_8dp.xml new file mode 100644 index 0000000000..f44b146021 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/bg_round_corner_8dp.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index bfe9a9a59c..927977431b 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -144,4 +144,8 @@ #FFE3E8F0 #FF394049 + + @color/palette_black_900 + @color/palette_gray_450 + diff --git a/library/ui-styles/src/main/res/values/styles_voice_message.xml b/library/ui-styles/src/main/res/values/styles_voice_message.xml index 29ba6e31d9..dff1d08d28 100644 --- a/library/ui-styles/src/main/res/values/styles_voice_message.xml +++ b/library/ui-styles/src/main/res/values/styles_voice_message.xml @@ -12,4 +12,15 @@ leftToRight + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index 6dcf02139a..43693031a3 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -140,6 +140,7 @@ @color/vctr_voice_message_playback_background_dark @color/vctr_voice_message_play_pause_button_background_dark @color/vctr_voice_message_recording_playback_background_dark + @color/vctr_voice_message_toast_background_dark diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 2eaba62550..974a9e0b24 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -140,7 +140,6 @@ @color/vctr_voice_message_lock_background_light @color/vctr_voice_message_playback_background_light - @color/vctr_voice_message_play_pause_button_background_light @color/vctr_voice_message_recording_playback_background_light @color/vctr_voice_message_toast_background_light diff --git a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml index 2e10ad1550..26b8742086 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml @@ -26,7 +26,7 @@ android:layout_width="32dp" android:layout_height="32dp" android:background="@drawable/bg_voice_play_pause_button" - android:backgroundTint="?vctr_voice_message_play_pause_button_background" + android:backgroundTint="?android:colorBackground" android:src="@drawable/ic_play_pause_play" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 0c975aa9cb..5b48ca4a43 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -173,7 +173,7 @@ android:layout_height="32dp" android:layout_marginStart="8dp" android:background="@drawable/bg_voice_play_pause_button" - android:backgroundTint="?vctr_voice_message_play_pause_button_background" + android:backgroundTint="?android:colorBackground" android:contentDescription="@string/a11y_play_voice_message" android:src="@drawable/ic_play_pause_play" app:layout_constraintBottom_toBottomOf="parent" From 14dbbee1e3a82ed9d236eb6f98d11c150c4275eb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 16:10:51 +0200 Subject: [PATCH 39/66] Fix background color of voice message item --- library/ui-styles/src/main/res/values/colors.xml | 4 ---- library/ui-styles/src/main/res/values/theme_dark.xml | 1 - library/ui-styles/src/main/res/values/theme_light.xml | 1 - vector/src/main/res/layout/item_timeline_event_voice_stub.xml | 2 +- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index d7097b404d..1a032d94b6 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -132,10 +132,6 @@ #FFF3F8FD #22252B - - #FFE3E8F0 - #FF394049 - #FFE3E8F0 #FF394049 diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index 18ed21ae14..5826f5fcd3 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -137,7 +137,6 @@ @color/vctr_voice_message_lock_background_dark - @color/vctr_voice_message_playback_background_dark @color/vctr_voice_message_recording_playback_background_dark @color/vctr_voice_message_toast_background_dark diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 974a9e0b24..0e0049010a 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -139,7 +139,6 @@ @color/vctr_voice_message_lock_background_light - @color/vctr_voice_message_playback_background_light @color/vctr_voice_message_recording_playback_background_light @color/vctr_voice_message_toast_background_light diff --git a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml index 26b8742086..f9107cc595 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml @@ -11,7 +11,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:background="@drawable/bg_voice_playback" - android:backgroundTint="?vctr_voice_message_playback_background" + android:backgroundTint="?vctr_content_quinary" android:minHeight="48dp" android:paddingStart="8dp" android:paddingTop="6dp" From 6530440069b70e4a551e987d536d09eb49bb7e1d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 16:12:54 +0200 Subject: [PATCH 40/66] Fix an issue in the color --- library/ui-styles/src/main/res/values/palette.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-styles/src/main/res/values/palette.xml b/library/ui-styles/src/main/res/values/palette.xml index 16cee0c620..ed12f10af3 100644 --- a/library/ui-styles/src/main/res/values/palette.xml +++ b/library/ui-styles/src/main/res/values/palette.xml @@ -23,7 +23,7 @@ #F4F6FA - #E6E8F0 + #E3E8F0 #C1C6CD #8D97A5 #737D8C From cf4e603f0937a309872ea614c1c7840f6329e81f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 16:14:02 +0200 Subject: [PATCH 41/66] Fix background color of voice message recorder --- library/ui-styles/src/main/res/values/colors.xml | 4 ---- library/ui-styles/src/main/res/values/theme_dark.xml | 1 - library/ui-styles/src/main/res/values/theme_light.xml | 1 - vector/src/main/res/layout/view_voice_message_recorder.xml | 2 +- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 1a032d94b6..de826806e1 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -132,10 +132,6 @@ #FFF3F8FD #22252B - - #FFE3E8F0 - #FF394049 - @color/palette_black_900 @color/palette_gray_450 diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index 5826f5fcd3..fd37a29832 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -137,7 +137,6 @@ @color/vctr_voice_message_lock_background_dark - @color/vctr_voice_message_recording_playback_background_dark @color/vctr_voice_message_toast_background_dark diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 0e0049010a..7d13c32268 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -139,7 +139,6 @@ @color/vctr_voice_message_lock_background_light - @color/vctr_voice_message_recording_playback_background_light @color/vctr_voice_message_toast_background_light diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 5b48ca4a43..854b731d90 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -147,7 +147,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/bg_voice_playback" - android:backgroundTint="?vctr_voice_message_recording_playback_background" + android:backgroundTint="?vctr_content_quinary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/voiceMessageDeletePlayback" From 9e0f3a151758c8a70d02010970deef5d7346d6bf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 16:17:22 +0200 Subject: [PATCH 42/66] Fix other color issue --- library/ui-styles/src/main/res/values/styles_voice_message.xml | 2 +- vector/src/main/res/layout/item_timeline_event_voice_stub.xml | 2 +- vector/src/main/res/layout/view_voice_message_recorder.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/ui-styles/src/main/res/values/styles_voice_message.xml b/library/ui-styles/src/main/res/values/styles_voice_message.xml index dff1d08d28..5d5fbef007 100644 --- a/library/ui-styles/src/main/res/values/styles_voice_message.xml +++ b/library/ui-styles/src/main/res/values/styles_voice_message.xml @@ -2,7 +2,7 @@ diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 7d13c32268..17e0ff2938 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -138,7 +138,6 @@ @style/Widget.Vector.JumpToUnread.Light - @color/vctr_voice_message_lock_background_light @color/vctr_voice_message_toast_background_light diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index e06de6c8d7..7736d76b0e 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -12,7 +12,7 @@ android:layout_width="52dp" android:layout_height="160dp" android:background="@drawable/bg_voice_message_lock" - android:backgroundTint="?vctr_voice_message_lock_background" + android:backgroundTint="?vctr_system" android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/voiceMessageMicButton" From 6a0ea11e7a16c49e8053e86f128419fef15d512e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 17:58:14 +0200 Subject: [PATCH 46/66] Follow the spec regarding waveform content --- .../room/model/message/AudioWaveformInfo.kt | 7 + .../room/send/LocalEchoEventFactory.kt | 3 +- .../session/room/send/WaveFormSanitizer.kt | 80 ++++++++++++ .../room/send/WaveFormSanitizerTest.kt | 123 ++++++++++++++++++ 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizerTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioWaveformInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioWaveformInfo.kt index 0e2f0f880c..d576f1057a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioWaveformInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioWaveformInfo.kt @@ -19,11 +19,18 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +/** + * See https://github.com/matrix-org/matrix-doc/blob/travis/msc/audio-waveform/proposals/3246-audio-waveform.md + */ @JsonClass(generateAdapter = true) data class AudioWaveformInfo( @Json(name = "duration") val duration: Int? = null, + /** + * The array should have no less than 30 elements and no more than 120. + * List of integers between zero and 1024, inclusive. + */ @Json(name = "waveform") val waveform: List? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index fb56587ac1..c610326a94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -75,6 +75,7 @@ internal class LocalEchoEventFactory @Inject constructor( private val markdownParser: MarkdownParser, private val textPillsUtils: TextPillsUtils, private val thumbnailExtractor: ThumbnailExtractor, + private val waveformSanitizer: WaveFormSanitizer, private val localEchoRepository: LocalEchoRepository, private val permalinkFactory: PermalinkFactory ) { @@ -302,7 +303,7 @@ internal class LocalEchoEventFactory @Inject constructor( url = attachment.queryUri.toString(), audioWaveformInfo = if (!isVoiceMessage) null else AudioWaveformInfo( duration = attachment.duration?.toInt(), - waveform = attachment.waveform + waveform = waveformSanitizer.sanitize(attachment.waveform) ), voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt new file mode 100644 index 0000000000..2bc5cb2475 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.ceil + +internal class WaveFormSanitizer @Inject constructor() { + private companion object { + const val MIN_NUMBER_OF_VALUES = 30 + const val MAX_NUMBER_OF_VALUES = 120 + + const val MAX_VALUE = 1024 + } + + /** + * The array should have no less than 30 elements and no more than 120. + * List of integers between zero and 1024, inclusive. + */ + fun sanitize(waveForm: List?): List? { + if (waveForm.isNullOrEmpty()) { + return null + } + + // Limit the number of items + val result = mutableListOf() + if (waveForm.size < MIN_NUMBER_OF_VALUES) { + // Repeat the same value to have at least 30 items + val repeatTimes = ceil(MIN_NUMBER_OF_VALUES / waveForm.size.toDouble()).toInt() + waveForm.map { value -> + repeat(repeatTimes) { + result.add(value) + } + } + } else if (waveForm.size > MAX_NUMBER_OF_VALUES) { + val keepOneOf = ceil(waveForm.size.toDouble() / MAX_NUMBER_OF_VALUES).toInt() + waveForm.mapIndexed { idx, value -> + if (idx % keepOneOf == 0) { + result.add(value) + } + } + } else { + result.addAll(waveForm) + } + + // OK, ensure all items are positive + val limited = result.map { + abs(it) + } + + // Ensure max is not above MAX_VALUE + val max = limited.maxOrNull() ?: MAX_VALUE + + val final = if (max > MAX_VALUE) { + // Reduce the range + limited.map { + it * MAX_VALUE / max + } + } else { + limited + } + + return final + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizerTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizerTest.kt new file mode 100644 index 0000000000..23c8aeb76b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizerTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeInRange +import org.junit.Test + +class WaveFormSanitizerTest { + + private val waveFormSanitizer = WaveFormSanitizer() + + @Test + fun sanitizeNull() { + waveFormSanitizer.sanitize(null) shouldBe null + } + + @Test + fun sanitizeEmpty() { + waveFormSanitizer.sanitize(emptyList()) shouldBe null + } + + @Test + fun sanitizeSingleton() { + val result = waveFormSanitizer.sanitize(listOf(1))!! + result.size shouldBe 30 + checkResult(result) + } + + @Test + fun sanitize29() { + val list = generateSequence { 1 }.take(29).toList() + val result = waveFormSanitizer.sanitize(list)!! + checkResult(result) + } + + @Test + fun sanitize30() { + val list = generateSequence { 1 }.take(30).toList() + val result = waveFormSanitizer.sanitize(list)!! + result.size shouldBe 30 + checkResult(result) + } + + @Test + fun sanitize31() { + val list = generateSequence { 1 }.take(31).toList() + val result = waveFormSanitizer.sanitize(list)!! + checkResult(result) + } + + @Test + fun sanitize119() { + val list = generateSequence { 1 }.take(119).toList() + val result = waveFormSanitizer.sanitize(list)!! + checkResult(result) + } + + @Test + fun sanitize120() { + val list = generateSequence { 1 }.take(120).toList() + val result = waveFormSanitizer.sanitize(list)!! + result.size shouldBe 120 + checkResult(result) + } + + @Test + fun sanitize121() { + val list = generateSequence { 1 }.take(121).toList() + val result = waveFormSanitizer.sanitize(list)!! + checkResult(result) + } + + @Test + fun sanitize1024() { + val list = generateSequence { 1 }.take(1024).toList() + val result = waveFormSanitizer.sanitize(list)!! + checkResult(result) + } + + @Test + fun sanitizeNegative() { + val list = generateSequence { -1 }.take(30).toList() + val result = waveFormSanitizer.sanitize(list)!! + checkResult(result) + } + + @Test + fun sanitizeMaxValue() { + val list = generateSequence { 1025 }.take(30).toList() + val result = waveFormSanitizer.sanitize(list)!! + checkResult(result) + } + + @Test + fun sanitizeNegativeMaxValue() { + val list = generateSequence { -1025 }.take(30).toList() + val result = waveFormSanitizer.sanitize(list)!! + checkResult(result) + } + + private fun checkResult(result: List) { + result.forEach { + it shouldBeInRange 0..1024 + } + + result.size shouldBeInRange 30..120 + } +} From c938a30dd9c26bf3bc950cb587ceef3e1259b924 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 18:12:55 +0200 Subject: [PATCH 47/66] Change filename. Later we will use the room id to save the draft. --- .../detail/composer/VoiceMessageHelper.kt | 19 +++++++++++-------- .../composer/VoiceMessageRecorderView.kt | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index efc0bdd82c..70f4964242 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -20,6 +20,7 @@ import android.content.Context import android.media.AudioAttributes import android.media.MediaPlayer import android.media.MediaRecorder +import android.os.Build import androidx.core.content.FileProvider import im.vector.app.BuildConfig import im.vector.app.core.utils.CountUpTimer @@ -33,7 +34,6 @@ import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.FileOutputStream -import java.util.UUID import javax.inject.Inject /** @@ -74,16 +74,19 @@ class VoiceMessageHelper @Inject constructor( stopPlayback() playbackTracker.makeAllPlaybacksIdle() - outputFile = File(outputDirectory, UUID.randomUUID().toString() + ".ogg") + outputFile = File(outputDirectory, "Voice message.ogg") lastRecordingFile = outputFile amplitudeList.clear() - FileOutputStream(outputFile).use { fos -> - refreshMediaRecorder() - mediaRecorder.setOutputFile(fos.fd) - mediaRecorder.prepare() - mediaRecorder.start() - startRecordingAmplitudes() + + refreshMediaRecorder() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mediaRecorder.setOutputFile(outputFile) + } else { + mediaRecorder.setOutputFile(FileOutputStream(outputFile).fd) } + mediaRecorder.prepare() + mediaRecorder.start() + startRecordingAmplitudes() } fun stopRecording(): MultiPickerAudioType? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index cb3fc1c319..fa68afa359 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -371,7 +371,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( is VoiceMessagePlaybackTracker.Listener.State.Playing -> { views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong()) - views.voicePlaybackTime.setText(formattedTimerText) + views.voicePlaybackTime.text = formattedTimerText } is VoiceMessagePlaybackTracker.Listener.State.Idle -> { views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) From df795d1881f342be92b7a8576f2c128aa1c81230 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 18:16:30 +0200 Subject: [PATCH 48/66] Cleanup --- .../session/room/send/WaveFormSanitizer.kt | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt index 2bc5cb2475..94490529c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room.send +import timber.log.Timber import javax.inject.Inject import kotlin.math.abs import kotlin.math.ceil @@ -38,43 +39,48 @@ internal class WaveFormSanitizer @Inject constructor() { } // Limit the number of items - val result = mutableListOf() - if (waveForm.size < MIN_NUMBER_OF_VALUES) { - // Repeat the same value to have at least 30 items - val repeatTimes = ceil(MIN_NUMBER_OF_VALUES / waveForm.size.toDouble()).toInt() - waveForm.map { value -> - repeat(repeatTimes) { - result.add(value) + val sizeInRangeList = mutableListOf() + when { + waveForm.size < MIN_NUMBER_OF_VALUES -> { + // Repeat the same value to have at least 30 items + val repeatTimes = ceil(MIN_NUMBER_OF_VALUES / waveForm.size.toDouble()).toInt() + waveForm.map { value -> + repeat(repeatTimes) { + sizeInRangeList.add(value) + } } } - } else if (waveForm.size > MAX_NUMBER_OF_VALUES) { - val keepOneOf = ceil(waveForm.size.toDouble() / MAX_NUMBER_OF_VALUES).toInt() - waveForm.mapIndexed { idx, value -> - if (idx % keepOneOf == 0) { - result.add(value) + waveForm.size > MAX_NUMBER_OF_VALUES -> { + val keepOneOf = ceil(waveForm.size.toDouble() / MAX_NUMBER_OF_VALUES).toInt() + waveForm.mapIndexed { idx, value -> + if (idx % keepOneOf == 0) { + sizeInRangeList.add(value) + } } } - } else { - result.addAll(waveForm) + else -> { + sizeInRangeList.addAll(waveForm) + } } // OK, ensure all items are positive - val limited = result.map { + val positiveList = sizeInRangeList.map { abs(it) } // Ensure max is not above MAX_VALUE - val max = limited.maxOrNull() ?: MAX_VALUE + val max = positiveList.maxOrNull() ?: MAX_VALUE - val final = if (max > MAX_VALUE) { - // Reduce the range - limited.map { + val finalList = if (max > MAX_VALUE) { + // Reduce the values + positiveList.map { it * MAX_VALUE / max } } else { - limited + positiveList } - return final + Timber.d("Sanitize from ${waveForm.size} items to ${finalList.size} items") + return finalList } } From 0cf10b2f84c6c9658401c1e2fedb46c8201ac498 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 18:48:59 +0200 Subject: [PATCH 49/66] Fix issue with waveform rendering --- .../sdk/internal/session/room/send/WaveFormSanitizer.kt | 2 +- .../room/detail/timeline/factory/MessageItemFactory.kt | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt index 94490529c0..78a03f3775 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt @@ -80,7 +80,7 @@ internal class WaveFormSanitizer @Inject constructor() { positiveList } - Timber.d("Sanitize from ${waveForm.size} items to ${finalList.size} items") + Timber.d("Sanitize from ${waveForm.size} items to ${finalList.size} items. Max value was $max") return finalList } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 6ab9f83c32..e67fa7cca0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -257,7 +257,7 @@ class MessageItemFactory @Inject constructor( return MessageVoiceItem_() .attributes(attributes) .duration(messageContent.audioWaveformInfo?.duration ?: 0) - .waveform(messageContent.audioWaveformInfo?.waveform ?: emptyList()) + .waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty()) .playbackControlButtonClickListener(playbackControlButtonClickListener) .voiceMessagePlaybackTracker(voiceMessagePlaybackTracker) .izLocalFile(localFilesHelper.isLocalFile(fileUrl)) @@ -622,6 +622,13 @@ class MessageItemFactory @Inject constructor( .highlighted(highlight) } + private fun List?.toFft(): List? { + return this?.map { + // Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec + it * 22760 / 1024 + } + } + companion object { private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5 } From 276808c8e99583bc97fab085fb84ed3f9e5841b3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jul 2021 20:05:50 +0200 Subject: [PATCH 50/66] Fix issue in RTL --- vector/src/main/res/drawable/ic_play_pause_play.xml | 1 + vector/src/main/res/layout/item_timeline_event_voice_stub.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/drawable/ic_play_pause_play.xml b/vector/src/main/res/drawable/ic_play_pause_play.xml index 84056219ec..f632fad8ed 100644 --- a/vector/src/main/res/drawable/ic_play_pause_play.xml +++ b/vector/src/main/res/drawable/ic_play_pause_play.xml @@ -1,6 +1,7 @@ From 6ab9b462a3c15a9b8ac00f4f6a94e568147b5738 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Jul 2021 13:42:40 +0200 Subject: [PATCH 51/66] Fix mic button color --- vector/src/main/res/drawable/ic_voice_mic.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/drawable/ic_voice_mic.xml b/vector/src/main/res/drawable/ic_voice_mic.xml index 7cb091afaa..fef5fb9dcf 100644 --- a/vector/src/main/res/drawable/ic_voice_mic.xml +++ b/vector/src/main/res/drawable/ic_voice_mic.xml @@ -5,8 +5,8 @@ android:viewportHeight="32"> + android:fillColor="?vctr_content_tertiary"/> + android:fillColor="?vctr_content_tertiary"/> From bb742eb483065ea014724744161dd6e7a49c9a98 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Jul 2021 14:27:08 +0200 Subject: [PATCH 52/66] Handle record/play error --- .../internal/session/DefaultFileService.kt | 19 +++-- .../vector/app/core/error/ErrorFormatter.kt | 9 +++ .../home/room/detail/RoomDetailFragment.kt | 13 ++- .../home/room/detail/RoomDetailViewModel.kt | 16 +++- .../detail/composer/VoiceMessageHelper.kt | 80 +++++++++++-------- .../vector/app/features/voice/VoiceFailure.kt | 22 +++++ vector/src/main/res/values/strings.xml | 2 + 7 files changed, 120 insertions(+), 41 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voice/VoiceFailure.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index a284d976d0..bcc0a74258 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.completeWith import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt @@ -120,13 +121,21 @@ internal class DefaultFileService @Inject constructor( .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) .build() - val response = okHttpClient.newCall(request).execute() - - if (!response.isSuccessful) { - throw IOException() + val response = try { + okHttpClient.newCall(request).execute() + } catch (failure: Throwable) { + throw if (failure is IOException) { + Failure.NetworkConnection(failure) + } else { + failure + } } - val source = response.body?.source() ?: throw IOException() + if (!response.isSuccessful) { + throw Failure.NetworkConnection(IOException()) + } + + val source = response.body?.source() ?: throw Failure.NetworkConnection(IOException()) Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index e7602e5cfe..f0ba79e31c 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -19,6 +19,7 @@ package im.vector.app.core.error import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.call.dialpad.DialPadLookup +import im.vector.app.features.voice.VoiceFailure import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixIdFailure @@ -123,11 +124,19 @@ class DefaultErrorFormatter @Inject constructor( stringProvider.getString(R.string.call_dial_pad_lookup_error) is MatrixIdFailure.InvalidMatrixId -> stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id) + is VoiceFailure -> voiceMessageError(throwable) else -> throwable.localizedMessage } ?: stringProvider.getString(R.string.unknown_error) } + private fun voiceMessageError(throwable: VoiceFailure): String { + return when (throwable) { + is VoiceFailure.UnableToPlay -> stringProvider.getString(R.string.error_voice_message_unable_to_play) + is VoiceFailure.UnableToRecord -> stringProvider.getString(R.string.error_voice_message_unable_to_record) + } + } + private fun limitExceededError(error: MatrixError): String { val delay = error.retryAfterMillis diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index fe8b40ca2e..da27c1adc8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -168,6 +168,7 @@ import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils +import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind @@ -386,7 +387,12 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.observeViewEvents { when (it) { - is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.Failure -> { + if (it.throwable is VoiceFailure.UnableToRecord) { + onCannotRecord() + } + showErrorInSnackbar(it.throwable) + } is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) @@ -428,6 +434,11 @@ class RoomDetailFragment @Inject constructor( } } + private fun onCannotRecord() { + // Update the UI, cancel the animation + views.voiceMessageRecorderView.initVoiceRecordingViews() + } + private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) { val intent = VectorCallActivity.newIntent( context = vectorBaseActivity, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 745a8e3019..4eb5519404 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -621,7 +621,11 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleStartRecordingVoiceMessage() { - voiceMessageHelper.startRecording() + try { + voiceMessageHelper.startRecording() + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.Failure(failure)) + } } private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) { @@ -640,8 +644,14 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handlePlayOrPauseVoicePlayback(action: RoomDetailAction.PlayOrPauseVoicePlayback) { viewModelScope.launch(Dispatchers.IO) { - val audioFile = session.fileService().downloadFile(action.messageAudioContent) - voiceMessageHelper.startOrPausePlayback(action.eventId, audioFile) + try { + // Download can fail + val audioFile = session.fileService().downloadFile(action.messageAudioContent) + // Play can fail + voiceMessageHelper.startOrPausePlayback(action.eventId, audioFile) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.Failure(failure)) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index 70f4964242..f202e0da56 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -25,6 +25,7 @@ import androidx.core.content.FileProvider import im.vector.app.BuildConfig import im.vector.app.core.utils.CountUpTimer import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker +import im.vector.app.features.voice.VoiceFailure import im.vector.lib.multipicker.entity.MultiPickerAudioType import im.vector.lib.multipicker.utils.toMultiPickerAudioType import org.matrix.android.sdk.api.extensions.orFalse @@ -44,7 +45,7 @@ class VoiceMessageHelper @Inject constructor( private val playbackTracker: VoiceMessagePlaybackTracker ) { private var mediaPlayer: MediaPlayer? = null - private lateinit var mediaRecorder: MediaRecorder + private var mediaRecorder: MediaRecorder? = null private val outputDirectory = File(context.cacheDir, "downloads") private var outputFile: File? = null private var lastRecordingFile: File? = null // In case of user pauses recording, plays another one in timeline @@ -60,13 +61,14 @@ class VoiceMessageHelper @Inject constructor( } } - private fun refreshMediaRecorder() { - mediaRecorder = MediaRecorder().apply { - setAudioSource(MediaRecorder.AudioSource.DEFAULT) - setOutputFormat(MediaRecorder.OutputFormat.OGG) - setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) - setAudioEncodingBitRate(24000) - setAudioSamplingRate(48000) + private fun initMediaRecorder() { + MediaRecorder().let { + it.setAudioSource(MediaRecorder.AudioSource.DEFAULT) + it.setOutputFormat(MediaRecorder.OutputFormat.OGG) + it.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) + it.setAudioEncodingBitRate(24000) + it.setAudioSamplingRate(48000) + mediaRecorder = it } } @@ -78,14 +80,19 @@ class VoiceMessageHelper @Inject constructor( lastRecordingFile = outputFile amplitudeList.clear() - refreshMediaRecorder() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mediaRecorder.setOutputFile(outputFile) - } else { - mediaRecorder.setOutputFile(FileOutputStream(outputFile).fd) + try { + initMediaRecorder() + val mr = mediaRecorder!! + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mr.setOutputFile(outputFile) + } else { + mr.setOutputFile(FileOutputStream(outputFile).fd) + } + mr.prepare() + mr.start() + } catch (failure: Throwable) { + throw VoiceFailure.UnableToRecord(failure) } - mediaRecorder.prepare() - mediaRecorder.start() startRecordingAmplitudes() } @@ -117,9 +124,13 @@ class VoiceMessageHelper @Inject constructor( } private fun releaseMediaRecorder() { - mediaRecorder.stop() - mediaRecorder.reset() - mediaRecorder.release() + mediaRecorder?.let { + it.stop() + it.reset() + it.release() + } + + mediaRecorder = null } fun pauseRecording() { @@ -143,27 +154,31 @@ class VoiceMessageHelper @Inject constructor( if (playbackTracker.getPlaybackState(id) is VoiceMessagePlaybackTracker.Listener.State.Playing) { playbackTracker.pausePlayback(id) } else { - playbackTracker.startPlayback(id) startPlayback(id, file) + playbackTracker.startPlayback(id) } } private fun startPlayback(id: String, file: File) { val currentPlaybackTime = playbackTracker.getPlaybackTime(id) - FileInputStream(file).use { fis -> - mediaPlayer = MediaPlayer().apply { - setAudioAttributes( - AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributes.USAGE_MEDIA) - .build() - ) - setDataSource(fis.fd) - prepare() - start() - seekTo(currentPlaybackTime) + try { + FileInputStream(file).use { fis -> + mediaPlayer = MediaPlayer().apply { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + ) + setDataSource(fis.fd) + prepare() + start() + seekTo(currentPlaybackTime) + } } + } catch (failure: Throwable) { + throw VoiceFailure.UnableToPlay(failure) } startPlaybackTicker(id) } @@ -186,8 +201,9 @@ class VoiceMessageHelper @Inject constructor( } private fun onAmplitudeTick() { + val mr = mediaRecorder ?: return try { - val maxAmplitude = mediaRecorder.maxAmplitude + val maxAmplitude = mr.maxAmplitude amplitudeList.add(maxAmplitude) playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList) } catch (e: IllegalStateException) { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceFailure.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceFailure.kt new file mode 100644 index 0000000000..9c4b345dc4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceFailure.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voice + +sealed class VoiceFailure(cause: Throwable? = null) : Throwable(cause = cause) { + data class UnableToPlay(val throwable: Throwable) : VoiceFailure(throwable) + data class UnableToRecord(val throwable: Throwable) : VoiceFailure(throwable) +} diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 1fb7c3cb09..a2c255466c 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3452,4 +3452,6 @@ Tap on the waveform to stop and playback Enable voice message Tap on the wavelength to stop and playback + Cannot play this voice message + Cannot record a voice message From 6f947e979b69cb13ddbd9b0bddb716d4ccefa076 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Jul 2021 15:14:26 +0200 Subject: [PATCH 53/66] Split to sub fun --- .../composer/VoiceMessageRecorderView.kt | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index fa68afa359..2390a86a4d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -116,42 +116,51 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } views.voiceMessageMicButton.setOnTouchListener { _, event -> - return@setOnTouchListener when (event.action) { + when (event.action) { MotionEvent.ACTION_DOWN -> { - val recordingStarted = callback?.onVoiceRecordingStarted().orFalse() - if (recordingStarted) { - startRecordingTicker() - renderToast(context.getString(R.string.voice_message_release_to_send_toast)) - recordingState = RecordingState.STARTED - showRecordingViews() - - firstX = event.rawX - firstY = event.rawY - lastX = firstX - lastY = firstY - } + handleMicActionDown(event) true } MotionEvent.ACTION_UP -> { - if (recordingState != RecordingState.LOCKED) { - stopRecordingTicker() - val isCancelled = recordingState == RecordingState.NONE || recordingState == RecordingState.CANCELLED - callback?.onVoiceRecordingEnded(isCancelled) - recordingState = RecordingState.NONE - hideRecordingViews() - } + handleMicActionUp() true } MotionEvent.ACTION_MOVE -> { - handleMoveAction(event) + handleMicActionMove(event) true } - else -> false + else -> + false } } } - private fun handleMoveAction(event: MotionEvent) { + private fun handleMicActionDown(event: MotionEvent) { + val recordingStarted = callback?.onVoiceRecordingStarted().orFalse() + if (recordingStarted) { + startRecordingTicker() + renderToast(context.getString(R.string.voice_message_release_to_send_toast)) + recordingState = RecordingState.STARTED + showRecordingViews() + + firstX = event.rawX + firstY = event.rawY + lastX = firstX + lastY = firstY + } + } + + private fun handleMicActionUp() { + if (recordingState != RecordingState.LOCKED) { + stopRecordingTicker() + val isCancelled = recordingState == RecordingState.NONE || recordingState == RecordingState.CANCELLED + callback?.onVoiceRecordingEnded(isCancelled) + recordingState = RecordingState.NONE + hideRecordingViews() + } + } + + private fun handleMicActionMove(event: MotionEvent) { val currentX = event.rawX val currentY = event.rawY From bfc70be5bb7fa1841c63f07c9b7ca2dfb3562715 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Jul 2021 17:04:10 +0200 Subject: [PATCH 54/66] Record voice on Android 21 --- vector/build.gradle | 5 +- .../detail/composer/VoiceMessageHelper.kt | 91 +++++------------- .../features/voice/AbstractVoiceRecorder.kt | 95 +++++++++++++++++++ .../app/features/voice/VoiceRecorder.kt | 48 ++++++++++ .../app/features/voice/VoiceRecorderL.kt | 67 +++++++++++++ .../features/voice/VoiceRecorderProvider.kt | 33 +++++++ .../app/features/voice/VoiceRecorderQ.kt | 37 ++++++++ 7 files changed, 310 insertions(+), 66 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt create mode 100644 vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt create mode 100644 vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt create mode 100644 vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt create mode 100644 vector/src/main/java/im/vector/app/features/voice/VoiceRecorderQ.kt diff --git a/vector/build.gradle b/vector/build.gradle index f1b71741aa..9ae5ffd56e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -144,7 +144,7 @@ android { buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" - buildConfigField "Long", "VOICE_MESSAGE_DURATION_LIMIT_MS", "120000L" + buildConfigField "Long", "VOICE_MESSAGE_DURATION_LIMIT_MS", "120_000L" // If set, MSC3086 asserted identity messages sent on VoIP calls will cause the call to appear in the room corresponding to the asserted identity. // This *must* only be set in trusted environments. @@ -411,6 +411,9 @@ dependencies { // Passphrase strength helper implementation 'com.nulab-inc:zxcvbn:1.5.2' + // To convert voice message on old platforms + implementation 'com.arthenica:ffmpeg-kit-audio:4.4.LTS' + //Alerter implementation 'com.tapadoo.android:alerter:7.0.1' diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index f202e0da56..4f07cf98fe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -19,13 +19,13 @@ package im.vector.app.features.home.room.detail.composer import android.content.Context import android.media.AudioAttributes import android.media.MediaPlayer -import android.media.MediaRecorder -import android.os.Build import androidx.core.content.FileProvider import im.vector.app.BuildConfig import im.vector.app.core.utils.CountUpTimer import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure +import im.vector.app.features.voice.VoiceRecorder +import im.vector.app.features.voice.VoiceRecorderProvider import im.vector.lib.multipicker.entity.MultiPickerAudioType import im.vector.lib.multipicker.utils.toMultiPickerAudioType import org.matrix.android.sdk.api.extensions.orFalse @@ -34,7 +34,6 @@ import timber.log.Timber import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException -import java.io.FileOutputStream import javax.inject.Inject /** @@ -42,54 +41,24 @@ import javax.inject.Inject */ class VoiceMessageHelper @Inject constructor( private val context: Context, - private val playbackTracker: VoiceMessagePlaybackTracker + private val playbackTracker: VoiceMessagePlaybackTracker, + voiceRecorderProvider: VoiceRecorderProvider ) { private var mediaPlayer: MediaPlayer? = null - private var mediaRecorder: MediaRecorder? = null - private val outputDirectory = File(context.cacheDir, "downloads") - private var outputFile: File? = null - private var lastRecordingFile: File? = null // In case of user pauses recording, plays another one in timeline + private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder() private val amplitudeList = mutableListOf() private var amplitudeTicker: CountUpTimer? = null private var playbackTicker: CountUpTimer? = null - init { - if (!outputDirectory.exists()) { - outputDirectory.mkdirs() - } - } - - private fun initMediaRecorder() { - MediaRecorder().let { - it.setAudioSource(MediaRecorder.AudioSource.DEFAULT) - it.setOutputFormat(MediaRecorder.OutputFormat.OGG) - it.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) - it.setAudioEncodingBitRate(24000) - it.setAudioSamplingRate(48000) - mediaRecorder = it - } - } - fun startRecording() { stopPlayback() playbackTracker.makeAllPlaybacksIdle() - - outputFile = File(outputDirectory, "Voice message.ogg") - lastRecordingFile = outputFile amplitudeList.clear() try { - initMediaRecorder() - val mr = mediaRecorder!! - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mr.setOutputFile(outputFile) - } else { - mr.setOutputFile(FileOutputStream(outputFile).fd) - } - mr.prepare() - mr.start() + voiceRecorder.startRecord() } catch (failure: Throwable) { throw VoiceFailure.UnableToRecord(failure) } @@ -97,9 +66,16 @@ class VoiceMessageHelper @Inject constructor( } fun stopRecording(): MultiPickerAudioType? { - internalStopRecording() + tryOrNull("Cannot stop media recording amplitude") { + stopRecordingAmplitudes() + } + val voiceMessageFile = tryOrNull("Cannot stop media recorder!") { + voiceRecorder.stopRecord() + voiceRecorder.getVoiceMessageFile() + } try { - outputFile?.let { + // TODO Improve this + voiceMessageFile?.let { val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it) return outputFileUri ?.toMultiPickerAudioType(context) @@ -113,38 +89,24 @@ class VoiceMessageHelper @Inject constructor( } } - private fun internalStopRecording() { + /** + * When entering in playback mode actually + */ + fun pauseRecording() { + voiceRecorder.stopRecord() + } + + fun deleteRecording() { tryOrNull("Cannot stop media recording amplitude") { stopRecordingAmplitudes() } tryOrNull("Cannot stop media recorder!") { - // Usually throws when the record is less than 1 second. - releaseMediaRecorder() + voiceRecorder.cancelRecord() } } - private fun releaseMediaRecorder() { - mediaRecorder?.let { - it.stop() - it.reset() - it.release() - } - - mediaRecorder = null - } - - fun pauseRecording() { - releaseMediaRecorder() - } - - fun deleteRecording() { - internalStopRecording() - outputFile?.delete() - outputFile = null - } - fun startOrPauseRecordingPlayback() { - lastRecordingFile?.let { + voiceRecorder.getCurrentRecord()?.let { startOrPausePlayback(VoiceMessagePlaybackTracker.RECORDING_ID, it) } } @@ -201,9 +163,8 @@ class VoiceMessageHelper @Inject constructor( } private fun onAmplitudeTick() { - val mr = mediaRecorder ?: return try { - val maxAmplitude = mr.maxAmplitude + val maxAmplitude = voiceRecorder.getMaxAmplitude() amplitudeList.add(maxAmplitude) playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList) } catch (e: IllegalStateException) { diff --git a/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt new file mode 100644 index 0000000000..8a0f829f94 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/AbstractVoiceRecorder.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voice + +import android.content.Context +import android.media.MediaRecorder +import android.os.Build +import java.io.File +import java.io.FileOutputStream + +abstract class AbstractVoiceRecorder( + context: Context, + private val filenameExt: String +) : VoiceRecorder { + private val outputDirectory = File(context.cacheDir, "voice_records") + + private var mediaRecorder: MediaRecorder? = null + private var outputFile: File? = null + + init { + if (!outputDirectory.exists()) { + outputDirectory.mkdirs() + } + } + + abstract fun setOutputFormat(mediaRecorder: MediaRecorder) + abstract fun convertFile(recordedFile: File?): File? + + private fun init() { + MediaRecorder().let { + it.setAudioSource(MediaRecorder.AudioSource.DEFAULT) + setOutputFormat(it) + it.setAudioEncodingBitRate(24000) + it.setAudioSamplingRate(48000) + mediaRecorder = it + } + } + + override fun startRecord() { + init() + outputFile = File(outputDirectory, "Voice message.$filenameExt") + + val mr = mediaRecorder ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mr.setOutputFile(outputFile) + } else { + mr.setOutputFile(FileOutputStream(outputFile).fd) + } + mr.prepare() + mr.start() + } + + override fun stopRecord() { + // Can throw when the record is less than 1 second. + mediaRecorder?.let { + it.stop() + it.reset() + it.release() + } + mediaRecorder = null + } + + override fun cancelRecord() { + stopRecord() + + outputFile?.delete() + outputFile = null + } + + override fun getMaxAmplitude(): Int { + return mediaRecorder?.maxAmplitude ?: 0 + } + + override fun getCurrentRecord(): File? { + return outputFile + } + + override fun getVoiceMessageFile(): File? { + return convertFile(outputFile) + } +} diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt new file mode 100644 index 0000000000..17e70997b2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorder.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voice + +import java.io.File + +interface VoiceRecorder { + /** + * Start the recording + */ + fun startRecord() + + /** + * Stop the recording + */ + fun stopRecord() + + /** + * Remove the file + */ + fun cancelRecord() + + fun getMaxAmplitude(): Int + + /** + * Not guaranteed to be a ogg file + */ + fun getCurrentRecord(): File? + + /** + * Guaranteed to be a ogg file + */ + fun getVoiceMessageFile(): File? +} diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt new file mode 100644 index 0000000000..2d40f5f7a3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voice + +import android.content.Context +import android.media.MediaRecorder +import com.arthenica.ffmpegkit.FFmpegKit +import com.arthenica.ffmpegkit.FFmpegKitConfig +import com.arthenica.ffmpegkit.Level +import com.arthenica.ffmpegkit.ReturnCode +import im.vector.app.BuildConfig +import timber.log.Timber +import java.io.File + +class VoiceRecorderL(context: Context) : AbstractVoiceRecorder(context, "mp4") { + override fun setOutputFormat(mediaRecorder: MediaRecorder) { + // Use AAC/MP4 format here + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + } + + override fun convertFile(recordedFile: File?): File? { + if (BuildConfig.DEBUG) { + FFmpegKitConfig.setLogLevel(Level.AV_LOG_INFO) + } + recordedFile ?: return null + // Convert to OGG + val targetFile = File(recordedFile.path.removeSuffix("mp4") + "ogg") + if (targetFile.exists()) { + targetFile.delete() + } + val start = System.currentTimeMillis() + val session = FFmpegKit.execute("-i \"${recordedFile.path}\" -c:a libvorbis \"${targetFile.path}\"") + val duration = System.currentTimeMillis() - start + Timber.d("Convert to ogg in $duration ms. Size in bytes from ${recordedFile.length()} to ${targetFile.length()}") + return when { + ReturnCode.isSuccess(session.returnCode) -> { + // SUCCESS + targetFile + } + ReturnCode.isCancel(session.returnCode) -> { + // CANCEL + null + } + else -> { + // FAILURE + Timber.e("Command failed with state ${session.state} and rc ${session.returnCode}.${session.failStackTrace}") + // TODO throw? + null + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt new file mode 100644 index 0000000000..004d520a6f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voice + +import android.content.Context +import android.os.Build +import javax.inject.Inject + +class VoiceRecorderProvider @Inject constructor( + private val context: Context +) { + fun provideVoiceRecorder(): VoiceRecorder { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + VoiceRecorderQ(context) + } else { + VoiceRecorderL(context) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderQ.kt new file mode 100644 index 0000000000..d6f4676893 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderQ.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voice + +import android.content.Context +import android.media.MediaRecorder +import android.os.Build +import androidx.annotation.RequiresApi +import java.io.File + +@RequiresApi(Build.VERSION_CODES.Q) +class VoiceRecorderQ(context: Context) : AbstractVoiceRecorder(context, "ogg") { + override fun setOutputFormat(mediaRecorder: MediaRecorder) { + // We can directly use OGG here + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG) + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) + } + + override fun convertFile(recordedFile: File?): File? { + // Nothing to do here + return recordedFile + } +} From 343ea42ef56a1dc8c97f993a0190a21f1a2de9b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Jul 2021 17:30:48 +0200 Subject: [PATCH 55/66] Fix issue on Android 21 --- .../java/org/matrix/android/sdk/api/util/MimeTypes.kt | 2 ++ .../lib/multipicker/utils/ContentResolverUtil.kt | 10 +++++++++- .../home/room/detail/composer/VoiceMessageHelper.kt | 1 - 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt index 182b37f2ad..ef47775f1b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -31,6 +31,8 @@ object MimeTypes { const val Jpeg = "image/jpeg" const val Gif = "image/gif" + const val Ogg = "audio/ogg" + fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse() diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt index 6cadfa6ce7..78136c274a 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt @@ -141,7 +141,7 @@ fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { MultiPickerAudioType( name, size, - context.contentResolver.getType(this), + sanitize(context.contentResolver.getType(this)), this, duration ) @@ -150,3 +150,11 @@ fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { } } } + +private fun sanitize(type: String?): String? { + if (type == "application/ogg") { + // Not supported on old system + return "audio/ogg" + } + return type +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index 4f07cf98fe..1b78425f5e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -74,7 +74,6 @@ class VoiceMessageHelper @Inject constructor( voiceRecorder.getVoiceMessageFile() } try { - // TODO Improve this voiceMessageFile?.let { val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it) return outputFileUri From 13ae0ba5f14d8004b18d3c9fc0365933b5bc3b83 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jul 2021 11:00:25 +0200 Subject: [PATCH 56/66] Convert voice message to be able to play on Android 28 and below --- .../home/room/detail/RoomDetailViewModel.kt | 6 +- .../app/features/voice/VoicePlayerHelper.kt | 73 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 4eb5519404..60400ce9c6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -57,6 +57,7 @@ import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.voice.VoicePlayerHelper import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers @@ -121,6 +122,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val directRoomHelper: DirectRoomHelper, private val jitsiService: JitsiService, private val voiceMessageHelper: VoiceMessageHelper, + private val voicePlayerHelper: VoicePlayerHelper, timelineFactory: TimelineFactory ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { @@ -647,8 +649,10 @@ class RoomDetailViewModel @AssistedInject constructor( try { // Download can fail val audioFile = session.fileService().downloadFile(action.messageAudioContent) + // Conversion can fail, fallback to the original file in this case and let the player fail for us + val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile // Play can fail - voiceMessageHelper.startOrPausePlayback(action.eventId, audioFile) + voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.Failure(failure)) } diff --git a/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt new file mode 100644 index 0000000000..f1b316c456 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/VoicePlayerHelper.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voice + +import android.content.Context +import android.os.Build +import com.arthenica.ffmpegkit.FFmpegKit +import com.arthenica.ffmpegkit.ReturnCode +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +class VoicePlayerHelper @Inject constructor( + context: Context +) { + private val outputDirectory = File(context.cacheDir, "voice_records") + + init { + if (!outputDirectory.exists()) { + outputDirectory.mkdirs() + } + } + + /** + * Ensure the file is encoded using aac audio codec + */ + fun convertFile(file: File): File? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Nothing to do + file + } else { + // Convert to mp4 + val targetFile = File(outputDirectory, "Voice.mp4") + if (targetFile.exists()) { + targetFile.delete() + } + val start = System.currentTimeMillis() + val session = FFmpegKit.execute("-i \"${file.path}\" -c:a aac \"${targetFile.path}\"") + val duration = System.currentTimeMillis() - start + Timber.d("Convert to mp4 in $duration ms. Size in bytes from ${file.length()} to ${targetFile.length()}") + return when { + ReturnCode.isSuccess(session.returnCode) -> { + // SUCCESS + targetFile + } + ReturnCode.isCancel(session.returnCode) -> { + // CANCEL + null + } + else -> { + // FAILURE + Timber.e("Command failed with state ${session.state} and rc ${session.returnCode}.${session.failStackTrace}") + // TODO throw? + null + } + } + } + } +} From 6da4f1d84f9ce9ab92800acdd0cde0bbe7e36b0e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jul 2021 11:19:06 +0200 Subject: [PATCH 57/66] Add comment --- .../main/java/im/vector/app/core/services/CallRingPlayer.kt | 3 +++ .../features/home/room/detail/composer/VoiceMessageHelper.kt | 1 + 2 files changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt index f725742711..f23eb07424 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt @@ -136,6 +136,9 @@ class CallRingPlayerOutgoing( mediaPlayer.setAudioAttributes(AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + // TODO Change to ? + // .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + // .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .build()) } else { @Suppress("DEPRECATION") diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index 1b78425f5e..cdb789997d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -128,6 +128,7 @@ class VoiceMessageHelper @Inject constructor( mediaPlayer = MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() + // Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() From 30bb91892da5918cd2a75361ee0e12336d9e2388 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jul 2021 16:04:50 +0200 Subject: [PATCH 58/66] Fix issue about move overflow. Now use limit for distances --- .../composer/VoiceMessageRecorderView.kt | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index 2390a86a4d..e473ddf3cf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -66,10 +66,15 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private var firstY: Float = 0f private var lastX: Float = 0f private var lastY: Float = 0f + private var lastDistanceX: Float = 0f + private var lastDistanceY: Float = 0f private var recordingTicker: CountUpTimer? = null private val dimensionConverter = DimensionConverter(context.resources) + private val minimumMove = dimensionConverter.dpToPx(10) + private val distanceToLock = dimensionConverter.dpToPx(34).toFloat() + private val distanceToCancel = dimensionConverter.dpToPx(120).toFloat() init { inflate(context, R.layout.view_voice_message_recorder, this) @@ -105,7 +110,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } views.voicePlaybackWaveform.setOnClickListener { - if (recordingState !== RecordingState.PLAYBACK) { + if (recordingState != RecordingState.PLAYBACK) { recordingState = RecordingState.PLAYBACK showPlaybackViews() } @@ -147,6 +152,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor( firstY = event.rawY lastX = firstX lastY = firstY + lastDistanceX = 0F + lastDistanceY = 0F } } @@ -164,11 +171,14 @@ class VoiceMessageRecorderView @JvmOverloads constructor( val currentX = event.rawX val currentY = event.rawY - val isRecordingStateChanged = updateRecordingState(currentX, currentY) + val distanceX = abs(firstX - currentX) + val distanceY = abs(firstY - currentY) + + val isRecordingStateChanged = updateRecordingState(currentX, currentY, distanceX, distanceY) when (recordingState) { RecordingState.CANCELLING -> { - val translationAmount = currentX - firstX + val translationAmount = -distanceX.coerceAtMost(distanceToCancel) views.voiceMessageMicButton.translationX = translationAmount views.voiceMessageSlideToCancel.translationX = translationAmount views.voiceMessageSlideToCancel.alpha = 1 - abs(translationAmount) / ((firstX - views.voiceMessageTimer.x) / 3) @@ -178,7 +188,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } RecordingState.LOCKING -> { views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_unlocked) - val translationAmount = currentY - firstY + val translationAmount = -distanceY.coerceIn(0F, distanceToLock) views.voiceMessageMicButton.translationY = translationAmount views.voiceMessageLockArrow.translationY = translationAmount } @@ -202,12 +212,12 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } lastX = currentX lastY = currentY + lastDistanceX = distanceX + lastDistanceY = distanceY } - private fun updateRecordingState(currentX: Float, currentY: Float): Boolean { + private fun updateRecordingState(currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): Boolean { val previousRecordingState = recordingState - val distanceX = abs(firstX - currentX) - val distanceY = abs(firstY - currentY) if (recordingState == RecordingState.STARTED) { // Determine if cancelling or locking for the first move action. if (currentX < firstX && distanceX > distanceY) { recordingState = RecordingState.CANCELLING @@ -215,28 +225,27 @@ class VoiceMessageRecorderView @JvmOverloads constructor( recordingState = RecordingState.LOCKING } } else if (recordingState == RecordingState.CANCELLING) { // Check if cancelling conditions met, also check if it should be initial state - if (abs(currentX - firstX) < 10 && lastX < currentX) { + if (distanceX < minimumMove && distanceX < lastDistanceX) { recordingState = RecordingState.STARTED - } else if (shouldCancelRecording()) { + } else if (shouldCancelRecording(distanceX)) { recordingState = RecordingState.CANCELLED } } else if (recordingState == RecordingState.LOCKING) { // Check if locking conditions met, also check if it should be initial state - if (abs(currentY - firstY) < 10 && lastY < currentY) { + if (distanceY < minimumMove && distanceY < lastDistanceY) { recordingState = RecordingState.STARTED - } else if (shouldLockRecording()) { + } else if (shouldLockRecording(distanceY)) { recordingState = RecordingState.LOCKED } } return previousRecordingState != recordingState } - private fun shouldCancelRecording(): Boolean { - return abs(views.voiceMessageTimer.x + views.voiceMessageTimer.width - views.voiceMessageSlideToCancel.x) < 10 - || views.voiceMessageSlideToCancel.x <= views.voiceMessageTimer.x + views.voiceMessageTimer.width // To handle super fast moving + private fun shouldCancelRecording(distanceX: Float): Boolean { + return distanceX >= distanceToCancel } - private fun shouldLockRecording(): Boolean { - return abs(views.voiceMessageLockImage.y + views.voiceMessageLockImage.height - views.voiceMessageLockArrow.y) < 10 + private fun shouldLockRecording(distanceY: Float): Boolean { + return distanceY >= distanceToLock } private fun startRecordingTicker() { From 6caa2b9ae0ced8b889ba90ce12f069d18ea0bd1f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jul 2021 17:12:08 +0200 Subject: [PATCH 59/66] Fix issue with RTL Still a pb when Mic is on, margins are not correct --- .../src/main/res/values-ldrtl/integers.xml | 1 + .../src/main/res/values/integers.xml | 1 + .../composer/VoiceMessageRecorderView.kt | 53 +++++++++++++------ .../layout/view_voice_message_recorder.xml | 2 +- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/library/ui-styles/src/main/res/values-ldrtl/integers.xml b/library/ui-styles/src/main/res/values-ldrtl/integers.xml index e8a746b687..88b587c96f 100644 --- a/library/ui-styles/src/main/res/values-ldrtl/integers.xml +++ b/library/ui-styles/src/main/res/values-ldrtl/integers.xml @@ -1,6 +1,7 @@ + -1 180 \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/integers.xml b/library/ui-styles/src/main/res/values/integers.xml index 75e8bb6f9a..2f6a1b0bc4 100644 --- a/library/ui-styles/src/main/res/values/integers.xml +++ b/library/ui-styles/src/main/res/values/integers.xml @@ -7,6 +7,7 @@ 200 + 1 0 750 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index e473ddf3cf..c59c039abf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -22,6 +22,7 @@ import android.util.AttributeSet import android.view.MotionEvent import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.hardware.vibrate @@ -75,6 +76,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private val minimumMove = dimensionConverter.dpToPx(10) private val distanceToLock = dimensionConverter.dpToPx(34).toFloat() private val distanceToCancel = dimensionConverter.dpToPx(120).toFloat() + private val rtlXMultiplier = context.resources.getInteger(R.integer.rtl_x_multiplier) init { inflate(context, R.layout.view_voice_message_recorder, this) @@ -85,7 +87,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } fun initVoiceRecordingViews() { - hideRecordingViews(animationDuration = 0) + hideRecordingViews() stopRecordingTicker() views.voiceMessageMicButton.isVisible = true @@ -95,7 +97,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private fun initListeners() { views.voiceMessageSendButton.setOnClickListener { stopRecordingTicker() - hideRecordingViews(animationDuration = 0) + hideRecordingViews() views.voiceMessageSendButton.isVisible = false recordingState = RecordingState.NONE callback?.onVoiceRecordingEnded(isCancelled = false) @@ -103,7 +105,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( views.voiceMessageDeletePlayback.setOnClickListener { stopRecordingTicker() - hideRecordingViews(animationDuration = 0) + hideRecordingViews() views.voiceMessageSendButton.isVisible = false recordingState = RecordingState.NONE callback?.onVoiceRecordingEnded(isCancelled = true) @@ -178,19 +180,25 @@ class VoiceMessageRecorderView @JvmOverloads constructor( when (recordingState) { RecordingState.CANCELLING -> { - val translationAmount = -distanceX.coerceAtMost(distanceToCancel) - views.voiceMessageMicButton.translationX = translationAmount - views.voiceMessageSlideToCancel.translationX = translationAmount - views.voiceMessageSlideToCancel.alpha = 1 - abs(translationAmount) / ((firstX - views.voiceMessageTimer.x) / 3) + val translationAmount = distanceX.coerceAtMost(distanceToCancel) + views.voiceMessageMicButton.translationX = -translationAmount * rtlXMultiplier + views.voiceMessageSlideToCancel.translationX = -translationAmount / 2 * rtlXMultiplier + views.voiceMessageSlideToCancel.alpha = 1 - translationAmount / distanceToCancel / 3 views.voiceMessageLockBackground.isVisible = false views.voiceMessageLockImage.isVisible = false views.voiceMessageLockArrow.isVisible = false + // Reset Y translations + views.voiceMessageMicButton.translationY = 0F + views.voiceMessageLockArrow.translationY = 0F } RecordingState.LOCKING -> { views.voiceMessageLockImage.setImageResource(R.drawable.ic_voice_message_unlocked) val translationAmount = -distanceY.coerceIn(0F, distanceToLock) views.voiceMessageMicButton.translationY = translationAmount views.voiceMessageLockArrow.translationY = translationAmount + // Reset X translations + views.voiceMessageMicButton.translationX = 0F + views.voiceMessageSlideToCancel.translationX = 0F } RecordingState.CANCELLED -> { callback?.onVoiceRecordingEnded(true) @@ -218,19 +226,23 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private fun updateRecordingState(currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): Boolean { val previousRecordingState = recordingState - if (recordingState == RecordingState.STARTED) { // Determine if cancelling or locking for the first move action. - if (currentX < firstX && distanceX > distanceY) { + if (recordingState == RecordingState.STARTED) { + // Determine if cancelling or locking for the first move action. + if (((currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1)) + && distanceX > distanceY) { recordingState = RecordingState.CANCELLING } else if (currentY < firstY && distanceY > distanceX) { recordingState = RecordingState.LOCKING } - } else if (recordingState == RecordingState.CANCELLING) { // Check if cancelling conditions met, also check if it should be initial state + } else if (recordingState == RecordingState.CANCELLING) { + // Check if cancelling conditions met, also check if it should be initial state if (distanceX < minimumMove && distanceX < lastDistanceX) { recordingState = RecordingState.STARTED } else if (shouldCancelRecording(distanceX)) { recordingState = RecordingState.CANCELLED } - } else if (recordingState == RecordingState.LOCKING) { // Check if locking conditions met, also check if it should be initial state + } else if (recordingState == RecordingState.LOCKING) { + // Check if locking conditions met, also check if it should be initial state if (distanceY < minimumMove && distanceY < lastDistanceY) { recordingState = RecordingState.STARTED } else if (shouldLockRecording(distanceY)) { @@ -323,7 +335,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private fun showRecordingViews() { views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic_recording) - (views.voiceMessageMicButton.layoutParams as MarginLayoutParams).apply { setMargins(0, 0, 0, 0) } + views.voiceMessageMicButton.updateLayoutParams { + setMargins(0, 0, 0, 0) + } views.voiceMessageLockBackground.isVisible = true views.voiceMessageLockBackground.animate().setDuration(300).translationY(-dimensionConverter.dpToPx(148).toFloat()).start() views.voiceMessageLockImage.isVisible = true @@ -337,11 +351,16 @@ class VoiceMessageRecorderView @JvmOverloads constructor( views.voiceMessageSendButton.isVisible = false } - private fun hideRecordingViews(animationDuration: Int = 300) { + private fun hideRecordingViews() { views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic) - views.voiceMessageMicButton.animate().translationX(0f).translationY(0f).setDuration(animationDuration.toLong()).setDuration(0).start() - (views.voiceMessageMicButton.layoutParams as MarginLayoutParams).apply { - setMargins(0, 0, dimensionConverter.dpToPx(12), dimensionConverter.dpToPx(12)) + views.voiceMessageMicButton.animate().translationX(0f).translationY(0f).setDuration(0).start() + views.voiceMessageMicButton.updateLayoutParams { + if (rtlXMultiplier == -1) { + // RTL + setMargins(dimensionConverter.dpToPx(10), 0, 0, dimensionConverter.dpToPx(12)) + } else { + setMargins(0, 0, dimensionConverter.dpToPx(12), dimensionConverter.dpToPx(10)) + } } views.voiceMessageLockBackground.isVisible = false views.voiceMessageLockBackground.animate().translationY(0f).start() @@ -357,7 +376,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } private fun showRecordingLockedViews() { - hideRecordingViews(animationDuration = 0) + hideRecordingViews() views.voiceMessagePlaybackLayout.isVisible = true views.voiceMessagePlaybackTimerIndicator.isVisible = true views.voicePlaybackControlButton.isVisible = false diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 7736d76b0e..7aaa0bd05f 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -23,7 +23,7 @@ android:id="@+id/voiceMessageMicButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="12dp" + android:layout_marginEnd="10dp" android:layout_marginBottom="12dp" android:background="?android:attr/selectableItemBackground" android:contentDescription="@string/a11y_start_voice_message" From a11941714d8604aff98d58dec544bb182ff7e4ca Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 23 Jul 2021 15:19:50 +0300 Subject: [PATCH 60/66] Code review fixes. --- .../src/main/assets/open_source_licenses.html | 13 ++++++++ .../home/room/detail/RoomDetailFragment.kt | 33 ++++++++++++++----- .../composer/VoiceMessageRecorderView.kt | 5 +++ vector/src/main/res/values/strings.xml | 1 + 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 107e8d362d..490de1f23a 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -595,5 +595,18 @@ Apache License +

+ GNU GENERAL PUBLIC LICENSE +
+ Version 3, 29 June 2007 +

+
    +
  • + ffmpeg-kit +
    + Copyright (c) 2021 Taner Sener +
  • +
+ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index da27c1adc8..d05d968fd9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1291,7 +1291,14 @@ class RoomDetailFragment @Inject constructor( } override fun onTextBlankStateChanged(isBlank: Boolean) { - views.voiceMessageRecorderView.isVisible = !views.composerLayout.views.sendButton.isVisible && vectorPreferences.labsUseVoiceMessage() + if (!views.composerLayout.views.sendButton.isVisible && vectorPreferences.labsUseVoiceMessage()) { + // Animate alpha to prevent overlapping with the animation of the send button + views.voiceMessageRecorderView.alpha = 0f + views.voiceMessageRecorderView.isVisible = true + views.voiceMessageRecorderView.animate().alpha(1f).setDuration(300).start() + } else { + views.voiceMessageRecorderView.isVisible = false + } } } } @@ -1356,11 +1363,13 @@ class RoomDetailFragment @Inject constructor( views.inviteView.isVisible = false if (state.tombstoneEvent == null) { if (state.canSendMessage) { - views.composerLayout.isVisible = true - views.voiceMessageRecorderView.isVisible = vectorPreferences.labsUseVoiceMessage() - views.composerLayout.setRoomEncrypted(summary.isEncrypted) - views.notificationAreaView.render(NotificationAreaView.State.Hidden) - views.composerLayout.alwaysShowSendButton = !vectorPreferences.labsUseVoiceMessage() + if (!views.voiceMessageRecorderView.isActive()) { + views.composerLayout.isVisible = true + views.voiceMessageRecorderView.isVisible = vectorPreferences.labsUseVoiceMessage() + views.composerLayout.setRoomEncrypted(summary.isEncrypted) + views.notificationAreaView.render(NotificationAreaView.State.Hidden) + views.composerLayout.alwaysShowSendButton = !vectorPreferences.labsUseVoiceMessage() + } } else { views.composerLayout.isVisible = false views.voiceMessageRecorderView.isVisible = false @@ -1904,13 +1913,21 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } is EventSharedAction.Edit -> { - roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) + if (!views.voiceMessageRecorderView.isActive()) { + roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) + } else { + requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) + } } is EventSharedAction.Quote -> { roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) } is EventSharedAction.Reply -> { - roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) + if (!views.voiceMessageRecorderView.isActive()) { + roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) + } else { + requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) + } } is EventSharedAction.CopyPermalink -> { val permalink = session.permalinkService().createPermalink(roomDetailArgs.roomId, action.eventId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index c59c039abf..dc7c0f3a03 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -400,6 +400,11 @@ class VoiceMessageRecorderView @JvmOverloads constructor( PLAYBACK } + /** + * Returns true if the voice message is recording or is in playback mode + */ + fun isActive() = recordingState !in listOf(RecordingState.NONE, RecordingState.CANCELLED) + override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { when (state) { is VoiceMessagePlaybackTracker.Listener.State.Recording -> { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index a2c255466c..63944fcb02 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3454,4 +3454,5 @@ Tap on the wavelength to stop and playback Cannot play this voice message Cannot record a voice message + Cannot reply or edit while voice message is active From bd2ed4c58ac5414aed5ef107bd6d8e97d17e9022 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 23 Jul 2021 16:53:34 +0300 Subject: [PATCH 61/66] Stop playback when deleting record on locked mode. --- .../app/features/home/room/detail/RoomDetailViewModel.kt | 1 + .../features/home/room/detail/composer/VoiceMessageHelper.kt | 4 +++- .../home/room/detail/composer/VoiceMessageRecorderView.kt | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 60400ce9c6..0ac638034d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -631,6 +631,7 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) { + voiceMessageHelper.stopPlayback() if (isCancelled) { voiceMessageHelper.deleteRecording() } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index cdb789997d..dc1115cbda 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -93,6 +93,7 @@ class VoiceMessageHelper @Inject constructor( */ fun pauseRecording() { voiceRecorder.stopRecord() + stopRecordingAmplitudes() } fun deleteRecording() { @@ -112,6 +113,7 @@ class VoiceMessageHelper @Inject constructor( fun startOrPausePlayback(id: String, file: File) { stopPlayback() + stopRecordingAmplitudes() if (playbackTracker.getPlaybackState(id) is VoiceMessagePlaybackTracker.Listener.State.Playing) { playbackTracker.pausePlayback(id) } else { @@ -145,7 +147,7 @@ class VoiceMessageHelper @Inject constructor( startPlaybackTicker(id) } - private fun stopPlayback() { + fun stopPlayback() { mediaPlayer?.stop() stopPlaybackTicker() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt index dc7c0f3a03..0dc3d198db 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -415,6 +415,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong()) views.voicePlaybackTime.text = formattedTimerText } + is VoiceMessagePlaybackTracker.Listener.State.Paused, is VoiceMessagePlaybackTracker.Listener.State.Idle -> { views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) } From 4c8a8d8cfb675e1fc550b9d67c5353852e2752d9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 27 Jul 2021 15:32:35 +0300 Subject: [PATCH 62/66] Design review fixes. --- library/ui-styles/src/main/res/values/colors.xml | 2 +- .../src/main/res/values/styles_voice_message.xml | 2 +- .../app/features/home/room/detail/RoomDetailFragment.kt | 8 +++++++- .../home/room/detail/composer/VoiceMessageHelper.kt | 8 ++++++-- .../home/room/detail/composer/VoiceMessageRecorderView.kt | 7 +++++-- .../home/room/detail/timeline/item/MessageVoiceItem.kt | 2 ++ vector/src/main/res/values/strings.xml | 3 ++- 7 files changed, 24 insertions(+), 8 deletions(-) diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index e31bfb4ab8..9d5f15cbde 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -130,6 +130,6 @@ @color/palette_black_900 - @color/palette_gray_450 + @color/palette_gray_400 diff --git a/library/ui-styles/src/main/res/values/styles_voice_message.xml b/library/ui-styles/src/main/res/values/styles_voice_message.xml index 5d5fbef007..59fea75074 100644 --- a/library/ui-styles/src/main/res/values/styles_voice_message.xml +++ b/library/ui-styles/src/main/res/values/styles_voice_message.xml @@ -9,7 +9,7 @@ true 2dp 2dp - leftToRight + rightToLeft