From 20d36c1eb960da4f52cff08e14b33018777a7647 Mon Sep 17 00:00:00 2001 From: Christian Reiner Date: Thu, 21 Nov 2024 21:52:37 +0100 Subject: [PATCH 1/3] playback speed control button for voice messages Signed-off-by: Christian Reiner Themed the PlaybackSpeedControl + Work around onBind bug Signed-off-by: rapterjet2004 --- .../IncomingVoiceMessageViewHolder.kt | 55 +++++++++----- .../OutcomingVoiceMessageViewHolder.kt | 15 +++- .../messages/VoiceMessageInterface.kt | 3 + .../com/nextcloud/talk/chat/ChatActivity.kt | 52 +++++++++++++ .../talk/chat/viewmodels/ChatViewModel.kt | 13 ++++ .../chat/viewmodels/MessageInputViewModel.kt | 22 +++--- .../nextcloud/talk/ui/PlaybackSpeedControl.kt | 75 +++++++++++++++++++ .../utils/preferences/AppPreferences.java | 7 ++ .../utils/preferences/AppPreferencesImpl.kt | 26 +++++++ ...fragment_message_input_voice_recording.xml | 1 + .../item_custom_incoming_voice_message.xml | 12 ++- .../item_custom_outcoming_voice_message.xml | 12 +++ app/src/main/res/values/strings.xml | 1 + 13 files changed, 260 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt 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 1708979028..f46b1f119e 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 @@ -68,10 +69,16 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : lateinit var voiceMessageInterface: VoiceMessageInterface lateinit var commonMessageInterface: CommonMessageInterface + private var isBound = false @SuppressLint("SetTextI18n") override fun onBind(message: ChatMessage) { super.onBind(message) + if (isBound) { + handleIsPlayingVoiceMessageState(message) + return + } + this.message = message sharedApplication!!.componentApplication.inject(this) @@ -100,25 +107,6 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar) viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT) - if (message.isPlayingVoiceMessage) { - showPlayButton() - binding.playPauseBtn.icon = ContextCompat.getDrawable( - context!!, - R.drawable.ic_baseline_pause_voice_message_24 - ) - val d = message.voiceMessageDuration.toLong() - val t = message.voiceMessagePlayedSeconds.toLong() - binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t) - binding.voiceMessageDuration.visibility = View.VISIBLE - binding.seekbar.progress = message.voiceMessageSeekbarProgress - } else { - binding.playPauseBtn.visibility = View.VISIBLE - binding.playPauseBtn.icon = ContextCompat.getDrawable( - context!!, - R.drawable.ic_baseline_play_arrow_voice_message_24 - ) - } - if (message.isDownloadingVoiceMessage) { showVoiceMessageLoading() } else { @@ -158,6 +146,10 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : } }) + voiceMessageInterface.registerMessageToObservePlaybackSpeedPreferences(message.user.id) { speed -> + binding.playbackSpeedControlBtn.setSpeed(speed) + } + Reaction().showReactions( message, ::clickOnReaction, @@ -167,6 +159,8 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : false, viewThemeUtils ) + + isBound = true } private fun longClickOnReaction(chatMessage: ChatMessage) { @@ -177,6 +171,29 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : commonMessageInterface.onClickReaction(chatMessage, emoji) } + private fun handleIsPlayingVoiceMessageState(message: ChatMessage) { + if (message.isPlayingVoiceMessage) { + showPlayButton() + binding.playPauseBtn.icon = ContextCompat.getDrawable( + context!!, + R.drawable.ic_baseline_pause_voice_message_24 + ) + + val d = message.voiceMessageDuration.toLong() + val t = message.voiceMessagePlayedSeconds.toLong() + binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t) + binding.voiceMessageDuration.visibility = View.VISIBLE + binding.seekbar.max = message.voiceMessageDuration * ONE_SEC + binding.seekbar.progress = message.voiceMessageSeekbarProgress + } else { + binding.playPauseBtn.visibility = View.VISIBLE + binding.playPauseBtn.icon = ContextCompat.getDrawable( + context!!, + R.drawable.ic_baseline_play_arrow_voice_message_24 + ) + } + } + private fun updateDownloadState(message: ChatMessage) { // check if download worker is already running val fileId = message.selectedIndividualHashMap!!["id"] 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..b9cd436298 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 @@ -74,10 +74,16 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : lateinit var voiceMessageInterface: VoiceMessageInterface lateinit var commonMessageInterface: CommonMessageInterface + private var isBound = false @SuppressLint("SetTextI18n") override fun onBind(message: ChatMessage) { super.onBind(message) + if (isBound) { + handleIsPlayingVoiceMessageState(message) + return + } + this.message = message sharedApplication!!.componentApplication.inject(this) viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) @@ -102,12 +108,9 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : setParentMessageDataOnMessageItem(message) updateDownloadState(message) - binding.seekbar.max = message.voiceMessageDuration * ONE_SEC viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar) viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT) - handleIsPlayingVoiceMessageState(message) - handleIsDownloadingVoiceMessageState(message) handleResetVoiceMessageState(message) @@ -149,6 +152,10 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : binding.checkMark.contentDescription = readStatusContentDescriptionString + voiceMessageInterface.registerMessageToObservePlaybackSpeedPreferences(message.user.id) { speed -> + binding.playbackSpeedControlBtn.setSpeed(speed) + } + Reaction().showReactions( message, ::clickOnReaction, @@ -158,6 +165,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : true, viewThemeUtils ) + isBound = true } private fun longClickOnReaction(chatMessage: ChatMessage) { @@ -207,6 +215,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : val t = message.voiceMessagePlayedSeconds.toLong() binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t) binding.voiceMessageDuration.visibility = View.VISIBLE + binding.seekbar.max = message.voiceMessageDuration * ONE_SEC binding.seekbar.progress = message.voiceMessageSeekbarProgress } else { binding.playPauseBtn.visibility = View.VISIBLE 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..5d5a9a0d6d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt @@ -0,0 +1,75 @@ +/* + * 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 autodagger.AutoInjector +import com.google.android.material.button.MaterialButton +import java.util.Locale +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +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 + +@AutoInjector(NextcloudTalkApplication::class) +class PlaybackSpeedControl @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialButton(context, attrs, defStyleAttr) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private var currentSpeed = PlaybackSpeed.NORMAL + + init { + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + text = currentSpeed.label + viewThemeUtils.material.colorMaterialButtonText(this) + } + + 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 7c23556ecb..a9265ae8ed 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(); + Long getNotificationWarningLastPostponedDate(); void setNotificationWarningLastPostponedDate(Long 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 d4ec5e7121..23dc371ca6 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") @@ -565,6 +570,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 getNotificationWarningLastPostponedDate(): Long = runBlocking { async { readLong(LAST_NOTIFICATION_WARNING).first() } @@ -661,6 +686,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_REGULAR_NOTIFICATION_WARNING = "show_regular_notification_warning" const val LAST_NOTIFICATION_WARNING = "last_notification_warning" private fun String.convertStringToArray(): Array { 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 From 720fdda0056a072d9fa8ce9d6e8a2793ea6b5b7f Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Mon, 2 Dec 2024 15:24:51 +0100 Subject: [PATCH 2/3] fix(detekt): naming convention of variables Signed-off-by: Andy Scherzinger --- .../com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 a76ed4415e..7ec377bc1b 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 @@ -109,9 +109,9 @@ class ChatViewModel @Inject constructor( val getVoiceRecordingLocked: LiveData get() = _getVoiceRecordingLocked - private val _voiceMessagePlaybackSpeeds: MutableLiveData> = MutableLiveData() + private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData> = MutableLiveData() val voiceMessagePlaybackSpeedPreferences: LiveData> - get() = _voiceMessagePlaybackSpeeds + get() = _voiceMessagePlaybackSpeedPreferences val getMessageFlow = chatRepository.messageFlow .onEach { @@ -651,11 +651,11 @@ class ChatViewModel @Inject constructor( } fun applyPlaybackSpeedPreferences(speeds: Map) { - _voiceMessagePlaybackSpeeds.postValue(speeds) + _voiceMessagePlaybackSpeedPreferences.postValue(speeds) } fun getPlaybackSpeedPreference(message: ChatMessage) = - _voiceMessagePlaybackSpeeds.value?.get(message.user.id) ?: PlaybackSpeed.NORMAL + _voiceMessagePlaybackSpeedPreferences.value?.get(message.user.id) ?: PlaybackSpeed.NORMAL // inner class GetRoomObserver : Observer { // override fun onSubscribe(d: Disposable) { From 5011649499a1e7fc406670683efe936bb6e15753 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Mon, 2 Dec 2024 15:25:15 +0100 Subject: [PATCH 3/3] fix(detekt): shorten test method by extracting conversation object creation Signed-off-by: Andy Scherzinger --- .../talk/utils/ParticipantPermissionsTest.kt | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt b/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt index 0e39963f71..239b2264b2 100644 --- a/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt +++ b/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt @@ -21,7 +21,34 @@ class ParticipantPermissionsTest : TestCase() { @Test fun test_areFlagsSet() { val spreedCapability = SpreedCapability() - val conversation = Conversation( + val conversation = createConversation() + + conversation.permissions = ParticipantPermissions.PUBLISH_SCREEN or + ParticipantPermissions.JOIN_CALL or + ParticipantPermissions.DEFAULT + + val user = User() + user.id = 1 + + val attendeePermissions = + ParticipantPermissions( + spreedCapability, + ConversationModel.mapToConversationModel(conversation, user) + ) + + assert(attendeePermissions.canPublishScreen) + assert(attendeePermissions.canJoinCall) + assert(attendeePermissions.isDefault) + + assertFalse(attendeePermissions.isCustom) + assertFalse(attendeePermissions.canStartCall()) + assertFalse(attendeePermissions.canIgnoreLobby()) + assertTrue(attendeePermissions.canPublishAudio()) + assertTrue(attendeePermissions.canPublishVideo()) + } + + private fun createConversation(): Conversation { + return Conversation( token = "test", name = "test", displayName = "test", @@ -67,28 +94,5 @@ class ParticipantPermissionsTest : TestCase() { remoteServer = "", remoteToken = "" ) - - conversation.permissions = ParticipantPermissions.PUBLISH_SCREEN or - ParticipantPermissions.JOIN_CALL or - ParticipantPermissions.DEFAULT - - val user = User() - user.id = 1 - - val attendeePermissions = - ParticipantPermissions( - spreedCapability, - ConversationModel.mapToConversationModel(conversation, user) - ) - - assert(attendeePermissions.canPublishScreen) - assert(attendeePermissions.canJoinCall) - assert(attendeePermissions.isDefault) - - assertFalse(attendeePermissions.isCustom) - assertFalse(attendeePermissions.canStartCall()) - assertFalse(attendeePermissions.canIgnoreLobby()) - assertTrue(attendeePermissions.canPublishAudio()) - assertTrue(attendeePermissions.canPublishVideo()) } }