diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index 36155bf093..acdc894477 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -1,6 +1,7 @@ /* * Nextcloud Talk - Android Client * + * SPDX-FileCopyrightText: 2024 Christian Reiner * SPDX-FileCopyrightText: 2021 Andy Scherzinger * SPDX-FileCopyrightText: 2021 Tim Krüger * SPDX-FileCopyrightText: 2021 Marcel Hibbe @@ -27,9 +28,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding -import com.nextcloud.talk.extensions.loadBotsAvatar -import com.nextcloud.talk.extensions.loadChangelogBotAvatar -import com.nextcloud.talk.extensions.loadFederatedUserAvatar import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ChatMessageUtils @@ -162,6 +160,10 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : } }) + voiceMessageInterface.registerMessageToObservePlaybackSpeedPreferences(message.user.id) { speed -> + binding.playbackSpeedControlBtn.setSpeed(speed) + } + Reaction().showReactions( message, ::clickOnReaction, diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt index 4f3f57a241..48bc28bdee 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt @@ -149,6 +149,10 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : binding.checkMark.contentDescription = readStatusContentDescriptionString + voiceMessageInterface.registerMessageToObservePlaybackSpeedPreferences(message.user.id) { speed -> + binding.playbackSpeedControlBtn.setSpeed(speed) + } + Reaction().showReactions( message, ::clickOnReaction, diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt index aada2ade23..34a9460e43 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt @@ -1,13 +1,16 @@ /* * Nextcloud Talk - Android Client * + * SPDX-FileCopyrightText: 2024 Christian Reiner * SPDX-FileCopyrightText: 2021 Marcel Hibbe * SPDX-License-Identifier: GPL-3.0-or-later */ package com.nextcloud.talk.adapters.messages import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.ui.PlaybackSpeed interface VoiceMessageInterface { fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int) + fun registerMessageToObservePlaybackSpeedPreferences(userId: String, listener: (speed: PlaybackSpeed) -> Unit) } diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 74271e0309..e76fd7f713 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -1,6 +1,7 @@ /* * Nextcloud Talk - Android Client * + * SPDX-FileCopyrightText: 2024 Christian Reiner * SPDX-FileCopyrightText: 2024 Parneet Singh * SPDX-FileCopyrightText: 2024 Giacomo Pacini * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham @@ -136,6 +137,8 @@ import com.nextcloud.talk.shareditems.activities.SharedItemsActivity import com.nextcloud.talk.signaling.SignalingMessageReceiver import com.nextcloud.talk.signaling.SignalingMessageSender import com.nextcloud.talk.translate.ui.TranslateActivity +import com.nextcloud.talk.ui.PlaybackSpeed +import com.nextcloud.talk.ui.PlaybackSpeedControl import com.nextcloud.talk.ui.StatusDrawable import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.dialog.DateTimePickerFragment @@ -205,6 +208,7 @@ import java.util.Date import java.util.Locale import java.util.concurrent.ExecutionException import javax.inject.Inject +import kotlin.String import kotlin.collections.set import kotlin.math.roundToInt @@ -357,6 +361,19 @@ class ChatActivity : private var voiceMessageToRestoreAudioPosition = 0 private var voiceMessageToRestoreWasPlaying = false + private val playbackSpeedPreferencesObserver: (Map) -> Unit = { speedPreferenceLiveData -> + mediaPlayer?.let { mediaPlayer -> + (mediaPlayer.isPlaying == true).also { + currentlyPlayedVoiceMessage?.let { message -> + mediaPlayer.playbackParams.let { params -> + params.setSpeed(chatViewModel.getPlaybackSpeedPreference(message).value) + mediaPlayer.playbackParams = params + } + } + } + } + } + private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener { override fun onSwitchTo(token: String?) { if (token != null) { @@ -434,6 +451,10 @@ class ChatActivity : onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + appPreferences.readVoiceMessagePlaybackSpeedPreferences().let { playbackSpeedPreferences -> + chatViewModel.applyPlaybackSpeedPreferences(playbackSpeedPreferences) + } + initObservers() if (savedInstanceState != null) { @@ -1045,6 +1066,8 @@ class ChatActivity : setupSwipeToReply() + chatViewModel.voiceMessagePlaybackSpeedPreferences.observe(this, playbackSpeedPreferencesObserver) + binding.unreadMessagesPopup.setOnClickListener { binding.messagesListView.smoothScrollToPosition(0) binding.unreadMessagesPopup.visibility = View.GONE @@ -1131,6 +1154,7 @@ class ChatActivity : adapter?.setLoadMoreListener(this) adapter?.setDateHeadersFormatter { format(it) } adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) } + adapter?.registerViewClickListener( R.id.playPauseBtn ) { _, message -> @@ -1154,6 +1178,15 @@ class ChatActivity : } } } + + adapter?.registerViewClickListener(R.id.playbackSpeedControlBtn) { button, message -> + val nextSpeed = (button as PlaybackSpeedControl).getSpeed().next() + HashMap(appPreferences.readVoiceMessagePlaybackSpeedPreferences()).let { playbackSpeedPreferences -> + playbackSpeedPreferences[message.user.id] = nextSpeed + chatViewModel.applyPlaybackSpeedPreferences(playbackSpeedPreferences) + appPreferences.saveVoiceMessagePlaybackSpeedPreferences(playbackSpeedPreferences) + } + } } private fun setUpWaveform(message: ChatMessage, thenPlay: Boolean = true) { @@ -1579,6 +1612,9 @@ class ChatActivity : mediaPlayer?.let { if (!it.isPlaying && doPlay) { chatViewModel.audioRequest(true) { + it.playbackParams = it.playbackParams.apply { + setSpeed(chatViewModel.getPlaybackSpeedPreference(message).value) + } it.start() } } @@ -1703,6 +1739,20 @@ class ChatActivity : } } + override fun registerMessageToObservePlaybackSpeedPreferences( + userId: String, + listener: (speed: PlaybackSpeed) -> Unit + ) { + chatViewModel.voiceMessagePlaybackSpeedPreferences.let { liveData -> + liveData.observe(this) { playbackSpeedPreferences -> + listener(playbackSpeedPreferences[userId] ?: PlaybackSpeed.NORMAL) + } + liveData.value?.let { playbackSpeedPreferences -> + listener(playbackSpeedPreferences[userId] ?: PlaybackSpeed.NORMAL) + } + } + } + @SuppressLint("NotifyDataSetChanged") override fun collapseSystemMessages() { adapter?.items?.forEach { @@ -2372,6 +2422,8 @@ class ChatActivity : if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { mentionAutocomplete?.dismissPopup() } + + chatViewModel.voiceMessagePlaybackSpeedPreferences.removeObserver(playbackSpeedPreferencesObserver) } private fun isActivityNotChangingConfigurations(): Boolean = !isChangingConfigurations diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 6b80b3d91d..a76ed4415e 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -1,6 +1,7 @@ /* * Nextcloud Talk - Android Client * + * SPDX-FileCopyrightText: 2024 Christian Reiner * SPDX-FileCopyrightText: 2023 Marcel Hibbe * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -33,6 +34,7 @@ import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.repositories.reactions.ReactionsRepository +import com.nextcloud.talk.ui.PlaybackSpeed import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew @@ -107,6 +109,10 @@ class ChatViewModel @Inject constructor( val getVoiceRecordingLocked: LiveData get() = _getVoiceRecordingLocked + private val _voiceMessagePlaybackSpeeds: MutableLiveData> = MutableLiveData() + val voiceMessagePlaybackSpeedPreferences: LiveData> + get() = _voiceMessagePlaybackSpeeds + val getMessageFlow = chatRepository.messageFlow .onEach { _chatMessageViewState.value = if (_chatMessageViewState.value == ChatMessageInitialState) { @@ -644,6 +650,13 @@ class ChatViewModel @Inject constructor( emit(message.first()) } + fun applyPlaybackSpeedPreferences(speeds: Map) { + _voiceMessagePlaybackSpeeds.postValue(speeds) + } + + fun getPlaybackSpeedPreference(message: ChatMessage) = + _voiceMessagePlaybackSpeeds.value?.get(message.user.id) ?: PlaybackSpeed.NORMAL + // inner class GetRoomObserver : Observer { // override fun onSubscribe(d: Disposable) { // // unused atm diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt index 4292a02817..54c73869d7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt @@ -38,7 +38,7 @@ class MessageInputViewModel @Inject constructor( private val audioRecorderManager: AudioRecorderManager, private val mediaPlayerManager: MediaPlayerManager, private val audioFocusRequestManager: AudioFocusRequestManager, - private val dataStore: AppPreferences + private val appPreferences: AppPreferences ) : ViewModel(), DefaultLifecycleObserver { enum class LifeCycleFlag { PAUSED, @@ -147,9 +147,9 @@ class MessageInputViewModel @Inject constructor( if (isQueueing) { val tempID = System.currentTimeMillis().toInt() val qMsg = QueuedMessage(tempID, message, displayName, replyTo, sendWithoutNotification) - messageQueue = dataStore.getMessageQueue(internalId) + messageQueue = appPreferences.getMessageQueue(internalId) messageQueue.add(qMsg) - dataStore.saveMessageQueue(internalId, messageQueue) + appPreferences.saveMessageQueue(internalId, messageQueue) _messageQueueSizeFlow.update { messageQueue.size } _messageQueueFlow.postValue(listOf(qMsg)) return @@ -260,8 +260,8 @@ class MessageInputViewModel @Inject constructor( if (isQueueing) return messageQueue.clear() - val queue = dataStore.getMessageQueue(internalId) - dataStore.saveMessageQueue(internalId, null) // empties the queue + val queue = appPreferences.getMessageQueue(internalId) + appPreferences.saveMessageQueue(internalId, null) // empties the queue while (queue.size > 0) { val msg = queue.removeAt(0) sendChatMessage( @@ -279,7 +279,7 @@ class MessageInputViewModel @Inject constructor( } fun getTempMessagesFromMessageQueue(internalId: String) { - val queue = dataStore.getMessageQueue(internalId) + val queue = appPreferences.getMessageQueue(internalId) val list = mutableListOf() for (msg in queue) { list.add(msg) @@ -292,31 +292,31 @@ class MessageInputViewModel @Inject constructor( } fun restoreMessageQueue(internalId: String) { - messageQueue = dataStore.getMessageQueue(internalId) + messageQueue = appPreferences.getMessageQueue(internalId) _messageQueueSizeFlow.tryEmit(messageQueue.size) } fun removeFromQueue(internalId: String, id: Int) { - val queue = dataStore.getMessageQueue(internalId) + val queue = appPreferences.getMessageQueue(internalId) for (qMsg in queue) { if (qMsg.id == id) { queue.remove(qMsg) break } } - dataStore.saveMessageQueue(internalId, queue) + appPreferences.saveMessageQueue(internalId, queue) _messageQueueSizeFlow.tryEmit(queue.size) } fun editQueuedMessage(internalId: String, id: Int, newMessage: String) { - val queue = dataStore.getMessageQueue(internalId) + val queue = appPreferences.getMessageQueue(internalId) for (qMsg in queue) { if (qMsg.id == id) { qMsg.message = newMessage break } } - dataStore.saveMessageQueue(internalId, queue) + appPreferences.saveMessageQueue(internalId, queue) } fun showCallStartedIndicator(recent: ChatMessage, show: Boolean) { diff --git a/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt b/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt new file mode 100644 index 0000000000..b3f826a63c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt @@ -0,0 +1,66 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.ui + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.app.AppCompatDelegate +import com.google.android.material.button.MaterialButton +import java.util.Locale + +internal const val SPEED_FACTOR_SLOW = 0.8f +internal const val SPEED_FACTOR_NORMAL = 1.0f +internal const val SPEED_FACTOR_FASTER = 1.5f +internal const val SPEED_FACTOR_FASTEST = 2.0f + +class PlaybackSpeedControl @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialButton(context, attrs, defStyleAttr) { + + private var currentSpeed = PlaybackSpeed.NORMAL + + init { + text = currentSpeed.label + } + + fun setSpeed(newSpeed: PlaybackSpeed) { + currentSpeed = newSpeed + text = currentSpeed.label + } + + fun getSpeed(): PlaybackSpeed { + return currentSpeed + } +} + +enum class PlaybackSpeed(val value: Float) { + SLOW(SPEED_FACTOR_SLOW), + NORMAL(SPEED_FACTOR_NORMAL), + FASTER(SPEED_FACTOR_FASTER), + FASTEST(SPEED_FACTOR_FASTEST); + + // no fixed, literal labels, since we want to obey numeric interpunctuation for different locales + val label: String = String.format(Locale.getDefault(), "%01.1fx", value) + + fun next(): PlaybackSpeed { + return entries[(ordinal + 1) % entries.size] + } + + companion object { + fun byName(name: String): PlaybackSpeed { + for (speed in entries) { + if (speed.name.equals(name, ignoreCase = true)) { + return speed + } + } + return NORMAL + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index ffe3e063b9..c10d4351a3 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -1,6 +1,7 @@ /* * Nextcloud Talk - Android Client * + * SPDX-FileCopyrightText: 2024 Christian Reiner * SPDX-FileCopyrightText: 2021 Andy Scherzinger * SPDX-FileCopyrightText: 2021 Tim Krüger * SPDX-FileCopyrightText: 2017 Mario Danic @@ -11,8 +12,10 @@ import android.annotation.SuppressLint; import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel; +import com.nextcloud.talk.ui.PlaybackSpeed; import java.util.List; +import java.util.Map; @SuppressLint("NonConstantResourceId") public interface AppPreferences { @@ -178,6 +181,10 @@ public interface AppPreferences { void deleteAllMessageQueuesFor(String userId); + void saveVoiceMessagePlaybackSpeedPreferences(Map speeds); + + Map readVoiceMessagePlaybackSpeedPreferences(); + boolean getShowNotificationWarning(); void setShowNotificationWarning(boolean showNotificationWarning); diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt index a576fe9a0b..f3e8f5517b 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt @@ -1,6 +1,7 @@ /* * Nextcloud Talk - Android Client * + * SPDX-FileCopyrightText: 2024 Christian Reiner * SPDX-FileCopyrightText: 2023 Julius Linus * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -17,12 +18,16 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.nextcloud.talk.R import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel +import com.nextcloud.talk.ui.PlaybackSpeed import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json @ExperimentalCoroutinesApi @Suppress("TooManyFunctions", "DeferredResultUnused", "EmptyFunctionBlock") @@ -544,6 +549,26 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { } } + override fun saveVoiceMessagePlaybackSpeedPreferences(speeds: Map) { + Json.encodeToString(speeds).let { + runBlocking { async { writeString(VOICE_MESSAGE_PLAYBACK_SPEEDS, it) } } + } + } + + override fun readVoiceMessagePlaybackSpeedPreferences(): Map { + return runBlocking { + async { readString(VOICE_MESSAGE_PLAYBACK_SPEEDS, "{}").first() } + }.getCompleted().let { + try { + Json.decodeFromString>(it) + .map { entry -> entry.key to PlaybackSpeed.byName(entry.value) }.toMap() + } catch (e: SerializationException) { + Log.e(TAG, "ignoring invalid json format in voice message playback speed preferences", e) + emptyMap() + } + } + } + override fun getShowNotificationWarning(): Boolean { return runBlocking { async { readBoolean(SHOW_NOTIFICATION_WARNING, true).first() @@ -641,6 +666,7 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { const val PHONE_BOOK_INTEGRATION_LAST_RUN = "phone_book_integration_last_run" const val TYPING_STATUS = "typing_status" const val MESSAGE_QUEUE = "@message_queue" + const val VOICE_MESSAGE_PLAYBACK_SPEEDS = "voice_message_playback_speeds" const val SHOW_NOTIFICATION_WARNING = "show_notification_warning" private fun String.convertStringToArray(): Array { var varString = this diff --git a/app/src/main/res/layout/fragment_message_input_voice_recording.xml b/app/src/main/res/layout/fragment_message_input_voice_recording.xml index ee5e1b87ec..cf5d667c50 100644 --- a/app/src/main/res/layout/fragment_message_input_voice_recording.xml +++ b/app/src/main/res/layout/fragment_message_input_voice_recording.xml @@ -50,6 +50,7 @@ tools:progress="50" tools:progressTint="@color/hwSecurityRed" tools:progressBackgroundTint="@color/blue"/> + ~ SPDX-FileCopyrightText: 2021 Andy Scherzinger ~ SPDX-FileCopyrightText: 2021 Marcel Hibbe ~ SPDX-FileCopyrightText: 2017-2018 Mario Danic @@ -76,7 +77,6 @@ app:iconSize="40dp" app:iconTint="@color/nc_incoming_text_default" /> - + diff --git a/app/src/main/res/layout/item_custom_outcoming_voice_message.xml b/app/src/main/res/layout/item_custom_outcoming_voice_message.xml index 1989536eac..8cea9c5531 100644 --- a/app/src/main/res/layout/item_custom_outcoming_voice_message.xml +++ b/app/src/main/res/layout/item_custom_outcoming_voice_message.xml @@ -2,6 +2,7 @@ Match contacts based on phone number to integrate Talk shortcut into system contacts app