From 6a88c61d1253e6b7f3d4de9c54da789213c6c896 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 21 Oct 2022 14:49:29 +0200 Subject: [PATCH 001/215] Group voice broadcast controller buttons in a Flow --- ...e_event_voice_broadcast_listening_stub.xml | 21 +++++++++---------- ...e_event_voice_broadcast_recording_stub.xml | 21 ++++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 248c04a2f6..97f15967e1 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -118,6 +118,15 @@ app:barrierMargin="12dp" app:constraint_referenced_ids="roomAvatarImageView,titleText,broadcasterViewGroup,voiceBroadcastViewGroup" /> + + - - + android:indeterminateTint="?vctr_content_secondary" /> diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml index e3bb85138d..7b45a194e8 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml @@ -64,6 +64,15 @@ app:barrierMargin="12dp" app:constraint_referenced_ids="roomAvatarImageView,titleText" /> + + + android:src="@drawable/ic_recording_dot" /> + android:src="@drawable/ic_stop" /> From 1566adb66992894f2713a06a73f5340aca19ead2 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 14:10:51 +0200 Subject: [PATCH 002/215] Timeline - Add abstraction on voice broadcast items --- .../src/main/res/values/donottranslate.xml | 1 + ...stylable_voice_broadcast_metadata_view.xml | 9 ++ .../factory/VoiceBroadcastItemFactory.kt | 4 + .../item/AbsMessageVoiceBroadcastItem.kt | 96 ++++++++++++++++ .../MessageVoiceBroadcastListeningItem.kt | 104 +++--------------- .../MessageVoiceBroadcastRecordingItem.kt | 54 ++------- .../voicebroadcast/VoiceBroadcastPlayer.kt | 29 +++-- .../views/VoiceBroadcastMetadataView.kt | 66 +++++++++++ vector/src/main/res/drawable/ic_timer.xml | 9 ++ .../res/drawable/ic_voice_broadcast_16.xml | 21 ---- .../res/drawable/ic_voice_broadcast_mic.xml | 12 ++ ...e_event_voice_broadcast_listening_stub.xml | 77 +++++-------- ...e_event_voice_broadcast_recording_stub.xml | 36 +++++- .../layout/view_voice_broadcast_metadata.xml | 27 +++++ 14 files changed, 329 insertions(+), 216 deletions(-) create mode 100644 library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt create mode 100644 vector/src/main/res/drawable/ic_timer.xml delete mode 100644 vector/src/main/res/drawable/ic_voice_broadcast_16.xml create mode 100644 vector/src/main/res/drawable/ic_voice_broadcast_mic.xml create mode 100644 vector/src/main/res/layout/view_voice_broadcast_metadata.xml diff --git a/library/ui-strings/src/main/res/values/donottranslate.xml b/library/ui-strings/src/main/res/values/donottranslate.xml index 741d23dbc6..bfe751ef5a 100755 --- a/library/ui-strings/src/main/res/values/donottranslate.xml +++ b/library/ui-strings/src/main/res/values/donottranslate.xml @@ -2,6 +2,7 @@ + Not implemented yet in ${app_name} diff --git a/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml b/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml new file mode 100644 index 0000000000..1f72eeb396 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 5dc601a91a..7b8c927186 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -67,6 +67,7 @@ class VoiceBroadcastItemFactory @Inject constructor( createRecordingItem( params.event.roomId, eventsGroup.groupId, + mostRecentMessageContent.voiceBroadcastState, highlight, callback, attributes @@ -87,6 +88,7 @@ class VoiceBroadcastItemFactory @Inject constructor( private fun createRecordingItem( roomId: String, voiceBroadcastId: String, + voiceBroadcastState: VoiceBroadcastState?, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, @@ -100,6 +102,8 @@ class VoiceBroadcastItemFactory @Inject constructor( .colorProvider(colorProvider) .drawableProvider(drawableProvider) .voiceBroadcastRecorder(voiceBroadcastRecorder) + .voiceBroadcastId(voiceBroadcastId) + .voiceBroadcastState(voiceBroadcastState) .leftGuideline(avatarSizeProvider.leftGuideline) .callback(callback) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt new file mode 100644 index 0000000000..cbf35e89d2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 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.widget.ImageView +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import im.vector.app.R +import im.vector.app.core.extensions.tintBackground +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.DrawableProvider +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import org.matrix.android.sdk.api.util.MatrixItem + +abstract class AbsMessageVoiceBroadcastItem : AbsMessageItem() { + + @EpoxyAttribute + var callback: TimelineEventController.Callback? = null + + @EpoxyAttribute + lateinit var colorProvider: ColorProvider + + @EpoxyAttribute + lateinit var drawableProvider: DrawableProvider + + @EpoxyAttribute + lateinit var voiceBroadcastId: String + + @EpoxyAttribute + var voiceBroadcastState: VoiceBroadcastState? = null + + @EpoxyAttribute + var roomItem: MatrixItem? = null + + override fun isCacheable(): Boolean = false + + override fun bind(holder: H) { + super.bind(holder) + renderHeader(holder) + } + + private fun renderHeader(holder: H) { + with(holder) { + roomItem?.let { + attributes.avatarRenderer.render(it, roomAvatarImageView) + titleText.text = it.displayName + } + } + renderLiveIcon(holder) + renderMetadata(holder) + } + + private fun renderLiveIcon(holder: H) { + with(holder) { + when (voiceBroadcastState) { + VoiceBroadcastState.STARTED, + VoiceBroadcastState.RESUMED -> { + liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) + liveIndicator.isVisible = true + } + VoiceBroadcastState.PAUSED -> { + liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary)) + liveIndicator.isVisible = true + } + VoiceBroadcastState.STOPPED, null -> { + liveIndicator.isVisible = false + } + } + } + } + + abstract fun renderMetadata(holder: H) + + abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) { + val liveIndicator by bind(R.id.liveIndicator) + val roomAvatarImageView by bind(R.id.roomAvatarImageView) + val titleText by bind(R.id.titleText) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 5b58dda4e6..135053d9a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -18,56 +18,26 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick -import im.vector.app.core.extensions.tintBackground -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.home.room.detail.RoomDetailAction -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer -import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import org.matrix.android.sdk.api.util.MatrixItem +import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass -abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem() { - - @EpoxyAttribute - var callback: TimelineEventController.Callback? = null +abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem() { @EpoxyAttribute var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null - @EpoxyAttribute - lateinit var voiceBroadcastId: String - - @EpoxyAttribute - var voiceBroadcastState: VoiceBroadcastState? = null - @EpoxyAttribute var broadcasterName: String? = null - @EpoxyAttribute - lateinit var colorProvider: ColorProvider - - @EpoxyAttribute - lateinit var drawableProvider: DrawableProvider - - @EpoxyAttribute - var roomItem: MatrixItem? = null - - @EpoxyAttribute - var title: String? = null - private lateinit var playerListener: VoiceBroadcastPlayer.Listener - override fun isCacheable(): Boolean = false - override fun bind(holder: Holder) { super.bind(holder) bindVoiceBroadcastItem(holder) @@ -75,51 +45,20 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem - renderState(holder, state) + renderPlayingState(holder, state) } - voiceBroadcastPlayer?.addListener(playerListener) - renderHeader(holder) - renderLiveIcon(holder) + voiceBroadcastPlayer?.addListener(voiceBroadcastId, playerListener) } - private fun renderHeader(holder: Holder) { + override fun renderMetadata(holder: Holder) { with(holder) { - roomItem?.let { - attributes.avatarRenderer.render(it, roomAvatarImageView) - titleText.text = it.displayName - } - broadcasterNameText.text = broadcasterName + broadcasterNameMetadata.value = broadcasterName.orEmpty() + voiceBroadcastMetadata.isVisible = true + listenersCountMetadata.isVisible = false } } - private fun renderLiveIcon(holder: Holder) { - with(holder) { - when (voiceBroadcastState) { - VoiceBroadcastState.STARTED, - VoiceBroadcastState.RESUMED -> { - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) - liveIndicator.isVisible = true - } - VoiceBroadcastState.PAUSED -> { - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary)) - liveIndicator.isVisible = true - } - VoiceBroadcastState.STOPPED, null -> { - liveIndicator.isVisible = false - } - } - } - } - - private fun renderState(holder: Holder, state: VoiceBroadcastPlayer.State) { - if (isCurrentMediaActive()) { - renderActiveMedia(holder, state) - } else { - renderInactiveMedia(holder) - } - } - - private fun renderActiveMedia(holder: Holder, state: VoiceBroadcastPlayer.State) { + private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) { with(holder) { bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING @@ -143,34 +82,19 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem(R.id.liveIndicator) - val roomAvatarImageView by bind(R.id.roomAvatarImageView) - val titleText by bind(R.id.titleText) + class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) { val playPauseButton by bind(R.id.playPauseButton) val bufferingView by bind(R.id.bufferingView) - val broadcasterNameText by bind(R.id.broadcasterNameText) + val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata) + val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata) + val listenersCountMetadata by bind(R.id.listenersCountMetadata) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index c417053b2a..b766698851 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -17,46 +17,23 @@ package im.vector.app.features.home.room.detail.timeline.item import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick -import im.vector.app.core.extensions.tintBackground -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder -import org.matrix.android.sdk.api.util.MatrixItem +import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass -abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem() { - - @EpoxyAttribute - var callback: TimelineEventController.Callback? = null +abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem() { @EpoxyAttribute var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null - @EpoxyAttribute - lateinit var colorProvider: ColorProvider - - @EpoxyAttribute - lateinit var drawableProvider: DrawableProvider - - @EpoxyAttribute - var roomItem: MatrixItem? = null - - @EpoxyAttribute - var title: String? = null - private lateinit var recorderListener: VoiceBroadcastRecorder.Listener - override fun isCacheable(): Boolean = false - override fun bind(holder: Holder) { super.bind(holder) bindVoiceBroadcastItem(holder) @@ -65,32 +42,26 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem { stopRecordButton.isEnabled = true recordButton.isEnabled = true - liveIndicator.isVisible = true - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) - val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) recordButton.setImageDrawable(drawable) @@ -102,9 +73,6 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem { recordButton.isEnabled = false stopRecordButton.isEnabled = false - liveIndicator.isVisible = false } } } @@ -126,10 +93,9 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem(R.id.liveIndicator) - val roomAvatarImageView by bind(R.id.roomAvatarImageView) - val titleText by bind(R.id.titleText) + class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) { + val listenersCountMetadata by bind(R.id.listenersCountMetadata) + val remainingTimeMetadata by bind(R.id.remainingTimeMetadata) val recordButton by bind(R.id.recordButton) val stopRecordButton by bind(R.id.stopRecordButton) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt index 2c892c8306..6545948021 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt @@ -82,10 +82,17 @@ class VoiceBroadcastPlayer @Inject constructor( set(value) { Timber.w("## VoiceBroadcastPlayer state: $field -> $value") field = value - listeners.forEach { it.onStateChanged(value) } + // Notify state change to all the listeners attached to the current voice broadcast id + currentVoiceBroadcastId?.let { voiceBroadcastId -> + listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } + } } private var currentRoomId: String? = null - private var listeners = CopyOnWriteArrayList() + + /** + * Map voiceBroadcastId to listeners + */ + private var listeners: MutableMap> = mutableMapOf() fun playOrResume(roomId: String, eventId: String) { val hasChanged = currentVoiceBroadcastId != eventId @@ -133,13 +140,21 @@ class VoiceBroadcastPlayer @Inject constructor( currentVoiceBroadcastId = null } - fun addListener(listener: Listener) { - listeners.add(listener) - listener.onStateChanged(state) + /** + * Add a [Listener] to the given voice broadcast id. + */ + fun addListener(voiceBroadcastId: String, listener: Listener) { + listeners[voiceBroadcastId]?.add(listener) ?: run { + listeners[voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } + } + if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(state) else listener.onStateChanged(State.IDLE) } - fun removeListener(listener: Listener) { - listeners.remove(listener) + /** + * Remove a [Listener] from the given voice broadcast id. + */ + fun removeListener(voiceBroadcastId: String, listener: Listener) { + listeners[voiceBroadcastId]?.remove(listener) } private fun startPlayback(roomId: String, eventId: String) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt new file mode 100644 index 0000000000..e142cb15ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 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.voicebroadcast.views + +import android.content.Context +import android.content.res.TypedArray +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.core.content.res.use +import im.vector.app.R +import im.vector.app.databinding.ViewVoiceBroadcastMetadataBinding + +class VoiceBroadcastMetadataView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private val views = ViewVoiceBroadcastMetadataBinding.inflate( + LayoutInflater.from(context), + this + ) + + var value: String + get() = views.metadataValue.text.toString() + set(newValue) { + views.metadataValue.text = newValue + } + + init { + context.obtainStyledAttributes( + attrs, + R.styleable.VoiceBroadcastMetadataView, + 0, + 0 + ).use { + setIcon(it) + setValue(it) + } + } + + private fun setIcon(typedArray: TypedArray) { + val icon = typedArray.getDrawable(R.styleable.VoiceBroadcastMetadataView_metadataIcon) + views.metadataIcon.setImageDrawable(icon) + } + + private fun setValue(typedArray: TypedArray) { + val value = typedArray.getString(R.styleable.VoiceBroadcastMetadataView_metadataValue) + views.metadataValue.text = value + } +} diff --git a/vector/src/main/res/drawable/ic_timer.xml b/vector/src/main/res/drawable/ic_timer.xml new file mode 100644 index 0000000000..11a42b0696 --- /dev/null +++ b/vector/src/main/res/drawable/ic_timer.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_16.xml b/vector/src/main/res/drawable/ic_voice_broadcast_16.xml deleted file mode 100644 index 7d427a56d0..0000000000 --- a/vector/src/main/res/drawable/ic_voice_broadcast_16.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml b/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml new file mode 100644 index 0000000000..edadb55b81 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 97f15967e1..16a5b17d68 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -7,8 +7,7 @@ android:layout_height="wrap_content" android:background="@drawable/rounded_rect_shape_8" android:backgroundTint="?vctr_content_quinary" - android:padding="@dimen/layout_vertical_margin" - tools:viewBindingIgnore="true"> + android:padding="@dimen/layout_vertical_margin"> @@ -54,61 +53,41 @@ android:contentDescription="@string/avatar" app:layout_constraintStart_toEndOf="@id/avatarRightBarrier" app:layout_constraintTop_toTopOf="parent" - tools:src="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/name" /> - + app:layout_constraintTop_toBottomOf="@id/titleText" /> - - - - - - + app:metadataIcon="@drawable/ic_voice_broadcast_mic" + tools:metadataValue="@sample/users.json/data/displayName" /> - + - - + + app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" /> + android:padding="@dimen/layout_vertical_margin"> @@ -54,7 +53,34 @@ android:contentDescription="@string/avatar" app:layout_constraintStart_toEndOf="@id/avatarRightBarrier" app:layout_constraintTop_toTopOf="parent" - tools:src="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + + + + + + + app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" /> + + + + + + From 4defc3dded84ac411544e5d8f8f8173becbc4509 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 14:50:56 +0200 Subject: [PATCH 003/215] Voice Broadcast - Add style for the "live" indicator --- .../res/values/styles_voice_broadcast.xml | 19 +++++++++++++++++ .../main/res/drawable/ic_voice_broadcast.xml | 21 +++++++++++++++++++ ...e_event_voice_broadcast_listening_stub.xml | 16 +++----------- ...e_event_voice_broadcast_recording_stub.xml | 14 ++----------- .../layout/view_voice_broadcast_metadata.xml | 2 +- 5 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 library/ui-styles/src/main/res/values/styles_voice_broadcast.xml create mode 100644 vector/src/main/res/drawable/ic_voice_broadcast.xml diff --git a/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml b/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml new file mode 100644 index 0000000000..eb85378141 --- /dev/null +++ b/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_voice_broadcast.xml b/vector/src/main/res/drawable/ic_voice_broadcast.xml new file mode 100644 index 0000000000..7d427a56d0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_broadcast.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 16a5b17d68..d508569cb0 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -11,20 +11,10 @@ @@ -78,7 +68,7 @@ android:id="@+id/voiceBroadcastMetadata" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:metadataIcon="@drawable/ic_attachment_voice_broadcast" + app:metadataIcon="@drawable/ic_voice_broadcast" app:metadataValue="@string/attachment_type_voice_broadcast" /> diff --git a/vector/src/main/res/layout/view_voice_broadcast_metadata.xml b/vector/src/main/res/layout/view_voice_broadcast_metadata.xml index 4f0c584d5c..3bc31cd9a0 100644 --- a/vector/src/main/res/layout/view_voice_broadcast_metadata.xml +++ b/vector/src/main/res/layout/view_voice_broadcast_metadata.xml @@ -15,7 +15,7 @@ android:layout_marginEnd="4dp" android:contentDescription="@null" app:tint="?vctr_content_secondary" - tools:src="@drawable/ic_attachment_voice_broadcast" /> + tools:src="@drawable/ic_voice_broadcast" /> Date: Mon, 24 Oct 2022 16:35:16 +0200 Subject: [PATCH 004/215] Improve VoiceBroadcastItemFactory --- .../factory/VoiceBroadcastItemFactory.kt | 48 +++++++++---------- .../timeline/helper/TimelineEventsGroups.kt | 3 ++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 7b8c927186..b639a2dbae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -15,14 +15,14 @@ */ package im.vector.app.features.home.room.detail.timeline.factory -import im.vector.app.core.epoxy.VectorEpoxyHolder -import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.app.features.home.room.detail.timeline.item.AbsMessageVoiceBroadcastItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem @@ -34,7 +34,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.getUser +import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -53,31 +53,31 @@ class VoiceBroadcastItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, - ): VectorEpoxyModel? { + ): AbsMessageVoiceBroadcastItem<*>? { // Only display item of the initial event with updated data if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null - val eventsGroup = params.eventsGroup ?: return null - val voiceBroadcastEventsGroup = VoiceBroadcastEventsGroup(eventsGroup) - val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent() - val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent() - val mostRecentMessageContent = mostRecentEvent?.content ?: return null - val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId - val recorderName = mostRecentTimelineEvent.root.stateKey?.let { session.getUser(it) }?.displayName ?: mostRecentTimelineEvent.root.stateKey + + val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null + val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null + val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null + val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId + + val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId + return if (isRecording) { createRecordingItem( - params.event.roomId, - eventsGroup.groupId, - mostRecentMessageContent.voiceBroadcastState, + params, + voiceBroadcastId, + voiceBroadcastContent.voiceBroadcastState, highlight, callback, attributes ) } else { createListeningItem( - params.event.roomId, - eventsGroup.groupId, - mostRecentMessageContent.voiceBroadcastState, - recorderName, + params, + voiceBroadcastId, + voiceBroadcastContent.voiceBroadcastState, highlight, callback, attributes @@ -86,14 +86,14 @@ class VoiceBroadcastItemFactory @Inject constructor( } private fun createRecordingItem( - roomId: String, + params: TimelineItemFactoryParams, voiceBroadcastId: String, voiceBroadcastState: VoiceBroadcastState?, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, ): MessageVoiceBroadcastRecordingItem { - val roomSummary = session.getRoom(roomId)?.roomSummary() + val roomSummary = session.getRoom(params.event.roomId)?.roomSummary() return MessageVoiceBroadcastRecordingItem_() .id("voice_broadcast_$voiceBroadcastId") .attributes(attributes) @@ -109,15 +109,15 @@ class VoiceBroadcastItemFactory @Inject constructor( } private fun createListeningItem( - roomId: String, + params: TimelineItemFactoryParams, voiceBroadcastId: String, voiceBroadcastState: VoiceBroadcastState?, - broadcasterName: String?, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, ): MessageVoiceBroadcastListeningItem { - val roomSummary = session.getRoom(roomId)?.roomSummary() + val roomSummary = session.getRoom(params.event.roomId)?.roomSummary() + val recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName() return MessageVoiceBroadcastListeningItem_() .id("voice_broadcast_$voiceBroadcastId") .attributes(attributes) @@ -128,7 +128,7 @@ class VoiceBroadcastItemFactory @Inject constructor( .voiceBroadcastPlayer(voiceBroadcastPlayer) .voiceBroadcastId(voiceBroadcastId) .voiceBroadcastState(voiceBroadcastState) - .broadcasterName(broadcasterName) + .broadcasterName(recorderName) .leftGuideline(avatarSizeProvider.leftGuideline) .callback(callback) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index d8817c1f44..8a3be7d5f2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -141,6 +141,9 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) { } class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { + + val voiceBroadcastId = group.groupId + fun getLastDisplayableEvent(): TimelineEvent { return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } ?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L } From 2c144614cabc6427319ebfcb3143ab176b6d565a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 16:49:59 +0200 Subject: [PATCH 005/215] Improve recording state rendering if app has been relaunched --- .../MessageVoiceBroadcastRecordingItem.kt | 87 +++++++++++-------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index b766698851..183d2a5577 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -24,6 +24,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass @@ -32,7 +33,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem @EpoxyAttribute var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null - private lateinit var recorderListener: VoiceBroadcastRecorder.Listener + private var recorderListener: VoiceBroadcastRecorder.Listener? = null override fun bind(holder: Holder) { super.bind(holder) @@ -40,12 +41,15 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - recorderListener = object : VoiceBroadcastRecorder.Listener { - override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { - renderRecordingState(holder, state) - } + if (voiceBroadcastRecorder != null && voiceBroadcastRecorder?.state != VoiceBroadcastRecorder.State.Idle) { + recorderListener = object : VoiceBroadcastRecorder.Listener { + override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { + renderRecordingState(holder, state) + } + }.also { voiceBroadcastRecorder?.addListener(it) } + } else { + renderVoiceBroadcastState(holder) } - voiceBroadcastRecorder?.addListener(recorderListener) } override fun renderMetadata(holder: Holder) { @@ -56,39 +60,54 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) { - with(holder) { - when (state) { - VoiceBroadcastRecorder.State.Recording -> { - stopRecordButton.isEnabled = true - recordButton.isEnabled = true - - val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) - val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) - recordButton.setImageDrawable(drawable) - recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record) - recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } - stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } - } - VoiceBroadcastRecorder.State.Paused -> { - stopRecordButton.isEnabled = true - recordButton.isEnabled = true - - recordButton.setImageResource(R.drawable.ic_recording_dot) - recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record) - recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } - stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } - } - VoiceBroadcastRecorder.State.Idle -> { - recordButton.isEnabled = false - stopRecordButton.isEnabled = false - } - } + when (state) { + VoiceBroadcastRecorder.State.Recording -> renderPlayingState(holder) + VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder) + VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder) } } + private fun renderVoiceBroadcastState(holder: Holder) { + when (voiceBroadcastState) { + VoiceBroadcastState.STARTED, + VoiceBroadcastState.RESUMED -> renderPlayingState(holder) + VoiceBroadcastState.PAUSED -> renderPausedState(holder) + VoiceBroadcastState.STOPPED, + null -> renderStoppedState(holder) + } + } + + private fun renderPlayingState(holder: Holder) = with(holder) { + stopRecordButton.isEnabled = true + recordButton.isEnabled = true + + val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) + val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) + recordButton.setImageDrawable(drawable) + recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record) + recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } + stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } + } + + private fun renderPausedState(holder: Holder) = with(holder) { + stopRecordButton.isEnabled = true + recordButton.isEnabled = true + + recordButton.setImageResource(R.drawable.ic_recording_dot) + recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record) + recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } + stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } + } + + private fun renderStoppedState(holder: Holder) = with(holder) { + recordButton.isEnabled = false + stopRecordButton.isEnabled = false + } + override fun unbind(holder: Holder) { super.unbind(holder) - voiceBroadcastRecorder?.removeListener(recorderListener) + recorderListener?.let { voiceBroadcastRecorder?.removeListener(it) } + recorderListener = null } override fun getViewStubId() = STUB_ID From f31429cf25ca232344715d7a39906472e2e290be Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 15:22:16 +0200 Subject: [PATCH 006/215] Rename renderLiveIcon method --- .../room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index cbf35e89d2..afe705ffb6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -63,11 +63,11 @@ abstract class AbsMessageVoiceBroadcastItem Date: Tue, 25 Oct 2022 16:29:42 +0200 Subject: [PATCH 007/215] Move voice broadcast item attributes to dedicated class --- .../timeline/factory/MessageItemFactory.kt | 2 +- .../factory/VoiceBroadcastItemFactory.kt | 65 ++++++------------- .../item/AbsMessageVoiceBroadcastItem.kt | 42 +++++++----- .../MessageVoiceBroadcastListeningItem.kt | 17 ++--- .../MessageVoiceBroadcastRecordingItem.kt | 18 ++--- 5 files changed, 57 insertions(+), 87 deletions(-) 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 245d92f95b..f4d506fa4b 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 @@ -201,7 +201,7 @@ class MessageItemFactory @Inject constructor( is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) - is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, callback, attributes) + is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } return messageItem?.apply { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index b639a2dbae..d43ccd9834 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.displayname.getBestName -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem @@ -51,7 +50,6 @@ class VoiceBroadcastItemFactory @Inject constructor( params: TimelineItemFactoryParams, messageContent: MessageVoiceBroadcastInfoContent, highlight: Boolean, - callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, ): AbsMessageVoiceBroadcastItem<*>? { // Only display item of the initial event with updated data @@ -64,72 +62,47 @@ class VoiceBroadcastItemFactory @Inject constructor( val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId + val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( + voiceBroadcastId = voiceBroadcastId, + voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, + recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), + recorder = voiceBroadcastRecorder, + player = voiceBroadcastPlayer, + roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(), + colorProvider = colorProvider, + drawableProvider = drawableProvider, + ) + return if (isRecording) { - createRecordingItem( - params, - voiceBroadcastId, - voiceBroadcastContent.voiceBroadcastState, - highlight, - callback, - attributes - ) + createRecordingItem(highlight, attributes, voiceBroadcastAttributes) } else { - createListeningItem( - params, - voiceBroadcastId, - voiceBroadcastContent.voiceBroadcastState, - highlight, - callback, - attributes - ) + createListeningItem(highlight, attributes, voiceBroadcastAttributes) } } private fun createRecordingItem( - params: TimelineItemFactoryParams, - voiceBroadcastId: String, - voiceBroadcastState: VoiceBroadcastState?, highlight: Boolean, - callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, + voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes, ): MessageVoiceBroadcastRecordingItem { - val roomSummary = session.getRoom(params.event.roomId)?.roomSummary() return MessageVoiceBroadcastRecordingItem_() - .id("voice_broadcast_$voiceBroadcastId") + .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}") .attributes(attributes) + .voiceBroadcastAttributes(voiceBroadcastAttributes) .highlighted(highlight) - .roomItem(roomSummary?.toMatrixItem()) - .colorProvider(colorProvider) - .drawableProvider(drawableProvider) - .voiceBroadcastRecorder(voiceBroadcastRecorder) - .voiceBroadcastId(voiceBroadcastId) - .voiceBroadcastState(voiceBroadcastState) .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) } private fun createListeningItem( - params: TimelineItemFactoryParams, - voiceBroadcastId: String, - voiceBroadcastState: VoiceBroadcastState?, highlight: Boolean, - callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, + voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes, ): MessageVoiceBroadcastListeningItem { - val roomSummary = session.getRoom(params.event.roomId)?.roomSummary() - val recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName() return MessageVoiceBroadcastListeningItem_() - .id("voice_broadcast_$voiceBroadcastId") + .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}") .attributes(attributes) + .voiceBroadcastAttributes(voiceBroadcastAttributes) .highlighted(highlight) - .roomItem(roomSummary?.toMatrixItem()) - .colorProvider(colorProvider) - .drawableProvider(drawableProvider) - .voiceBroadcastPlayer(voiceBroadcastPlayer) - .voiceBroadcastId(voiceBroadcastId) - .voiceBroadcastState(voiceBroadcastState) - .broadcasterName(recorderName) .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index afe705ffb6..45f10b68d0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -25,29 +25,26 @@ import im.vector.app.R import im.vector.app.core.extensions.tintBackground import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import org.matrix.android.sdk.api.util.MatrixItem abstract class AbsMessageVoiceBroadcastItem : AbsMessageItem() { @EpoxyAttribute - var callback: TimelineEventController.Callback? = null + lateinit var voiceBroadcastAttributes: Attributes - @EpoxyAttribute - lateinit var colorProvider: ColorProvider - - @EpoxyAttribute - lateinit var drawableProvider: DrawableProvider - - @EpoxyAttribute - lateinit var voiceBroadcastId: String - - @EpoxyAttribute - var voiceBroadcastState: VoiceBroadcastState? = null - - @EpoxyAttribute - var roomItem: MatrixItem? = null + protected val voiceBroadcastId get() = voiceBroadcastAttributes.voiceBroadcastId + protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState + protected val recorderName get() = voiceBroadcastAttributes.recorderName + protected val recorder get() = voiceBroadcastAttributes.recorder + protected val player get() = voiceBroadcastAttributes.player + protected val roomItem get() = voiceBroadcastAttributes.roomItem + protected val colorProvider get() = voiceBroadcastAttributes.colorProvider + protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider + protected val avatarRenderer get() = attributes.avatarRenderer + protected val callback get() = attributes.callback override fun isCacheable(): Boolean = false @@ -59,7 +56,7 @@ abstract class AbsMessageVoiceBroadcastItem(R.id.roomAvatarImageView) val titleText by bind(R.id.titleText) } + + data class Attributes( + val voiceBroadcastId: String, + val voiceBroadcastState: VoiceBroadcastState?, + val recorderName: String, + val recorder: VoiceBroadcastRecorder?, + val player: VoiceBroadcastPlayer, + val roomItem: MatrixItem?, + val colorProvider: ColorProvider, + val drawableProvider: DrawableProvider, + ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 135053d9a9..d94bee3672 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageButton import androidx.core.view.isVisible -import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick @@ -30,12 +29,6 @@ import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem() { - @EpoxyAttribute - var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null - - @EpoxyAttribute - var broadcasterName: String? = null - private lateinit var playerListener: VoiceBroadcastPlayer.Listener override fun bind(holder: Holder) { @@ -47,12 +40,12 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem playerListener = VoiceBroadcastPlayer.Listener { state -> renderPlayingState(holder, state) } - voiceBroadcastPlayer?.addListener(voiceBroadcastId, playerListener) + player.addListener(voiceBroadcastId, playerListener) } override fun renderMetadata(holder: Holder) { with(holder) { - broadcasterNameMetadata.value = broadcasterName.orEmpty() + broadcasterNameMetadata.value = recorderName voiceBroadcastMetadata.isVisible = true listenersCountMetadata.isVisible = false } @@ -67,14 +60,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem VoiceBroadcastPlayer.State.PLAYING -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) - playPauseButton.onClick { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } + playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.onClick { - attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) + callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } } VoiceBroadcastPlayer.State.BUFFERING -> Unit @@ -84,7 +77,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem override fun unbind(holder: Holder) { super.unbind(holder) - voiceBroadcastPlayer?.removeListener(voiceBroadcastId, playerListener) + player.removeListener(voiceBroadcastId, playerListener) } override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index 183d2a5577..47e89658ca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.item import android.widget.ImageButton import androidx.core.view.isVisible -import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick @@ -30,9 +29,6 @@ import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem() { - @EpoxyAttribute - var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null - private var recorderListener: VoiceBroadcastRecorder.Listener? = null override fun bind(holder: Holder) { @@ -41,12 +37,12 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - if (voiceBroadcastRecorder != null && voiceBroadcastRecorder?.state != VoiceBroadcastRecorder.State.Idle) { + if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) { recorderListener = object : VoiceBroadcastRecorder.Listener { override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { renderRecordingState(holder, state) } - }.also { voiceBroadcastRecorder?.addListener(it) } + }.also { recorder?.addListener(it) } } else { renderVoiceBroadcastState(holder) } @@ -85,8 +81,8 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) recordButton.setImageDrawable(drawable) recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record) - recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } - stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } + recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } + stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } } private fun renderPausedState(holder: Holder) = with(holder) { @@ -95,8 +91,8 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem recordButton.setImageResource(R.drawable.ic_recording_dot) recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record) - recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } - stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } + recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } + stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } } private fun renderStoppedState(holder: Holder) = with(holder) { @@ -106,7 +102,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem override fun unbind(holder: Holder) { super.unbind(holder) - recorderListener?.let { voiceBroadcastRecorder?.removeListener(it) } + recorderListener?.let { recorder?.removeListener(it) } recorderListener = null } From 513097585a7ab9592eb50d5172e30eaf3c428406 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 17:38:05 +0200 Subject: [PATCH 008/215] Fix kdoc issue --- .../vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt index 6545948021..d8a062c8f8 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt @@ -90,7 +90,7 @@ class VoiceBroadcastPlayer @Inject constructor( private var currentRoomId: String? = null /** - * Map voiceBroadcastId to listeners + * Map voiceBroadcastId to listeners. */ private var listeners: MutableMap> = mutableMapOf() From 0f21f404e694a465cfa82a0da8e178dc9a0af821 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 17:41:36 +0200 Subject: [PATCH 009/215] Add changelog --- changelog.d/7448.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7448.wip diff --git a/changelog.d/7448.wip b/changelog.d/7448.wip new file mode 100644 index 0000000000..a99e5bbcfa --- /dev/null +++ b/changelog.d/7448.wip @@ -0,0 +1 @@ +[Voice Broadcast] Improve timeline items factory and handle bad recording state display From c7c05d1fe6a293b9233276a8b3d2c68628ecd1e3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 16:35:59 +0200 Subject: [PATCH 010/215] Add check on deviceId before showing recording tile --- .../room/detail/timeline/factory/VoiceBroadcastItemFactory.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index d43ccd9834..7a7cb73471 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -60,7 +60,9 @@ class VoiceBroadcastItemFactory @Inject constructor( val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId - val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId + val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && + voiceBroadcastEvent.root.stateKey == session.myUserId && + messageContent.deviceId == session.sessionParams.deviceId val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( voiceBroadcastId = voiceBroadcastId, From a4eff0cc78d8066ca0fa31a13028b7a81b3c31ed Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 17:56:27 +0200 Subject: [PATCH 011/215] Add changelog --- changelog.d/7431.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7431.bugfix diff --git a/changelog.d/7431.bugfix b/changelog.d/7431.bugfix new file mode 100644 index 0000000000..681a1e9aa5 --- /dev/null +++ b/changelog.d/7431.bugfix @@ -0,0 +1 @@ + [Voice Broadcast] Do not display the recorder view for a live broadcast started from another session \ No newline at end of file From 6eeb54ae40dbd995a55822659c40365d288e0727 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 23:59:29 +0200 Subject: [PATCH 012/215] Stop ongoing voice broadcast on app restart --- .../features/home/HomeActivityViewModel.kt | 30 +++++++++++++ .../voicebroadcast/VoiceBroadcastHelper.kt | 4 ++ .../usecase/GetLastVoiceBroadcastUseCase.kt | 45 +++++++++++++++++++ .../usecase/StartVoiceBroadcastUseCase.kt | 10 +---- 4 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 61a8e5b79e..1e79dc5844 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -42,6 +42,8 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -60,12 +62,14 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.room.model.Membership @@ -92,6 +96,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val analyticsConfig: AnalyticsConfig, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, private val vectorFeatures: VectorFeatures, + private val voiceBroadcastHelper: VoiceBroadcastHelper, ) : VectorViewModel(initialState) { @AssistedFactory @@ -123,6 +128,7 @@ class HomeActivityViewModel @AssistedInject constructor( observeReleaseNotes() observeLocalNotificationsSilenced() initThreadsMigration() + stopOngoingVoiceBroadcast() } private fun observeReleaseNotes() = withState { state -> @@ -490,6 +496,30 @@ class HomeActivityViewModel @AssistedInject constructor( } } + /** + * Stop ongoing voice broadcast if any. + */ + private fun stopOngoingVoiceBroadcast() { + val session = activeSessionHolder.getSafeActiveSession() ?: return + + // FIXME Iterate only on recent rooms for the moment, improve this + val recentRooms = session.roomService().getBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }).mapNotNull { session.getRoom(it.roomId) } + + recentRooms + .forEach { room -> + val ongoingVoiceBroadcasts = voiceBroadcastHelper.getOngoingVoiceBroadcasts(room.roomId) + val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId + val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } + if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { + viewModelScope.launch { voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) } + return // No need to iterate more as we should not have more than one recording VB + } + } + } + override fun handle(action: HomeActivityViewActions) { when (action) { HomeActivityViewActions.PushPromptHasBeenReviewed -> { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 58e7de7f32..ee9034661c 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -16,6 +16,7 @@ package im.vector.app.features.voicebroadcast +import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase @@ -30,6 +31,7 @@ class VoiceBroadcastHelper @Inject constructor( private val pauseVoiceBroadcastUseCase: PauseVoiceBroadcastUseCase, private val resumeVoiceBroadcastUseCase: ResumeVoiceBroadcastUseCase, private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, private val voiceBroadcastPlayer: VoiceBroadcastPlayer, ) { suspend fun startVoiceBroadcast(roomId: String) = startVoiceBroadcastUseCase.execute(roomId) @@ -45,4 +47,6 @@ class VoiceBroadcastHelper @Inject constructor( fun pausePlayback() = voiceBroadcastPlayer.pause() fun stopPlayback() = voiceBroadcastPlayer.stop() + + fun getOngoingVoiceBroadcasts(roomId: String) = getOngoingVoiceBroadcastsUseCase.execute(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt new file mode 100644 index 0000000000..db2c625161 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 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.voicebroadcast.usecase + +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom +import timber.log.Timber +import javax.inject.Inject + +class GetOngoingVoiceBroadcastsUseCase @Inject constructor( + private val session: Session, +) { + + fun execute(roomId: String): List { + val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") + + Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") + + return room.stateService().getStateEvents( + setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), + QueryStringValue.IsNotEmpty + ) + .mapNotNull { it.asVoiceBroadcastEvent() } + .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt index 7934d18e36..2b7ca7b9f1 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt @@ -25,9 +25,7 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.lib.multipicker.utils.toMultiPickerAudioType -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toContent @@ -43,6 +41,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val context: Context, private val buildMeta: BuildMeta, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, ) { suspend fun execute(roomId: String): Result = runCatching { @@ -50,12 +49,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested") - val onGoingVoiceBroadcastEvents = room.stateService().getStateEvents( - setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), - QueryStringValue.IsNotEmpty - ) - .mapNotNull { it.asVoiceBroadcastEvent() } - .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + val onGoingVoiceBroadcastEvents = getOngoingVoiceBroadcastsUseCase.execute(roomId) if (onGoingVoiceBroadcastEvents.isEmpty()) { startVoiceBroadcast(room) From 53db04c8cf28d3b3ce2993367f1df986eccad962 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 17:58:09 +0200 Subject: [PATCH 013/215] Add changelog --- changelog.d/7450.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7450.wip diff --git a/changelog.d/7450.wip b/changelog.d/7450.wip new file mode 100644 index 0000000000..de4d3dc5e1 --- /dev/null +++ b/changelog.d/7450.wip @@ -0,0 +1 @@ +[Voice Broadcast] Stop recording when opening the room after an app restart From 85bc78bd72f15b2741b02b3a6f8389e0d4474761 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 09:50:58 +0200 Subject: [PATCH 014/215] Do not pause already paused voice broadcast --- .../home/room/detail/composer/MessageComposerFragment.kt | 2 +- .../home/room/detail/composer/MessageComposerViewState.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 55ec922a57..e01dd31516 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -234,7 +234,7 @@ class MessageComposerFragment : VectorBaseFragment(), A } // TODO remove this when there will be a recording indicator outside of the timeline // Pause voice broadcast if the timeline is not shown anymore - it.isVoiceBroadcasting && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause) + it.isRecordingVoiceBroadcast && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause) else -> { timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause) messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString())) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 0df1dbebd8..a4021f87b2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -79,9 +79,8 @@ data class MessageComposerViewState( is VoiceMessageRecorderView.RecordingUiState.Recording -> true } - val isVoiceBroadcasting = when (voiceBroadcastState) { + val isRecordingVoiceBroadcast = when (voiceBroadcastState) { VoiceBroadcastState.STARTED, - VoiceBroadcastState.PAUSED, VoiceBroadcastState.RESUMED -> true else -> false } From 47047b20349c423b6f070e78797ffbbd599d764c Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:00:56 +0200 Subject: [PATCH 015/215] move map operator in a new line --- .../vector/app/features/home/HomeActivityViewModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 1e79dc5844..2c45709291 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -503,10 +503,12 @@ class HomeActivityViewModel @AssistedInject constructor( val session = activeSessionHolder.getSafeActiveSession() ?: return // FIXME Iterate only on recent rooms for the moment, improve this - val recentRooms = session.roomService().getBreadcrumbs(roomSummaryQueryParams { - displayName = QueryStringValue.NoCondition - memberships = listOf(Membership.JOIN) - }).mapNotNull { session.getRoom(it.roomId) } + val recentRooms = session.roomService() + .getBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }) + .mapNotNull { session.getRoom(it.roomId) } recentRooms .forEach { room -> From ec80adc8aa416c07ebd61faff5cab5c26e7ccff4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:10:56 +0200 Subject: [PATCH 016/215] Rename usecase file --- ...iceBroadcastUseCase.kt => GetOngoingVoiceBroadcastsUseCase.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/{GetLastVoiceBroadcastUseCase.kt => GetOngoingVoiceBroadcastsUseCase.kt} (100%) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt similarity index 100% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt From 6091ec4ce3731b6498c42860d1d82dce677c2a8d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:45:25 +0200 Subject: [PATCH 017/215] Fix wrong content description --- .../timeline/item/MessageVoiceBroadcastListeningItem.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index d94bee3672..a3e7cc55d5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -59,13 +59,13 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem when (state) { VoiceBroadcastPlayer.State.PLAYING -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) - playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) + playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) - playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) + playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } From 8fe3b5e75077d21b32cf6c2ba82fc7c9e47200d7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:46:33 +0200 Subject: [PATCH 018/215] Rename method renderPlayingState to renderRecordingState --- .../timeline/item/MessageVoiceBroadcastRecordingItem.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index 47e89658ca..e3e86f38e3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -57,7 +57,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) { when (state) { - VoiceBroadcastRecorder.State.Recording -> renderPlayingState(holder) + VoiceBroadcastRecorder.State.Recording -> renderRecordingState(holder) VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder) VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder) } @@ -66,14 +66,14 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem private fun renderVoiceBroadcastState(holder: Holder) { when (voiceBroadcastState) { VoiceBroadcastState.STARTED, - VoiceBroadcastState.RESUMED -> renderPlayingState(holder) + VoiceBroadcastState.RESUMED -> renderRecordingState(holder) VoiceBroadcastState.PAUSED -> renderPausedState(holder) VoiceBroadcastState.STOPPED, null -> renderStoppedState(holder) } } - private fun renderPlayingState(holder: Holder) = with(holder) { + private fun renderRecordingState(holder: Holder) = with(holder) { stopRecordButton.isEnabled = true recordButton.isEnabled = true From 1554d79f1a57095f3f98b20ffee8e944e7d60374 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:48:11 +0200 Subject: [PATCH 019/215] Change listeners Map variable to immutable --- .../vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt index d8a062c8f8..5a04904f69 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt @@ -92,7 +92,7 @@ class VoiceBroadcastPlayer @Inject constructor( /** * Map voiceBroadcastId to listeners. */ - private var listeners: MutableMap> = mutableMapOf() + private val listeners: MutableMap> = mutableMapOf() fun playOrResume(roomId: String, eventId: String) { val hasChanged = currentVoiceBroadcastId != eventId From 2f14d191302b581a37fc6cd3cc5846d1009530b9 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 11:26:12 +0200 Subject: [PATCH 020/215] Fix failing test --- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 9fa6b7a450..f95ab2053b 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -49,10 +49,11 @@ class StartVoiceBroadcastUseCaseTest { private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeVoiceBroadcastRecorder = mockk(relaxed = true) private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase( - fakeSession, - fakeVoiceBroadcastRecorder, - FakeContext().instance, - mockk() + session = fakeSession, + voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, + context = FakeContext().instance, + buildMeta = mockk(), + getOngoingVoiceBroadcastsUseCase = GetOngoingVoiceBroadcastsUseCase(fakeSession), ) @Test From 5855fe1242d7e99317b79435a541bf2bd68980ef Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 12:44:18 +0200 Subject: [PATCH 021/215] Add StopOngoingVoiceBroadcastUseCase --- .../features/home/HomeActivityViewModel.kt | 35 +---------- .../StopOngoingVoiceBroadcastUseCase.kt | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 2c45709291..c3abdde022 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -42,8 +42,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper -import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.usecase.StopOngoingVoiceBroadcastUseCase import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -62,14 +61,12 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.room.model.Membership @@ -96,7 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val analyticsConfig: AnalyticsConfig, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, private val vectorFeatures: VectorFeatures, - private val voiceBroadcastHelper: VoiceBroadcastHelper, + private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -128,7 +125,7 @@ class HomeActivityViewModel @AssistedInject constructor( observeReleaseNotes() observeLocalNotificationsSilenced() initThreadsMigration() - stopOngoingVoiceBroadcast() + viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() } } private fun observeReleaseNotes() = withState { state -> @@ -496,32 +493,6 @@ class HomeActivityViewModel @AssistedInject constructor( } } - /** - * Stop ongoing voice broadcast if any. - */ - private fun stopOngoingVoiceBroadcast() { - val session = activeSessionHolder.getSafeActiveSession() ?: return - - // FIXME Iterate only on recent rooms for the moment, improve this - val recentRooms = session.roomService() - .getBreadcrumbs(roomSummaryQueryParams { - displayName = QueryStringValue.NoCondition - memberships = listOf(Membership.JOIN) - }) - .mapNotNull { session.getRoom(it.roomId) } - - recentRooms - .forEach { room -> - val ongoingVoiceBroadcasts = voiceBroadcastHelper.getOngoingVoiceBroadcasts(room.roomId) - val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId - val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } - if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { - viewModelScope.launch { voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) } - return // No need to iterate more as we should not have more than one recording VB - } - } - } - override fun handle(action: HomeActivityViewActions) { when (action) { HomeActivityViewActions.PushPromptHasBeenReviewed -> { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt new file mode 100644 index 0000000000..82baa5e6a8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 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.voicebroadcast.usecase + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import timber.log.Timber +import javax.inject.Inject + +/** + * Stop ongoing voice broadcast if any. + */ +class StopOngoingVoiceBroadcastUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val voiceBroadcastHelper: VoiceBroadcastHelper, +) { + + suspend fun execute() { + Timber.d("## StopOngoingVoiceBroadcastUseCase: Stop ongoing voice broadcast requested") + + val session = activeSessionHolder.getSafeActiveSession() ?: run { + Timber.w("## StopOngoingVoiceBroadcastUseCase: no active session") + return + } + // FIXME Iterate only on recent rooms for the moment, improve this + val recentRooms = session.roomService() + .getBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }) + .mapNotNull { session.getRoom(it.roomId) } + + recentRooms + .forEach { room -> + val ongoingVoiceBroadcasts = voiceBroadcastHelper.getOngoingVoiceBroadcasts(room.roomId) + val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId + val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } + if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { + voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) + return // No need to iterate more as we should not have more than one recording VB + } + } + } +} From 443d573205bec14e5a7f12d0220da0a659194f4c Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 12:48:32 +0200 Subject: [PATCH 022/215] Remove getOngoingVoiceBroadcasts from VoiceBroadcastHelper --- .../app/features/voicebroadcast/VoiceBroadcastHelper.kt | 4 ---- .../usecase/StopOngoingVoiceBroadcastUseCase.kt | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index ee9034661c..58e7de7f32 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -16,7 +16,6 @@ package im.vector.app.features.voicebroadcast -import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase @@ -31,7 +30,6 @@ class VoiceBroadcastHelper @Inject constructor( private val pauseVoiceBroadcastUseCase: PauseVoiceBroadcastUseCase, private val resumeVoiceBroadcastUseCase: ResumeVoiceBroadcastUseCase, private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase, - private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, private val voiceBroadcastPlayer: VoiceBroadcastPlayer, ) { suspend fun startVoiceBroadcast(roomId: String) = startVoiceBroadcastUseCase.execute(roomId) @@ -47,6 +45,4 @@ class VoiceBroadcastHelper @Inject constructor( fun pausePlayback() = voiceBroadcastPlayer.pause() fun stopPlayback() = voiceBroadcastPlayer.stop() - - fun getOngoingVoiceBroadcasts(roomId: String) = getOngoingVoiceBroadcastsUseCase.execute(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt index 82baa5e6a8..ab4d16ab60 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -31,6 +31,7 @@ import javax.inject.Inject */ class StopOngoingVoiceBroadcastUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, private val voiceBroadcastHelper: VoiceBroadcastHelper, ) { @@ -51,7 +52,7 @@ class StopOngoingVoiceBroadcastUseCase @Inject constructor( recentRooms .forEach { room -> - val ongoingVoiceBroadcasts = voiceBroadcastHelper.getOngoingVoiceBroadcasts(room.roomId) + val ongoingVoiceBroadcasts = getOngoingVoiceBroadcastsUseCase.execute(room.roomId) val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { From 23b4f6d42f467d9257f8aae068941ab566af6afb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 12:49:51 +0200 Subject: [PATCH 023/215] Inject ActiveSessionHolder in GetOngoingVoiceBroadcastsUseCase --- .../usecase/GetOngoingVoiceBroadcastsUseCase.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index db2c625161..47a9ed7b4a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -16,21 +16,22 @@ package im.vector.app.features.voicebroadcast.usecase +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import timber.log.Timber import javax.inject.Inject class GetOngoingVoiceBroadcastsUseCase @Inject constructor( - private val session: Session, + private val activeSessionHolder: ActiveSessionHolder, ) { fun execute(roomId: String): List { + val session = activeSessionHolder.getSafeActiveSession() ?: return emptyList() val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") From 0cc2a477b473aeaec0183add3dd3243fc2c49a18 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 14:54:55 +0200 Subject: [PATCH 024/215] Mockk GetOngoingVoiceBroadcastsUseCase and adapt tests --- .../usecase/GetOngoingVoiceBroadcastsUseCase.kt | 8 ++++++-- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 16 ++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index 47a9ed7b4a..cb228ad8aa 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -31,8 +31,12 @@ class GetOngoingVoiceBroadcastsUseCase @Inject constructor( ) { fun execute(roomId: String): List { - val session = activeSessionHolder.getSafeActiveSession() ?: return emptyList() - val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") + println("## GetOngoingVoiceBroadcastsUseCase") + println("## GetOngoingVoiceBroadcastsUseCase activeSessionHolder $activeSessionHolder") + val session = activeSessionHolder.getSafeActiveSession() + println("## GetOngoingVoiceBroadcastsUseCase session $session") + val room = session?.getRoom(roomId) ?: error("Unknown roomId: $roomId") + println("## GetOngoingVoiceBroadcastsUseCase room $room") Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index f95ab2053b..217a395076 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -20,6 +20,7 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService @@ -27,13 +28,13 @@ import im.vector.app.test.fakes.FakeSession import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import io.mockk.slot import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeNull import org.junit.Test -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -48,12 +49,13 @@ class StartVoiceBroadcastUseCaseTest { private val fakeRoom = FakeRoom() private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeVoiceBroadcastRecorder = mockk(relaxed = true) + private val fakeGetOngoingVoiceBroadcastsUseCase = mockk() private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase( session = fakeSession, voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, context = FakeContext().instance, buildMeta = mockk(), - getOngoingVoiceBroadcastsUseCase = GetOngoingVoiceBroadcastsUseCase(fakeSession), + getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, ) @Test @@ -81,7 +83,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List) { // Given clearAllMocks() - givenAVoiceBroadcasts(voiceBroadcasts) + givenVoiceBroadcasts(voiceBroadcasts) val voiceBroadcastInfoContentInterceptor = slot() coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID } @@ -104,7 +106,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List) { // Given clearAllMocks() - givenAVoiceBroadcasts(voiceBroadcasts) + givenVoiceBroadcasts(voiceBroadcasts) // When startVoiceBroadcastUseCase.execute(A_ROOM_ID) @@ -113,7 +115,7 @@ class StartVoiceBroadcastUseCaseTest { coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) } } - private fun givenAVoiceBroadcasts(voiceBroadcasts: List) { + private fun givenVoiceBroadcasts(voiceBroadcasts: List) { val events = voiceBroadcasts.map { Event( type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, @@ -123,7 +125,9 @@ class StartVoiceBroadcastUseCaseTest { ).toContent() ) } - fakeRoom.stateService().givenGetStateEvents(QueryStringValue.IsNotEmpty, events) + .mapNotNull { it.asVoiceBroadcastEvent() } + .filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + every { fakeGetOngoingVoiceBroadcastsUseCase.execute(any()) } returns events } private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState) From d242ab049b0c5c09ab1ea0f8b3dda2aa805472a0 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 26 Oct 2022 15:15:48 +0200 Subject: [PATCH 025/215] [Rich text editor] Implement full screen editor mode (simple approach) (#7436) * Rich text editor: implement full screen editor mode using ConstraintSets * Add back press handler * Change ToggleFullScreen to SetFullScreen, fix rebase issues * Add warning to fragment_timeline* files --- changelog.d/7436.feature | 1 + .../src/main/res/values/strings.xml | 1 + vector/src/main/AndroidManifest.xml | 3 +- .../app/core/extensions/ViewExtensions.kt | 21 ++ .../JumpToBottomViewVisibilityManager.kt | 10 +- .../home/room/detail/TimelineFragment.kt | 52 +++- .../detail/composer/MessageComposerAction.kt | 2 + .../composer/MessageComposerFragment.kt | 22 +- .../detail/composer/MessageComposerView.kt | 12 +- .../composer/MessageComposerViewModel.kt | 15 +- .../composer/MessageComposerViewState.kt | 1 + .../composer/PlainTextComposerLayout.kt | 12 +- .../detail/composer/RichTextComposerLayout.kt | 47 ++-- .../res/drawable/ic_composer_full_screen.xml | 9 + .../res/layout/composer_rich_text_layout.xml | 14 +- ...ich_text_layout_constraint_set_compact.xml | 21 +- ...ch_text_layout_constraint_set_expanded.xml | 18 +- ..._text_layout_constraint_set_fullscreen.xml | 217 +++++++++++++++ .../src/main/res/layout/fragment_composer.xml | 4 +- .../src/main/res/layout/fragment_timeline.xml | 18 ++ .../layout/fragment_timeline_fullscreen.xml | 258 ++++++++++++++++++ 21 files changed, 705 insertions(+), 53 deletions(-) create mode 100644 changelog.d/7436.feature create mode 100644 vector/src/main/res/drawable/ic_composer_full_screen.xml create mode 100644 vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml create mode 100644 vector/src/main/res/layout/fragment_timeline_fullscreen.xml diff --git a/changelog.d/7436.feature b/changelog.d/7436.feature new file mode 100644 index 0000000000..b038c975e1 --- /dev/null +++ b/changelog.d/7436.feature @@ -0,0 +1 @@ +Rich text editor: add full screen mode. diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index ea9b4b5999..450dcab1f7 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3423,5 +3423,6 @@ Apply italic format Apply strikethrough format Apply underline format + Toggle full screen mode diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index b0cd202d12..11a54e9f82 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -150,7 +150,8 @@ + android:parentActivityName=".features.home.HomeActivity" + android:windowSoftInputMode="adjustResize"> diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt index 625ff15ef7..156809d5ad 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt @@ -29,7 +29,13 @@ import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible +import androidx.transition.ChangeBounds +import androidx.transition.Fade +import androidx.transition.Transition +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet import im.vector.app.R +import im.vector.app.core.animations.SimpleTransitionListener import im.vector.app.features.themes.ThemeUtils /** @@ -90,3 +96,18 @@ fun View.setAttributeBackground(@AttrRes attributeId: Int) { val attribute = ThemeUtils.getAttribute(context, attributeId)!! setBackgroundResource(attribute.resourceId) } + +fun ViewGroup.animateLayoutChange(animationDuration: Long, transitionComplete: (() -> Unit)? = null) { + val transition = TransitionSet().apply { + ordering = TransitionSet.ORDERING_SEQUENTIAL + addTransition(ChangeBounds()) + addTransition(Fade(Fade.IN)) + duration = animationDuration + addListener(object : SimpleTransitionListener() { + override fun onTransitionEnd(transition: Transition) { + transitionComplete?.invoke() + } + }) + } + TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt index 0f7dc251ae..1368b71ec6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt @@ -34,6 +34,8 @@ class JumpToBottomViewVisibilityManager( private val layoutManager: LinearLayoutManager ) { + private var canShowButtonOnScroll = true + init { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -43,7 +45,7 @@ class JumpToBottomViewVisibilityManager( if (scrollingToPast) { jumpToBottomView.hide() - } else { + } else if (canShowButtonOnScroll) { maybeShowJumpToBottomViewVisibility() } } @@ -66,7 +68,13 @@ class JumpToBottomViewVisibilityManager( } } + fun hideAndPreventVisibilityChangesWithScrolling() { + jumpToBottomView.hide() + canShowButtonOnScroll = false + } + private fun maybeShowJumpToBottomViewVisibility() { + canShowButtonOnScroll = true if (layoutManager.findFirstVisibleItemPosition() > 1) { jumpToBottomView.show() } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 9d50cdb070..4f51922a62 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -32,7 +32,9 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.activity.addCallback import androidx.appcompat.view.menu.MenuBuilder +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.net.toUri @@ -64,6 +66,7 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.GalleryOrCameraDialogHelperFactory import im.vector.app.core.epoxy.LayoutManagerStateRestorer +import im.vector.app.core.extensions.animateLayoutChange import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.containsRtLOverride @@ -183,7 +186,9 @@ import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -337,6 +342,7 @@ class TimelineFragment : setupJumpToBottomView() setupRemoveJitsiWidgetView() setupLiveLocationIndicator() + setupBackPressHandling() views.includeRoomToolbar.roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) @@ -414,6 +420,31 @@ class TimelineFragment : if (savedInstanceState == null) { handleSpaceShare() } + + views.scrim.setOnClickListener { + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false)) + } + + messageComposerViewModel.stateFlow.map { it.isFullScreen } + .distinctUntilChanged() + .onEach { isFullScreen -> + toggleFullScreenEditor(isFullScreen) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun setupBackPressHandling() { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + withState(messageComposerViewModel) { state -> + if (state.isFullScreen) { + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false)) + } else { + remove() // Remove callback to avoid infinite loop + @Suppress("DEPRECATION") + requireActivity().onBackPressed() + } + } + } } private fun setupRemoveJitsiWidgetView() { @@ -1016,7 +1047,13 @@ class TimelineFragment : override fun onLayoutCompleted(state: RecyclerView.State) { super.onLayoutCompleted(state) updateJumpToReadMarkerViewVisibility() - jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() + withState(messageComposerViewModel) { composerState -> + if (!composerState.isFullScreen) { + jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() + } else { + jumpToBottomViewVisibilityManager.hideAndPreventVisibilityChangesWithScrolling() + } + } } }.apply { // For local rooms, pin the view's content to the top edge (the layout is reversed) @@ -2002,6 +2039,19 @@ class TimelineFragment : } } + private fun toggleFullScreenEditor(isFullScreen: Boolean) { + views.composerContainer.animateLayoutChange(200) + + val constraintSet = ConstraintSet() + val constraintSetId = if (isFullScreen) { + R.layout.fragment_timeline_fullscreen + } else { + R.layout.fragment_timeline + } + constraintSet.clone(requireContext(), constraintSetId) + constraintSet.applyTo(views.rootConstraintLayout) + } + /** * Returns true if the current room is a Thread room, false otherwise. */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt index 82adcd014a..30437a016d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt @@ -34,6 +34,8 @@ sealed class MessageComposerAction : VectorViewModelAction { data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction() data class InsertUserDisplayName(val userId: String) : MessageComposerAction() + data class SetFullScreen(val isFullScreen: Boolean) : MessageComposerAction() + // Voice Message data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction() data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 55ec922a57..beb7215c22 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -92,6 +92,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.share.SharedData import im.vector.app.features.voice.VoiceFailure import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -219,6 +220,13 @@ class MessageComposerFragment : VectorBaseFragment(), A } } + messageComposerViewModel.stateFlow.map { it.isFullScreen } + .distinctUntilChanged() + .onEach { isFullScreen -> + composer.toggleFullScreen(isFullScreen) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + if (savedInstanceState != null) { handleShareData() } @@ -297,7 +305,7 @@ class MessageComposerFragment : VectorBaseFragment(), A // Show keyboard when the user started a thread composerEditText.showKeyboard(andRequestFocus = true) } - composer.callback = object : PlainTextComposerLayout.Callback { + composer.callback = object : Callback { override fun onAddAttachment() { if (!::attachmentTypeSelector.isInitialized) { attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment) @@ -320,8 +328,12 @@ class MessageComposerFragment : VectorBaseFragment(), A composer.emojiButton?.isVisible = isEmojiKeyboardVisible } - override fun onSendMessage(text: CharSequence) { + override fun onSendMessage(text: CharSequence) = withState(messageComposerViewModel) { state -> sendTextMessage(text, composer.formattedText) + + if (state.isFullScreen) { + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false)) + } } override fun onCloseRelatedMessage() { @@ -335,6 +347,10 @@ class MessageComposerFragment : VectorBaseFragment(), A override fun onTextChanged(text: CharSequence) { messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text)) } + + override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state -> + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen)) + } } } @@ -461,7 +477,7 @@ class MessageComposerFragment : VectorBaseFragment(), A composer.sendButton.alpha = 0f composer.sendButton.isVisible = true composer.sendButton.animate().alpha(1f).setDuration(150).start() - } else { + } else if (!event.isVisible) { composer.sendButton.isInvisible = true } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt index 09357191b4..b7e0e29679 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt @@ -30,13 +30,14 @@ interface MessageComposerView { val emojiButton: ImageButton? val sendButton: ImageButton val attachmentButton: ImageButton + val fullScreenButton: ImageButton? val composerRelatedMessageTitle: TextView val composerRelatedMessageContent: TextView val composerRelatedMessageImage: ImageView val composerRelatedMessageActionIcon: ImageView val composerRelatedMessageAvatar: ImageView - var callback: PlainTextComposerLayout.Callback? + var callback: Callback? var isVisible: Boolean @@ -44,6 +45,15 @@ interface MessageComposerView { fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) fun setTextIfDifferent(text: CharSequence?): Boolean fun replaceFormattedContent(text: CharSequence) + fun toggleFullScreen(newValue: Boolean) fun setInvisible(isInvisible: Boolean) } + +interface Callback : ComposerEditText.Callback { + fun onCloseRelatedMessage() + fun onSendMessage(text: CharSequence) + fun onAddAttachment() + fun onExpandOrCompactChange() + fun onFullScreenModeChanged() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 1a9f9e6291..23d6e71114 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail.composer +import android.text.SpannableString import androidx.lifecycle.asFlow import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted @@ -122,6 +123,7 @@ class MessageComposerViewModel @AssistedInject constructor( is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action) + is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action) } } @@ -130,12 +132,11 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) { - setState { - // Makes sure currentComposerText is upToDate when accessing further setState - currentComposerText = action.text - this + val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty() + currentComposerText = SpannableString(action.text) + if (needsSendButtonVisibilityUpdate) { + updateIsSendButtonVisibility(true) } - updateIsSendButtonVisibility(true) } private fun subscribeToStateInternal() { @@ -163,6 +164,10 @@ class MessageComposerViewModel @AssistedInject constructor( } } + private fun handleSetFullScreen(action: MessageComposerAction.SetFullScreen) { + setState { copy(isFullScreen = action.isFullScreen) } + } + private fun observePowerLevelAndEncryption() { combine( PowerLevelsFlowFactory(room).createFlow(), diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 0df1dbebd8..7bb9509599 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -70,6 +70,7 @@ data class MessageComposerViewState( val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle, val voiceBroadcastState: VoiceBroadcastState? = null, val text: CharSequence? = null, + val isFullScreen: Boolean = false, ) : MavericksState { val isVoiceRecording = when (voiceRecordingUiState) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt index acb5a1b42a..939a59fcca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt @@ -49,13 +49,6 @@ class PlainTextComposerLayout @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView { - interface Callback : ComposerEditText.Callback { - fun onCloseRelatedMessage() - fun onSendMessage(text: CharSequence) - fun onAddAttachment() - fun onExpandOrCompactChange() - } - private val views: ComposerLayoutBinding override var callback: Callback? = null @@ -83,6 +76,7 @@ class PlainTextComposerLayout @JvmOverloads constructor( } override val attachmentButton: ImageButton get() = views.attachmentButton + override val fullScreenButton: ImageButton? = null override val composerRelatedMessageActionIcon: ImageView get() = views.composerRelatedMessageActionIcon override val composerRelatedMessageAvatar: ImageView @@ -155,6 +149,10 @@ class PlainTextComposerLayout @JvmOverloads constructor( return views.composerEditText.setTextIfDifferent(text) } + override fun toggleFullScreen(newValue: Boolean) { + // Plain text composer has no full screen + } + private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { // val wasSendButtonInvisible = views.sendButton.isInvisible if (animate) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 07b7d151ad..cac8f8bed4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -21,7 +21,6 @@ import android.text.Editable import android.text.TextWatcher import android.util.AttributeSet import android.view.LayoutInflater -import android.view.ViewGroup import android.widget.EditText import android.widget.ImageButton import android.widget.ImageView @@ -33,13 +32,8 @@ 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 -import androidx.transition.Transition -import androidx.transition.TransitionManager -import androidx.transition.TransitionSet import im.vector.app.R -import im.vector.app.core.animations.SimpleTransitionListener +import im.vector.app.core.extensions.animateLayoutChange import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding @@ -56,12 +50,13 @@ class RichTextComposerLayout @JvmOverloads constructor( private val views: ComposerRichTextLayoutBinding - override var callback: PlainTextComposerLayout.Callback? = null + override var callback: Callback? = null private var currentConstraintSetId: Int = -1 - private val animationDuration = 100L + private var isFullScreen = false + override val text: Editable? get() = views.composerEditText.text override val formattedText: String? @@ -74,6 +69,8 @@ class RichTextComposerLayout @JvmOverloads constructor( get() = views.sendButton override val attachmentButton: ImageButton get() = views.attachmentButton + override val fullScreenButton: ImageButton? + get() = views.composerFullScreenButton override val composerRelatedMessageActionIcon: ImageView get() = views.composerRelatedMessageActionIcon override val composerRelatedMessageAvatar: ImageView @@ -124,6 +121,10 @@ class RichTextComposerLayout @JvmOverloads constructor( callback?.onAddAttachment() } + views.composerFullScreenButton.setOnClickListener { + callback?.onFullScreenModeChanged() + } + setupRichTextMenu() } @@ -205,34 +206,30 @@ class RichTextComposerLayout @JvmOverloads constructor( return views.composerEditText.setTextIfDifferent(text) } + override fun toggleFullScreen(newValue: Boolean) { + val constraintSetId = if (newValue) R.layout.composer_rich_text_layout_constraint_set_fullscreen else currentConstraintSetId + ConstraintSet().also { + it.clone(context, constraintSetId) + it.applyTo(this) + } + + updateTextFieldBorder(newValue) + } + private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { // val wasSendButtonInvisible = views.sendButton.isInvisible if (animate) { - configureAndBeginTransition(transitionComplete) + animateLayoutChange(animationDuration, transitionComplete) } ConstraintSet().also { it.clone(context, currentConstraintSetId) it.applyTo(this) } + // Might be updated by view state just after, but avoid blinks // views.sendButton.isInvisible = wasSendButtonInvisible } - private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) { - val transition = TransitionSet().apply { - ordering = TransitionSet.ORDERING_SEQUENTIAL - addTransition(ChangeBounds()) - addTransition(Fade(Fade.IN)) - duration = animationDuration - addListener(object : SimpleTransitionListener() { - override fun onTransitionEnd(transition: Transition) { - transitionComplete?.invoke() - } - }) - } - TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition) - } - override fun setInvisible(isInvisible: Boolean) { this.isInvisible = isInvisible } diff --git a/vector/src/main/res/drawable/ic_composer_full_screen.xml b/vector/src/main/res/drawable/ic_composer_full_screen.xml new file mode 100644 index 0000000000..394dc52279 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_full_screen.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml index 09e4b03887..9f49b8f9d6 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="match_parent" tools:constraintSet="@layout/composer_rich_text_layout_constraint_set_compact" tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> @@ -108,12 +108,24 @@ style="@style/Widget.Vector.EditText.RichTextComposer" android:layout_width="0dp" android:layout_height="wrap_content" + android:gravity="top" android:nextFocusLeft="@id/composerEditText" android:nextFocusUp="@id/composerEditText" tools:hint="@string/room_message_placeholder" tools:text="@tools:sample/lorem/random" tools:ignore="MissingConstraints" /> + + @@ -114,6 +114,7 @@ android:background="?android:attr/selectableItemBackground" android:contentDescription="@string/option_send_files" android:src="@drawable/ic_attachment" + app:layout_constraintVertical_bias="1" app:layout_constraintBottom_toBottomOf="@id/sendButton" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/sendButton" @@ -142,14 +143,26 @@ android:hint="@string/room_message_placeholder" android:nextFocusLeft="@id/composerEditText" android:nextFocusUp="@id/composerEditText" - android:layout_marginHorizontal="12dp" + android:layout_marginStart="12dp" android:layout_marginVertical="10dp" app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder" - app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder" + app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton" app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder" app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder" tools:text="@tools:sample/lorem/random" /> + + @@ -173,6 +187,7 @@ app:layout_constraintStart_toEndOf="@id/attachmentButton" app:layout_constraintEnd_toStartOf="@id/sendButton" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintVertical_bias="1" android:fillViewport="true"> @@ -156,14 +156,26 @@ android:hint="@string/room_message_placeholder" android:nextFocusLeft="@id/composerEditText" android:nextFocusUp="@id/composerEditText" - android:layout_marginHorizontal="12dp" + android:layout_marginStart="12dp" android:layout_marginVertical="10dp" app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder" - app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder" + app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton" app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder" app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder" tools:text="@tools:sample/lorem/random" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_composer.xml b/vector/src/main/res/layout/fragment_composer.xml index 8703af7471..41c052367a 100644 --- a/vector/src/main/res/layout/fragment_composer.xml +++ b/vector/src/main/res/layout/fragment_composer.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> + + + + @@ -165,6 +182,7 @@ android:layout_margin="16dp" android:contentDescription="@string/a11y_jump_to_bottom" android:src="@drawable/ic_expand_more" + android:visibility="gone" app:backgroundTint="#FFFFFF" app:badgeBackgroundColor="?colorPrimary" app:badgeTextColor="?colorOnPrimary" diff --git a/vector/src/main/res/layout/fragment_timeline_fullscreen.xml b/vector/src/main/res/layout/fragment_timeline_fullscreen.xml new file mode 100644 index 0000000000..373ca74f56 --- /dev/null +++ b/vector/src/main/res/layout/fragment_timeline_fullscreen.xml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c20f6fe3262761b918cbc5c686335884bea6e57f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 16:07:38 +0200 Subject: [PATCH 026/215] GetOngoingVoiceBroadcastsUseCase: Remove debug logs --- .../usecase/GetOngoingVoiceBroadcastsUseCase.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index cb228ad8aa..0f5e413719 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -31,12 +31,8 @@ class GetOngoingVoiceBroadcastsUseCase @Inject constructor( ) { fun execute(roomId: String): List { - println("## GetOngoingVoiceBroadcastsUseCase") - println("## GetOngoingVoiceBroadcastsUseCase activeSessionHolder $activeSessionHolder") val session = activeSessionHolder.getSafeActiveSession() - println("## GetOngoingVoiceBroadcastsUseCase session $session") val room = session?.getRoom(roomId) ?: error("Unknown roomId: $roomId") - println("## GetOngoingVoiceBroadcastsUseCase room $room") Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") From cb5fc75c5d19751dba201b41f52cccde778893d0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 16:08:03 +0200 Subject: [PATCH 027/215] GetOngoingVoiceBroadcastsUseCase: Return empty list if there is no session --- .../usecase/GetOngoingVoiceBroadcastsUseCase.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index 0f5e413719..ec50618969 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -31,8 +31,11 @@ class GetOngoingVoiceBroadcastsUseCase @Inject constructor( ) { fun execute(roomId: String): List { - val session = activeSessionHolder.getSafeActiveSession() - val room = session?.getRoom(roomId) ?: error("Unknown roomId: $roomId") + val session = activeSessionHolder.getSafeActiveSession() ?: run { + Timber.d("## GetOngoingVoiceBroadcastsUseCase: no active session") + return emptyList() + } + val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") From bdfc96ff666859b11376e91635c458dc242412ca Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 16:36:02 +0200 Subject: [PATCH 028/215] Fix merge conflicts --- .../detail/composer/MessageComposerFragment.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 3cb3eb1a4b..463a8fe440 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -91,8 +91,8 @@ import im.vector.app.features.poll.PollMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.share.SharedData import im.vector.app.features.voice.VoiceFailure -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -207,6 +207,13 @@ class MessageComposerFragment : VectorBaseFragment(), A } } + messageComposerViewModel.stateFlow.map { it.isFullScreen } + .distinctUntilChanged() + .onEach { isFullScreen -> + composer.toggleFullScreen(isFullScreen) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend -> if (!canSend.boolean()) { return@onEach @@ -220,13 +227,6 @@ class MessageComposerFragment : VectorBaseFragment(), A } } - messageComposerViewModel.stateFlow.map { it.isFullScreen } - .distinctUntilChanged() - .onEach { isFullScreen -> - composer.toggleFullScreen(isFullScreen) - } - .launchIn(viewLifecycleOwner.lifecycleScope) - if (savedInstanceState != null) { handleShareData() } From c776aae9d06052abe2eb0d799ac33cc77dc94e9f Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Wed, 26 Oct 2022 17:37:40 +0100 Subject: [PATCH 029/215] [Rich text editor] Add plain text mode and new attachment UI (#7459) * Add new attachments selection dialog * Add rounded corners to bottom sheet dialog. Note these are currently only visible in the collapsed state. - [Google issue](https://issuetracker.google.com/issues/144859239) - [Rejected PR](https://github.com/material-components/material-components-android/pull/437) - [Github issue](https://github.com/material-components/material-components-android/issues/1278) * Add changelog entry * Remove redundant call to superclass click listener * Refactor to use view visibility helper * Change redundant sealed class to interface * Remove unused string * Revert "Add rounded corners to bottom sheet dialog." This reverts commit 17c43c91888162d3c7675511ff910c46c3aa32fc. * Remove redundant view group * Remove redundant `this` * Update rich text editor to latest * Update rich text editor version * Allow toggling rich text in the new editor * Persist the text formatting setting * Add changelog entry --- changelog.d/7429.feature | 1 + changelog.d/7452.feature | 1 + dependencies.gradle | 2 +- .../src/main/res/values/strings.xml | 10 ++ .../app/core/di/MavericksViewModelModule.kt | 6 + .../core/ui/views/BottomSheetActionButton.kt | 4 + .../features/attachments/AttachmentType.kt | 37 +++++ .../AttachmentTypeSelectorBottomSheet.kt | 92 ++++++++++++ ...chmentTypeSelectorSharedActionViewModel.kt | 30 ++++ .../attachments/AttachmentTypeSelectorView.kt | 70 +++++---- .../AttachmentTypeSelectorViewModel.kt | 76 ++++++++++ .../features/attachments/AttachmentsHelper.kt | 2 +- .../composer/MessageComposerFragment.kt | 74 +++++---- .../detail/composer/RichTextComposerLayout.kt | 99 ++++++++---- .../features/settings/VectorPreferences.kt | 19 +++ .../main/res/drawable/ic_text_formatting.xml | 13 ++ .../drawable/ic_text_formatting_disabled.xml | 18 +++ .../bottom_sheet_attachment_type_selector.xml | 106 +++++++++++++ .../res/layout/composer_rich_text_layout.xml | 19 ++- ...ich_text_layout_constraint_set_compact.xml | 22 ++- ...ch_text_layout_constraint_set_expanded.xml | 22 ++- ..._text_layout_constraint_set_fullscreen.xml | 23 ++- .../AttachmentTypeSelectorViewModelTest.kt | 142 ++++++++++++++++++ .../app/test/fakes/FakeVectorFeatures.kt | 8 + .../app/test/fakes/FakeVectorPreferences.kt | 3 + 25 files changed, 797 insertions(+), 102 deletions(-) create mode 100644 changelog.d/7429.feature create mode 100644 changelog.d/7452.feature create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt create mode 100644 vector/src/main/res/drawable/ic_text_formatting.xml create mode 100644 vector/src/main/res/drawable/ic_text_formatting_disabled.xml create mode 100644 vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml create mode 100644 vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt diff --git a/changelog.d/7429.feature b/changelog.d/7429.feature new file mode 100644 index 0000000000..9857452eca --- /dev/null +++ b/changelog.d/7429.feature @@ -0,0 +1 @@ +Add new UI for selecting an attachment diff --git a/changelog.d/7452.feature b/changelog.d/7452.feature new file mode 100644 index 0000000000..a811f87c84 --- /dev/null +++ b/changelog.d/7452.feature @@ -0,0 +1 @@ +[Rich text editor] Add plain text mode diff --git a/dependencies.gradle b/dependencies.gradle index f081e0a874..db6e92552a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.2.1" + 'wysiwyg' : "io.element.android:wysiwyg:0.4.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 450dcab1f7..9edd7d836a 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3205,6 +3205,16 @@ Share location Start a voice broadcast + Photo library + Stickers + Attachments + Voice broadcast + Polls + Location + Camera + Contact + Text formatting + Show less "%1$d more" diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 97590028d8..2242abb7aa 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -22,6 +22,7 @@ import dagger.hilt.InstallIn import dagger.multibindings.IntoMap import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel +import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel import im.vector.app.features.auth.ReAuthViewModel import im.vector.app.features.call.VectorCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel @@ -677,4 +678,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(VectorSettingsLabsViewModel::class) fun vectorSettingsLabsViewModelFactory(factory: VectorSettingsLabsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(AttachmentTypeSelectorViewModel::class) + fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt index a3e8b3780c..ca3e6a360a 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt @@ -38,6 +38,10 @@ class BottomSheetActionButton @JvmOverloads constructor( ) : FrameLayout(context, attrs, defStyleAttr) { val views: ViewBottomSheetActionButtonBinding + override fun setOnClickListener(l: OnClickListener?) { + views.bottomSheetActionClickableZone.setOnClickListener(l) + } + var title: String? = null set(value) { field = value diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt new file mode 100644 index 0000000000..f4b97b9f9c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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.attachments + +import im.vector.app.core.utils.PERMISSIONS_EMPTY +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING +import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST + +/** + * The all possible types to pick with their required permissions. + */ +enum class AttachmentType(val permissions: List) { + CAMERA(PERMISSIONS_FOR_TAKING_PHOTO), + GALLERY(PERMISSIONS_EMPTY), + FILE(PERMISSIONS_EMPTY), + STICKER(PERMISSIONS_EMPTY), + CONTACT(PERMISSIONS_FOR_PICKING_CONTACT), + POLL(PERMISSIONS_EMPTY), + LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING), + VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST), +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt new file mode 100644 index 0000000000..f8d5d768ef --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2022 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.attachments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding +import im.vector.app.features.home.room.detail.TimelineViewModel + +@AndroidEntryPoint +class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment() { + + private val viewModel: AttachmentTypeSelectorViewModel by parentFragmentViewModel() + private val timelineViewModel: TimelineViewModel by parentFragmentViewModel() + private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels( + ownerProducer = { requireParentFragment() } + ) + + override val showExpanded = true + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetAttachmentTypeSelectorBinding { + return BottomSheetAttachmentTypeSelectorBinding.inflate(inflater, container, false) + } + + override fun invalidate() = withState(viewModel, timelineViewModel) { viewState, timelineState -> + super.invalidate() + views.location.isVisible = viewState.isLocationVisible + views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible + views.poll.isVisible = !timelineState.isThreadTimeline() + views.textFormatting.isChecked = viewState.isTextFormattingEnabled + views.textFormatting.setCompoundDrawablesRelativeWithIntrinsicBounds( + if (viewState.isTextFormattingEnabled) { + R.drawable.ic_text_formatting + } else { + R.drawable.ic_text_formatting_disabled + }, 0, 0, 0 + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.gallery.debouncedClicks { onAttachmentSelected(AttachmentType.GALLERY) } + views.stickers.debouncedClicks { onAttachmentSelected(AttachmentType.STICKER) } + views.file.debouncedClicks { onAttachmentSelected(AttachmentType.FILE) } + views.voiceBroadcast.debouncedClicks { onAttachmentSelected(AttachmentType.VOICE_BROADCAST) } + views.poll.debouncedClicks { onAttachmentSelected(AttachmentType.POLL) } + views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) } + views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) } + views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) } + views.textFormatting.setOnCheckedChangeListener { _, isChecked -> onTextFormattingToggled(isChecked) } + } + + private fun onAttachmentSelected(attachmentType: AttachmentType) { + val action = AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction(attachmentType) + sharedActionViewModel.post(action) + dismiss() + } + + private fun onTextFormattingToggled(isEnabled: Boolean) = + viewModel.handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled)) + + companion object { + fun show(fragmentManager: FragmentManager) { + val bottomSheet = AttachmentTypeSelectorBottomSheet() + bottomSheet.show(fragmentManager, "AttachmentTypeSelectorBottomSheet") + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt new file mode 100644 index 0000000000..e02b10c54b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 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.attachments + +import im.vector.app.core.platform.VectorSharedAction +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class AttachmentTypeSelectorSharedActionViewModel @Inject constructor() : + VectorSharedActionViewModel() + +sealed interface AttachmentTypeSelectorSharedAction : VectorSharedAction { + data class SelectAttachmentTypeAction( + val attachmentType: AttachmentType + ) : AttachmentTypeSelectorSharedAction +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index 8536b765d4..55805a0728 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -30,17 +30,11 @@ import android.view.animation.TranslateAnimation import android.widget.ImageButton import android.widget.LinearLayout import android.widget.PopupWindow -import androidx.annotation.StringRes import androidx.appcompat.widget.TooltipCompat import androidx.core.view.doOnNextLayout import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.epoxy.onClick -import im.vector.app.core.utils.PERMISSIONS_EMPTY -import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING -import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT -import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback import kotlin.math.max @@ -59,7 +53,7 @@ class AttachmentTypeSelectorView( ) : PopupWindow(context) { interface Callback { - fun onTypeSelected(type: Type) + fun onTypeSelected(type: AttachmentType) } private val views: ViewAttachmentTypeSelectorBinding @@ -69,14 +63,14 @@ class AttachmentTypeSelectorView( init { contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false) views = ViewAttachmentTypeSelectorBinding.bind(contentView) - views.attachmentGalleryButton.configure(Type.GALLERY) - views.attachmentCameraButton.configure(Type.CAMERA) - views.attachmentFileButton.configure(Type.FILE) - views.attachmentStickersButton.configure(Type.STICKER) - views.attachmentContactButton.configure(Type.CONTACT) - views.attachmentPollButton.configure(Type.POLL) - views.attachmentLocationButton.configure(Type.LOCATION) - views.attachmentVoiceBroadcast.configure(Type.VOICE_BROADCAST) + views.attachmentGalleryButton.configure(AttachmentType.GALLERY) + views.attachmentCameraButton.configure(AttachmentType.CAMERA) + views.attachmentFileButton.configure(AttachmentType.FILE) + views.attachmentStickersButton.configure(AttachmentType.STICKER) + views.attachmentContactButton.configure(AttachmentType.CONTACT) + views.attachmentPollButton.configure(AttachmentType.POLL) + views.attachmentLocationButton.configure(AttachmentType.LOCATION) + views.attachmentVoiceBroadcast.configure(AttachmentType.VOICE_BROADCAST) width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.WRAP_CONTENT animationStyle = 0 @@ -127,16 +121,16 @@ class AttachmentTypeSelectorView( } } - fun setAttachmentVisibility(type: Type, isVisible: Boolean) { + fun setAttachmentVisibility(type: AttachmentType, isVisible: Boolean) { when (type) { - Type.CAMERA -> views.attachmentCameraButton - Type.GALLERY -> views.attachmentGalleryButton - Type.FILE -> views.attachmentFileButton - Type.STICKER -> views.attachmentStickersButton - Type.CONTACT -> views.attachmentContactButton - Type.POLL -> views.attachmentPollButton - Type.LOCATION -> views.attachmentLocationButton - Type.VOICE_BROADCAST -> views.attachmentVoiceBroadcast + AttachmentType.CAMERA -> views.attachmentCameraButton + AttachmentType.GALLERY -> views.attachmentGalleryButton + AttachmentType.FILE -> views.attachmentFileButton + AttachmentType.STICKER -> views.attachmentStickersButton + AttachmentType.CONTACT -> views.attachmentContactButton + AttachmentType.POLL -> views.attachmentPollButton + AttachmentType.LOCATION -> views.attachmentLocationButton + AttachmentType.VOICE_BROADCAST -> views.attachmentVoiceBroadcast }.let { it.isVisible = isVisible } @@ -200,13 +194,13 @@ class AttachmentTypeSelectorView( return Pair(x, y) } - private fun ImageButton.configure(type: Type): ImageButton { + private fun ImageButton.configure(type: AttachmentType): ImageButton { this.setOnClickListener(TypeClickListener(type)) - TooltipCompat.setTooltipText(this, context.getString(type.tooltipRes)) + TooltipCompat.setTooltipText(this, context.getString(attachmentTooltipLabels.getValue(type))) return this } - private inner class TypeClickListener(private val type: Type) : View.OnClickListener { + private inner class TypeClickListener(private val type: AttachmentType) : View.OnClickListener { override fun onClick(v: View) { dismiss() @@ -217,14 +211,18 @@ class AttachmentTypeSelectorView( /** * The all possible types to pick with their required permissions and tooltip resource. */ - enum class Type(val permissions: List, @StringRes val tooltipRes: Int) { - CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo), - GALLERY(PERMISSIONS_EMPTY, R.string.tooltip_attachment_gallery), - FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file), - STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), - CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), - POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), - LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location), - VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST, R.string.tooltip_attachment_voice_broadcast), + private companion object { + private val attachmentTooltipLabels: Map = AttachmentType.values().associateWith { + when (it) { + AttachmentType.CAMERA -> R.string.tooltip_attachment_photo + AttachmentType.GALLERY -> R.string.tooltip_attachment_gallery + AttachmentType.FILE -> R.string.tooltip_attachment_file + AttachmentType.STICKER -> R.string.tooltip_attachment_sticker + AttachmentType.CONTACT -> R.string.tooltip_attachment_contact + AttachmentType.POLL -> R.string.tooltip_attachment_poll + AttachmentType.LOCATION -> R.string.tooltip_attachment_location + AttachmentType.VOICE_BROADCAST -> R.string.tooltip_attachment_voice_broadcast + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt new file mode 100644 index 0000000000..cb74661eba --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 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.attachments + +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.VectorFeatures +import im.vector.app.features.settings.VectorPreferences + +class AttachmentTypeSelectorViewModel @AssistedInject constructor( + @Assisted initialState: AttachmentTypeSelectorViewState, + private val vectorFeatures: VectorFeatures, + private val vectorPreferences: VectorPreferences, +) : VectorViewModel(initialState) { + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: AttachmentTypeSelectorAction) = when (action) { + is AttachmentTypeSelectorAction.ToggleTextFormatting -> setTextFormattingEnabled(action.isEnabled) + } + + init { + setState { + copy( + isLocationVisible = vectorFeatures.isLocationSharingEnabled(), + isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(), + isTextFormattingEnabled = vectorPreferences.isTextFormattingEnabled(), + ) + } + } + + private fun setTextFormattingEnabled(isEnabled: Boolean) { + vectorPreferences.setTextFormattingEnabled(isEnabled) + setState { + copy( + isTextFormattingEnabled = isEnabled + ) + } + } +} + +data class AttachmentTypeSelectorViewState( + val isLocationVisible: Boolean = false, + val isVoiceBroadcastVisible: Boolean = false, + val isTextFormattingEnabled: Boolean = false, +) : MavericksState + +sealed interface AttachmentTypeSelectorAction : VectorViewModelAction { + data class ToggleTextFormatting(val isEnabled: Boolean) : AttachmentTypeSelectorAction +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index 1a8e10d102..9692777e15 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -54,7 +54,7 @@ class AttachmentsHelper( private var captureUri: Uri? = null // The pending type is set if we have to handle permission request. It must be restored if the activity gets killed. - var pendingType: AttachmentTypeSelectorView.Type? = null + var pendingType: AttachmentType? = null // Restorable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 463a8fe440..5666c28605 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -40,8 +40,10 @@ import androidx.core.text.buildSpannedString import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -63,7 +65,12 @@ import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentComposerBinding import im.vector.app.features.VectorFeatures +import im.vector.app.features.attachments.AttachmentType +import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet +import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction +import im.vector.app.features.attachments.AttachmentTypeSelectorSharedActionViewModel import im.vector.app.features.attachments.AttachmentTypeSelectorView +import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.ShareIntentHandler @@ -91,8 +98,9 @@ import im.vector.app.features.poll.PollMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.share.SharedData import im.vector.app.features.voice.VoiceFailure -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -162,6 +170,8 @@ class MessageComposerFragment : VectorBaseFragment(), A private val timelineViewModel: TimelineViewModel by parentFragmentViewModel() private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel() private lateinit var sharedActionViewModel: MessageSharedActionViewModel + private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel() + private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels() private val composer: MessageComposerView get() { return if (vectorPreferences.isRichTextEditorEnabled()) { @@ -227,6 +237,11 @@ class MessageComposerFragment : VectorBaseFragment(), A } } + attachmentActionsViewModel.stream() + .filterIsInstance() + .onEach { onTypeSelected(it.attachmentType) } + .launchIn(lifecycleScope) + if (savedInstanceState != null) { handleShareData() } @@ -260,11 +275,14 @@ class MessageComposerFragment : VectorBaseFragment(), A messageComposerViewModel.endAllVoiceActions() } - override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState -> + override fun invalidate() = withState( + timelineViewModel, messageComposerViewModel, attachmentViewModel + ) { mainState, messageComposerState, attachmentState -> if (mainState.tombstoneEvent != null) return@withState composer.setInvisible(!messageComposerState.isComposerVisible) composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible + (composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled } private fun setupComposer() { @@ -307,21 +325,25 @@ class MessageComposerFragment : VectorBaseFragment(), A } composer.callback = object : Callback { override fun onAddAttachment() { - if (!::attachmentTypeSelector.isInitialized) { - attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment) - attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.LOCATION, - vectorFeatures.isLocationSharingEnabled(), - ) - attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine() - ) - attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.VOICE_BROADCAST, - vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission - ) + if (vectorPreferences.isRichTextEditorEnabled()) { + AttachmentTypeSelectorBottomSheet.show(childFragmentManager) + } else { + if (!::attachmentTypeSelector.isInitialized) { + attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentType.LOCATION, + vectorFeatures.isLocationSharingEnabled(), + ) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentType.POLL, !isThreadTimeLine() + ) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentType.VOICE_BROADCAST, + vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission + ) + } + attachmentTypeSelector.show(composer.attachmentButton) } - attachmentTypeSelector.show(composer.attachmentButton) } override fun onExpandOrCompactChange() { @@ -678,20 +700,20 @@ class MessageComposerFragment : VectorBaseFragment(), A } } - private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { + private fun launchAttachmentProcess(type: AttachmentType) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( + AttachmentType.CAMERA -> attachmentsHelper.openCamera( activity = requireActivity(), vectorPreferences = vectorPreferences, cameraActivityResultLauncher = attachmentCameraActivityResultLauncher, cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher ) - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) - AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) - AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) - AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment) - AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE) - AttachmentTypeSelectorView.Type.LOCATION -> { + AttachmentType.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) + AttachmentType.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) + AttachmentType.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) + AttachmentType.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment) + AttachmentType.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE) + AttachmentType.LOCATION -> { navigator .openLocationSharing( context = requireContext(), @@ -701,11 +723,11 @@ class MessageComposerFragment : VectorBaseFragment(), A locationOwnerId = session.myUserId ) } - AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start) + AttachmentType.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start) } } - override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) { + override fun onTypeSelected(type: AttachmentType) { if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) { launchAttachmentProcess(type) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index cac8f8bed4..2c09f351bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -38,7 +38,7 @@ import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding import io.element.android.wysiwyg.EditorEditText -import io.element.android.wysiwyg.InlineFormat +import io.element.android.wysiwyg.inputhandlers.models.InlineFormat import uniffi.wysiwyg_composer.ComposerAction import uniffi.wysiwyg_composer.MenuState @@ -57,12 +57,24 @@ class RichTextComposerLayout @JvmOverloads constructor( private var isFullScreen = false + var isTextFormattingEnabled = true + set(value) { + if (field == value) return + syncEditTexts() + field = value + updateEditTextVisibility() + } + override val text: Editable? - get() = views.composerEditText.text + get() = editText.text override val formattedText: String? - get() = views.composerEditText.getHtmlOutput() + get() = (editText as? EditorEditText)?.getHtmlOutput() override val editText: EditText - get() = views.composerEditText + get() = if (isTextFormattingEnabled) { + views.richTextComposerEditText + } else { + views.plainTextComposerEditText + } override val emojiButton: ImageButton? get() = null override val sendButton: ImageButton @@ -91,21 +103,12 @@ class RichTextComposerLayout @JvmOverloads constructor( collapse(false) - views.composerEditText.addTextChangedListener(object : TextWatcher { - private var previousTextWasExpanded = false - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable) { - callback?.onTextChanged(s) - - val isExpanded = s.lines().count() > 1 - if (previousTextWasExpanded != isExpanded) { - updateTextFieldBorder(isExpanded) - } - previousTextWasExpanded = isExpanded - } - }) + views.richTextComposerEditText.addTextChangedListener( + TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder) + ) + views.plainTextComposerEditText.addTextChangedListener( + TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder) + ) views.composerRelatedMessageCloseButton.setOnClickListener { collapse() @@ -130,19 +133,23 @@ class RichTextComposerLayout @JvmOverloads constructor( private fun setupRichTextMenu() { addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.Bold) { - views.composerEditText.toggleInlineFormat(InlineFormat.Bold) + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold) } addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.Italic) { - views.composerEditText.toggleInlineFormat(InlineFormat.Italic) + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic) } addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.Underline) { - views.composerEditText.toggleInlineFormat(InlineFormat.Underline) + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline) } addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.StrikeThrough) { - views.composerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) } + } - views.composerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state -> + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + views.richTextComposerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state -> if (state is MenuState.Update) { updateMenuStateFor(ComposerAction.Bold, state) updateMenuStateFor(ComposerAction.Italic, state) @@ -150,8 +157,26 @@ class RichTextComposerLayout @JvmOverloads constructor( updateMenuStateFor(ComposerAction.StrikeThrough, state) } } + + updateEditTextVisibility() } + private fun updateEditTextVisibility() { + views.richTextComposerEditText.isVisible = isTextFormattingEnabled + views.richTextMenu.isVisible = isTextFormattingEnabled + views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled + } + + /** + * Updates the non-active input with the contents of the active input. + */ + private fun syncEditTexts() = + if (isTextFormattingEnabled) { + views.plainTextComposerEditText.setText(views.richTextComposerEditText.getPlainText()) + } else { + views.richTextComposerEditText.setText(views.plainTextComposerEditText.text.toString()) + } + private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) { val inflater = LayoutInflater.from(context) val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true) @@ -181,7 +206,7 @@ class RichTextComposerLayout @JvmOverloads constructor( } override fun replaceFormattedContent(text: CharSequence) { - views.composerEditText.setHtml(text.toString()) + views.richTextComposerEditText.setHtml(text.toString()) } override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) { @@ -191,6 +216,7 @@ class RichTextComposerLayout @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) + updateEditTextVisibility() } override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) { @@ -200,10 +226,11 @@ class RichTextComposerLayout @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) + updateEditTextVisibility() } override fun setTextIfDifferent(text: CharSequence?): Boolean { - return views.composerEditText.setTextIfDifferent(text) + return editText.setTextIfDifferent(text) } override fun toggleFullScreen(newValue: Boolean) { @@ -214,6 +241,7 @@ class RichTextComposerLayout @JvmOverloads constructor( } updateTextFieldBorder(newValue) + updateEditTextVisibility() } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { @@ -233,4 +261,23 @@ class RichTextComposerLayout @JvmOverloads constructor( override fun setInvisible(isInvisible: Boolean) { this.isInvisible = isInvisible } + + private class TextChangeListener( + private val onTextChanged: (s: Editable) -> Unit, + private val onExpandedChanged: (isExpanded: Boolean) -> Unit, + ) : TextWatcher { + private var previousTextWasExpanded = false + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + onTextChanged.invoke(s) + + val isExpanded = s.lines().count() > 1 + if (previousTextWasExpanded != isExpanded) { + onExpandedChanged(isExpanded) + } + previousTextWasExpanded = isExpanded + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 2dc8b12160..9f40a7cede 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -109,6 +109,7 @@ class VectorPreferences @Inject constructor( const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY" private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY" private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY" + private const val SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY = "SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY" private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY" private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY" private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY" @@ -759,6 +760,24 @@ class VectorPreferences @Inject constructor( } } + /** + * Tells if text formatting is enabled within the rich text editor. + * + * @return true if the text formatting is enabled + */ + fun isTextFormattingEnabled(): Boolean = + defaultPrefs.getBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, true) + + /** + * Update whether text formatting is enabled within the rich text editor. + * + * @param isEnabled true to enable the text formatting + */ + fun setTextFormattingEnabled(isEnabled: Boolean) = + defaultPrefs.edit { + putBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, isEnabled) + } + /** * Tells if a confirmation dialog should be displayed before staring a call. */ diff --git a/vector/src/main/res/drawable/ic_text_formatting.xml b/vector/src/main/res/drawable/ic_text_formatting.xml new file mode 100644 index 0000000000..375c459692 --- /dev/null +++ b/vector/src/main/res/drawable/ic_text_formatting.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_text_formatting_disabled.xml b/vector/src/main/res/drawable/ic_text_formatting_disabled.xml new file mode 100644 index 0000000000..bb34211c7a --- /dev/null +++ b/vector/src/main/res/drawable/ic_text_formatting_disabled.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml new file mode 100644 index 0000000000..7a22ab57f8 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml index 9f49b8f9d6..c5afe1eb44 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout.xml @@ -104,13 +104,26 @@ android:background="@drawable/bg_composer_rich_edit_text_single_line" /> + + + diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml index 7aaa9f6a07..1a3023a805 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml @@ -136,13 +136,29 @@ app:layout_constraintEnd_toEndOf="parent" /> + + + + + + () { fun givenCombinedLoginDisabled() { every { isOnboardingCombinedLoginEnabled() } returns false } + + fun givenLocationSharing(isEnabled: Boolean) { + every { isLocationSharingEnabled() } returns isEnabled + } + + fun givenVoiceBroadcast(isEnabled: Boolean) { + every { isVoiceBroadcastEnabled() } returns isEnabled + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 8b0630c24f..cd4f70bf63 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -40,4 +40,7 @@ class FakeVectorPreferences { fun givenIsClientInfoRecordingEnabled(isEnabled: Boolean) { every { instance.isClientInfoRecordingEnabled() } returns isEnabled } + + fun givenTextFormatting(isEnabled: Boolean) = + every { instance.isTextFormattingEnabled() } returns isEnabled } From 174ba4f4cc68890e333af86eead16e46989123b3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 16:03:06 +0200 Subject: [PATCH 031/215] VoiceBroadcastPlayer - Create player interface and move implementation to dedicated class --- .../java/im/vector/app/core/di/VoiceModule.kt | 31 +++++--- .../features/home/HomeActivityViewModel.kt | 2 +- .../factory/VoiceBroadcastItemFactory.kt | 4 +- .../item/AbsMessageVoiceBroadcastItem.kt | 4 +- .../MessageVoiceBroadcastListeningItem.kt | 2 +- .../MessageVoiceBroadcastRecordingItem.kt | 2 +- .../voicebroadcast/VoiceBroadcastHelper.kt | 9 ++- .../listening/VoiceBroadcastPlayer.kt | 75 +++++++++++++++++++ .../VoiceBroadcastPlayerImpl.kt} | 62 +++++++-------- .../{ => recording}/VoiceBroadcastRecorder.kt | 2 +- .../VoiceBroadcastRecorderQ.kt | 2 +- .../usecase/PauseVoiceBroadcastUseCase.kt | 4 +- .../usecase/ResumeVoiceBroadcastUseCase.kt | 4 +- .../usecase/StartVoiceBroadcastUseCase.kt | 5 +- .../StopOngoingVoiceBroadcastUseCase.kt | 3 +- .../usecase/StopVoiceBroadcastUseCase.kt | 4 +- .../usecase/PauseVoiceBroadcastUseCaseTest.kt | 3 +- .../ResumeVoiceBroadcastUseCaseTest.kt | 3 +- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 3 +- .../usecase/StopVoiceBroadcastUseCaseTest.kt | 3 +- 20 files changed, 156 insertions(+), 71 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt rename vector/src/main/java/im/vector/app/features/voicebroadcast/{VoiceBroadcastPlayer.kt => listening/VoiceBroadcastPlayerImpl.kt} (89%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/VoiceBroadcastRecorder.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/VoiceBroadcastRecorderQ.kt (98%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/PauseVoiceBroadcastUseCase.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/ResumeVoiceBroadcastUseCase.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/StartVoiceBroadcastUseCase.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/StopOngoingVoiceBroadcastUseCase.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/StopVoiceBroadcastUseCase.kt (95%) diff --git a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt index 54d556ea91..30a8565771 100644 --- a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt @@ -18,24 +18,33 @@ package im.vector.app.core.di import android.content.Context import android.os.Build +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorderQ +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ import javax.inject.Singleton -@Module @InstallIn(SingletonComponent::class) -object VoiceModule { - @Provides - @Singleton - fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - VoiceBroadcastRecorderQ(context) - } else { - null +@Module +abstract class VoiceModule { + + companion object { + @Provides + @Singleton + fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + VoiceBroadcastRecorderQ(context) + } else { + null + } } } + + @Binds + abstract fun bindVoiceBroadcastPlayer(player: VoiceBroadcastPlayerImpl): VoiceBroadcastPlayer } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index c3abdde022..49f2079625 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -42,7 +42,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.voicebroadcast.usecase.StopOngoingVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.recording.usecase.StopOngoingVoiceBroadcastUseCase import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 7a7cb73471..56498fa8d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -26,11 +26,11 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadca import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_ -import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getUserOrDefault diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 45f10b68d0..ba9d582ea4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -25,9 +25,9 @@ import im.vector.app.R import im.vector.app.core.extensions.tintBackground import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider -import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.util.MatrixItem abstract class AbsMessageVoiceBroadcastItem : AbsMessageItem() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index a3e7cc55d5..8df7a9d1a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -23,7 +23,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction -import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index e3e86f38e3..17aa1543c0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -22,8 +22,8 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 58e7de7f32..dfc8e35422 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -16,10 +16,11 @@ package im.vector.app.features.voicebroadcast -import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase -import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase -import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase -import im.vector.app.features.voicebroadcast.usecase.StopVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt new file mode 100644 index 0000000000..e2870c4011 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 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.voicebroadcast.listening + +interface VoiceBroadcastPlayer { + + /** + * The current playing voice broadcast identifier, if any. + */ + val currentVoiceBroadcastId: String? + + /** + * The current playing [State], [State.IDLE] by default. + */ + val playingState: State + + /** + * Start playback of the given voice broadcast. + */ + fun playOrResume(roomId: String, voiceBroadcastId: String) + + /** + * Pause playback of the current voice broadcast, if any. + */ + fun pause() + + /** + * Stop playback of the current voice broadcast, if any, and reset the player state. + */ + fun stop() + + /** + * Add a [Listener] to the given voice broadcast id. + */ + fun addListener(voiceBroadcastId: String, listener: Listener) + + /** + * Remove a [Listener] from the given voice broadcast id. + */ + fun removeListener(voiceBroadcastId: String, listener: Listener) + + /** + * Player states. + */ + enum class State { + PLAYING, + PAUSED, + BUFFERING, + IDLE + } + + /** + * Listener related to [VoiceBroadcastPlayer]. + */ + fun interface Listener { + /** + * Notify about [VoiceBroadcastPlayer.playingState] changes. + */ + fun onStateChanged(state: State) + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt similarity index 89% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 5a04904f69..168b921c2e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast +package im.vector.app.features.voicebroadcast.listening import android.media.AudioAttributes import android.media.MediaPlayer @@ -22,8 +22,14 @@ import androidx.annotation.MainThread import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure +import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk +import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId +import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -43,14 +49,13 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject -import javax.inject.Singleton @Singleton -class VoiceBroadcastPlayer @Inject constructor( +class VoiceBroadcastPlayerImpl @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val playbackTracker: AudioMessagePlaybackTracker, private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, -) { +) : VoiceBroadcastPlayer { private val session get() = sessionHolder.getActiveSession() @@ -75,9 +80,9 @@ class VoiceBroadcastPlayer @Inject constructor( private var currentSequence: Int? = null private var playlist = emptyList() - var currentVoiceBroadcastId: String? = null + override var currentVoiceBroadcastId: String? = null - private var state: State = State.IDLE + override var playingState = State.IDLE @MainThread set(value) { Timber.w("## VoiceBroadcastPlayer state: $field -> $value") @@ -94,22 +99,22 @@ class VoiceBroadcastPlayer @Inject constructor( */ private val listeners: MutableMap> = mutableMapOf() - fun playOrResume(roomId: String, eventId: String) { - val hasChanged = currentVoiceBroadcastId != eventId + override fun playOrResume(roomId: String, voiceBroadcastId: String) { + val hasChanged = currentVoiceBroadcastId != voiceBroadcastId when { - hasChanged -> startPlayback(roomId, eventId) - state == State.PAUSED -> resumePlayback() + hasChanged -> startPlayback(roomId, voiceBroadcastId) + playingState == State.PAUSED -> resumePlayback() else -> Unit } } - fun pause() { + override fun pause() { currentMediaPlayer?.pause() currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) } - state = State.PAUSED + playingState = State.PAUSED } - fun stop() { + override fun stop() { // Stop playback currentMediaPlayer?.stop() currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) } @@ -131,7 +136,7 @@ class VoiceBroadcastPlayer @Inject constructor( timelineListener = null // Update state - state = State.IDLE + playingState = State.IDLE // Clear playlist playlist = emptyList() @@ -143,29 +148,29 @@ class VoiceBroadcastPlayer @Inject constructor( /** * Add a [Listener] to the given voice broadcast id. */ - fun addListener(voiceBroadcastId: String, listener: Listener) { + override fun addListener(voiceBroadcastId: String, listener: Listener) { listeners[voiceBroadcastId]?.add(listener) ?: run { listeners[voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } - if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(state) else listener.onStateChanged(State.IDLE) + if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE) } /** * Remove a [Listener] from the given voice broadcast id. */ - fun removeListener(voiceBroadcastId: String, listener: Listener) { + override fun removeListener(voiceBroadcastId: String, listener: Listener) { listeners[voiceBroadcastId]?.remove(listener) } private fun startPlayback(roomId: String, eventId: String) { val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") // Stop listening previous voice broadcast if any - if (state != State.IDLE) stop() + if (playingState != State.IDLE) stop() currentRoomId = roomId currentVoiceBroadcastId = eventId - state = State.BUFFERING + playingState = State.BUFFERING val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState if (voiceBroadcastState == VoiceBroadcastState.STOPPED) { @@ -187,7 +192,7 @@ class VoiceBroadcastPlayer @Inject constructor( currentMediaPlayer?.start() currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } currentSequence = sequence - withContext(Dispatchers.Main) { state = State.PLAYING } + withContext(Dispatchers.Main) { playingState = State.PLAYING } nextMediaPlayer = prepareNextMediaPlayer() } catch (failure: Throwable) { Timber.e(failure, "Unable to start playback") @@ -219,7 +224,7 @@ class VoiceBroadcastPlayer @Inject constructor( private fun resumePlayback() { currentMediaPlayer?.start() currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } - state = State.PLAYING + playingState = State.PLAYING } private fun updatePlaylist(playlist: List) { @@ -285,7 +290,7 @@ class VoiceBroadcastPlayer @Inject constructor( if (newChunks.isEmpty()) return updatePlaylist(playlist + newChunks) - when (state) { + when (playingState) { State.PLAYING -> { if (nextMediaPlayer == null) { coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } @@ -330,7 +335,7 @@ class VoiceBroadcastPlayer @Inject constructor( // We'll not receive new chunks anymore so we can stop the live listening stop() } else { - state = State.BUFFERING + playingState = State.BUFFERING } } @@ -339,15 +344,4 @@ class VoiceBroadcastPlayer @Inject constructor( return true } } - - enum class State { - PLAYING, - PAUSED, - BUFFERING, - IDLE - } - - fun interface Listener { - fun onStateChanged(state: State) - } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt index 8b69051823..8bc33ed769 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast +package im.vector.app.features.voicebroadcast.recording import androidx.annotation.IntRange import im.vector.app.features.voice.VoiceRecorder diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt similarity index 98% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index 5285dc5e3b..519f1f24aa 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast +package im.vector.app.features.voicebroadcast.recording import android.content.Context import android.media.MediaRecorder diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt index 1430dd8c86..58e1f26f44 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toContent diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt index 2f03d4194c..524b64e095 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toContent diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 2b7ca7b9f1..a1a519a656 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -14,17 +14,18 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import android.content.Context import androidx.core.content.FileProvider import im.vector.app.core.resources.BuildMeta import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt index ab4d16ab60..791409b869 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -14,11 +14,12 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.Membership diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt index bc6a3e7be6..da13100609 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toContent diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt index 5c42b26c54..a1ec91aab8 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt @@ -17,9 +17,10 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeSession diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt index a1bc3a04ec..8b66d45dd4 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt @@ -17,9 +17,10 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeSession diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 217a395076..59929ef0d7 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -17,10 +17,11 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt index ee6b141bd9..4b15f50be9 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt @@ -17,9 +17,10 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeSession From 3fcac097d38a27f12eab252ab0469e793e34ac40 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 27 Oct 2022 16:26:13 +0200 Subject: [PATCH 032/215] VoiceBroadcastPlayer - Fetch playlist in dedicated use case and improve player --- .../listening/VoiceBroadcastPlayerImpl.kt | 130 ++++++------------ .../GetLiveVoiceBroadcastChunksUseCase.kt | 130 ++++++++++++++++++ 2 files changed, 174 insertions(+), 86 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 168b921c2e..9afe428e59 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -23,53 +23,42 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk -import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId -import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State +import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent -import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent -import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject +import javax.inject.Singleton @Singleton class VoiceBroadcastPlayerImpl @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val playbackTracker: AudioMessagePlaybackTracker, private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, + private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase ) : VoiceBroadcastPlayer { + private val session get() = sessionHolder.getActiveSession() private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private var voiceBroadcastStateJob: Job? = null - private var currentTimeline: Timeline? = null - set(value) { - field?.removeAllListeners() - field?.dispose() - field = value - } private val mediaPlayerListener = MediaPlayerListener() - private var timelineListener: TimelineListener? = null private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null @@ -79,7 +68,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private var currentSequence: Int? = null + private var fetchPlaylistJob: Job? = null private var playlist = emptyList() + private var isLive: Boolean = false + override var currentVoiceBroadcastId: String? = null override var playingState = State.IDLE @@ -118,6 +110,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // Stop playback currentMediaPlayer?.stop() currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) } + isLive = false // Release current player release(currentMediaPlayer) @@ -131,9 +124,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( voiceBroadcastStateJob?.cancel() voiceBroadcastStateJob = null - // In case of live broadcast, stop observing new chunks - currentTimeline = null - timelineListener = null + // Do not fetch the playlist anymore + fetchPlaylistJob?.cancel() + fetchPlaylistJob = null // Update state playingState = State.IDLE @@ -141,13 +134,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // Clear playlist playlist = emptyList() currentSequence = null + currentRoomId = null currentVoiceBroadcastId = null } - /** - * Add a [Listener] to the given voice broadcast id. - */ override fun addListener(voiceBroadcastId: String, listener: Listener) { listeners[voiceBroadcastId]?.add(listener) ?: run { listeners[voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } @@ -155,15 +146,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE) } - /** - * Remove a [Listener] from the given voice broadcast id. - */ override fun removeListener(voiceBroadcastId: String, listener: Listener) { listeners[voiceBroadcastId]?.remove(listener) } private fun startPlayback(roomId: String, eventId: String) { - val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") // Stop listening previous voice broadcast if any if (playingState != State.IDLE) stop() @@ -173,16 +160,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.BUFFERING val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState - if (voiceBroadcastState == VoiceBroadcastState.STOPPED) { - // Get static playlist - updatePlaylist(getExistingChunks(room, eventId)) - startPlayback(false) - } else { - playLiveVoiceBroadcast(room, eventId) - } + isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED + observeIncomingEvents(roomId, eventId) } - private fun startPlayback(isLive: Boolean) { + private fun startPlayback() { val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } val sequence = event.getVoiceBroadcastChunk()?.sequence @@ -201,24 +183,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun playLiveVoiceBroadcast(room: Room, eventId: String) { - room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() ?: error("Cannot retrieve voice broadcast $eventId") - updatePlaylist(getExistingChunks(room, eventId)) - startPlayback(true) - observeIncomingEvents(room, eventId) - } - - private fun getExistingChunks(room: Room, eventId: String): List { - return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId) - .mapNotNull { it.root.asMessageAudioEvent() } - .filter { it.isVoiceBroadcast() } - } - - private fun observeIncomingEvents(room: Room, eventId: String) { - currentTimeline = room.timelineService().createTimeline(null, TimelineSettings(5)).also { timeline -> - timelineListener = TimelineListener(eventId).also { timeline.addListener(it) } - timeline.start() - } + private fun observeIncomingEvents(roomId: String, voiceBroadcastId: String) { + fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId) + .onEach(this::updatePlaylist) + .launchIn(coroutineScope) } private fun resumePlayback() { @@ -229,11 +197,32 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun updatePlaylist(playlist: List) { this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } + onPlaylistUpdated() + } + + private fun onPlaylistUpdated() { + when (playingState) { + State.PLAYING -> { + if (nextMediaPlayer == null) { + coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + } + } + State.PAUSED -> { + if (nextMediaPlayer == null) { + coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + } + } + State.BUFFERING -> { + val newMediaContent = getNextAudioContent() + if (newMediaContent != null) startPlayback() + } + State.IDLE -> startPlayback() + } } private fun getNextAudioContent(): MessageAudioContent? { val nextSequence = currentSequence?.plus(1) - ?: timelineListener?.let { playlist.lastOrNull()?.sequence } + ?: playlist.lastOrNull()?.sequence ?: 1 return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content } @@ -279,37 +268,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private inner class TimelineListener(private val voiceBroadcastId: String) : Timeline.Listener { - override fun onTimelineUpdated(snapshot: List) { - val currentSequences = playlist.map { it.sequence } - val newChunks = snapshot - .mapNotNull { timelineEvent -> - timelineEvent.root.asMessageAudioEvent() - ?.takeIf { it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && it.sequence !in currentSequences } - } - if (newChunks.isEmpty()) return - updatePlaylist(playlist + newChunks) - - when (playingState) { - State.PLAYING -> { - if (nextMediaPlayer == null) { - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } - } - } - State.PAUSED -> { - if (nextMediaPlayer == null) { - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } - } - } - State.BUFFERING -> { - val newMediaContent = getNextAudioContent() - if (newMediaContent != null) startPlayback(true) - } - State.IDLE -> startPlayback(true) - } - } - } - private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { @@ -329,7 +287,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val roomId = currentRoomId ?: return val voiceBroadcastId = currentVoiceBroadcastId ?: return val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return - val isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED + isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) { // We'll not receive new chunks anymore so we can stop the live listening diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt new file mode 100644 index 0000000000..8fbd32767d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2022 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.voicebroadcast.listening.usecase + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId +import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.sequence +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.runningReduce +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent +import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import javax.inject.Inject + +/** + * Get a [Flow] of [MessageAudioEvent]s related to the given voice broadcast. + */ +class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, +) { + + fun execute(roomId: String, voiceBroadcastId: String): Flow> { + val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow() + val room = session.roomService().getRoom(roomId) ?: return emptyFlow() + val timeline = room.timelineService().createTimeline(null, TimelineSettings(5)) + + // Get initial chunks + val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcastId) + .mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } } + + val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId) + val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState + + return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) { + // Just send the existing chunks if voice broadcast is stopped + flowOf(existingChunks) + } else { + // Observe new timeline events if voice broadcast is ongoing + callbackFlow { + // Init with existing chunks + send(existingChunks) + + // Observe new timeline events + val listener = object : Timeline.Listener { + private var lastEventId: String? = null + private var lastSequence: Int? = null + + override fun onTimelineUpdated(snapshot: List) { + val newEvents = lastEventId?.let { eventId -> snapshot.subList(0, snapshot.indexOfFirst { it.eventId == eventId }) } ?: snapshot + + // Detect a potential stopped voice broadcast state event + val stopEvent = newEvents.findStopEvent() + if (stopEvent != null) { + lastSequence = stopEvent.content?.lastChunkSequence + } + + val newChunks = newEvents.mapToChunkEvents(voiceBroadcastId, voiceBroadcastEvent.root.senderId) + + // Notify about new chunks + if (newChunks.isNotEmpty()) { + trySend(newChunks) + } + + // Automatically stop observing the timeline if the last chunk has been received + if (lastSequence != null && newChunks.any { it.sequence == lastSequence }) { + timeline.removeListener(this) + timeline.dispose() + } + + lastEventId = snapshot.firstOrNull()?.eventId + } + } + + timeline.addListener(listener) + timeline.start() + awaitClose { + timeline.removeListener(listener) + timeline.dispose() + } + } + .runningReduce { accumulator: List, value: List -> accumulator.plus(value) } + } + } + + /** + * Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state. + */ + private fun List.findStopEvent(): VoiceBroadcastEvent? = + this.mapNotNull { it.root.asVoiceBroadcastEvent() } + .find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } + + /** + * Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast. + */ + private fun List.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List = + this.mapNotNull { timelineEvent -> + timelineEvent.root.asMessageAudioEvent() + ?.takeIf { + it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && + it.root.senderId == senderId + } + } +} From 14f1925cd357206adc841ea60c01c0ea617b957d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Oct 2022 23:17:24 +0000 Subject: [PATCH 033/215] Bump sonarqube-gradle-plugin from 3.4.0.2513 to 3.5.0.2730 Bumps sonarqube-gradle-plugin from 3.4.0.2513 to 3.5.0.2730. --- updated-dependencies: - dependency-name: org.sonarsource.scanner.gradle:sonarqube-gradle-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6ccb83e703..fb34ff63a0 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ buildscript { classpath libs.gradle.hiltPlugin classpath 'com.google.firebase:firebase-appdistribution-gradle:3.0.3' classpath 'com.google.gms:google-services:4.3.14' - classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513' + classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.2.3" classpath 'org.owasp:dependency-check-gradle:7.3.0' From 62c574b96634709a456615ef71ac143186c545e7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 10:29:48 +0200 Subject: [PATCH 034/215] Add changelog --- changelog.d/7478.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7478.wip diff --git a/changelog.d/7478.wip b/changelog.d/7478.wip new file mode 100644 index 0000000000..2e6602b16d --- /dev/null +++ b/changelog.d/7478.wip @@ -0,0 +1 @@ +[Voice Broadcast] Improve playlist fetching and player codebase From 838e11c167a5bdf9d4b36ef1221f0518029a94b8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 10:43:05 +0200 Subject: [PATCH 035/215] rename observeIncomingEvents method and reorder some methods --- .../listening/VoiceBroadcastPlayerImpl.kt | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 9afe428e59..3999a0e0af 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -161,40 +161,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED - observeIncomingEvents(roomId, eventId) + fetchPlaylistAndStartPlayback(roomId, eventId) } - private fun startPlayback() { - val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() - val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = event.getVoiceBroadcastChunk()?.sequence - coroutineScope.launch { - try { - currentMediaPlayer = prepareMediaPlayer(content) - currentMediaPlayer?.start() - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } - currentSequence = sequence - withContext(Dispatchers.Main) { playingState = State.PLAYING } - nextMediaPlayer = prepareNextMediaPlayer() - } catch (failure: Throwable) { - Timber.e(failure, "Unable to start playback") - throw VoiceFailure.UnableToPlay(failure) - } - } - } - - private fun observeIncomingEvents(roomId: String, voiceBroadcastId: String) { + private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) { fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId) .onEach(this::updatePlaylist) .launchIn(coroutineScope) } - private fun resumePlayback() { - currentMediaPlayer?.start() - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } - playingState = State.PLAYING - } - private fun updatePlaylist(playlist: List) { this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } onPlaylistUpdated() @@ -220,6 +195,31 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } + private fun startPlayback() { + val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() + val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } + val sequence = event.getVoiceBroadcastChunk()?.sequence + coroutineScope.launch { + try { + currentMediaPlayer = prepareMediaPlayer(content) + currentMediaPlayer?.start() + currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } + currentSequence = sequence + withContext(Dispatchers.Main) { playingState = State.PLAYING } + nextMediaPlayer = prepareNextMediaPlayer() + } catch (failure: Throwable) { + Timber.e(failure, "Unable to start playback") + throw VoiceFailure.UnableToPlay(failure) + } + } + } + + private fun resumePlayback() { + currentMediaPlayer?.start() + currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } + playingState = State.PLAYING + } + private fun getNextAudioContent(): MessageAudioContent? { val nextSequence = currentSequence?.plus(1) ?: playlist.lastOrNull()?.sequence From 362696cfc88da4a67d4a527fccc9917d6508f124 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 10:27:05 +0200 Subject: [PATCH 036/215] VoiceBroadcast - Show error dialog if user is not able to record a voice broadcast --- .../src/main/res/values/strings.xml | 4 +++ .../vector/app/core/error/ErrorFormatter.kt | 11 +++++++ .../home/room/detail/TimelineFragment.kt | 7 +++- .../home/room/detail/TimelineViewModel.kt | 7 +++- .../voicebroadcast/VoiceBroadcastFailure.kt | 25 +++++++++++++++ .../usecase/StartVoiceBroadcastUseCase.kt | 32 ++++++++++++++++--- 6 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 9edd7d836a..b5abefec94 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3085,6 +3085,10 @@ Play or resume voice broadcast Pause voice broadcast Buffering + Can’t start a new voice broadcast + You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. + Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. + You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. 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 a09f852958..380c80775b 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 @@ -21,6 +21,8 @@ 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 im.vector.app.features.voicebroadcast.VoiceBroadcastFailure +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixIdFailure @@ -135,6 +137,7 @@ class DefaultErrorFormatter @Inject constructor( is MatrixIdFailure.InvalidMatrixId -> stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id) is VoiceFailure -> voiceMessageError(throwable) + is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable) is ActivityNotFoundException -> stringProvider.getString(R.string.error_no_external_application_found) else -> throwable.localizedMessage @@ -149,6 +152,14 @@ class DefaultErrorFormatter @Inject constructor( } } + private fun voiceBroadcastMessageError(throwable: VoiceBroadcastFailure): String { + return when (throwable) { + RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message) + RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message) + RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message) + } + } + private fun limitExceededError(error: MatrixError): String { val delay = error.retryAfterMillis diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 4f51922a62..b259d51947 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -33,6 +33,7 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.activity.addCallback +import androidx.annotation.StringRes import androidx.appcompat.view.menu.MenuBuilder import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat @@ -1320,8 +1321,12 @@ class TimelineFragment : } private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) { + @StringRes val titleResId = when(result.action) { + RoomDetailAction.VoiceBroadcastAction.Recording.Start -> R.string.error_voice_broadcast_unauthorized_title + else -> R.string.dialog_title_error + } MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.dialog_title_error) + .setTitle(titleResId) .setMessage(errorFormatter.toHumanReadable(result.throwable)) .setPositiveButton(R.string.ok, null) .show() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 82ad96d645..ac117558be 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -604,7 +604,12 @@ class TimelineViewModel @AssistedInject constructor( if (room == null) return viewModelScope.launch { when (action) { - RoomDetailAction.VoiceBroadcastAction.Recording.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId) + RoomDetailAction.VoiceBroadcastAction.Recording.Start -> { + voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold( + { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) }, + { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) }, + ) + } RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt new file mode 100644 index 0000000000..76b50c78ab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 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.voicebroadcast + +sealed class VoiceBroadcastFailure : Throwable() { + sealed class RecordingError : VoiceBroadcastFailure() { + object NoPermission : RecordingError() + object BlockedBySomeoneElse : RecordingError() + object UserAlreadyBroadcasting : RecordingError() + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index a1a519a656..f6870f859f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -24,15 +24,22 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.getStateEvent +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import timber.log.Timber import java.io.File import javax.inject.Inject @@ -50,12 +57,27 @@ class StartVoiceBroadcastUseCase @Inject constructor( Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested") - val onGoingVoiceBroadcastEvents = getOngoingVoiceBroadcastsUseCase.execute(roomId) + val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.let { PowerLevelsHelper(it) } - if (onGoingVoiceBroadcastEvents.isEmpty()) { - startVoiceBroadcast(room) - } else { - Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: currentVoiceBroadcastEvents=$onGoingVoiceBroadcastEvents") + when { + powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission") + throw VoiceBroadcastFailure.RecordingError.NoPermission + } + voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") + throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting + } + getOngoingVoiceBroadcastsUseCase.execute(roomId).isNotEmpty() -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting") + throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse + } + else -> { + startVoiceBroadcast(room) + } } } From b510919d59d92466f29f68d7c6cfd79c0b6e363b Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 10:53:35 +0200 Subject: [PATCH 037/215] Add changelog --- changelog.d/7485.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7485.wip diff --git a/changelog.d/7485.wip b/changelog.d/7485.wip new file mode 100644 index 0000000000..30cab45d9c --- /dev/null +++ b/changelog.d/7485.wip @@ -0,0 +1 @@ +[Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast From d1f5fa5b59a439742ac8ccd3f228513ede310736 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Oct 2022 23:06:32 +0000 Subject: [PATCH 038/215] Bump flipper from 0.171.1 to 0.173.0 Bumps `flipper` from 0.171.1 to 0.173.0. Updates `flipper` from 0.171.1 to 0.173.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.171.1...v0.173.0) Updates `flipper-network-plugin` from 0.171.1 to 0.173.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.171.1...v0.173.0) --- updated-dependencies: - dependency-name: com.facebook.flipper:flipper dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.facebook.flipper:flipper-network-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 33a2096a43..1bc9be92a4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,7 +17,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.171.1" +def flipper = "0.173.0" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" From a06efb7abdf2e150cb744ef891dc9567aedd0139 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 10:58:09 +0100 Subject: [PATCH 039/215] Fix lint issues --- .../im/vector/app/features/home/room/detail/TimelineFragment.kt | 2 +- .../recording/usecase/StartVoiceBroadcastUseCase.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index b259d51947..120e5e22cb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1321,7 +1321,7 @@ class TimelineFragment : } private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) { - @StringRes val titleResId = when(result.action) { + @StringRes val titleResId = when (result.action) { RoomDetailAction.VoiceBroadcastAction.Recording.Start -> R.string.error_voice_broadcast_unauthorized_title else -> R.string.dialog_title_error } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index f6870f859f..8a335eccac 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -21,10 +21,10 @@ import androidx.core.content.FileProvider import im.vector.app.core.resources.BuildMeta import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType From d7791402b79d63c63566564b95af94e404c0bdc8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 15:18:24 +0100 Subject: [PATCH 040/215] Fix unit tests --- .../usecase/StartVoiceBroadcastUseCase.kt | 57 ++++++++++++------- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 29 +++++++--- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 8a335eccac..85f72c09da 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -28,6 +28,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import org.jetbrains.annotations.VisibleForTesting import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType @@ -57,28 +58,8 @@ class StartVoiceBroadcastUseCase @Inject constructor( Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested") - val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - ?.let { PowerLevelsHelper(it) } - - when { - powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true -> { - Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission") - throw VoiceBroadcastFailure.RecordingError.NoPermission - } - voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> { - Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") - throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting - } - getOngoingVoiceBroadcastsUseCase.execute(roomId).isNotEmpty() -> { - Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting") - throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse - } - else -> { - startVoiceBroadcast(room) - } - } + assertCanStartVoiceBroadcast(room) + startVoiceBroadcast(room) } private suspend fun startVoiceBroadcast(room: Room) { @@ -124,4 +105,36 @@ class StartVoiceBroadcastUseCase @Inject constructor( ) ) } + + private fun assertCanStartVoiceBroadcast(room: Room) { + assertHasEnoughPowerLevels(room) + assertNoOngoingVoiceBroadcast(room) + } + + @VisibleForTesting + fun assertHasEnoughPowerLevels(room: Room) { + val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.let { PowerLevelsHelper(it) } + + if (powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true) { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission") + throw VoiceBroadcastFailure.RecordingError.NoPermission + } + } + + @VisibleForTesting + fun assertNoOngoingVoiceBroadcast(room: Room) { + when { + voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") + throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting + } + getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting") + throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse + } + } + } } diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 59929ef0d7..ef78f1c80d 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -26,15 +26,17 @@ import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeSession -import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.slot +import io.mockk.spyk import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeNull +import org.junit.Before import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -51,14 +53,23 @@ class StartVoiceBroadcastUseCaseTest { private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeVoiceBroadcastRecorder = mockk(relaxed = true) private val fakeGetOngoingVoiceBroadcastsUseCase = mockk() - private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase( - session = fakeSession, - voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, - context = FakeContext().instance, - buildMeta = mockk(), - getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, + private val startVoiceBroadcastUseCase = spyk( + StartVoiceBroadcastUseCase( + session = fakeSession, + voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, + context = FakeContext().instance, + buildMeta = mockk(), + getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, + ) ) + @Before + fun setup() { + every { fakeRoom.roomId } returns A_ROOM_ID + justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) } + every { fakeVoiceBroadcastRecorder.state } returns VoiceBroadcastRecorder.State.Idle + } + @Test fun `given a room id with potential several existing voice broadcast states when calling execute then the voice broadcast is started or not`() = runTest { val cases = VoiceBroadcastState.values() @@ -83,7 +94,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List) { // Given - clearAllMocks() + setup() givenVoiceBroadcasts(voiceBroadcasts) val voiceBroadcastInfoContentInterceptor = slot() coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID } @@ -106,7 +117,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List) { // Given - clearAllMocks() + setup() givenVoiceBroadcasts(voiceBroadcasts) // When From 7ba1052bcf6311547ac0ca9ecc1b09720bd0c9b9 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 31 Oct 2022 16:43:01 +0100 Subject: [PATCH 041/215] Fix rich text editor EditText not resizing properly in full screen (#7491) * Fix rich text editor full screen mode * Add changelog * Address review comments. --- changelog.d/7491.bugfix | 1 + .../detail/composer/RichTextComposerLayout.kt | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 changelog.d/7491.bugfix diff --git a/changelog.d/7491.bugfix b/changelog.d/7491.bugfix new file mode 100644 index 0000000000..1a87bd03bd --- /dev/null +++ b/changelog.d/7491.bugfix @@ -0,0 +1 @@ +Fix rich text editor textfield not growing to fill parent on full screen. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 2c09f351bb..2d2a4a8cd2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -54,8 +54,9 @@ class RichTextComposerLayout @JvmOverloads constructor( private var currentConstraintSetId: Int = -1 private val animationDuration = 100L + private val maxEditTextLinesWhenCollapsed = 12 - private var isFullScreen = false + private val isFullScreen: Boolean get() = currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_fullscreen var isTextFormattingEnabled = true set(value) { @@ -104,10 +105,10 @@ class RichTextComposerLayout @JvmOverloads constructor( collapse(false) views.richTextComposerEditText.addTextChangedListener( - TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder) + TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() }) ) views.plainTextComposerEditText.addTextChangedListener( - TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder) + TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() }) ) views.composerRelatedMessageCloseButton.setOnClickListener { @@ -196,8 +197,9 @@ class RichTextComposerLayout @JvmOverloads constructor( button.isSelected = menuState.reversedActions.contains(action) } - private fun updateTextFieldBorder(isExpanded: Boolean) { - val borderResource = if (isExpanded) { + private fun updateTextFieldBorder() { + val isExpanded = editText.editableText.lines().count() > 1 + val borderResource = if (isExpanded || isFullScreen) { R.drawable.bg_composer_rich_edit_text_expanded } else { R.drawable.bg_composer_rich_edit_text_single_line @@ -240,8 +242,21 @@ class RichTextComposerLayout @JvmOverloads constructor( it.applyTo(this) } - updateTextFieldBorder(newValue) + updateTextFieldBorder() updateEditTextVisibility() + + updateEditTextFullScreenState(views.richTextComposerEditText, newValue) + updateEditTextFullScreenState(views.plainTextComposerEditText, newValue) + } + + private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) { + if (isFullScreen) { + editText.maxLines = Int.MAX_VALUE + // This is a workaround to fix incorrect scroll position when maximised + post { editText.requestLayout() } + } else { + editText.maxLines = maxEditTextLinesWhenCollapsed + } } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { From d4234ae3bd6b63c0d126b940fdbefd7acb06a4b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:08:36 +0000 Subject: [PATCH 042/215] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b6333c5940..a44872e0ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build docs run: ./gradlew dokkaHtml From d2012ae0222bb21f8692aa0e9a5c75dafe8d913d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:10:15 +0000 Subject: [PATCH 043/215] Bump lazythreetenbp from 0.11.0 to 0.12.0 Bumps [lazythreetenbp](https://github.com/gabrielittner/lazythreetenbp) from 0.11.0 to 0.12.0. - [Release notes](https://github.com/gabrielittner/lazythreetenbp/releases) - [Changelog](https://github.com/gabrielittner/lazythreetenbp/blob/main/CHANGELOG.md) - [Commits](https://github.com/gabrielittner/lazythreetenbp/compare/0.11.0...0.12.0) --- updated-dependencies: - dependency-name: com.gabrielittner.threetenbp:lazythreetenbp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 0b884f4d99..90992205e3 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -132,7 +132,7 @@ dependencies { implementation libs.androidx.biometric api "org.threeten:threetenbp:1.4.0:no-tzdb" - api "com.gabrielittner.threetenbp:lazythreetenbp:0.11.0" + api "com.gabrielittner.threetenbp:lazythreetenbp:0.12.0" implementation libs.squareup.moshi kapt libs.squareup.moshiKotlin From 411c8c90961d83a7e5bea10e5c055b6245ee7d92 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 1 Nov 2022 11:21:27 +0100 Subject: [PATCH 044/215] Fix duplicated pills when pills contain other spans Fixes following issues: - Duplicated pills if the mention contains an image: https://github.com/SchildiChat/SchildiChat-android/issues/148 - Duplicated pills if these contain underscores: https://github.com/SchildiChat/SchildiChat-android/issues/156 --- .../vector/app/features/html/PillsPostProcessor.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt index 85cfb76ff7..f6e10a6df9 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt @@ -83,6 +83,20 @@ class PillsPostProcessor @AssistedInject constructor( val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach val startSpan = renderedText.getSpanStart(linkSpan) val endSpan = renderedText.getSpanEnd(linkSpan) + // GlideImagesPlugin causes duplicated pills if we have a nested spans in the pill span, + // such as images or italic text. + // Accordingly, it's better to remove all spans that are contained in this span before rendering. + renderedText.getSpans(startSpan, endSpan, Any::class.java).forEach remove@{ + if (it !is LinkSpan) { + // Make sure to only remove spans that are contained in this link, and not are bigger than this link, e.g. like reply-blocks + val start = renderedText.getSpanStart(it) + if (start < startSpan) return@remove + val end = renderedText.getSpanEnd(it) + if (end > endSpan) return@remove + + renderedText.removeSpan(it) + } + } addPillSpan(renderedText, pillSpan, startSpan, endSpan) } } From 99d510773269be972b9b8926b9584816d07a552b Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 1 Nov 2022 15:49:46 +0100 Subject: [PATCH 045/215] Changelog 7501 --- changelog.d/7501.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7501.bugfix diff --git a/changelog.d/7501.bugfix b/changelog.d/7501.bugfix new file mode 100644 index 0000000000..b86258d427 --- /dev/null +++ b/changelog.d/7501.bugfix @@ -0,0 +1 @@ +Fix duplicated mention pills in some cases From 20abef26b0a6eda4ef7d600072a64f4587cffba0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 18:19:21 +0100 Subject: [PATCH 046/215] Filter duplicated events in live voice broadcasts --- .../listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 8fbd32767d..4f9f2de673 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningReduce import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent @@ -106,6 +107,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( } } .runningReduce { accumulator: List, value: List -> accumulator.plus(value) } + .map { events -> events.distinctBy { it.sequence } } } } From 68062911a98da9e23c1f7c0b47b6a166973eee8f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 1 Nov 2022 18:17:23 +0100 Subject: [PATCH 047/215] Changelog --- changelog.d/7502.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7502.bugfix diff --git a/changelog.d/7502.bugfix b/changelog.d/7502.bugfix new file mode 100644 index 0000000000..8785310498 --- /dev/null +++ b/changelog.d/7502.bugfix @@ -0,0 +1 @@ +Voice Broadcast - Fix duplicated voice messages in the internal playlist From 38fe5569789931b72ca6ecce6571a8317df1caff Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:30:44 +0200 Subject: [PATCH 048/215] Adding changelog entry --- changelog.d/7457.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7457.bugfix diff --git a/changelog.d/7457.bugfix b/changelog.d/7457.bugfix new file mode 100644 index 0000000000..9dfbc53329 --- /dev/null +++ b/changelog.d/7457.bugfix @@ -0,0 +1 @@ +[Session manager] Hide push notification toggle when there is no server support From 1acb42f61d95b6dbbe2a9392312c40aeaeb6b933 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 11:34:41 +0200 Subject: [PATCH 049/215] Adding use case to check support for new enabled field support --- .../homeserver/HomeServerCapabilities.kt | 5 ++ .../sdk/internal/auth/version/Versions.kt | 11 ++++ .../database/RealmSessionStoreMigration.kt | 4 +- .../mapper/HomeServerCapabilitiesMapper.kt | 3 +- .../database/migration/MigrateSessionTo042.kt | 31 ++++++++++ .../model/HomeServerCapabilitiesEntity.kt | 1 + .../GetHomeServerCapabilitiesTask.kt | 12 +++- ...TogglePushNotificationsViaPusherUseCase.kt | 35 +++++++++++ ...lePushNotificationsViaPusherUseCaseTest.kt | 61 +++++++++++++++++++ .../fixtures/HomeserverCapabilityFixture.kt | 20 +++--- 10 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 773e870ffd..11638837cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -70,6 +70,11 @@ data class HomeServerCapabilities( * True if the home server supports threaded read receipts and unread notifications. */ val canUseThreadReadReceiptsAndNotifications: Boolean = false, + + /** + * True if the home server supports remote toggle of Pusher for a given device. + */ + val canRemotelyTogglePushNotificationsOfDevices: Boolean = false, ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 1245d8df4b..bc2d4a5aef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.version import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse /** * Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions. @@ -56,6 +57,7 @@ private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" +private const val FEATURE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" /** * Return true if the SDK supports this homeserver version. @@ -142,3 +144,12 @@ private fun Versions.getMaxVersion(): HomeServerVersion { ?.maxOrNull() ?: HomeServerVersion.r0_0_0 } + +/** + * Indicate if the server supports MSC3881: https://github.com/matrix-org/matrix-spec-proposals/pull/3881. + * + * @return true if remote toggle of push notifications is supported + */ +internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean { + return unstableFeatures?.get(FEATURE_PUSH_NOTIFICATIONS_MSC3881).orFalse() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 58c015b13b..30836c027e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -58,6 +58,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -66,7 +67,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 41L, + schemaVersion = 42L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -117,5 +118,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 39) MigrateSessionTo039(realm).perform() if (oldVersion < 40) MigrateSessionTo040(realm).perform() if (oldVersion < 41) MigrateSessionTo041(realm).perform() + if (oldVersion < 42) MigrateSessionTo042(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 3528ca0051..89657ad882 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -45,7 +45,8 @@ internal object HomeServerCapabilitiesMapper { canUseThreading = entity.canUseThreading, canControlLogoutDevices = entity.canControlLogoutDevices, canLoginWithQrCode = entity.canLoginWithQrCode, - canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications + canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, + canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt new file mode 100644 index 0000000000..8826d894c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 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.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo042(realm: DynamicRealm) : RealmMigrator(realm, 42) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.CAN_REMOTELY_TOGGLE_PUSH_NOTIFICATIONS_OF_DEVICES, Boolean::class.java) + ?.forceRefreshOfHomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 89f1e50b30..2b60f7723c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -33,6 +33,7 @@ internal open class HomeServerCapabilitiesEntity( var canControlLogoutDevices: Boolean = false, var canLoginWithQrCode: Boolean = false, var canUseThreadReadReceiptsAndNotifications: Boolean = false, + var canRemotelyTogglePushNotificationsOfDevices: Boolean = false, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index a5953d870c..11e86a5c51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin +import org.matrix.android.sdk.internal.auth.version.doesServerSupportRemoteToggleOfPushNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreadUnreadNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk @@ -141,13 +142,18 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } if (getVersionResult != null) { - homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() - homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices() + homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = + getVersionResult.isLoginAndRegistrationSupportedBySdk() + homeServerCapabilitiesEntity.canControlLogoutDevices = + getVersionResult.doesServerSupportLogoutDevices() homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */ getVersionResult.doesServerSupportThreads() homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications = getVersionResult.doesServerSupportThreadUnreadNotifications() - homeServerCapabilitiesEntity.canLoginWithQrCode = getVersionResult.doesServerSupportQrCodeLogin() + homeServerCapabilitiesEntity.canLoginWithQrCode = + getVersionResult.doesServerSupportQrCodeLogin() + homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices = + getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() } if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt new file mode 100644 index 0000000000..0d5bce663a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(): Boolean { + return activeSessionHolder + .getSafeActiveSession() + ?.homeServerCapabilitiesService() + ?.getHomeServerCapabilities() + ?.canRemotelyTogglePushNotificationsOfDevices + .orFalse() + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt new file mode 100644 index 0000000000..51874be1bc --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fixtures.aHomeServerCapabilities +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) + +class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCanTogglePushNotificationsViaPusherUseCase = + CheckIfCanTogglePushNotificationsViaPusherUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given current session when execute then toggle capability is returned`() { + // Given + fakeActiveSessionHolder + .fakeSession + .fakeHomeServerCapabilitiesService + .givenCapabilities(A_HOMESERVER_CAPABILITIES) + + // When + val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() + + // Then + result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices + } + + @Test + fun `given no current session when execute then false is returned`() { + // Given + fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) + + // When + val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() + + // Then + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt index a4d9869a89..c9f32c2cf2 100644 --- a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt +++ b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt @@ -27,14 +27,16 @@ fun aHomeServerCapabilities( maxUploadFileSize: Long = 100L, lastVersionIdentityServerSupported: Boolean = false, defaultIdentityServerUrl: String? = null, - roomVersions: RoomVersionCapabilities? = null + roomVersions: RoomVersionCapabilities? = null, + canRemotelyTogglePushNotificationsOfDevices: Boolean = true, ) = HomeServerCapabilities( - canChangePassword, - canChangeDisplayName, - canChangeAvatar, - canChange3pid, - maxUploadFileSize, - lastVersionIdentityServerSupported, - defaultIdentityServerUrl, - roomVersions + canChangePassword = canChangePassword, + canChangeDisplayName = canChangeDisplayName, + canChangeAvatar = canChangeAvatar, + canChange3pid = canChange3pid, + maxUploadFileSize = maxUploadFileSize, + lastVersionIdentityServerSupported = lastVersionIdentityServerSupported, + defaultIdentityServerUrl = defaultIdentityServerUrl, + roomVersions = roomVersions, + canRemotelyTogglePushNotificationsOfDevices = canRemotelyTogglePushNotificationsOfDevices, ) From 62912f891cde60bd07fd73735dff126816d85910 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 14:36:06 +0200 Subject: [PATCH 050/215] Introducing a NotificationsStatus to render the push notification toggle in session overview screen --- ...ePushNotificationsViaAccountDataUseCase.kt | 33 +++++++++ .../GetNotificationsStatusUseCase.kt | 59 +++++++++++++++ .../v2/notification/NotificationsStatus.kt | 23 ++++++ .../TogglePushNotificationUseCase.kt | 16 +++-- .../v2/overview/SessionOverviewFragment.kt | 22 +++--- .../v2/overview/SessionOverviewViewModel.kt | 33 +++------ .../v2/overview/SessionOverviewViewState.kt | 3 +- ...hNotificationsViaAccountDataUseCaseTest.kt | 71 +++++++++++++++++++ .../TogglePushNotificationUseCaseTest.kt | 26 ++++++- .../overview/SessionOverviewViewModelTest.kt | 23 +++--- .../fakes/FakeSessionAccountDataService.kt | 4 +- .../FakeTogglePushNotificationUseCase.kt | 2 +- 12 files changed, 264 insertions(+), 51 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt rename vector/src/main/java/im/vector/app/features/settings/devices/v2/{overview => notification}/TogglePushNotificationUseCase.kt (67%) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt rename vector/src/test/java/im/vector/app/features/settings/devices/v2/{overview => notification}/TogglePushNotificationUseCaseTest.kt (67%) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt new file mode 100644 index 0000000000..dbf9adca14 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import javax.inject.Inject + +class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(deviceId: String): Boolean { + return activeSessionHolder + .getSafeActiveSession() + ?.accountDataService() + ?.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt new file mode 100644 index 0000000000..1eb612988a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap +import javax.inject.Inject + +class GetNotificationsStatusUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, +) { + + // TODO add unit tests + fun execute(deviceId: String): Flow { + val session = activeSessionHolder.getSafeActiveSession() + return when { + session == null -> emptyFlow() + checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { + session.flow() + .livePushers() + .map { it.filter { pusher -> pusher.deviceId == deviceId } } + .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + } + checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { + session.flow() + .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + .unwrap() + .map { it.content.toModel()?.isSilenced?.not() } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + } + else -> flowOf(NotificationsStatus.NOT_SUPPORTED) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt new file mode 100644 index 0000000000..7ff1f04381 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +enum class NotificationsStatus { + ENABLED, + DISABLED, + NOT_SUPPORTED, +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt similarity index 67% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index 45c234aaef..be9012e9f1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2.overview +package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -24,17 +24,21 @@ import javax.inject.Inject class TogglePushNotificationUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return - val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } - devicePusher?.let { pusher -> - session.pushersService().togglePusher(pusher, enabled) + + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } + devicePusher?.let { pusher -> + session.pushersService().togglePusher(pusher, enabled) + } } - val accountData = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - if (accountData != null) { + if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) session.accountDataService().updateUserAccountData( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index a1cd7ea586..620372f810 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -43,6 +44,7 @@ import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.workers.signout.SignOutUiWorker import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse @@ -177,7 +179,7 @@ class SessionOverviewFragment : updateEntryDetails(state.deviceId) updateSessionInfo(state) updateLoading(state.isLoading) - updatePushNotificationToggle(state.deviceId, state.notificationsEnabled) + updatePushNotificationToggle(state.deviceId, state.notificationsStatus) } private fun updateToolbar(viewState: SessionOverviewViewState) { @@ -218,15 +220,19 @@ class SessionOverviewFragment : } } - private fun updatePushNotificationToggle(deviceId: String, enabled: Boolean) { - views.sessionOverviewPushNotifications.apply { - setOnCheckedChangeListener(null) - setChecked(enabled) - post { - setOnCheckedChangeListener { _, isChecked -> - viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked)) + private fun updatePushNotificationToggle(deviceId: String, notificationsStatus: NotificationsStatus) { + views.sessionOverviewPushNotifications.isGone = notificationsStatus == NotificationsStatus.NOT_SUPPORTED + when (notificationsStatus) { + NotificationsStatus.ENABLED, NotificationsStatus.DISABLED -> { + views.sessionOverviewPushNotifications.setOnCheckedChangeListener(null) + views.sessionOverviewPushNotifications.setChecked(notificationsStatus == NotificationsStatus.ENABLED) + views.sessionOverviewPushNotifications.post { + views.sessionOverviewPushNotifications.setOnCheckedChangeListener { _, isChecked -> + viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked)) + } } } + else -> Unit } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 21054270f8..1aa5f676cc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -29,6 +29,9 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase @@ -36,21 +39,15 @@ import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSes import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.unwrap import timber.log.Timber import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation @@ -65,6 +62,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase @@ -81,7 +79,7 @@ class SessionOverviewViewModel @AssistedInject constructor( refreshPushers() observeSessionInfo(initialState.deviceId) observeCurrentSessionInfo() - observePushers(initialState.deviceId) + observeNotificationsStatus(initialState.deviceId) } private fun refreshPushers() { @@ -107,20 +105,9 @@ class SessionOverviewViewModel @AssistedInject constructor( } } - private fun observePushers(deviceId: String) { - val session = activeSessionHolder.getSafeActiveSession() ?: return - val pusherFlow = session.flow() - .livePushers() - .map { it.filter { pusher -> pusher.deviceId == deviceId } } - .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - - val accountDataFlow = session.flow() - .liveUserAccountData(TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - .unwrap() - .map { it.content.toModel()?.isSilenced?.not() } - - merge(pusherFlow, accountDataFlow) - .onEach { it?.let { setState { copy(notificationsEnabled = it) } } } + private fun observeNotificationsStatus(deviceId: String) { + getNotificationsStatusUseCase.execute(deviceId) + .onEach { setState { copy(notificationsStatus = it) } } .launchIn(viewModelScope) } @@ -233,7 +220,9 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { togglePushNotificationUseCase.execute(action.deviceId, action.enabled) - setState { copy(notificationsEnabled = action.enabled) } + // TODO should not be needed => test without + val status = if (action.enabled) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED + setState { copy(notificationsStatus = status) } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt index 440805bad6..019dd2d724 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt @@ -20,13 +20,14 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus data class SessionOverviewViewState( val deviceId: String, val isCurrentSessionTrusted: Boolean = false, val deviceInfo: Async = Uninitialized, val isLoading: Boolean = false, - val notificationsEnabled: Boolean = false, + val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED, ) : MavericksState { constructor(args: SessionOverviewArgs) : this( deviceId = args.deviceId diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..0303444605 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes + +private const val A_DEVICE_ID = "device-id" + +class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase = + CheckIfCanTogglePushNotificationsViaAccountDataUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given current session and an account data for the device id when execute then result is true`() { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = mockk(), + ) + + // When + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + + // Then + result shouldBeEqualTo true + } + + @Test + fun `given current session and NO account data for the device id when execute then result is false`() { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = null, + ) + + // When + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + + // Then + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt similarity index 67% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt index dc64c74836..0a649354f9 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt @@ -14,10 +14,12 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2.overview +package im.vector.app.features.settings.devices.v2.notification import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fixtures.PusherFixture +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -27,10 +29,21 @@ import org.matrix.android.sdk.api.session.events.model.toContent class TogglePushNotificationUseCaseTest { private val activeSessionHolder = FakeActiveSessionHolder() - private val togglePushNotificationUseCase = TogglePushNotificationUseCase(activeSessionHolder.instance) + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = + mockk() + private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = + mockk() + + private val togglePushNotificationUseCase = + TogglePushNotificationUseCase( + activeSessionHolder = activeSessionHolder.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + ) @Test fun `when execute, then toggle enabled for device pushers`() = runTest { + // Given val sessionId = "a_session_id" val pushers = listOf( PusherFixture.aPusher(deviceId = sessionId, enabled = false), @@ -38,14 +51,19 @@ class TogglePushNotificationUseCaseTest { ) activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) activeSessionHolder.fakeSession.pushersService().givenGetPushers(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns false + // When togglePushNotificationUseCase.execute(sessionId, true) + // Then activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true) } @Test fun `when execute, then toggle local notification settings`() = runTest { + // Given val sessionId = "a_session_id" val pushers = listOf( PusherFixture.aPusher(deviceId = sessionId, enabled = false), @@ -56,9 +74,13 @@ class TogglePushNotificationUseCaseTest { UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, LocalNotificationSettingsContent(isSilenced = true).toContent() ) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns true + // When togglePushNotificationUseCase.execute(sessionId, true) + // Then activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, LocalNotificationSettingsContent(isSilenced = false).toContent(), diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 544059b77f..c0ba6ce28b 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -23,6 +23,8 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase @@ -32,7 +34,6 @@ import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService -import im.vector.app.test.fixtures.PusherFixture.aPusher import im.vector.app.test.test import im.vector.app.test.testDispatcher import io.mockk.coEvery @@ -87,6 +88,8 @@ class SessionOverviewViewModelTest { private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk() private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() + private val fakeGetNotificationsStatusUseCase = mockk() + private val notificationsStatus = NotificationsStatus.ENABLED private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), @@ -99,6 +102,7 @@ class SessionOverviewViewModelTest { activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, togglePushNotificationUseCase = togglePushNotificationUseCase.instance, + getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase, ) @Before @@ -108,6 +112,7 @@ class SessionOverviewViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() + every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) } @After @@ -131,7 +136,7 @@ class SessionOverviewViewModelTest { deviceId = A_SESSION_ID_1, deviceInfo = Success(deviceFullInfo), isCurrentSessionTrusted = true, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) val viewModel = createViewModel() @@ -227,7 +232,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) // When @@ -264,7 +269,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) @@ -299,7 +304,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) @@ -466,13 +471,13 @@ class SessionOverviewViewModelTest { @Test fun `when viewModel init, then observe pushers and emit to state`() { - val pushers = listOf(aPusher(deviceId = A_SESSION_ID_1)) - fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) + val notificationStatus = NotificationsStatus.ENABLED + every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationStatus) val viewModel = createViewModel() viewModel.test() - .assertLatestState { state -> state.notificationsEnabled } + .assertLatestState { state -> state.notificationsStatus == notificationStatus } .finish() } @@ -483,6 +488,6 @@ class SessionOverviewViewModelTest { viewModel.handle(SessionOverviewAction.TogglePushNotifications(A_SESSION_ID_1, true)) togglePushNotificationUseCase.verifyExecute(A_SESSION_ID_1, true) - viewModel.test().assertLatestState { state -> state.notificationsEnabled }.finish() + viewModel.test().assertLatestState { state -> state.notificationsStatus == NotificationsStatus.ENABLED }.finish() } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt index 615330463b..c44fc4a497 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt @@ -28,8 +28,8 @@ import org.matrix.android.sdk.api.session.events.model.Content class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed = true) { - fun givenGetUserAccountDataEventReturns(type: String, content: Content) { - every { getUserAccountDataEvent(type) } returns UserAccountDataEvent(type, content) + fun givenGetUserAccountDataEventReturns(type: String, content: Content?) { + every { getUserAccountDataEvent(type) } returns content?.let { UserAccountDataEvent(type, it) } } fun givenUpdateUserAccountDataEventSucceeds() { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt index 92e311cfb7..bfbbb87705 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt @@ -16,7 +16,7 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.devices.v2.overview.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk From e67cc2b2dbb73a37dbadca599159fea8fa5adf0b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:03:11 +0200 Subject: [PATCH 051/215] Adding unit tests on GetNotificationsStatusUseCase --- .../GetNotificationsStatusUseCase.kt | 4 +- .../GetNotificationsStatusUseCaseTest.kt | 135 ++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 1eb612988a..091338c46a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -34,11 +33,10 @@ class GetNotificationsStatusUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - // TODO add unit tests fun execute(deviceId: String): Flow { val session = activeSessionHolder.getSafeActiveSession() return when { - session == null -> emptyFlow() + session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED) checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { session.flow() .livePushers() diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt new file mode 100644 index 0000000000..598c8df83f --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fixtures.PusherFixture +import im.vector.app.test.testDispatcher +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +private const val A_DEVICE_ID = "device-id" + +class GetNotificationsStatusUseCaseTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = + mockk() + private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = + mockk() + + private val getNotificationsStatusUseCase = + GetNotificationsStatusUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `given NO current session when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { + // Given + fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + } + + @Test + fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { + // Given + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + } + + @Test + fun `given current session and toggle via pusher is supported when execute then resulting flow contains status based on pusher value`() = runTest { + // Given + val pushers = listOf( + PusherFixture.aPusher( + deviceId = A_DEVICE_ID, + enabled = true, + ) + ) + fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED + } + + @Test + fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = LocalNotificationSettingsContent( + isSilenced = false + ).toContent(), + ) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns true + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED + } +} From 52a77e074f4d7503dc1a362b6e4b14d0c6f61cf2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:12:17 +0200 Subject: [PATCH 052/215] Renaming const for feature value --- .../org/matrix/android/sdk/internal/auth/version/Versions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index bc2d4a5aef..f4de6a9ae9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -57,7 +57,7 @@ private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" -private const val FEATURE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" +private const val FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" /** * Return true if the SDK supports this homeserver version. @@ -151,5 +151,5 @@ private fun Versions.getMaxVersion(): HomeServerVersion { * @return true if remote toggle of push notifications is supported */ internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean { - return unstableFeatures?.get(FEATURE_PUSH_NOTIFICATIONS_MSC3881).orFalse() + return unstableFeatures?.get(FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881).orFalse() } From ac05e757beb8d40e4c41782d2230a5d8b6035ad9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:20:43 +0200 Subject: [PATCH 053/215] Small improvement to avoid tou many viewState updates --- .../devices/v2/notification/GetNotificationsStatusUseCase.kt | 3 +++ .../settings/devices/v2/overview/SessionOverviewViewModel.kt | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 091338c46a..313c1678cb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -43,6 +44,7 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { it.filter { pusher -> pusher.deviceId == deviceId } } .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() } checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { session.flow() @@ -50,6 +52,7 @@ class GetNotificationsStatusUseCase @Inject constructor( .unwrap() .map { it.content.toModel()?.isSilenced?.not() } .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() } else -> flowOf(NotificationsStatus.NOT_SUPPORTED) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 1aa5f676cc..e6aa7c2747 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -30,7 +30,6 @@ import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase -import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult @@ -220,9 +219,6 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { togglePushNotificationUseCase.execute(action.deviceId, action.enabled) - // TODO should not be needed => test without - val status = if (action.enabled) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED - setState { copy(notificationsStatus = status) } } } } From a851e5aa8504a6fce051b0e345606c505c35f080 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 15:57:33 +0200 Subject: [PATCH 054/215] VoiceBroadcastPlayer - Add seek control views --- .../src/main/res/values/strings.xml | 2 + .../ui-styles/src/main/res/values/dimens.xml | 3 +- .../MessageVoiceBroadcastListeningItem.kt | 10 ++++ .../res/drawable/ic_player_backward_30.xml | 12 ++++ .../res/drawable/ic_player_forward_30.xml | 12 ++++ ...e_event_voice_broadcast_listening_stub.xml | 58 ++++++++++++++++--- ...e_event_voice_broadcast_recording_stub.xml | 8 +-- 7 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_player_backward_30.xml create mode 100644 vector/src/main/res/drawable/ic_player_forward_30.xml diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 2ea209a8f0..450eb64849 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3094,6 +3094,8 @@ Play or resume voice broadcast Pause voice broadcast Buffering + Fast backward 30 seconds + Fast forward 30 seconds Can’t start a new voice broadcast You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 50d5aaf014..22c2a3e62c 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -74,7 +74,8 @@ 22dp - 48dp + 48dp + 36dp 112dp diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 8df7a9d1a6..7a586ad411 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -18,6 +18,9 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageButton +import android.widget.SeekBar +import android.widget.TextView +import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -56,6 +59,9 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING + fastBackwardButton.isInvisible = true + fastForwardButton.isInvisible = true + when (state) { VoiceBroadcastPlayer.State.PLAYING -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) @@ -85,6 +91,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) { val playPauseButton by bind(R.id.playPauseButton) val bufferingView by bind(R.id.bufferingView) + val fastBackwardButton by bind(R.id.fastBackwardButton) + val fastForwardButton by bind(R.id.fastForwardButton) + val seekBar by bind(R.id.seekBar) + val durationView by bind(R.id.playbackDuration) val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata) val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata) val listenersCountMetadata by bind(R.id.listenersCountMetadata) diff --git a/vector/src/main/res/drawable/ic_player_backward_30.xml b/vector/src/main/res/drawable/ic_player_backward_30.xml new file mode 100644 index 0000000000..cb244806b3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_player_backward_30.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_player_forward_30.xml b/vector/src/main/res/drawable/ic_player_forward_30.xml new file mode 100644 index 0000000000..be61fda8ff --- /dev/null +++ b/vector/src/main/res/drawable/ic_player_forward_30.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index d508569cb0..bed9407dfa 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -84,22 +84,31 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="bottom" - app:barrierMargin="12dp" + app:barrierMargin="10dp" app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" /> + + + + + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml index 3296134919..7da0701cc7 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml @@ -91,8 +91,8 @@ Date: Mon, 31 Oct 2022 10:01:45 +0100 Subject: [PATCH 055/215] VoiceBroadcastPlayer - seek implementation --- .../home/room/detail/RoomDetailAction.kt | 3 +- .../home/room/detail/TimelineViewModel.kt | 20 +++--- .../factory/VoiceBroadcastItemFactory.kt | 1 + .../timeline/helper/TimelineEventsGroups.kt | 5 ++ .../item/AbsMessageVoiceBroadcastItem.kt | 1 + .../MessageVoiceBroadcastListeningItem.kt | 27 ++++++-- .../VoiceBroadcastExtensions.kt | 2 + .../voicebroadcast/VoiceBroadcastHelper.kt | 8 ++- .../listening/VoiceBroadcastPlayer.kt | 5 ++ .../listening/VoiceBroadcastPlayerImpl.kt | 62 +++++++++++++++---- 10 files changed, 107 insertions(+), 27 deletions(-) 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 f773671694..8c49213a42 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 @@ -129,9 +129,10 @@ sealed class RoomDetailAction : VectorViewModelAction { } sealed class Listening : VoiceBroadcastAction() { - data class PlayOrResume(val eventId: String) : Listening() + data class PlayOrResume(val voiceBroadcastId: String) : Listening() object Pause : Listening() object Stop : Listening() + data class SeekTo(val voiceBroadcastId: String, val positionMillis: Int) : Listening() } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 50bebc81e4..3f4fae1ce9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -50,6 +50,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager 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.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.error.RoomNotFound import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler @@ -478,7 +479,7 @@ class TimelineViewModel @AssistedInject constructor( is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() - is RoomDetailAction.VoiceBroadcastAction -> handleVoiceBroadcastAction(action) + is VoiceBroadcastAction -> handleVoiceBroadcastAction(action) is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.AcceptCall -> handleAcceptCall(action) @@ -620,22 +621,23 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleVoiceBroadcastAction(action: RoomDetailAction.VoiceBroadcastAction) { + private fun handleVoiceBroadcastAction(action: VoiceBroadcastAction) { if (room == null) return viewModelScope.launch { when (action) { - RoomDetailAction.VoiceBroadcastAction.Recording.Start -> { + VoiceBroadcastAction.Recording.Start -> { voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold( { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) }, { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) }, ) } - RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) - RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) - RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) - is RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.eventId) - RoomDetailAction.VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() - RoomDetailAction.VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() + VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) + VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) + VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) + is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.voiceBroadcastId) + VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() + VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() + is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcastId, action.positionMillis) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 56498fa8d3..5d9c663210 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -67,6 +67,7 @@ class VoiceBroadcastItemFactory @Inject constructor( val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( voiceBroadcastId = voiceBroadcastId, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, + duration = voiceBroadcastEventsGroup.getDuration(), recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), recorder = voiceBroadcastRecorder, player = voiceBroadcastPlayer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index 8a3be7d5f2..7738b6b680 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.utils.TextUtils import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState @@ -148,4 +149,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } ?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L } } + + fun getDuration(): Int { + return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.reduceOrNull { acc, duration -> acc + duration } ?: 0 + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index ba9d582ea4..7ada0c71f2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -94,6 +94,7 @@ abstract class AbsMessageVoiceBroadcastItem { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) - playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } + playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) } } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) - playPauseButton.onClick { - callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) - } + playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } } VoiceBroadcastPlayer.State.BUFFERING -> Unit } } } + private fun bindSeekBar(holder: Holder) { + holder.durationView.text = formatPlaybackTime(voiceBroadcastAttributes.duration) + holder.seekBar.max = voiceBroadcastAttributes.duration + holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit + + override fun onStartTrackingTouch(seekBar: SeekBar) = Unit + + override fun onStopTrackingTouch(seekBar: SeekBar) { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress)) + } + }) + } + + private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) + override fun unbind(holder: Holder) { super.unbind(holder) player.removeListener(voiceBroadcastId, playerListener) + holder.seekBar.setOnSeekBarChangeListener(null) } override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index f9da2e76b1..48554f51d0 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -32,3 +32,5 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? { } val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence + +val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index dfc8e35422..7864d3b4e3 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -41,9 +41,15 @@ class VoiceBroadcastHelper @Inject constructor( suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId) - fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.playOrResume(roomId, eventId) + fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId) fun pausePlayback() = voiceBroadcastPlayer.pause() fun stopPlayback() = voiceBroadcastPlayer.stop() + + fun seekTo(voiceBroadcastId: String, positionMillis: Int) { + if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) { + voiceBroadcastPlayer.seekTo(positionMillis) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index e2870c4011..2a2a549af0 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -43,6 +43,11 @@ interface VoiceBroadcastPlayer { */ fun stop() + /** + * Seek to the given playback position, is milliseconds. + */ + fun seekTo(positionMillis: Int) + /** * Add a [Listener] to the given voice broadcast id. */ diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 3999a0e0af..b0e5d93d1e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -22,7 +22,7 @@ import androidx.annotation.MainThread import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure -import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk +import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase @@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import timber.log.Timber @@ -69,7 +70,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentSequence: Int? = null private var fetchPlaylistJob: Job? = null - private var playlist = emptyList() + private var playlist = emptyList() + private var isLive: Boolean = false override var currentVoiceBroadcastId: String? = null @@ -170,8 +172,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor( .launchIn(coroutineScope) } - private fun updatePlaylist(playlist: List) { - this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } + private fun updatePlaylist(audioEvents: List) { + val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs } + val chunkPositions = sorted + .map { it.duration } + .runningFold(0) { acc, i -> acc + i } + .dropLast(1) + this.playlist = sorted.mapIndexed { index, messageAudioEvent -> + PlaylistItem( + audioEvent = messageAudioEvent, + startTime = chunkPositions.getOrNull(index) ?: 0 + ) + } onPlaylistUpdated() } @@ -195,16 +207,23 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun startPlayback() { - val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() - val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = event.getVoiceBroadcastChunk()?.sequence + private fun startPlayback(sequence: Int? = null, position: Int = 0) { + val playlistItem = when { + sequence != null -> playlist.find { it.audioEvent.sequence == sequence } + isLive -> playlist.lastOrNull() + else -> playlist.firstOrNull() + } + val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } + val computedSequence = playlistItem.audioEvent.sequence coroutineScope.launch { try { currentMediaPlayer = prepareMediaPlayer(content) currentMediaPlayer?.start() + if (position > 0) { + currentMediaPlayer?.seekTo(position) + } currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } - currentSequence = sequence + currentSequence = computedSequence withContext(Dispatchers.Main) { playingState = State.PLAYING } nextMediaPlayer = prepareNextMediaPlayer() } catch (failure: Throwable) { @@ -220,11 +239,27 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.PLAYING } + override fun seekTo(positionMillis: Int) { + val duration = getVoiceBroadcastDuration() + val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return + val chunk = playlistItem.audioEvent + val chunkPosition = positionMillis - playlistItem.startTime + + Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${chunk.sequence}, sequencePosition=$chunkPosition") + + tryOrNull { currentMediaPlayer?.stop() } + release(currentMediaPlayer) + tryOrNull { nextMediaPlayer?.stop() } + release(nextMediaPlayer) + + startPlayback(chunk.sequence, chunkPosition) + } + private fun getNextAudioContent(): MessageAudioContent? { val nextSequence = currentSequence?.plus(1) - ?: playlist.lastOrNull()?.sequence + ?: playlist.lastOrNull()?.audioEvent?.sequence ?: 1 - return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content + return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content } private suspend fun prepareNextMediaPlayer(): MediaPlayer? { @@ -302,4 +337,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( return true } } + + private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 + + private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) } + From bc3fe4e5f68892cdedd3133283ce4fcde34b0bb4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 15:45:07 +0100 Subject: [PATCH 056/215] Minor cleanup --- .../listening/VoiceBroadcastPlayerImpl.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index b0e5d93d1e..e6d1c88b1a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -178,7 +178,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( .map { it.duration } .runningFold(0) { acc, i -> acc + i } .dropLast(1) - this.playlist = sorted.mapIndexed { index, messageAudioEvent -> + playlist = sorted.mapIndexed { index, messageAudioEvent -> PlaylistItem( audioEvent = messageAudioEvent, startTime = chunkPositions.getOrNull(index) ?: 0 @@ -242,17 +242,17 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun seekTo(positionMillis: Int) { val duration = getVoiceBroadcastDuration() val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return - val chunk = playlistItem.audioEvent - val chunkPosition = positionMillis - playlistItem.startTime + val audioEvent = playlistItem.audioEvent + val eventPosition = positionMillis - playlistItem.startTime - Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${chunk.sequence}, sequencePosition=$chunkPosition") + Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${audioEvent.sequence}, sequencePosition=$eventPosition") tryOrNull { currentMediaPlayer?.stop() } release(currentMediaPlayer) tryOrNull { nextMediaPlayer?.stop() } release(nextMediaPlayer) - startPlayback(chunk.sequence, chunkPosition) + startPlayback(audioEvent.sequence, eventPosition) } private fun getNextAudioContent(): MessageAudioContent? { @@ -342,4 +342,3 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) } - From 7d3f6365e28c77de9115b1b2777e149c53a2380f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 17:08:56 +0100 Subject: [PATCH 057/215] Use sum() instead of reduce operator --- .../home/room/detail/timeline/helper/TimelineEventsGroups.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index 7738b6b680..a4bfa9e155 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -151,6 +151,6 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { } fun getDuration(): Int { - return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.reduceOrNull { acc, duration -> acc + duration } ?: 0 + return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum() } } From eb61a23bf6bbafda36f2633d106497209e1f48c0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 18:42:26 +0100 Subject: [PATCH 058/215] Temporary disable seekBar if playing state is paused or idle --- .../timeline/item/MessageVoiceBroadcastListeningItem.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 20d75a6d8d..a2d1e30c99 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -69,14 +69,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) } + seekBar.isEnabled = true } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } + seekBar.isEnabled = false + } + VoiceBroadcastPlayer.State.BUFFERING -> { + seekBar.isEnabled = true } - VoiceBroadcastPlayer.State.BUFFERING -> Unit } } } From b41346cdcecf3a7750ba52d86b66b6b39bac7ff7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 18:42:52 +0100 Subject: [PATCH 059/215] Improve player transitions --- .../listening/VoiceBroadcastPlayerImpl.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index e6d1c88b1a..ee537b9e61 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -63,10 +63,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - set(value) { - field = value - currentMediaPlayer?.setNextMediaPlayer(value) - } private var currentSequence: Int? = null private var fetchPlaylistJob: Job? = null @@ -303,7 +299,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { + private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { @@ -317,6 +313,17 @@ class VoiceBroadcastPlayerImpl @Inject constructor( return false } + override fun onPrepared(mp: MediaPlayer) { + when (mp) { + currentMediaPlayer -> { + nextMediaPlayer?.let { mp.setNextMediaPlayer(it) } + } + nextMediaPlayer -> { + tryOrNull { currentMediaPlayer?.setNextMediaPlayer(mp) } + } + } + } + override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return val roomId = currentRoomId ?: return From 481388ed329b33ee2deb3150e8ae5ddf46956200 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 19:01:41 +0100 Subject: [PATCH 060/215] Fix line length --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index ee537b9e61..166e5a12e5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -299,7 +299,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { + private inner class MediaPlayerListener : + MediaPlayer.OnInfoListener, + MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { From 404383e683aa87b391601657afdd5edd9fd682e4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 2 Nov 2022 17:58:10 +0100 Subject: [PATCH 061/215] Update versions --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 968d8515ac..f50b672077 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.6\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.8\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 9a0bfdfa63..f793fff2c8 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 6 +ext.versionPatch = 8 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From bb02209537e3b4e04147ea43fb665717d4550d62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Nov 2022 23:10:39 +0000 Subject: [PATCH 062/215] Bump checker from 3.11.0 to 3.27.0 Bumps [checker](https://github.com/typetools/checker-framework) from 3.11.0 to 3.27.0. - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-3.11.0...checker-framework-3.27.0) --- updated-dependencies: - dependency-name: org.checkerframework:checker dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 9857f88479..2f67a8eedb 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -308,7 +308,7 @@ dependencies { // Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0) //noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26. - implementation "org.checkerframework:checker:3.11.0" + implementation "org.checkerframework:checker:3.27.0" androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testRunner From e9daef97b660ce8dda1db215517b982b52889ac5 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 11:27:02 +0100 Subject: [PATCH 063/215] Fix order of check to get notification status --- .../GetNotificationsStatusUseCase.kt | 16 ++++++++-------- .../GetNotificationsStatusUseCaseTest.kt | 6 ++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 313c1678cb..69659bf23f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -38,14 +38,6 @@ class GetNotificationsStatusUseCase @Inject constructor( val session = activeSessionHolder.getSafeActiveSession() return when { session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED) - checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { - session.flow() - .livePushers() - .map { it.filter { pusher -> pusher.deviceId == deviceId } } - .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } - .distinctUntilChanged() - } checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { session.flow() .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) @@ -54,6 +46,14 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } .distinctUntilChanged() } + checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { + session.flow() + .livePushers() + .map { it.filter { pusher -> pusher.deviceId == deviceId } } + .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() + } else -> flowOf(NotificationsStatus.NOT_SUPPORTED) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index 598c8df83f..b13018a20d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -22,6 +22,7 @@ import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.testDispatcher import io.mockk.every import io.mockk.mockk +import io.mockk.verifyOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.test.resetMain @@ -89,6 +90,11 @@ class GetNotificationsStatusUseCaseTest { // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + verifyOrder { + // we should first check account data + fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() + } } @Test From c0ba2f2f48f0b6c850aa8c29f508db3aba61b3c6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 3 Nov 2022 00:11:23 +0100 Subject: [PATCH 064/215] Fix bad content types when sending unencrypted media --- changelog.d/7519.bugfix | 1 + .../android/sdk/internal/session/content/UploadContentWorker.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7519.bugfix diff --git a/changelog.d/7519.bugfix b/changelog.d/7519.bugfix new file mode 100644 index 0000000000..c687bded49 --- /dev/null +++ b/changelog.d/7519.bugfix @@ -0,0 +1 @@ +Voice Broadcast - Fix error on voice messages in unencrypted rooms diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index db1cd1b33b..3dd440737a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -408,7 +408,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter newAttachmentAttributes: NewAttachmentAttributes ) { localEchoRepository.updateEcho(eventId) { _, event -> - val content: Content? = event.asDomain().content + val content: Content? = event.asDomain(castJsonNumbers = true).content val messageContent: MessageContent? = content.toModel() // Retrieve potential additional content from the original event val additionalContent = content.orEmpty() - messageContent?.toContent().orEmpty().keys From b0a31304a1ae0649c478884996013c70718486dc Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 17:04:49 +0100 Subject: [PATCH 065/215] Update seek bar tick progress while playing --- .../factory/VoiceBroadcastItemFactory.kt | 3 + .../item/AbsMessageVoiceBroadcastItem.kt | 4 + .../MessageVoiceBroadcastListeningItem.kt | 30 +++++++- .../listening/VoiceBroadcastPlayerImpl.kt | 74 +++++++++++++++++-- 4 files changed, 100 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 5d9c663210..06d3563303 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem @@ -44,6 +45,7 @@ class VoiceBroadcastItemFactory @Inject constructor( private val drawableProvider: DrawableProvider, private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val voiceBroadcastPlayer: VoiceBroadcastPlayer, + private val playbackTracker: AudioMessagePlaybackTracker, ) { fun create( @@ -71,6 +73,7 @@ class VoiceBroadcastItemFactory @Inject constructor( recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), recorder = voiceBroadcastRecorder, player = voiceBroadcastPlayer, + playbackTracker = playbackTracker, roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(), colorProvider = colorProvider, drawableProvider = drawableProvider, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 7ada0c71f2..9ea0a634c5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -25,6 +25,7 @@ import im.vector.app.R import im.vector.app.core.extensions.tintBackground import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider +import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder @@ -40,6 +41,8 @@ abstract class AbsMessageVoiceBroadcastItem() { private lateinit var playerListener: VoiceBroadcastPlayer.Listener + private var isUserSeeking = false override fun bind(holder: Holder) { super.bind(holder) @@ -86,15 +88,36 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindSeekBar(holder: Holder) { - holder.durationView.text = formatPlaybackTime(voiceBroadcastAttributes.duration) - holder.seekBar.max = voiceBroadcastAttributes.duration + holder.durationView.text = formatPlaybackTime(duration) + holder.seekBar.max = duration holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit - override fun onStartTrackingTouch(seekBar: SeekBar) = Unit + override fun onStartTrackingTouch(seekBar: SeekBar) { + isUserSeeking = true + } override fun onStopTrackingTouch(seekBar: SeekBar) { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress)) + isUserSeeking = false + } + }) + playbackTracker.track(voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { + override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { + when (state) { + is AudioMessagePlaybackTracker.Listener.State.Paused -> { + if (!isUserSeeking) { + holder.seekBar.progress = state.playbackTime + } + } + is AudioMessagePlaybackTracker.Listener.State.Playing -> { + if (!isUserSeeking) { + holder.seekBar.progress = state.playbackTime + } + } + AudioMessagePlaybackTracker.Listener.State.Idle -> Unit + is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit + } } }) } @@ -105,6 +128,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem super.unbind(holder) player.removeListener(voiceBroadcastId, playerListener) holder.seekBar.setOnSeekBarChangeListener(null) + playbackTracker.untrack(voiceBroadcastId) } override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 166e5a12e5..4fbaee8374 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -29,6 +29,7 @@ import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroad import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent @@ -60,6 +62,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var voiceBroadcastStateJob: Job? = null private val mediaPlayerListener = MediaPlayerListener() + private val playbackTicker = PlaybackTicker() private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null @@ -79,6 +82,24 @@ class VoiceBroadcastPlayerImpl @Inject constructor( field = value // Notify state change to all the listeners attached to the current voice broadcast id currentVoiceBroadcastId?.let { voiceBroadcastId -> + when (value) { + State.PLAYING -> { + playbackTracker.startPlayback(voiceBroadcastId) + playbackTicker.startPlaybackTicker(voiceBroadcastId) + } + State.PAUSED -> { + playbackTracker.pausePlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + State.BUFFERING -> { + playbackTracker.pausePlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + State.IDLE -> { + playbackTracker.stopPlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + } listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } } } @@ -99,15 +120,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } override fun pause() { - currentMediaPlayer?.pause() - currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) } playingState = State.PAUSED + currentMediaPlayer?.pause() } override fun stop() { + // Update state + playingState = State.IDLE + // Stop playback currentMediaPlayer?.stop() - currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) } isLive = false // Release current player @@ -126,9 +148,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( fetchPlaylistJob?.cancel() fetchPlaylistJob = null - // Update state - playingState = State.IDLE - // Clear playlist playlist = emptyList() currentSequence = null @@ -218,7 +237,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (position > 0) { currentMediaPlayer?.seekTo(position) } - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } currentSequence = computedSequence withContext(Dispatchers.Main) { playingState = State.PLAYING } nextMediaPlayer = prepareNextMediaPlayer() @@ -231,7 +249,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun resumePlayback() { currentMediaPlayer?.start() - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } playingState = State.PLAYING } @@ -352,4 +369,45 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) + + private inner class PlaybackTicker( + private var playbackTicker: CountUpTimer? = null, + ) { + + fun startPlaybackTicker(id: String) { + playbackTicker?.stop() + playbackTicker = CountUpTimer().apply { + tickListener = object : CountUpTimer.TickListener { + override fun onTick(milliseconds: Long) { + onPlaybackTick(id) + } + } + resume() + } + onPlaybackTick(id) + } + + private fun onPlaybackTick(id: String) { + if (currentMediaPlayer?.isPlaying.orFalse()) { + val itemStartPosition = currentSequence?.let { seq -> playlist.find { it.audioEvent.sequence == seq } }?.startTime + val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) + if (currentVoiceBroadcastPosition != null) { + val totalDuration = getVoiceBroadcastDuration() + val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration + playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) + } else { + playbackTracker.stopPlayback(id) + stopPlaybackTicker() + } + } else { + playbackTracker.stopPlayback(id) + stopPlaybackTicker() + } + } + + fun stopPlaybackTicker() { + playbackTicker?.stop() + playbackTicker = null + } + } } From 20d62b14dea44cacfa8c770a339bd37b3bcaaf14 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 1 Nov 2022 10:28:24 +0100 Subject: [PATCH 066/215] Changelog --- changelog.d/7496.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7496.wip diff --git a/changelog.d/7496.wip b/changelog.d/7496.wip new file mode 100644 index 0000000000..49d15d084f --- /dev/null +++ b/changelog.d/7496.wip @@ -0,0 +1 @@ +[Voice Broadcast] Add seekbar in listening tile From 6d850b30306936b5b4602b48242c3524b5dfb730 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 3 Nov 2022 16:17:55 +0100 Subject: [PATCH 067/215] Create VoiceBroadcast model with roomId and eventId --- .../home/room/detail/RoomDetailAction.kt | 5 +- .../home/room/detail/TimelineViewModel.kt | 4 +- .../factory/VoiceBroadcastItemFactory.kt | 9 ++-- .../item/AbsMessageVoiceBroadcastItem.kt | 5 +- .../MessageVoiceBroadcastListeningItem.kt | 12 ++--- .../voicebroadcast/VoiceBroadcastHelper.kt | 7 +-- .../listening/VoiceBroadcastPlayer.kt | 16 +++--- .../listening/VoiceBroadcastPlayerImpl.kt | 49 +++++++++---------- .../GetLiveVoiceBroadcastChunksUseCase.kt | 15 +++--- .../voicebroadcast/model/VoiceBroadcast.kt | 22 +++++++++ ...se.kt => GetVoiceBroadcastEventUseCase.kt} | 14 +++--- 11 files changed, 93 insertions(+), 65 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt rename vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/{GetVoiceBroadcastUseCase.kt => GetVoiceBroadcastEventUseCase.kt} (72%) 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 8c49213a42..ba0f7dbdf8 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 @@ -20,6 +20,7 @@ import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.call.conference.ConferenceEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent @@ -129,10 +130,10 @@ sealed class RoomDetailAction : VectorViewModelAction { } sealed class Listening : VoiceBroadcastAction() { - data class PlayOrResume(val voiceBroadcastId: String) : Listening() + data class PlayOrResume(val voiceBroadcast: VoiceBroadcast) : Listening() object Pause : Listening() object Stop : Listening() - data class SeekTo(val voiceBroadcastId: String, val positionMillis: Int) : Listening() + data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int) : Listening() } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 3f4fae1ce9..252823b2a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -634,10 +634,10 @@ class TimelineViewModel @AssistedInject constructor( VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) - is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.voiceBroadcastId) + is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast) VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() - is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcastId, action.positionMillis) + is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 06d3563303..e4f7bed72f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -29,6 +29,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadca import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder @@ -60,14 +61,14 @@ class VoiceBroadcastItemFactory @Inject constructor( val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null - val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId + val voiceBroadcast = VoiceBroadcast(voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId, roomId = params.event.roomId) val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId && messageContent.deviceId == session.sessionParams.deviceId val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( - voiceBroadcastId = voiceBroadcastId, + voiceBroadcast = voiceBroadcast, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, duration = voiceBroadcastEventsGroup.getDuration(), recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), @@ -92,7 +93,7 @@ class VoiceBroadcastItemFactory @Inject constructor( voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes, ): MessageVoiceBroadcastRecordingItem { return MessageVoiceBroadcastRecordingItem_() - .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}") + .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}") .attributes(attributes) .voiceBroadcastAttributes(voiceBroadcastAttributes) .highlighted(highlight) @@ -105,7 +106,7 @@ class VoiceBroadcastItemFactory @Inject constructor( voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes, ): MessageVoiceBroadcastListeningItem { return MessageVoiceBroadcastListeningItem_() - .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}") + .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}") .attributes(attributes) .voiceBroadcastAttributes(voiceBroadcastAttributes) .highlighted(highlight) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 9ea0a634c5..0329adf12b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -27,6 +27,7 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.util.MatrixItem @@ -36,7 +37,7 @@ abstract class AbsMessageVoiceBroadcastItem renderPlayingState(holder, state) } - player.addListener(voiceBroadcastId, playerListener) + player.addListener(voiceBroadcast, playerListener) bindSeekBar(holder) } @@ -77,7 +77,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) - playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } + playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } seekBar.isEnabled = false } VoiceBroadcastPlayer.State.BUFFERING -> { @@ -98,11 +98,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } override fun onStopTrackingTouch(seekBar: SeekBar) { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress)) + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress)) isUserSeeking = false } }) - playbackTracker.track(voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { + playbackTracker.track(voiceBroadcast.voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { when (state) { is AudioMessagePlaybackTracker.Listener.State.Paused -> { @@ -126,9 +126,9 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem override fun unbind(holder: Holder) { super.unbind(holder) - player.removeListener(voiceBroadcastId, playerListener) + player.removeListener(voiceBroadcast, playerListener) holder.seekBar.setOnSeekBarChangeListener(null) - playbackTracker.untrack(voiceBroadcastId) + playbackTracker.untrack(voiceBroadcast.voiceBroadcastId) } override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 7864d3b4e3..6839056520 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -17,6 +17,7 @@ package im.vector.app.features.voicebroadcast import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase @@ -41,14 +42,14 @@ class VoiceBroadcastHelper @Inject constructor( suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId) - fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId) + fun playOrResumePlayback(voiceBroadcast: VoiceBroadcast) = voiceBroadcastPlayer.playOrResume(voiceBroadcast) fun pausePlayback() = voiceBroadcastPlayer.pause() fun stopPlayback() = voiceBroadcastPlayer.stop() - fun seekTo(voiceBroadcastId: String, positionMillis: Int) { - if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) { + fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { + if (voiceBroadcastPlayer.currentVoiceBroadcast == voiceBroadcast) { voiceBroadcastPlayer.seekTo(positionMillis) } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 2a2a549af0..b4806ba57d 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -16,12 +16,14 @@ package im.vector.app.features.voicebroadcast.listening +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast + interface VoiceBroadcastPlayer { /** - * The current playing voice broadcast identifier, if any. + * The current playing voice broadcast, if any. */ - val currentVoiceBroadcastId: String? + val currentVoiceBroadcast: VoiceBroadcast? /** * The current playing [State], [State.IDLE] by default. @@ -31,7 +33,7 @@ interface VoiceBroadcastPlayer { /** * Start playback of the given voice broadcast. */ - fun playOrResume(roomId: String, voiceBroadcastId: String) + fun playOrResume(voiceBroadcast: VoiceBroadcast) /** * Pause playback of the current voice broadcast, if any. @@ -49,14 +51,14 @@ interface VoiceBroadcastPlayer { fun seekTo(positionMillis: Int) /** - * Add a [Listener] to the given voice broadcast id. + * Add a [Listener] to the given voice broadcast. */ - fun addListener(voiceBroadcastId: String, listener: Listener) + fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener) /** - * Remove a [Listener] from the given voice broadcast id. + * Remove a [Listener] from the given voice broadcast. */ - fun removeListener(voiceBroadcastId: String, listener: Listener) + fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) /** * Player states. diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 4fbaee8374..fc983e4112 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -26,9 +26,10 @@ import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.sequence -import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -51,7 +52,7 @@ import javax.inject.Singleton class VoiceBroadcastPlayerImpl @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val playbackTracker: AudioMessagePlaybackTracker, - private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, + private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase, private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase ) : VoiceBroadcastPlayer { @@ -73,7 +74,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var isLive: Boolean = false - override var currentVoiceBroadcastId: String? = null + override var currentVoiceBroadcast: VoiceBroadcast? = null override var playingState = State.IDLE @MainThread @@ -81,7 +82,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( Timber.w("## VoiceBroadcastPlayer state: $field -> $value") field = value // Notify state change to all the listeners attached to the current voice broadcast id - currentVoiceBroadcastId?.let { voiceBroadcastId -> + currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> when (value) { State.PLAYING -> { playbackTracker.startPlayback(voiceBroadcastId) @@ -103,17 +104,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } } } - private var currentRoomId: String? = null /** * Map voiceBroadcastId to listeners. */ private val listeners: MutableMap> = mutableMapOf() - override fun playOrResume(roomId: String, voiceBroadcastId: String) { - val hasChanged = currentVoiceBroadcastId != voiceBroadcastId + override fun playOrResume(voiceBroadcast: VoiceBroadcast) { + val hasChanged = currentVoiceBroadcast != voiceBroadcast when { - hasChanged -> startPlayback(roomId, voiceBroadcastId) + hasChanged -> startPlayback(voiceBroadcast) playingState == State.PAUSED -> resumePlayback() else -> Unit } @@ -152,37 +152,35 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playlist = emptyList() currentSequence = null - currentRoomId = null - currentVoiceBroadcastId = null + currentVoiceBroadcast = null } - override fun addListener(voiceBroadcastId: String, listener: Listener) { - listeners[voiceBroadcastId]?.add(listener) ?: run { - listeners[voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } + override fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { + listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run { + listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } - if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE) + listener.onStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) } - override fun removeListener(voiceBroadcastId: String, listener: Listener) { - listeners[voiceBroadcastId]?.remove(listener) + override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { + listeners[voiceBroadcast.voiceBroadcastId]?.remove(listener) } - private fun startPlayback(roomId: String, eventId: String) { + private fun startPlayback(voiceBroadcast: VoiceBroadcast) { // Stop listening previous voice broadcast if any if (playingState != State.IDLE) stop() - currentRoomId = roomId - currentVoiceBroadcastId = eventId + currentVoiceBroadcast = voiceBroadcast playingState = State.BUFFERING - val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState + val voiceBroadcastState = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)?.content?.voiceBroadcastState isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED - fetchPlaylistAndStartPlayback(roomId, eventId) + fetchPlaylistAndStartPlayback(voiceBroadcast) } - private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) { - fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId) + private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { + fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) .onEach(this::updatePlaylist) .launchIn(coroutineScope) } @@ -347,9 +345,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return - val roomId = currentRoomId ?: return - val voiceBroadcastId = currentVoiceBroadcastId ?: return - val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return + val voiceBroadcast = currentVoiceBroadcast ?: return + val voiceBroadcastEventContent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)?.content ?: return isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 4f9f2de673..2e8fc31870 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -19,11 +19,12 @@ package im.vector.app.features.voicebroadcast.listening.usecase import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence -import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -44,19 +45,19 @@ import javax.inject.Inject */ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, + private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase, ) { - fun execute(roomId: String, voiceBroadcastId: String): Flow> { + fun execute(voiceBroadcast: VoiceBroadcast): Flow> { val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow() - val room = session.roomService().getRoom(roomId) ?: return emptyFlow() + val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow() val timeline = room.timelineService().createTimeline(null, TimelineSettings(5)) // Get initial chunks - val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcastId) + val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) .mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } } - val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId) + val voiceBroadcastEvent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) { @@ -82,7 +83,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( lastSequence = stopEvent.content?.lastChunkSequence } - val newChunks = newEvents.mapToChunkEvents(voiceBroadcastId, voiceBroadcastEvent.root.senderId) + val newChunks = newEvents.mapToChunkEvents(voiceBroadcast.voiceBroadcastId, voiceBroadcastEvent.root.senderId) // Notify about new chunks if (newChunks.isNotEmpty()) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt new file mode 100644 index 0000000000..62207d5b87 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 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.voicebroadcast.model + +data class VoiceBroadcast( + val voiceBroadcastId: String, + val roomId: String, +) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt similarity index 72% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt index d08fa14a95..26ba3209b7 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt @@ -16,6 +16,7 @@ package im.vector.app.features.voicebroadcast.usecase +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.session.Session @@ -24,17 +25,18 @@ import org.matrix.android.sdk.api.session.getRoom import timber.log.Timber import javax.inject.Inject -class GetVoiceBroadcastUseCase @Inject constructor( +class GetVoiceBroadcastEventUseCase @Inject constructor( private val session: Session, ) { - fun execute(roomId: String, eventId: String): VoiceBroadcastEvent? { - val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") + fun execute(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { + val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}") - Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $eventId") + Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast") - val initialEvent = room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() // Fallback to initial event - val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId).sortedBy { it.root.originServerTs } + val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent() + val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) + .sortedBy { it.root.originServerTs } return relatedEvents.mapNotNull { it.root.asVoiceBroadcastEvent() }.lastOrNull() ?: initialEvent } } From e2327eaf79d1ae402d2b90934a6f47321e92add1 Mon Sep 17 00:00:00 2001 From: Mubark Date: Thu, 3 Nov 2022 10:23:40 +0000 Subject: [PATCH 068/215] Translated using Weblate (Arabic) Currently translated at 39.5% (1001 of 2531 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ar/ --- library/ui-strings/src/main/res/values-ar/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-ar/strings.xml b/library/ui-strings/src/main/res/values-ar/strings.xml index 70b9a33ab5..a49ecc3d08 100644 --- a/library/ui-strings/src/main/res/values-ar/strings.xml +++ b/library/ui-strings/src/main/res/values-ar/strings.xml @@ -1167,4 +1167,12 @@ البريد الإلكتروني كلمة السر الجديدة التالي - + + صفر + واحد + اثنان + قليلة + كثيرة + اخرى + + \ No newline at end of file From 7b8274710853ff64ee46bc7912c23c41cd8b15b9 Mon Sep 17 00:00:00 2001 From: Nizami Date: Thu, 3 Nov 2022 13:29:06 +0000 Subject: [PATCH 069/215] Translated using Weblate (Azerbaijani) Currently translated at 4.8% (123 of 2531 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/az/ --- library/ui-strings/src/main/res/values-az/strings.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/library/ui-strings/src/main/res/values-az/strings.xml b/library/ui-strings/src/main/res/values-az/strings.xml index 044ecf900c..6fe322bdd0 100644 --- a/library/ui-strings/src/main/res/values-az/strings.xml +++ b/library/ui-strings/src/main/res/values-az/strings.xml @@ -20,7 +20,7 @@ %s səsli zəng etdi. %s zəngə cavab verdi. %s zəng başa çatdı. - "%1$s gələcək otaq tarixçəsini %2$s-ə görünən etdi" + %1$s gələcək otaq tarixçəsini %2$s-ə görünən etdi bütün otaq üzvləri, dəvət olunduğu andan. bütün otaq üzvləri, qoşulduğu andan. bütün otaq üzvləri. @@ -48,8 +48,9 @@ \nKriptografiyanın idxalı İlkin sinxronizasiya: \nOtaqlar idxalı - İlkin sinxronizasiya: -\nOtaqlara daxil olmaq + İlkin sinxronizasiya: +\nSöhbətləriniz yüklənilir +\nƏgər çoxlu otaqlara qoşulmusunuzsa, bu, bir az vaxt apara bilər İlkin sinxronizasiya: \nDəvət olunmuş otaqların idxalı İlkin sinxronizasiya: @@ -133,4 +134,6 @@ Otağa qoşulmaq üçün %1$s-a dəvət göndərdiniz %s, bu otaq üçün server ACL-lərini dəyişdi. • %s ilə uyğunlaşan serverlərə icazə verildi. + Siz %1$s üçün otağa qoşulmaq dəvətin ləğv etdiniz + %1$s-ı dəvət etdiniz \ No newline at end of file From d6819dd8d72d94c5da7a577ae08a71938ab78649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= Date: Wed, 2 Nov 2022 14:17:06 +0000 Subject: [PATCH 070/215] Translated using Weblate (Slovak) Currently translated at 100.0% (2531 of 2531 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- library/ui-strings/src/main/res/values-sk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index bf57233b37..a41aca05dc 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2653,7 +2653,7 @@ V záujme čo najlepšieho zabezpečenia, overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate. Iné relácie Relácie - Otvoriť zoznam priestorov + Zoznam priestorov Vytvoriť novú konverzáciu alebo miestnosť Ľudia Obľúbené From ec22278eed3fa341cd2f824e4ab5f504b11b2439 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 2 Nov 2022 22:11:12 +0000 Subject: [PATCH 071/215] Translated using Weblate (Albanian) Currently translated at 98.8% (2501 of 2531 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/ --- .../src/main/res/values-sq/strings.xml | 418 ++++++++++++++++-- 1 file changed, 388 insertions(+), 30 deletions(-) diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index a6af0a4921..b1d8eb9564 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -601,9 +601,7 @@ Formatojini mesazhet duke përdorur sintaksën Markdown përpara se të dërgohen. Kjo lejon formatim të thelluar, f.v., përdorimi i yllthit për ta shfaqur tekstin me të pjerrëta. Nuk prek ftesat, heqjet dhe dëbimet. ${app_name}-i grumbullon të dhëna analitike anonime që të na lejojë ta përmirësojmë aplikacionin. - Të shfaqen krejt mesazhet prej %s\? -\n -\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. + Të shfaqen krejt mesazhet prej %s\? Nis kamerën e sistemit, në vend se skenën e kamerës vetjake. Shfaq veprimin On/Off sintakse Markdown @@ -897,10 +895,10 @@ S’u arrit të dërgohej sugjerimi (%s) Shfaq te rrjedha kohore akte të fshehura Përgjegjës integrimesh - app_id: - push_key: - app_display_name: - emër_sesioni: + ID Aplikacioni: + + Emër Aplikacioni Në Ekran: + Emër Sesioni Në Ekran: Mesazhe të Drejtpërdrejtë Po pritet… Po fshehtëzohet miniatura… @@ -949,11 +947,11 @@ Po përdorni %1$s për të zbuluar dhe për të qenë i zbulueshëm nga kontakte ekzistues që njihni. S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jini i zbulueshëm nga kontakte ekzistuese që njihni, formësoni një të tillë më poshtë. Adresa email të zbulueshme - Mundësitë rreth zbulimesh do të shfaqen sapo të keni shtuar një email. + Mundësitë e zbulimit do të shfaqen sapo të keni shtuar një adresë email. Mundësi zbulimesh do të shfaqen sapo të keni shtuar një numër telefoni. Shkëputja prej shërbyesit tuaj të identiteteve do të thotë se s’do të jeni i zbulueshëm prej përdoruesish të tjerë dhe s’do të jeni në gjendje të ftoni të tjerë me email ose telefon. Numra telefoni të zbulueshëm - Ju dërguam një email ripohimi te %s, hapeni dhe klikoni mbi lidhjen e ripohimit + Ju dërguam një email te %s, hapeni dhe klikoni mbi lidhjen e ripohimit Jepni një URL shërbyesi identitetesh S’u lidh dot te shërbyes identitetesh Ju lutemi, jepni URL-në e shërbyesit të identiteteve @@ -1080,7 +1078,7 @@ Aplikacioni s’është në gjendje të krijojë llogari në këtë shërbyes Home. \n \nDoni të regjistroheni duke përdorur një klient web\? - Ky emai s’është përshoqëruar me ndonjë llogari. + Kjo adresë email s’është e përshoqëruar me ndonjë llogari. Ricaktoni fjalëkalimin në %1$s Te mesazhet tuaj do të dërgohet një email verifikimi, për të ripohuar caktimin e fjalëkalimit tuaj të ri. Pasuesi @@ -1089,7 +1087,7 @@ Kujdes! Ndryshimi i fjalëkalimit tuaj do të sjellë zerim të çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt sesionet tuaj, duke e bërë të palexueshëm historikun e bisedave të fshehtëzuara. Ujdisni një Kopjeruajtje Kyçesh ose eksportoni kyçet e dhomës tuaj prej një tjetër sesioni, përpara se të ricaktoni fjalëkalimin tuaj. Vazhdo - Ky email s’është i lidhur me ndonjë llogari + Kjo adresë email s’është e lidhur me ndonjë llogari Kontrolloni te mesazhet tuaj të marrë Një email verifikimi u dërgua te %1$s. Prekni mbi lidhjen që të ripohohet fjalëkalimi juaj i ri. Pasi të keni ndjekur lidhjen që përmban, klikoni më poshtë. @@ -1103,7 +1101,7 @@ \n \nTë ndalet procesi i ndryshimit të fjalëkalimit\? Caktoni adresë email - Caktoni një email për rimarrje të llogarisë tuaj. Më vonë, mundeni të lejoni persona që njihni t’ju zbulojnë përmes email-it tuaj. + Caktoni një adresë email për rimarrje të llogarisë tuaj. Më vonë, mundeni të lejoni persona që njihni t’ju zbulojnë përmes kësaj adrese. Email Email (në daçi) Pasuesi @@ -1277,7 +1275,7 @@ \n - Shërbyesi Home te i cili është lidhur përdoruesi që po verifikoni \n - Lidhja juaj internet ose ajo e përdoruesit tjetër \n - Pajisja juaj ose ajo e përdoruesit tjetër - %s u anulua + %s u pranua Skanojeni kodin me pajisjen e përdoruesit tjetër, për të verifikuar në mënyrë të sigurt njëri-tjetrin Nëse s’jeni vetë atje, krahasoni emoji-n @@ -1445,7 +1443,7 @@ Mesazhi u fshi Shfaq mesazhe të hequr Shfaq një vendmbajtëse për mesazhe të hequr - Ju dërguam një email ripohimi te %s, ju lutemi, së pari, shihni email-in tuaj dhe klikoni mbi lidhjen e ripohimit + Ju dërguam një email te %s, ju lutemi, së pari, shihni email-in tuaj dhe klikoni mbi lidhjen e ripohimit Kodi i verifikimit s’është i saktë. MEDIA S’ka media në këtë dhomë @@ -1518,9 +1516,7 @@ \n \nKëtë veprim mund ta zhbëni në çfarëdo kohe, te rregullimet e përgjithshme. Hiqe shpërfilljen e përdoruesit - Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij. -\n -\nKini parasysh se ky veprim do të sjellë rinisjen e aplikacionit dhe do të hajë ca kohë. + Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij. Anuloje ftesën Jeni i sigurt se doni të anulohet ftesa për këtë përdorues\? Përzëre përdoruesin @@ -1534,7 +1530,7 @@ Heqja e dëbimit përdoruesit do t’i lejojë të marrë pjesë sërish në dhomë. Te llogaria juaj s’është shtuar ndonjë numër telefoni Adresa email - Te llogaria juaj s’është shtuar ndonjë email + Te llogaria juaj s’është shtuar ndonjë adresë email Numra telefoni Të hiqet %s\? Sigurohuni që keni klikuar te lidhja në email-in që ju kemi dërguar. @@ -1552,7 +1548,7 @@ Integrimet janë të çaktivizuara Që të bëhet kjo, aktivizoni “Lejo integrime”, te Rregullimet. Email-e dhe numra telefonash - Administroni email-e dhe numra telefonash të lidhur me llogarinë tuaj Matrix + Administroni adresa email dhe numra telefonash të lidhur me llogarinë tuaj Matrix %d përdorues i dëbuar %d përdorues të dëbuar @@ -1605,7 +1601,7 @@ Kjo llogari është çaktivizuar. S’u ruajt dot kartelë media Ripohoni identitetin tuaj duke verifikuar këto kredenciale hyrjeje, duke i akorduar hyrje te mesazhe të fshehtëzuar. - Për privatësinë tuaj, ${app_name}-i mbulon vetëm dërgim email-esh dhe numrash telefoni përdoruesi të koduar. + Për privatësinë tuaj, ${app_name}-i mbulon vetëm dërgim adresash email dhe numrash telefoni përdoruesi të koduar. Caktoni rol Rol Hapni fjalosje @@ -1769,7 +1765,7 @@ %1$d nga %2$d Jepe pranimin Shfuqizoje pranimin tim - Keni dhënë pranimin tuaj për të dërguar email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj. + Keni dhënë pranimin tuaj për të dërguar adresa email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj. Dërgo email-e dhe numra telefonash Sugjerime Përdorues të Ditur @@ -2135,7 +2131,7 @@ Përmendje dhe Fjalëkyçe Njoftime Parazgjedhje %s te Rregullimet, që të merrni ftesa drejt e në ${app_name}. - Lidheni këtë email me llogarinë tuaj + Lidheni këtë adresë email me llogarinë tuaj Kjo ftesë për te kjo hapësirë u dërgua te %s që s’është i përshoqëruar me llogarinë tuaj Kjo ftesë për te kjo dhomë qe dërguar për %s që s’është i përshoqëruar me llogarinë tuaj Krejt dhomat ku gjendeni do të shfaqen te Home. @@ -2203,7 +2199,7 @@ Hyrje në hapësirë Kush mund të hyjë\? Aktivizo njoftime me email për %s - Që të merrni email me njoftim, ju lutemi, përshoqërojini llogarisë tuaj Matrix një email + Që të merrni email me njoftim, ju lutemi, përshoqërojini llogarisë tuaj Matrix një adresë email Njoftim me email Të përmirësojë hapësirën Të ndryshojë emrin e hapësirës @@ -2249,8 +2245,8 @@ Pyetje ose temë pyetësori Krijoni Pyetësor A pranoni të dërgohen këto hollësi\? - Për të zbuluar kontakte ekzistuese, duhet të dërgoni hollësi kontakti (email-e dhe numra telefonash) te shërbyesi juaj i identiteteve. Para dërgimit, i fshehtëzojmë të dhënat tuaja, për privatësi. - Dërgo email-e dhe numra telefonash te %s + Për të zbuluar kontakte ekzistuese, duhet të dërgoni hollësi kontakti (adresa email dhe numra telefonash) te shërbyesi juaj i identiteteve. Para dërgimit, i fshehtëzojmë të dhënat tuaja, për privatësi. + Dërgo adresa email dhe numra telefonash te %s Kontaktet tuaja janë private. Për të zbuluar përdorues prej kontakteve tuaja, na duhet leja juaj për të dërguar hollësi kontakti te shërbyesi juaj i identiteteve. Është bërë dalja nga sesioni! U dol nga dhoma! @@ -2355,7 +2351,7 @@ Bashkësi Ekipe Shokë dhe familje - Do t’ju ndihmojmë të lidheni. + Do t’ju ndihmojmë të lidheni Me kë do të bisedoni më shumë\? Po e shihni tashmë këtë rrjedhë! Shiheni në Dhomë @@ -2411,15 +2407,15 @@ Shërbyesi Home s’pranon emër përdorues vetëm me shifra. Anashkalojeni këtë hap Ruajeni dhe vazhdoni - Parapëlqimet tuaja u ruajtën. + Kaloni te rregullimet, kur të doni, që të përditësoni profilin tuaj Kaq qe! Shkojmë - Këtë mund ta ndryshoni kurdo. + Erdh koha t’i jepet surrat emrit Shtoni një foto profili Këtë mund ta ndryshoni më vonë Emër Në Ekran Zgjidhni një emër për në ekran - Llogaria juaj %s u krijua. + Llogaria juaj %s u krijua Përgëzime! Shpjemëni në shtëpi Personalizoni profil @@ -2450,4 +2446,366 @@ Prani Mësoni më tepër Provojeni - + Aktivizo shkurtore lejesh për Thirrje Element + S’u gjet metodë tjetër veç njëkohësimit në prapaskenë. + ${app_name}-it i duhet një fshehtinë e pastër, për të qenë i përditësuar, për arsyen vijuese: +\n%s +\n +\nKini parasysh se ky veprim do të sjellë rinisjen e aplikacionit dhe mund të dojë ca kohë. + Regjistro emrin, versionin dhe URL-në e klientit, për të dalluar më kollaj sesionet te përgjegjës sesionesh. + Veprimtaria e fundit më %1$s + Apliko format me të nënvizuara + Apliko format me të hequravije + Apliko format me të pjerrta + Apliko format me të trasha + Ju lutemi, sigurohuni se e dini origjinën e këtij kodi. Duke lidhur pajisje, do t’i jepni dikujt hyrje të plotë në llogarinë tuaj. + Ripohojeni + Riprovoni + Pa përputhje\? + Po bëhet hyrja juaj + Po lidhet me pajisjen + Skanoni kodin QR + Po bëhet hyrja te një pajisje celulare\? + Shfaq kod QR te kjo pajisje + Përzgjidhni “Skanoni kod QR” + Filloja në skenën e hyrjes + Përzgjidhni “Hyni me kod QR” + Filloja në skenën e hyrjes + Përzgjidhni “Shfaq kod QR” + Kaloni te Rregullime -> Siguri & Privatësi + Hapeni aplikacionin në pajisjen tuaj tjetër + Hyrja u anulua në pajisjen tuaj tjetër. + Ai kod QR është i pavlefshëm. + Duhet bërë hyrja te pajisja tjetër. + Nga pajisja tjetër është bërë tashmë hyrja. + Kërkesa dështoi. + Kërkesa u hodh poshtë në pajisjen tjetër. + Lidhja me këtë pajisje nuk mbulohet. + Lidhje e pasuksesshme + U vendos lidhje e siguruar + Hyni me kod QR + Skanoni kodin QR + 3 + 2 + 1 + Provojeni + Prekeni djathtas në krye që të shihni mundësinë për dhënie përshtypjesh. + Jepni Përshtypje + Hyni në Hapësirat tuaja (poshtë djathtas) më shpejt dhe më kollaj se kurrë më parë. + Hyni Në Hapësira + Që të thjeshtohet ${app_name} juaj, skedat tanimë janë opsionale. Administrojini duke përdorur menunë djathtas në krye. + Mirë se vini te një pamje e re! + Ky është vendi ku do të shfaqen mesazhet tuaj të palexuar, kur të ketë të tillë. + S’ka gjë për ta raportuar. + Aplikacioni “all-in-one” i fjalosjeve të siguruara, për ekipe, shokë dhe ente. Që t’ia filloni, krijoni një fjalosje, ose hyni në një dhomë ekzistuese. + Mirë se vini te ${app_name}, +\n%s. + Hapësirat janë një mënyrë e re për të grupuar dhoma dhe persona. Shtoni një dhomë ekzistuese, ose krijoni një të re, duke përdorur butonin poshtë djathtas. + %s +\nduket paksa si i zbrazët. + Jini në gjendje të incizoni dhe dërgoni transmetim zanor në rrjedhën kohore të dhomës. + Aktivizoni transmetim zanor (nën zhvillim aktiv) + Aktivizo regjistrim hollësish klienti + Shihini më qartë dhe kontrolloni më mirë krejt sesionet tuaj. + Aktivizo përgjegjës të ri sesionesh + Përdorues të tjerë në mesazhe të drejtpërdrejtë dhe dhoma ku hyni janë në gjendje të shohin një listë të plotë të sesioneve tuaj. +\n +\nKjo u jep atyre besim se po flasin vërtet me ju, por do të thotë gjithashtu që mund shohin emrin e sesionit që jepni këtu. + Riemërtim sesionesh + Sesionet e verifikuar përfaqësojnë sesione ku është bërë hyrja dhe janë verifikuar, ose duke përdorur togfjalëshin tuaj të sigurt, ose me verifikim. +\n +\nKjo do të thotë se zotërojnë kyçe fshehtëzimi për mesazhe tuajt të mëparshëm dhe u ripohojnë përdoruesve të tjerë, me të cilët po komunikoni, se këto sesione ju takojnë juve. + Sesione të verifikuar + Sesionet e paverifikuar janë sesione në të cilët është bërë hyrja me kredencialet tuaja, por pa u bërë verifikim. +\n +\nDuhet të jeni posaçërisht të qartë se i njihni këto sesione, ngaqë mund të përbëjnë përdorim të paautorizuar të llogarisë tuaj. + Sesione të paverifikuar + Sesioni joaktive janë sesione që keni ca kohë që s’i përdorni, por që vazhdojnë të marrin kyçe fshehtëzimi. +\n +\nHeqja e sesioneve joaktive përmirëson sigurinë dhe punimin dhe e bën më të lehtë për ju të pikasni nëse një sesion i ri është i dyshimtë. + Sesione joaktive + Mund të përdorni këtë pajisje për të bërë hyrjen në një pajisje celulare apo web me një kod QR. Për ta bërë këtë ka dy mënyra: + Hyni me Kod QR + Ju lutemi, kini parasysh se emrat e sesioneve janë të dukshëm edhe për personat me të cilët komunikoni. + Emra vetjakë sesionesh mund t’ju ndihmojnë të njihni më kollaj pajisjet tuaja. + Emër sesioni + Riemërtoni sesionin + Adresë IP + Sistem operativ + Model + Shfletues + URL + Version + Ëmër + Aplikacion + Veprimtaria e fundit + Emër sesioni + Merrni njoftime push për këtë sesion. + Njoftime Push + Hollësi aplikacioni, pajisjeje dhe veprimtarie. + Hollësi sesioni + Dilni nga ky sesion + Përzgjidhni sesione + Spastroje Filtrin + S’u gjetën sesione joaktive. + S’u gjetën seanca të paverifikuara. + S’u gjetën sesione të verifikuara. + + Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. + Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. + + Joaktive + Verifikoni sesionet tuaj, për shkëmbim më të sigurt mesazhesh, ose dilni prej atyre që nuk i njihni, apo përdorni më. + Të paverifikuar + Për sigurinë më të mirë, dilni nga çfarëdo sesioni që nuk e njihni apo përdorni më. + Të verifikuar + Filtroji + + Joaktiv për %1$d ditë, ose më gjatë + Joaktiv për %1$d ditë, ose më gjatë + + Jo aktiv + Jo gati për shkëmbim të sigurt mesazhesh + E paverifikuar + Gati për shkëmbim të sigurt mesazhesh + E verifikuar + Krejt sesionet + Filtroji + Pajisje + Sesion + Sesioni i Tanishëm + + Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. + Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. + + Sesione joaktive + Verifikojini, ose dilni nga sesione të paverifikuar. + Sesione të paverifikuar + Përmirësoni sigurinë e llogarisë tuaj duke ndjekur këto rekomandime. + Rekomandime sigurie + + Joaktiv për %1$d+ ditë (%2$s) + Joaktiv për %1$d+ ditë (%2$s) + + I paverifikuar · Sesioni juaj i tanishëm + I paverifikuar · Veprimtari së fundi më %1$s + I verifikuar · Veprimtaria e fundit më %1$s + Shihni Krejt (%1$d) + Shihni Hollësitë + Verifiko Sesion + Verifikoni sesionin tuaj të tanishëm, që të shfaqni gjendjen e verifikimit të këtij sesioni. + Për sigurinë dhe besueshmërinë më të mirë, verifikojeni, ose dilni nga ky sesion. + Verifikoni sesionin tuaj të tanishëm, për shkëmbim më të sigurt të mesazheve. + Ky sesion është gati për shkëmbim të sigurt mesazhesh. + Sesioni juaj i tanishëm është gati për shkëmbim të sigurt mesazhesh. + Gjendje e panjohur verifikimi + Sesion i paverifikuar + Sesion i verifikuar + Lloj i panjohur pajisjeje + Desktop + Web + Celular + Për sigurinë më të mirë, verifikoni sesionet tuaja dhe dilni nga çfarëdo sesioni që s’e njihni, ose s’e përdorni më. + Sesione të tjera + + U hoq %d mesazh + U hoqë %d mesazhe + + Aktivizoni tregim vendndodhjeje + Ju lutemi, kini parasysh: kjo është një veçori në zhvillim, që përdor një sendërtim të përkohshëm. Kjo do të thotë se s’do të jeni në gjendje të fshini historikun e vendndodhjeve tuaja dhe përdoruesit e përparuar do të jenë në gjendje të shohin historikun e vendndodhjeve tuaja, edhe pasi të keni ndalur dhënien “live” për këtë dhomë të vendndodhjes tuaj. + Tregim “live” vendndodhjeje + Kanal i tanishëm: %s + Kanal + S’gjendet pikëmbarimi. + Pikëmbarim i tanishëm: %s + Pikëmbarim + Hëpërhë po përdoret %s. + Metodë + + U gjet %d metodë. + U gjetën %d metoda. + + S’u gjet metodë tjetër veç Google Play Service. + Metoda të gatshme + Metodë njoftimi + Njëkohësim në prapaskenë + Shërbime Google + Zgjidhni si të merren njoftime + Tregimi i ekranit është në punë e sipër + Tregim Ekrani ${app_name} + Kontakt + Kamerë + Vendndodhje + Pyetësorë + Transmetim zanor + Bashkëngjitje + Ngjitës + Fototekë + Nisni një transmetim zanor + Vendndodhje drejtpërsëdrejti + Jepe vendndodhjen + Që të mund të ndani drejtpërsëdrejti vendndodhje me të tjerë në këtë dhomë, lypset të keni lejet e duhura. + S’keni leje të tregoni vendndodhje drejtpërsëdrejti + Përditësuar %1$s më parë + Sendërtim i përkohshëm: vendndodhjet mbeten në historikun e dhomës + Aktivizo Tregim Vendndodhjeje “Live” + Vendndodhje Drejtpërsëdrejti ${app_name} + Edhe %1$s + “Live” deri më %1$s + Shihni vendndodhje “live” + Tregimi “live” i vendndodhjes përfundoi + Po ngarkohet vendndodhje “live”… + S’arrihet të ngarkohet hartë +\nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta. + Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori + Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm. + Përdo + Ndal transmetim zanor + Luani ose vazhdoni luajtje transmetimi zanor + Ndal incizim transmetimi zanor + Ndal incizim transmetimi zanor + Vazhdo incizim transmetimi zanor + Drejtpërdrejt + Shfaq hollësitë më të reja të përdoruesit + Disa përfundime mund të jenë të fshehura, ngaqë janë private dhe ju duhet një ftesë për to. + S’u gjetën përfundime + Mos braktis ndonjë + Braktisi krejt + Gjëra në këtë hapësirë + I zënë + Hap rregullimet + S’u aktivizua dot mirëfilltësim biometrik. + Mirëfilltësimi biometrik qe çaktivizuar ngaqë tani së fundi është shtuar një metodë e re mirëfilltësimi biometrik. Mund ta riaktivizoni që nga Rregullimet. + S’mund të garantohet mirëfilltësia e këtij mesazhi të fshehtëzuar në këtë pajisje. + Tastierë inkonjito + Dërgoni mesazhin tuaj të parë për të ftuar në fjalosje %s + Mesazhet në këtë fjalosje do të jenë të fshehtëzuar skaj-më-skaj. + S’do të jeni në gjendje të shihni historikun e mesazheve të fshehtëzuara. Që t’ia rifilloni nga e para, ricaktoni kyçet tuaja për Kopjeruajtje të Sigurt Mesazhesh dhe kyçe verifikimi. + S’arrihet të verifikohet kjo pajisje + Sesione + Tregoi vendndodhjen e vet drejtpërsëdrejti + E paraprin një mesazh tekst i thjeshtë me (╯°□°)╯︵ ┻━┻ + S’hapet dot kjo lidhje: bashkësitë janë zëvendësuar nga hapësirat + Skanoni kodin QR + Emër përdoruesi / Email / Telefon + Jeni qenie njerëzore\? + Ndiqni udhëzimet e dërguara te %s + Ricaktim fjalëkalimi + Harrova fjalëkalimin + Ridërgo email + S’morët email\? + Ndiqni udhëzimet e dërguara te %s + Verifikoni email-in tuaj + Ridërgomëni kodin + Te %s u dërgua një kod + Ripohoni numrin e telefonit tuaj + Dil nga krejt pajisjet + Ricaktoni fjalëkalimin + Sigurohuni të jetë 8 ose më shumë shenja. + Zgjidhni një fjalëkalim të ri + Fjalëkalim i Ri + Kontrolloni email-in tuaj. + %s do t’ju dërgojë një lidhje verifikimi + Kod ripohimi + Numër Telefoni + %s lyp verifikimin e llogarisë tuaj + Jepni numrin e telefonit tuaj + Email + %s lyp verifikimin e llogarisë tuaj + Jepni email-in tuaj + Ju lutemi, lexoni kushte dhe rregulla të %s + Rregulla shërbyesi + Lidhuni + Element Matrix Services (EMS) është një shërbim strehimi i fuqishëm dhe i besueshëm, për komunikim të shpejtë, të sigurt dhe të atypëratyshëm. Shihni më tepër se si, teelement.io/ems + Doni të strehoni shërbyesin tuaj\? + URL Shërbyesi + Cila është adresa e shërbyesit tuaj\? + Cila është adresa e shërbyesit tuaj\? Kjo është si një shtëpi për krejt të dhënat tuaja + Përzgjidhni shërbyesin tuaj + Mirë se u kthyet! + Përpunojeni + Ose + Ku gjenden bisedat tuaja + Ku do të gjenden bisedat tuaja + Duhet të jetë 8 ose më shumë shenja + Të tjerët mund t’ju zbulojnë %s + Krijoni llogarinë tuaj + Transmetim Zanor + Hap listë hapësirash + Krijoni një bisedë ose dhomë të re + Ricaktoni metodë njoftimesh + Të aktivizuara: + Etiketë profili: + ID sesioni: + Jepi + Po përditësohen të dhënat tuaja… + Diç shkoi ters. Ju lutemi, kontrolloni lidhjen tuaj në rrjet dhe riprovoni. + Persona + Të parapëlqyera + Të palexuara + Krejt + Kopjeruajtja ka një nënshkrim të vlefshëm prej këtij përdoruesi. + Hap skenën e mjeteve të zhvilluesit + Na ndjeni, kjo dhomë s’u gjet. +\nJu lutemi, riprovoni më vonë.%s + Përdor parazgjedhje sistemi + Zgjidheni dorazi + Caktoje vetvetiu + Zgjidhni madhësi shkronjash + ⚠ Në këtë dhomë ka pajisje të paverifikuara, ato s’do të jenë në gjendje të shfshehtëzojnë mesazhet që dërgoni. + Mos dërgo kurrë prej këtij sesioni mesazhe të fshehtëzuar te sesione të paverifikuar në këtë dhomë. + Figurat e animuara vetëluaji + S’u arrit të regjistrohej token pikëmbarimi te shërbyesi Home: +\n%1$s + Pikëmbarim i regjistruar me sukses te shërbyesi Home. + Regjistrim Pikëmbarimi + Akordojini Leje + ${app_name} lyp lejen për shfaqje njoftimesh. +\nJu lutemi, akordoni lejen. + + %1$s dhe %2$d tjetër + %1$s dhe %2$d të tjerë + + %1$s dhe %2$s + ${app_name} lyp leje të shfaqë njoftime. Njoftimet mund të shfaqin mesazhet tuaja, ftesa tuajat, etj. +\n +\nJu lutemi, lejoni përdorimin e tyre te flluska pasuese, që të jeni në gjendje të shihni njoftime. + Email jo i verifikuar, kontrolloni te Të marrët tuaj + Reshtni tregimin e ekranit tuaj + Tregojuani ekranin të tjerëve + Ky është vendi ku do të gjenden kërkesat dhe ftesat tuaja të reja. + S’ka gjë të re. + Ftesa + Hapësirat janë një mënyrë e re për të grupuar dhoma dhe njerëz. Që t’ia filloni, krijoni një hapësirë. + Ende pa hapësira. + Provoni përpunuesin e teksteve të pasur (për tekst të thjeshtë vjen së shpejti) + Aktivizo përpunues teksti të pasur + Krijo MD vetëm për mesazhin e parë + Një Element i thjeshtuar, me skeda opsionale + Aktivizo skemë të re + A - Z + Veprimtari + Renditi sipas + Shfaq të freskëta + Shfaq filtra + Parapëlqime skeme grafike + Shpërzgjidhi krejt + Përzgjidhi krejt + E mora + Më pas + Rifillo + sek + min + h + - Për disa përdorues u hoq shpërfillja + Kërkesë njëkohësimi fillestar + Eksploroni Dhoma + Ndërroni Hapësire + Krijo Dhomë + Filloni Fjalosje + Krejt Fjalosjet + + %1$d i përzgjedhura + %1$d të përzgjedhura + + \ No newline at end of file From de56e08cd2499d41b8d9ded1ff826c490a00fc3d Mon Sep 17 00:00:00 2001 From: PotLice Date: Thu, 3 Nov 2022 03:37:20 +0000 Subject: [PATCH 072/215] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2519 of 2519 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hans/ --- .../src/main/res/values-zh-rCN/strings.xml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 688652265b..112b900da7 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -30,7 +30,7 @@ 发送者的设备没有向我们发送此消息的密钥。 无法发送消息 Matrix 错误 - 电子邮箱地址 + 电子邮件地址 手机号码 %1$s 撤回了对 %2$s 的邀请 %1$s 让未来的房间历史记录对 %2$s 可见 @@ -214,8 +214,8 @@ 登录 提交 错误的用户名和/或密码 - 此电子邮箱地址似乎无效 - 此电子邮箱地址已被使用。 + 此电子邮件地址似乎无效 + 此电子邮件地址已被使用。 忘记密码? 请输入有效的 URL 没有包含有效的 JSON @@ -228,7 +228,7 @@ 搜索 过滤房间成员 没有结果 - 添加电子邮箱地址 + 添加电子邮件地址 添加手机号码 版本 olm 版本 @@ -257,7 +257,7 @@ 开始视频通话 拍摄照片或视频 此主服务器想确认你不是机器人 - 电子邮箱地址验证失败:请确保你已点击邮件中的链接 + 电子邮件地址验证失败:请确保你已点击邮件中的链接 原始 通话正在连接…… ${app_name} 需要权限以访问你的麦克风来进行语音通话。 @@ -351,8 +351,8 @@ 其他 通知目标 登录为 - 请检查你的电子邮箱并点击里面包含的链接。完成时请点击继续。 - 此电子邮箱地址已被使用。 + 请检查你的电子邮件并点击里面包含的链接。完成时请点击继续。 + 此电子邮件地址已被使用。 此手机号码已被使用。 设置为主要地址 取消设置为主要地址 @@ -828,7 +828,7 @@ 撤消 断开连接 拒绝 - 这不是有效的Matrix服务器地址 + 这不是有效的 Matrix 服务器地址 无法在此 URL 找到主服务器,请检查 播放 忽略 @@ -990,13 +990,13 @@ 更改身份服务器 你正在使用 %1$s 与你知道的现有联系人相互发现。 你当前未使用身份服务器。若要与你知道的现有联系人相互发现,请在下方配置。 - 可发现电子邮件地址 + 可发现的电子邮件地址 发现选项将在你添加电子邮件地址后出现。 发现选项将在你添加电话号码后出现。 - 与你的身份服务器断开意味着你将无法被其它用户发现并且无法通过电子邮件和电话邀请他人。 + 与您的身份服务器断开连接意味着您将不会被其他用户发现,并且您将无法通过电子邮件或电话邀请其他人。 可发现电话号码 我们向%s发送了一封电子邮件,请检查你的电子邮件并点击确认链接 - 我们向%s发送了电子邮件,请先检查你的电子邮件并点击确认链接 + 我们向 %s 发送了一封电子邮件,请先检查您的电子邮件并点击确认链接 输入身份服务器 URL 无法连接到身份服务器 请输入身份服务器 url @@ -1143,14 +1143,14 @@ 输入验证码 重新发送 下一个 - 国际电话号码必须以 ‘+’ 开头 + 国际电话号码必须以“+”开头 电话号码似乎无效。请检查 在 %1$s 上注册 用户名或电子邮件 用户名 密码 下一个 - 用户名已占用 + 该用户名已被使用 警告 你的账户尚未创建。是否中止注册过程? 选择 matrix.org @@ -1171,7 +1171,7 @@ 如果你在主服务器上设置了账户,在下方使用你的 Matrix ID(例 @user:domain.com)和密码。 Matrix ID 如果你不知道你的密码,返回并重置。 - 这不是一个有效的用户标识符。期望的格式:\'@user:homeserver.org\' + 这不是有效的用户标识符。预期格式:\'@user:homeserver.org\' 无法找到有效的主服务器。请检查你的标识符 你已登出 这可能由于多种原因: @@ -1199,7 +1199,7 @@ 除非你登录以恢复加密密钥,否则你将无法访问安全消息。 当前会话用于用户 %1$s 而你提供了用户 %2$s 的凭证。${app_name} 不支持此功能。 \n请先清除数据,然后重新登录另一个账户。 - 你的 matrix.to 链接更是不正确 + 您的 matrix.to 链接格式错误 描述太短 初始同步… 高级设置 @@ -1531,7 +1531,7 @@ 你无法访问此消息因为发送者有意不发送密钥 正在等待加密历史 Riot 现已成为 Element! - 我们很高兴地宣布我们改名了!你的应用已经更新到最新版本,并且你已登录你的账户。 + 我们很高兴地宣布我们已经更名了!您的应用程序是最新的,并且您已登录到您的帐户。 明白了 了解更多 将恢复密钥保存到 @@ -1588,9 +1588,9 @@ 移除 %s? 请确认你已点击我们向你发送的电子邮件中的链接。 电子邮件和电话号码 - 管理链接到你的Matrix账户的电子邮件地址和电话号码 + 管理与您的 Matrix 帐户链接的电子邮件地址和电话号码 代码 - 请使用国际格式(电话号码必须以“+”开始) + 请使用国际格式(电话号码必须以“+”开头) 验证此登录来确认你的身份,授权其访问加密消息。 无法打开你被封禁的房间。 无法找到此房间。请确认它存在。 @@ -1804,7 +1804,7 @@ %d 个条目 - 不是有效的 Matrix 二维码 + 这不是有效的 Matrix 二维码 扫描二维码 添加人员 邀请朋友 @@ -2105,9 +2105,9 @@ 可用视频通话 可用语音通话 在 ${app_name} 中直接接收邀请的设置 %s。 - 将此电子邮件地址与您的帐户相关联 - 加入这个空间的邀请被发送至 %s,此邮箱未与您的账户相关联 - 加入这个房间的邀请被发送至 %s,此邮箱未与您的账户相关联 + 将此电子邮件地址与您的帐户链接 + 此空间的邀请已发送至与您的帐户无关的 %s + 此房间的邀请已发送至与您的帐户无关的 %s 你所在的全部房间将显示在主页上。 在主页上显示所有房间 滑动结束通话 @@ -2171,7 +2171,7 @@ 空间访问 谁可以访问? 为 %s 启用电子邮件通知 - 要接收通知邮件,请将一个电子邮件地址关联到你的Matrix账户 + 要接收带有通知的电子邮件,请将电子邮件地址链接到您的 Matrix 帐户 电子邮件通知 升级空间 更改空间名称 @@ -2409,7 +2409,7 @@ 发送图片和视频 打开相机 服务器政策 - Element Matrix Services(EMS)是一个健壮且可靠的主机托管服务,可实现快速、安全和实时的通信。在<a href=\"${ftue_ems_url}\">element.io/ems</a>上了解如何使用 + Element Matrix Services (EMS) 是一种强大且可靠的托管服务,可实现快速、安全和实时的通信。 了解如何在 <a href=\"${ftue_ems_url}\">element.io/ems</a> 想架设自己的服务器? 服务器URL 选择你的服务器 @@ -2545,7 +2545,7 @@ 你的服务器地址是什么? 你的对话发生的地方 %1$s 和 %2$s - 电子邮件未确认,检查你的收件箱 + 电子邮件未验证,请检查您的收件箱 无法加载地图 \n此主服务器可能没有设置好显示地图。 打开设置 From 97841e117ddc2733e73d177fc3e1f0da05bab346 Mon Sep 17 00:00:00 2001 From: Nizami Date: Thu, 3 Nov 2022 13:35:54 +0000 Subject: [PATCH 073/215] Translated using Weblate (Azerbaijani) Currently translated at 2.5% (2 of 79 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/az/ --- fastlane/metadata/android/az/short_description.txt | 1 + fastlane/metadata/android/az/title.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/az/short_description.txt create mode 100644 fastlane/metadata/android/az/title.txt diff --git a/fastlane/metadata/android/az/short_description.txt b/fastlane/metadata/android/az/short_description.txt new file mode 100644 index 0000000000..ecf3d5008c --- /dev/null +++ b/fastlane/metadata/android/az/short_description.txt @@ -0,0 +1 @@ +Qrup mesajlaşma - şifrəli mesajlaşma, qrup söhbəti və video zənglər diff --git a/fastlane/metadata/android/az/title.txt b/fastlane/metadata/android/az/title.txt new file mode 100644 index 0000000000..4ca0ffb55b --- /dev/null +++ b/fastlane/metadata/android/az/title.txt @@ -0,0 +1 @@ +Element - Təhlükəsiz Mesajlaşma From 46260b57685ae1c6aa44672139dcd598601a8ef8 Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 7 Nov 2022 00:04:35 +0000 Subject: [PATCH 074/215] Sync analytics plan --- .../features/analytics/plan/UserProperties.kt | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt index 28732c9a42..01720453ce 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt @@ -24,26 +24,6 @@ package im.vector.app.features.analytics.plan * definition. These properties must all be device independent. */ data class UserProperties( - /** - * Whether the user has the favourites space enabled. - */ - val webMetaSpaceFavouritesEnabled: Boolean? = null, - /** - * Whether the user has the home space set to all rooms. - */ - val webMetaSpaceHomeAllRooms: Boolean? = null, - /** - * Whether the user has the home space enabled. - */ - val webMetaSpaceHomeEnabled: Boolean? = null, - /** - * Whether the user has the other rooms space enabled. - */ - val webMetaSpaceOrphansEnabled: Boolean? = null, - /** - * Whether the user has the people space enabled. - */ - val webMetaSpacePeopleEnabled: Boolean? = null, /** * The active filter in the All Chats screen. */ @@ -109,11 +89,6 @@ data class UserProperties( fun getProperties(): Map? { return mutableMapOf().apply { - webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) } - webMetaSpaceHomeAllRooms?.let { put("WebMetaSpaceHomeAllRooms", it) } - webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) } - webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) } - webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) } allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) } ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } numFavouriteRooms?.let { put("numFavouriteRooms", it) } From 97cfc7dde47ec3fd00e1cde434ebe707048550a4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 09:37:12 +0200 Subject: [PATCH 075/215] Adding changelog entry --- changelog.d/7418.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7418.feature diff --git a/changelog.d/7418.feature b/changelog.d/7418.feature new file mode 100644 index 0000000000..b68ef700da --- /dev/null +++ b/changelog.d/7418.feature @@ -0,0 +1 @@ +[Session manager] Multi-session signout From f45cc715d16f1ae1b70953e561b47ebb0c4e2c79 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 10:13:34 +0200 Subject: [PATCH 076/215] Adding new menu entry for multi signout --- .../src/main/res/values/strings.xml | 2 ++ .../v2/othersessions/OtherSessionsFragment.kt | 20 +++++++++++++++++++ .../src/main/res/menu/menu_other_sessions.xml | 5 +++++ 3 files changed, 27 insertions(+) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 450eb64849..da62e4c300 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3345,6 +3345,8 @@ No inactive sessions found. Clear Filter Select sessions + Sign out of these sessions + Sign out Sign out of this session Session details Application, device, and activity information. diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 4f1c8353f5..7737caa689 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback import androidx.annotation.StringRes +import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.args @@ -32,12 +33,14 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.orEmpty import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet @@ -77,9 +80,26 @@ class OtherSessionsFragment : menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse() + val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) + multiSignoutItem.title = if (isSelectModeEnabled) { + getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() + } else { + getString(R.string.device_manager_other_sessions_multi_signout_all) + } + val showAsActionFlag = if (isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) + changeTextColorOfDestructiveAction(multiSignoutItem) } } + private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { + val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) + val currentTitle = menuItem.title.orEmpty().toString() + menuItem.title = currentTitle + .toSpannable() + .colorizeMatchingText(currentTitle, titleColor) + } + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.otherSessionsSelect -> { diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml index 8339286fe7..d4a75bd0df 100644 --- a/vector/src/main/res/menu/menu_other_sessions.xml +++ b/vector/src/main/res/menu/menu_other_sessions.xml @@ -9,6 +9,11 @@ android:title="@string/device_manager_other_sessions_select" app:showAsAction="withText|never" /> + + Date: Thu, 20 Oct 2022 16:22:29 +0200 Subject: [PATCH 077/215] Adding overflow menu capability in sessions list header view --- .../stylable_sessions_list_header_view.xml | 1 + .../vector/app/core/extensions/MenuItemExt.kt | 29 +++++++++++++++++++ .../v2/VectorSettingsDevicesFragment.kt | 7 +++++ .../devices/v2/list/SessionsListHeaderView.kt | 16 ++++++++++ .../v2/othersessions/OtherSessionsFragment.kt | 9 ++---- .../res/layout/fragment_settings_devices.xml | 3 +- .../res/layout/view_sessions_list_header.xml | 11 ++++++- .../res/menu/menu_other_sessions_header.xml | 12 ++++++++ 8 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt create mode 100644 vector/src/main/res/menu/menu_other_sessions_header.xml diff --git a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml index 098ec263fc..c1a51000b7 100644 --- a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml +++ b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml @@ -5,6 +5,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt b/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt new file mode 100644 index 0000000000..7d62a0c357 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 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.core.extensions + +import android.view.MenuItem +import androidx.annotation.ColorInt +import androidx.core.text.toSpannable +import im.vector.app.core.utils.colorizeMatchingText + +fun MenuItem.setTextColor(@ColorInt color: Int) { + val currentTitle = title.orEmpty().toString() + title = currentTitle + .toSpannable() + .colorizeMatchingText(currentTitle, color) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 1c348af4f9..d192eef778 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -30,6 +30,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.dialogs.ManuallyVerifyDialog +import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider @@ -91,6 +92,7 @@ class VectorSettingsDevicesFragment : super.onViewCreated(view, savedInstanceState) initWaitingView() + initOtherSessionsHeaderView() initOtherSessionsView() initSecurityRecommendationsView() initQrLoginView() @@ -131,6 +133,11 @@ class VectorSettingsDevicesFragment : views.waitingView.waitingStatusText.isVisible = true } + private fun initOtherSessionsHeaderView() { + val color = colorProvider.getColorFromAttribute(R.attr.colorError) + views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) + } + private fun initOtherSessionsView() { views.deviceListOtherSessions.callback = this } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index 0660e7d642..51408931c7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -20,6 +20,9 @@ import android.content.Context import android.content.res.TypedArray import android.util.AttributeSet import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import androidx.appcompat.view.menu.MenuBuilder import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.view.isVisible @@ -39,6 +42,7 @@ class SessionsListHeaderView @JvmOverloads constructor( this ) + val menu: Menu = binding.sessionsListHeaderMenu.menu var onLearnMoreClickListener: (() -> Unit)? = null init { @@ -50,6 +54,7 @@ class SessionsListHeaderView @JvmOverloads constructor( ).use { setTitle(it) setDescription(it) + setMenu(it) } } @@ -90,4 +95,15 @@ class SessionsListHeaderView @JvmOverloads constructor( onLearnMoreClickListener?.invoke() } } + + private fun setMenu(typedArray: TypedArray) { + val menuResId = typedArray.getResourceId(R.styleable.SessionsListHeaderView_sessionsListHeaderMenu, -1) + if (menuResId == -1) { + binding.sessionsListHeaderMenu.isVisible = false + } else { + binding.sessionsListHeaderMenu.showOverflowMenu() + val menuBuilder = binding.sessionsListHeaderMenu.menu as? MenuBuilder + menuBuilder?.let { MenuInflater(context).inflate(menuResId, it) } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 7737caa689..2bed0c943b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -25,7 +25,6 @@ import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback import androidx.annotation.StringRes -import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.args @@ -33,14 +32,13 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.extensions.orEmpty +import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet @@ -94,10 +92,7 @@ class OtherSessionsFragment : private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) - val currentTitle = menuItem.title.orEmpty().toString() - menuItem.title = currentTitle - .toSpannable() - .colorizeMatchingText(currentTitle, titleColor) + menuItem.setTextColor(titleColor) } override fun handleMenuItemSelected(item: MenuItem): Boolean { diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml index 38137b2029..8134774887 100644 --- a/vector/src/main/res/layout/fragment_settings_devices.xml +++ b/vector/src/main/res/layout/fragment_settings_devices.xml @@ -98,6 +98,7 @@ app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession" app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description" app:sessionsListHeaderHasLearnMoreLink="false" + app:sessionsListHeaderMenu="@menu/menu_other_sessions_header" app:sessionsListHeaderTitle="@string/device_manager_sessions_other_title" /> diff --git a/vector/src/main/res/layout/view_sessions_list_header.xml b/vector/src/main/res/layout/view_sessions_list_header.xml index 6139ff4815..9f581a1d03 100644 --- a/vector/src/main/res/layout/view_sessions_list_header.xml +++ b/vector/src/main/res/layout/view_sessions_list_header.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/layout_horizontal_margin" android:layout_marginTop="20dp" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/sessionsListHeaderMenu" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Other sessions" /> @@ -29,4 +29,13 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title" tools:text="For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Learn More." /> + + diff --git a/vector/src/main/res/menu/menu_other_sessions_header.xml b/vector/src/main/res/menu/menu_other_sessions_header.xml new file mode 100644 index 0000000000..4ab0b7465c --- /dev/null +++ b/vector/src/main/res/menu/menu_other_sessions_header.xml @@ -0,0 +1,12 @@ + + + + + + From ae4a7283581bacb5b8c9b860d65505adad956538 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 16:45:31 +0200 Subject: [PATCH 078/215] Handling press on multi signout action in other sessions list screen --- .../v2/othersessions/OtherSessionsAction.kt | 1 + .../v2/othersessions/OtherSessionsFragment.kt | 31 +++++++++++++------ .../othersessions/OtherSessionsViewModel.kt | 6 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt index 1978708ebf..33bc8b3f4f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -26,4 +26,5 @@ sealed class OtherSessionsAction : VectorViewModelAction { data class ToggleSelectionForDevice(val deviceId: String) : OtherSessionsAction() object SelectAll : OtherSessionsAction() object DeselectAll : OtherSessionsAction() + object MultiSignout : OtherSessionsAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 2bed0c943b..8059a75c12 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -78,18 +78,27 @@ class OtherSessionsFragment : menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse() - val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) - multiSignoutItem.title = if (isSelectModeEnabled) { - getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() - } else { - getString(R.string.device_manager_other_sessions_multi_signout_all) - } - val showAsActionFlag = if (isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER - multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) - changeTextColorOfDestructiveAction(multiSignoutItem) + updateMultiSignoutMenuItem(menu, state) } } + private fun updateMultiSignoutMenuItem(menu: Menu, viewState: OtherSessionsViewState) { + val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) + multiSignoutItem.title = if (viewState.isSelectModeEnabled) { + getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() + } else { + getString(R.string.device_manager_other_sessions_multi_signout_all) + } + multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) { + viewState.devices.invoke()?.any { it.isSelected }.orFalse() + } else { + viewState.devices.invoke()?.isNotEmpty().orFalse() + } + val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) + changeTextColorOfDestructiveAction(multiSignoutItem) + } + private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) menuItem.setTextColor(titleColor) @@ -109,6 +118,10 @@ class OtherSessionsFragment : viewModel.handle(OtherSessionsAction.DeselectAll) true } + R.id.otherSessionsMultiSignout -> { + viewModel.handle(OtherSessionsAction.MultiSignout) + true + } else -> false } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index 2cd0c6af66..cac5ce7d3b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -65,6 +65,7 @@ class OtherSessionsViewModel @AssistedInject constructor( } } + // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) @@ -73,6 +74,7 @@ class OtherSessionsViewModel @AssistedInject constructor( is OtherSessionsAction.ToggleSelectionForDevice -> handleToggleSelectionForDevice(action.deviceId) OtherSessionsAction.DeselectAll -> handleDeselectAll() OtherSessionsAction.SelectAll -> handleSelectAll() + OtherSessionsAction.MultiSignout -> handleMultiSignout() } } @@ -142,4 +144,8 @@ class OtherSessionsViewModel @AssistedInject constructor( ) } } + + private fun handleMultiSignout() { + // TODO call multi signout use case with all or only selected devices depending on the ViewState + } } From 810c93cef9f440efb4c88fa7956b14a1e9f46e1d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 17:41:47 +0200 Subject: [PATCH 079/215] Handling press on multi signout action from header menu in other sessions section --- .../features/settings/devices/v2/DevicesAction.kt | 1 + .../features/settings/devices/v2/DevicesViewModel.kt | 6 ++++++ .../devices/v2/VectorSettingsDevicesFragment.kt | 12 ++++++++++++ .../devices/v2/list/SessionsListHeaderView.kt | 5 +++++ 4 files changed, 24 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index c7437db44c..9ecb72a25c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -22,4 +22,5 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() + object MultiSignoutOtherSessions : DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index a5405756eb..8f12bf28b6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -95,10 +95,12 @@ class DevicesViewModel @AssistedInject constructor( } } + // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() + DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions() } } @@ -116,4 +118,8 @@ class DevicesViewModel @AssistedInject constructor( private fun handleMarkAsManuallyVerifiedAction() { // TODO implement when needed } + + private fun handleMultiSignoutOtherSessions() { + // TODO call multi signout use case with all other devices than the current one + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index d192eef778..f3de06a324 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -19,9 +19,11 @@ package im.vector.app.features.settings.devices.v2 import android.content.Context import android.os.Bundle import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -48,6 +50,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsAction import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -136,6 +139,15 @@ class VectorSettingsDevicesFragment : private fun initOtherSessionsHeaderView() { val color = colorProvider.getColorFromAttribute(R.attr.colorError) views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) + views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> + when(menuItem.itemId) { + R.id.otherSessionsHeaderMultiSignout -> { + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + true + } + else -> false + } + } } private fun initOtherSessionsView() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index 51408931c7..f74d88790c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -23,6 +23,7 @@ import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.view.isVisible @@ -106,4 +107,8 @@ class SessionsListHeaderView @JvmOverloads constructor( menuBuilder?.let { MenuInflater(context).inflate(menuResId, it) } } } + + fun setOnMenuItemClickListener(listener: OnMenuItemClickListener) { + binding.sessionsListHeaderMenu.setOnMenuItemClickListener(listener) + } } From 7e836c0e97d4551d61a33a1e50efab4526c7bae3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 13:59:09 +0200 Subject: [PATCH 080/215] Updating the action title to include sessions number --- library/ui-strings/src/main/res/values/strings.xml | 4 ++++ .../devices/v2/VectorSettingsDevicesFragment.kt | 12 ++++++------ .../v2/othersessions/OtherSessionsFragment.kt | 3 ++- vector/src/main/res/menu/menu_other_sessions.xml | 2 +- .../src/main/res/menu/menu_other_sessions_header.xml | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index da62e4c300..e772748a41 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3347,6 +3347,10 @@ Select sessions Sign out of these sessions Sign out + + Sign out of %1$d session + Sign out of %1$d sessions + Sign out of this session Session details Application, device, and activity information. diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index f3de06a324..e9778e1368 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -19,11 +19,9 @@ package im.vector.app.features.settings.devices.v2 import android.content.Context import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -50,7 +48,6 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState -import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsAction import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -137,10 +134,8 @@ class VectorSettingsDevicesFragment : } private fun initOtherSessionsHeaderView() { - val color = colorProvider.getColorFromAttribute(R.attr.colorError) - views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> - when(menuItem.itemId) { + when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { viewModel.handle(DevicesAction.MultiSignoutOtherSessions) true @@ -290,6 +285,11 @@ class VectorSettingsDevicesFragment : hideOtherSessionsView() } else { views.deviceListHeaderOtherSessions.isVisible = true + val color = colorProvider.getColorFromAttribute(R.attr.colorError) + val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout) + val nbDevices = otherDevices.size + multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) + multiSignoutItem.setTextColor(color) views.deviceListOtherSessions.isVisible = true views.deviceListOtherSessions.render( devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 8059a75c12..0429c3bbb3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -87,7 +87,8 @@ class OtherSessionsFragment : multiSignoutItem.title = if (viewState.isSelectModeEnabled) { getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() } else { - getString(R.string.device_manager_other_sessions_multi_signout_all) + val nbDevices = viewState.devices()?.size ?: 0 + stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) } multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) { viewState.devices.invoke()?.any { it.isSelected }.orFalse() diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml index d4a75bd0df..7893575dde 100644 --- a/vector/src/main/res/menu/menu_other_sessions.xml +++ b/vector/src/main/res/menu/menu_other_sessions.xml @@ -11,7 +11,7 @@ From bb262f0c4164af94234d2f4157c9e0e99177b57c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 16:12:32 +0200 Subject: [PATCH 081/215] Adding new "delete_devices" request API --- .../sdk/api/session/crypto/CryptoService.kt | 3 ++ .../internal/crypto/DefaultCryptoService.kt | 8 +++- .../sdk/internal/crypto/api/CryptoApi.kt | 12 ++++++ .../crypto/model/rest/DeleteDeviceParams.kt | 5 ++- .../crypto/model/rest/DeleteDevicesParams.kt | 37 +++++++++++++++++++ .../internal/crypto/tasks/DeleteDeviceTask.kt | 21 ++++++++++- .../sdk/internal/network/NetworkConstants.kt | 1 + 7 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index d2aa8020e8..971d04261e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.crypto import android.content.Context +import androidx.annotation.Size import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback @@ -55,6 +56,8 @@ interface CryptoService { fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + fun deleteDevices(@Size(min = 1) deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + fun getCryptoVersion(context: Context, longFormat: Boolean): String fun isCryptoEnabled(): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 9c3e0ba1c5..032d649421 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -241,9 +241,15 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } + // TODO add unit test override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { + deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback) + } + + // TODO add unit test + override fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) { + .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) { this.executionThread = TaskThread.CRYPTO this.callback = callback } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt index d5a8bdfd7c..cfe4681bfd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.api import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse @@ -136,6 +137,17 @@ internal interface CryptoApi { @Body params: DeleteDeviceParams ) + /** + * Deletes the given devices, and invalidates any access token associated with them. + * Doc: https://spec.matrix.org/v1.4/client-server-api/#post_matrixclientv3delete_devices + * + * @param params the deletion parameters + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_V3 + "delete_devices") + suspend fun deleteDevices( + @Body params: DeleteDevicesParams + ) + /** * Update the device information. * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt index c26c6107c4..24dccc4d90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -23,6 +23,9 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) internal data class DeleteDeviceParams( + /** + * Additional authentication information for the user-interactive authentication API. + */ @Json(name = "auth") - val auth: Map? = null + val auth: Map? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt new file mode 100644 index 0000000000..19b33b2a69 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 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.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class provides the parameter to delete several devices. + */ +@JsonClass(generateAdapter = true) +internal data class DeleteDevicesParams( + /** + * Additional authentication information for the user-interactive authentication API. + */ + @Json(name = "auth") + val auth: Map? = null, + + /** + * Required: The list of device IDs to delete. + */ + @Json(name = "devices") + val deviceIds: List, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 0a77d33acc..fc6bc9b1bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.uia.UiaResult import org.matrix.android.sdk.internal.auth.registration.handleUIA import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -30,21 +31,37 @@ import javax.inject.Inject internal interface DeleteDeviceTask : Task { data class Params( - val deviceId: String, + val deviceIds: List, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?, val userAuthParam: UIABaseAuth? ) } +// TODO add unit tests internal class DefaultDeleteDeviceTask @Inject constructor( private val cryptoApi: CryptoApi, private val globalErrorReceiver: GlobalErrorReceiver ) : DeleteDeviceTask { override suspend fun execute(params: DeleteDeviceTask.Params) { + require(params.deviceIds.isNotEmpty()) + try { executeRequest(globalErrorReceiver) { - cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap())) + val userAuthParam = params.userAuthParam?.asMap() + if (params.deviceIds.size == 1) { + cryptoApi.deleteDevice( + deviceId = params.deviceIds.first(), + DeleteDeviceParams(auth = userAuthParam) + ) + } else { + cryptoApi.deleteDevices( + DeleteDevicesParams( + auth = userAuthParam, + deviceIds = params.deviceIds + ) + ) + } } } catch (throwable: Throwable) { if (params.userInteractiveAuthInterceptor == null || diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt index 5aec7db66c..4bfda0bf3c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt @@ -22,6 +22,7 @@ internal object NetworkConstants { const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/" + const val URI_API_PREFIX_PATH_V3 = "$URI_API_PREFIX_PATH/v3/" const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" // Media From 1bda54323a586818ae155d68cefd08947e5f80bb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 16:51:22 +0200 Subject: [PATCH 082/215] Calling signout multi sessions use case in other sessions screen --- .../v2/othersessions/OtherSessionsAction.kt | 6 ++ .../v2/othersessions/OtherSessionsFragment.kt | 51 +++++++++- .../othersessions/OtherSessionsViewEvents.kt | 10 +- .../othersessions/OtherSessionsViewModel.kt | 99 ++++++++++++++++++- .../othersessions/OtherSessionsViewState.kt | 1 + .../v2/signout/SignoutSessionUseCase.kt | 3 + .../v2/signout/SignoutSessionsUseCase.kt | 43 ++++++++ 7 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt index 33bc8b3f4f..24d2a08bdc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -20,6 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType sealed class OtherSessionsAction : VectorViewModelAction { + // ReAuth + object SsoAuthDone : OtherSessionsAction() + data class PasswordAuthDone(val password: String) : OtherSessionsAction() + object ReAuthCancelled : OtherSessionsAction() + + // Others data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction() data class EnableSelectMode(val deviceId: String?) : OtherSessionsAction() object DisableSelectMode : OtherSessionsAction() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 0429c3bbb3..ca9334ad08 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.othersessions +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -32,6 +33,7 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK @@ -40,6 +42,7 @@ import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.databinding.FragmentOtherSessionsBinding +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType @@ -47,6 +50,7 @@ import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet import im.vector.app.features.themes.ThemeUtils +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject @@ -158,8 +162,9 @@ class OtherSessionsFragment : private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is OtherSessionsViewEvents.Loading -> showLoading(it.message) - is OtherSessionsViewEvents.Failure -> showFailure(it.throwable) + is OtherSessionsViewEvents.SignoutError -> showFailure(it.error) + is OtherSessionsViewEvents.RequestReAuth -> askForReAuthentication(it) + OtherSessionsViewEvents.SignoutSuccess -> enableSelectMode(false) } } } @@ -191,6 +196,7 @@ class OtherSessionsFragment : } override fun invalidate() = withState(viewModel) { state -> + updateLoading(state.isLoading) if (state.devices is Success) { val devices = state.devices.invoke() renderDevices(devices, state.currentFilter) @@ -198,6 +204,14 @@ class OtherSessionsFragment : } } + private fun updateLoading(isLoading: Boolean) { + if (isLoading) { + showLoading(null) + } else { + dismissLoadingDialog() + } + } + private fun updateToolbar(devices: List, isSelectModeEnabled: Boolean) { invalidateOptionsMenu() val title = if (isSelectModeEnabled) { @@ -312,4 +326,37 @@ class OtherSessionsFragment : override fun onViewAllOtherSessionsClicked() { // NOOP. We don't have this button in this screen } + + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(OtherSessionsAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(OtherSessionsAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + } + } + + /** + * Launch the re auth activity to get credentials. + */ + private fun askForReAuthentication(reAuthReq: OtherSessionsViewEvents.RequestReAuth) { + ReAuthActivity.newIntent( + requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title) + ).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt index 95f9c72b33..55753e35be 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt @@ -17,8 +17,14 @@ package im.vector.app.features.settings.devices.v2.othersessions import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse sealed class OtherSessionsViewEvents : VectorViewEvents { - data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents() - data class Failure(val throwable: Throwable) : OtherSessionsViewEvents() + data class RequestReAuth( + val registrationFlowResponse: RegistrationFlowResponse, + val lastErrorCode: String? + ) : OtherSessionsViewEvents() + + object SignoutSuccess : OtherSessionsViewEvents() + data class SignoutError(val error: Throwable) : OtherSessionsViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index cac5ce7d3b..052ec7025d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -21,19 +21,38 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, + private val stringProvider: StringProvider, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase @@ -68,6 +87,9 @@ class OtherSessionsViewModel @AssistedInject constructor( // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { + is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action) + OtherSessionsAction.ReAuthCancelled -> handleReAuthCancelled() + OtherSessionsAction.SsoAuthDone -> handleSsoAuthDone() is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) OtherSessionsAction.DisableSelectMode -> handleDisableSelectMode() is OtherSessionsAction.EnableSelectMode -> handleEnableSelectMode(action.deviceId) @@ -145,7 +167,80 @@ class OtherSessionsViewModel @AssistedInject constructor( } } - private fun handleMultiSignout() { - // TODO call multi signout use case with all or only selected devices depending on the ViewState + private fun handleMultiSignout() = withState { state -> + viewModelScope.launch { + setLoading(true) + val deviceIds = getDeviceIdsToSignout(state) + if (deviceIds.isEmpty()) { + return@launch + } + val signoutResult = signout(deviceIds) + setLoading(false) + + if (signoutResult.isSuccess) { + onSignoutSuccess() + } else { + when (val failure = signoutResult.exceptionOrNull()) { + null -> onSignoutSuccess() + else -> onSignoutFailure(failure) + } + } + } + } + + private fun getDeviceIdsToSignout(state: OtherSessionsViewState): List { + return if (state.isSelectModeEnabled) { + state.devices()?.filter { it.isSelected }.orEmpty() + } else { + state.devices().orEmpty() + }.mapNotNull { it.deviceInfo.deviceId } + } + + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { + is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) + is SignoutSessionResult.Completed -> Unit + } + } + }) + + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + Timber.d("onReAuthNeeded") + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation + _viewEvents.post(OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) + } + + private fun setLoading(isLoading: Boolean) { + setState { copy(isLoading = isLoading) } + } + + private fun onSignoutSuccess() { + Timber.d("signout success") + refreshDeviceList() + _viewEvents.post(OtherSessionsViewEvents.SignoutSuccess) + } + + private fun onSignoutFailure(failure: Throwable) { + Timber.e("signout failure", failure) + val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { + stringProvider.getString(R.string.authentication_error) + } else { + stringProvider.getString(R.string.matrix_error) + } + _viewEvents.post(OtherSessionsViewEvents.SignoutError(Exception(failureMessage))) + } + + private fun handleSsoAuthDone() { + pendingAuthHandler.ssoAuthDone() + } + + private fun handlePasswordAuthDone(action: OtherSessionsAction.PasswordAuthDone) { + pendingAuthHandler.passwordAuthDone(action.password) + } + + private fun handleReAuthCancelled() { + pendingAuthHandler.reAuthCancelled() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt index 0db3c8cd0e..c0b50fded8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt @@ -27,6 +27,7 @@ data class OtherSessionsViewState( val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, val excludeCurrentDevice: Boolean = false, val isSelectModeEnabled: Boolean = false, + val isLoading: Boolean = false, ) : MavericksState { constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt index 60ca8e91c6..bc6cff0d43 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt @@ -21,6 +21,9 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.util.awaitCallback import javax.inject.Inject +/** + * Use case to signout a single session. + */ class SignoutSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt new file mode 100644 index 0000000000..82b03247c4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.signout + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.util.awaitCallback +import javax.inject.Inject + +/** + * Use case to signout several sessions. + */ +class SignoutSessionsUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + // TODO add unit tests + suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { + return deleteDevices(deviceIds, userInteractiveAuthInterceptor) + } + + private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) + } + } +} From 0f8e5919daeed2d2642215a99361f8dd63b0f07c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 17:52:13 +0200 Subject: [PATCH 083/215] Calling signout multi sessions use case in main screen for other sessions --- .../settings/devices/v2/DevicesAction.kt | 6 ++ .../settings/devices/v2/DevicesViewEvent.kt | 12 ++- .../settings/devices/v2/DevicesViewModel.kt | 97 ++++++++++++++++++- .../v2/VectorSettingsDevicesFragment.kt | 45 ++++++++- 4 files changed, 150 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index 9ecb72a25c..21cbb86e94 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -20,6 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { + // ReAuth + object SsoAuthDone : DevicesAction() + data class PasswordAuthDone(val password: String) : DevicesAction() + object ReAuthCancelled : DevicesAction() + + // Others object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() object MultiSignoutOtherSessions : DevicesAction() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt index c78c20f792..770ffc2513 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt @@ -17,17 +17,21 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewEvents import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo sealed class DevicesViewEvent : VectorViewEvents { - data class Loading(val message: CharSequence? = null) : DevicesViewEvent() - data class Failure(val throwable: Throwable) : DevicesViewEvent() - data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent() - data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent() + data class RequestReAuth( + val registrationFlowResponse: RegistrationFlowResponse, + val lastErrorCode: String? + ) : DevicesViewEvent() + data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent() object SelfVerification : DevicesViewEvent() data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent() object PromptResetSecrets : DevicesViewEvent() + object SignoutSuccess : DevicesViewEvent() + data class SignoutError(val error: Throwable) : DevicesViewEvent() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index 8f12bf28b6..abe0e2719f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -21,24 +21,42 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, activeSessionHolder: ActiveSessionHolder, + private val stringProvider: StringProvider, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { @@ -98,6 +116,9 @@ class DevicesViewModel @AssistedInject constructor( // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { + is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action) + DevicesAction.ReAuthCancelled -> handleReAuthCancelled() + DevicesAction.SsoAuthDone -> handleSsoAuthDone() is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions() @@ -119,7 +140,79 @@ class DevicesViewModel @AssistedInject constructor( // TODO implement when needed } - private fun handleMultiSignoutOtherSessions() { - // TODO call multi signout use case with all other devices than the current one + private fun handleMultiSignoutOtherSessions() = withState { state -> + viewModelScope.launch { + setLoading(true) + val deviceIds = getDeviceIdsOfOtherSessions(state) + if (deviceIds.isEmpty()) { + return@launch + } + val signoutResult = signout(deviceIds) + setLoading(false) + + if (signoutResult.isSuccess) { + onSignoutSuccess() + } else { + when (val failure = signoutResult.exceptionOrNull()) { + null -> onSignoutSuccess() + else -> onSignoutFailure(failure) + } + } + } + } + + private fun getDeviceIdsOfOtherSessions(state: DevicesViewState): List { + val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId + return state.devices() + ?.mapNotNull { fullInfo -> fullInfo.deviceInfo.deviceId.takeUnless { it == currentDeviceId } } + .orEmpty() + } + + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { + is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) + is SignoutSessionResult.Completed -> Unit + } + } + }) + + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + Timber.d("onReAuthNeeded") + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation + _viewEvents.post(DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) + } + + private fun setLoading(isLoading: Boolean) { + setState { copy(isLoading = isLoading) } + } + + private fun onSignoutSuccess() { + Timber.d("signout success") + refreshDeviceList() + _viewEvents.post(DevicesViewEvent.SignoutSuccess) + } + + private fun onSignoutFailure(failure: Throwable) { + Timber.e("signout failure", failure) + val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { + stringProvider.getString(R.string.authentication_error) + } else { + stringProvider.getString(R.string.matrix_error) + } + _viewEvents.post(DevicesViewEvent.SignoutError(Exception(failureMessage))) + } + + private fun handleSsoAuthDone() { + pendingAuthHandler.ssoAuthDone() + } + + private fun handlePasswordAuthDone(action: DevicesAction.PasswordAuthDone) { + pendingAuthHandler.passwordAuthDone(action.password) + } + + private fun handleReAuthCancelled() { + pendingAuthHandler.reAuthCancelled() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index e9778e1368..4f507b2a3d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2 +import android.app.Activity import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -30,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.dialogs.ManuallyVerifyDialog +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -37,6 +39,7 @@ import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.features.VectorFeatures +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.login.qr.QrCodeLoginArgs @@ -48,6 +51,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -102,10 +106,7 @@ class VectorSettingsDevicesFragment : private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is DevicesViewEvent.Loading -> showLoading(it.message) - is DevicesViewEvent.Failure -> showFailure(it.throwable) - is DevicesViewEvent.RequestReAuth -> Unit // TODO. Next PR - is DevicesViewEvent.PromptRenameDevice -> Unit // TODO. Next PR + is DevicesViewEvent.RequestReAuth -> askForReAuthentication(it) is DevicesViewEvent.ShowVerifyDevice -> { VerificationBottomSheet.withArgs( roomId = null, @@ -124,6 +125,8 @@ class VectorSettingsDevicesFragment : is DevicesViewEvent.PromptResetSecrets -> { navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) } + is DevicesViewEvent.SignoutError -> showFailure(it.error) + is DevicesViewEvent.SignoutSuccess -> Unit // do nothing } } } @@ -137,6 +140,7 @@ class VectorSettingsDevicesFragment : views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { + // TODO ask for confirmation viewModel.handle(DevicesAction.MultiSignoutOtherSessions) true } @@ -366,4 +370,37 @@ class VectorSettingsDevicesFragment : excludeCurrentDevice = true ) } + + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(DevicesAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(DevicesAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + + /** + * Launch the re auth activity to get credentials. + */ + private fun askForReAuthentication(reAuthReq: DevicesViewEvent.RequestReAuth) { + ReAuthActivity.newIntent( + requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title) + ).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } } From 727c7462df2d910ecfdb911b749345365edf8658 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 10:17:23 +0200 Subject: [PATCH 084/215] Adding confirmation dialog before signout process --- .../v2/VectorSettingsDevicesFragment.kt | 17 ++++++++- .../v2/othersessions/OtherSessionsFragment.kt | 16 +++++++- .../v2/overview/SessionOverviewFragment.kt | 12 ++---- .../BuildConfirmSignoutDialogUseCase.kt | 37 +++++++++++++++++++ 4 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 4f507b2a3d..98c7016d29 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -51,6 +51,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -75,6 +76,8 @@ class VectorSettingsDevicesFragment : @Inject lateinit var stringProvider: StringProvider + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + private val viewModel: DevicesViewModel by fragmentViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding { @@ -140,8 +143,7 @@ class VectorSettingsDevicesFragment : views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { - // TODO ask for confirmation - viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + confirmMultiSignoutOtherSessions() true } else -> false @@ -149,6 +151,17 @@ class VectorSettingsDevicesFragment : } } + private fun confirmMultiSignoutOtherSessions() { + activity?.let { + buildConfirmSignoutDialogUseCase.execute(it, this::multiSignoutOtherSessions) + .show() + } + } + + private fun multiSignoutOtherSessions() { + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + } + private fun initOtherSessionsView() { views.deviceListOtherSessions.callback = this } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index ca9334ad08..d2bb1d443b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -49,6 +49,7 @@ import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse @@ -70,6 +71,8 @@ class OtherSessionsFragment : @Inject lateinit var viewNavigator: OtherSessionsViewNavigator + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding { return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false) } @@ -124,13 +127,24 @@ class OtherSessionsFragment : true } R.id.otherSessionsMultiSignout -> { - viewModel.handle(OtherSessionsAction.MultiSignout) + confirmMultiSignout() true } else -> false } } + private fun confirmMultiSignout() { + activity?.let { + buildConfirmSignoutDialogUseCase.execute(it, this::multiSignout) + .show() + } + } + + private fun multiSignout() { + viewModel.handle(OtherSessionsAction.MultiSignout) + } + private fun enableSelectMode(isEnabled: Boolean, deviceId: String? = null) { val action = if (isEnabled) OtherSessionsAction.EnableSelectMode(deviceId) else OtherSessionsAction.DisableSelectMode viewModel.handle(action) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index 620372f810..e149023f22 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -29,7 +29,6 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter @@ -45,6 +44,7 @@ import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import im.vector.app.features.workers.signout.SignOutUiWorker import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse @@ -69,6 +69,8 @@ class SessionOverviewFragment : @Inject lateinit var stringProvider: StringProvider + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + private val viewModel: SessionOverviewViewModel by fragmentViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding { @@ -134,13 +136,7 @@ class SessionOverviewFragment : private fun confirmSignoutOtherSession() { activity?.let { - MaterialAlertDialogBuilder(it) - .setTitle(R.string.action_sign_out) - .setMessage(R.string.action_sign_out_confirmation_simple) - .setPositiveButton(R.string.action_sign_out) { _, _ -> - signoutSession() - } - .setNegativeButton(R.string.action_cancel, null) + buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession) .show() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt new file mode 100644 index 0000000000..9959bd1828 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.signout + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import javax.inject.Inject + +class BuildConfirmSignoutDialogUseCase @Inject constructor() { + + fun execute(context: Context, onConfirm: () -> Unit): AlertDialog { + return MaterialAlertDialogBuilder(context) + .setTitle(R.string.action_sign_out) + .setMessage(R.string.action_sign_out_confirmation_simple) + .setPositiveButton(R.string.action_sign_out) { _, _ -> + onConfirm() + } + .setNegativeButton(R.string.action_cancel, null) + .create() + } +} From a968ac08c363779d7cba8f3c91060e03ca21b53c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 14:20:38 +0200 Subject: [PATCH 085/215] Adding unit tests for signout sessions use case --- .../v2/signout/SignoutSessionsUseCase.kt | 1 - .../devices/v2/DevicesViewModelTest.kt | 28 +++++-- .../OtherSessionsViewModelTest.kt | 25 ++++-- .../v2/signout/SignoutSessionsUseCaseTest.kt | 83 +++++++++++++++++++ .../app/test/fakes/FakeCryptoService.kt | 14 ++++ 5 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt index 82b03247c4..b4fc78043e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -28,7 +28,6 @@ class SignoutSessionsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { - // TODO add unit tests suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { return deleteDevices(deviceIds, userInteractiveAuthInterceptor) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index c5edfb868d..bf06dd7329 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -22,10 +22,14 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test import im.vector.app.test.testDispatcher @@ -53,21 +57,29 @@ class DevicesViewModelTest { val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() - private val refreshDevicesUseCase = mockk(relaxUnitFun = true) private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() + private val fakeSignoutSessionsUseCase = mockk() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() + private val fakePendingAuthHandler = FakePendingAuthHandler() + private val refreshDevicesUseCase = mockk(relaxUnitFun = true) private fun createViewModel(): DevicesViewModel { return DevicesViewModel( - DevicesViewState(), - fakeActiveSessionHolder.instance, - getCurrentSessionCrossSigningInfoUseCase, - getDeviceFullInfoListUseCase, - refreshDevicesOnCryptoDevicesChangeUseCase, - checkIfCurrentSessionCanBeVerifiedUseCase, - refreshDevicesUseCase, + initialState = DevicesViewState(), + activeSessionHolder = fakeActiveSessionHolder.instance, + stringProvider = fakeStringProvider.instance, + getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, + getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, + refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, + checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, + pendingAuthHandler = fakePendingAuthHandler.instance, + refreshDevicesUseCase = refreshDevicesUseCase, ) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index e7b8eeee9b..7cf624e569 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -23,7 +23,11 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test @@ -54,15 +58,24 @@ class OtherSessionsViewModelTest { ) private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCaseUseCase = mockk() + private val fakeSignoutSessionsUseCase = mockk() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() + private val fakePendingAuthHandler = FakePendingAuthHandler() - private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = OtherSessionsViewModel( - initialState = OtherSessionsViewState(args), - activeSessionHolder = fakeActiveSessionHolder.instance, - getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, - refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, - ) + private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = + OtherSessionsViewModel( + initialState = OtherSessionsViewState(args), + stringProvider = fakeStringProvider.instance, + activeSessionHolder = fakeActiveSessionHolder.instance, + getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, + pendingAuthHandler = fakePendingAuthHandler.instance, + refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, + ) @Before fun setup() { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt new file mode 100644 index 0000000000..208ce8b334 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.signout + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.Test +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor + +private const val A_DEVICE_ID_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" + +class SignoutSessionsUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val signoutSessionsUseCase = SignoutSessionsUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance + ) + + @Test + fun `given a list of device ids when signing out with success then success result is returned`() = runTest { + // Given + val interceptor = givenAuthInterceptor() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesSucceeds(deviceIds) + + // When + val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + + // Then + result.isSuccess shouldBe true + every { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, interceptor, any()) + } + } + + @Test + fun `given a list of device ids when signing out with error then failure result is returned`() = runTest { + // Given + val interceptor = givenAuthInterceptor() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + val error = mockk() + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesFailsWithError(deviceIds, error) + + // When + val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + + // Then + result.isFailure shouldBe true + every { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, interceptor, any()) + } + } + + private fun givenAuthInterceptor() = mockk() +} + diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index e96a58faa0..5f34c45fa7 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -84,5 +84,19 @@ class FakeCryptoService( } } + fun givenDeleteDevicesSucceeds(deviceIds: List) { + val matrixCallback = slot>() + every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + thirdArg>().onSuccess(Unit) + } + } + + fun givenDeleteDevicesFailsWithError(deviceIds: List, error: Exception) { + val matrixCallback = slot>() + every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + thirdArg>().onFailure(error) + } + } + override fun getMyDevice() = cryptoDeviceInfo } From 5bcf2ac51ea4ce40b43700e0f03d662985872b5f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 15:37:13 +0200 Subject: [PATCH 086/215] Adding unit tests for other sessions list view model --- .../othersessions/OtherSessionsViewModel.kt | 1 - .../OtherSessionsViewModelTest.kt | 295 +++++++++++++++++- .../overview/SessionOverviewViewModelTest.kt | 109 +++---- .../test/fakes/FakeSignoutSessionUseCase.kt | 77 +++++ .../test/fakes/FakeSignoutSessionsUseCase.kt | 77 +++++ 5 files changed, 479 insertions(+), 80 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index 052ec7025d..a26187b797 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -84,7 +84,6 @@ class OtherSessionsViewModel @AssistedInject constructor( } } - // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 7cf624e569..28b97ed70d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -19,32 +19,43 @@ package im.vector.app.features.settings.devices.v2.othersessions import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test import im.vector.app.test.testDispatcher import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll +import io.mockk.verify import io.mockk.verifyAll import kotlinx.coroutines.flow.flowOf +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.net.ssl.HttpsURLConnection private const val A_TITLE_RES_ID = 1 -private const val A_DEVICE_ID = "device-id" +private const val A_DEVICE_ID_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" +private const val A_PASSWORD = "password" +private const val AUTH_ERROR_MESSAGE = "auth-error-message" +private const val AN_ERROR_MESSAGE = "error-message" class OtherSessionsViewModelTest { @@ -60,18 +71,18 @@ class OtherSessionsViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() - private val fakeRefreshDevicesUseCaseUseCase = mockk() - private val fakeSignoutSessionsUseCase = mockk() + private val fakeRefreshDevicesUseCaseUseCase = mockk(relaxed = true) + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = + private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( - initialState = OtherSessionsViewState(args), + initialState = viewState, stringProvider = fakeStringProvider.instance, activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, - signoutSessionsUseCase = fakeSignoutSessionsUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, @@ -101,6 +112,39 @@ class OtherSessionsViewModelTest { unmockkAll() } + @Test + fun `given the viewModel when initializing it then verification listener is added`() { + // Given + val fakeVerificationService = givenVerificationService() + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + + // When + val viewModel = createViewModel() + + // Then + verify { + fakeVerificationService.addListener(viewModel) + } + } + + @Test + fun `given the viewModel when clearing it then verification listener is removed`() { + // Given + val fakeVerificationService = givenVerificationService() + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + + // When + val viewModel = createViewModel() + viewModel.onCleared() + + // Then + verify { + fakeVerificationService.removeListener(viewModel) + } + } + @Test fun `given the viewModel has been initialized then viewState is updated with devices list`() { // Given @@ -156,7 +200,7 @@ class OtherSessionsViewModelTest { @Test fun `given enable select mode action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) + val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) val devices: List = listOf(deviceFullInfo) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -169,7 +213,7 @@ class OtherSessionsViewModelTest { // When val viewModel = createViewModel() val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID)) + viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID_1)) // Then viewModelTest @@ -180,8 +224,8 @@ class OtherSessionsViewModelTest { @Test fun `given disable select mode action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -205,7 +249,7 @@ class OtherSessionsViewModelTest { @Test fun `given toggle selection for device action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) + val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) val devices: List = listOf(deviceFullInfo) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -218,7 +262,7 @@ class OtherSessionsViewModelTest { // When val viewModel = createViewModel() val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID)) + viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID_1)) // Then viewModelTest @@ -229,8 +273,8 @@ class OtherSessionsViewModelTest { @Test fun `given select all action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -254,8 +298,8 @@ class OtherSessionsViewModelTest { @Test fun `given deselect all action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -276,6 +320,223 @@ class OtherSessionsViewModelTest { .finish() } + @Test + fun `given no reAuth is needed and in selectMode when handling multiSignout action then signout process is performed`() { + // Given + val isSelectModeEnabled = true + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + // signout only selected devices + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + isSelectModeEnabled = isSelectModeEnabled, + ) + + // When + val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled)) + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCaseUseCase.execute() + } + } + + @Test + fun `given no reAuth is needed and NOT in selectMode when handling multiSignout action then signout process is performed`() { + // Given + val isSelectModeEnabled = false + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + // signout all devices + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + isSelectModeEnabled = isSelectModeEnabled, + ) + + // When + val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled)) + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCaseUseCase.execute() + } + } + + @Test + fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + ) + fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val error = Exception() + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + ) + fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertEvent { it == expectedReAuthEvent } + .finish() + fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth + fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation + } + + @Test + fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() { + // Given + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.ssoAuthDone() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.SsoAuthDone) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.ssoAuthDone() + } + } + + @Test + fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() { + // Given + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.PasswordAuthDone(A_PASSWORD)) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD) + } + } + + @Test + fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() { + // Given + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.reAuthCancelled() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.reAuthCancelled() + } + } + private fun givenGetDeviceFullInfoListReturns( filterType: DeviceManagerFilterType, devices: List, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index c0ba6ce28b..289279b8f6 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -26,11 +26,10 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSignoutSessionUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService @@ -43,7 +42,6 @@ import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs -import io.mockk.slot import io.mockk.unmockkAll import io.mockk.verify import io.mockk.verifyAll @@ -53,14 +51,10 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import javax.net.ssl.HttpsURLConnection -import kotlin.coroutines.Continuation private const val A_SESSION_ID_1 = "session-id-1" private const val A_SESSION_ID_2 = "session-id-2" @@ -83,10 +77,10 @@ class SessionOverviewViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val signoutSessionUseCase = mockk() + private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private val refreshDevicesUseCase = mockk() + private val refreshDevicesUseCase = mockk(relaxed = true) private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() private val fakeGetNotificationsStatusUseCase = mockk() private val notificationsStatus = NotificationsStatus.ENABLED @@ -96,7 +90,7 @@ class SessionOverviewViewModelTest { stringProvider = fakeStringProvider.instance, getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionUseCase = signoutSessionUseCase, + signoutSessionUseCase = fakeSignoutSessionUseCase.instance, interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, @@ -115,11 +109,50 @@ class SessionOverviewViewModelTest { every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) } + private fun givenVerificationService(): FakeVerificationService { + val fakeVerificationService = fakeActiveSessionHolder + .fakeSession + .fakeCryptoService + .fakeVerificationService + fakeVerificationService.givenAddListenerSucceeds() + fakeVerificationService.givenRemoveListenerSucceeds() + return fakeVerificationService + } + @After fun tearDown() { unmockkAll() } + @Test + fun `given the viewModel when initializing it then verification listener is added`() { + // Given + val fakeVerificationService = givenVerificationService() + + // When + val viewModel = createViewModel() + + // Then + verify { + fakeVerificationService.addListener(viewModel) + } + } + + @Test + fun `given the viewModel when clearing it then verification listener is removed`() { + // Given + val fakeVerificationService = givenVerificationService() + + // When + val viewModel = createViewModel() + viewModel.onCleared() + + // Then + verify { + fakeVerificationService.removeListener(viewModel) + } + } + @Test fun `given the viewModel has been initialized then pushers are refreshed`() { createViewModel() @@ -223,8 +256,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - givenSignoutSuccess(A_SESSION_ID_1) - every { refreshDevicesUseCase.execute() } just runs + fakeSignoutSessionUseCase.givenSignoutSuccess(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -261,7 +293,7 @@ class SessionOverviewViewModelTest { every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - givenSignoutError(A_SESSION_ID_1, serverError) + fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, serverError) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -296,7 +328,7 @@ class SessionOverviewViewModelTest { every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) val error = Exception() - givenSignoutError(A_SESSION_ID_1, error) + fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, error) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -330,7 +362,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val reAuthNeeded = givenSignoutReAuthNeeded(A_SESSION_ID_1) + val reAuthNeeded = fakeSignoutSessionUseCase.givenSignoutReAuthNeeded(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) @@ -415,53 +447,6 @@ class SessionOverviewViewModelTest { } } - private fun givenSignoutSuccess(deviceId: String) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - } - - private fun givenSignoutReAuthNeeded(deviceId: String): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() - val flowResponse = mockk() - every { flowResponse.session } returns A_SESSION_ID_1 - val errorCode = "errorCode" - val promise = mockk>() - val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( - pendingAuth = mockk(), - uiaContinuation = promise, - flowResponse = flowResponse, - errCode = errorCode, - ) - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded - coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - - return reAuthNeeded - } - - private fun givenSignoutError(deviceId: String, error: Throwable) { - coEvery { signoutSessionUseCase.execute(deviceId, any()) } returns Result.failure(error) - } - - private fun givenVerificationService(): FakeVerificationService { - val fakeVerificationService = fakeActiveSessionHolder - .fakeSession - .fakeCryptoService - .fakeVerificationService - fakeVerificationService.givenAddListenerSucceeds() - fakeVerificationService.givenRemoveListenerSucceeds() - return fakeVerificationService - } - private fun givenCurrentSessionIsTrusted() { fakeActiveSessionHolder.fakeSession.givenSessionId(A_SESSION_ID_2) val deviceFullInfo = mockk() diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt new file mode 100644 index 0000000000..8a6b101ff6 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import kotlin.coroutines.Continuation + +class FakeSignoutSessionUseCase { + + val instance = mockk() + + fun givenSignoutSuccess( + deviceId: String, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ) { + val interceptor = slot() + val flowResponse = mockk() + val errorCode = "errorCode" + val promise = mockk>() + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed + coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + } + + fun givenSignoutReAuthNeeded( + deviceId: String, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ): SignoutSessionResult.ReAuthNeeded { + val interceptor = slot() + val flowResponse = mockk() + every { flowResponse.session } returns "a-session-id" + val errorCode = "errorCode" + val promise = mockk>() + val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = promise, + flowResponse = flowResponse, + errCode = errorCode, + ) + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded + coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + + return reAuthNeeded + } + + fun givenSignoutError(deviceId: String, error: Throwable) { + coEvery { instance.execute(deviceId, any()) } returns Result.failure(error) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt new file mode 100644 index 0000000000..04d05b1d8a --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import kotlin.coroutines.Continuation + +class FakeSignoutSessionsUseCase { + + val instance = mockk() + + fun givenSignoutSuccess( + deviceIds: List, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ) { + val interceptor = slot() + val flowResponse = mockk() + val errorCode = "errorCode" + val promise = mockk>() + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed + coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + } + + fun givenSignoutReAuthNeeded( + deviceIds: List, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ): SignoutSessionResult.ReAuthNeeded { + val interceptor = slot() + val flowResponse = mockk() + every { flowResponse.session } returns "a-session-id" + val errorCode = "errorCode" + val promise = mockk>() + val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = promise, + flowResponse = flowResponse, + errCode = errorCode, + ) + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded + coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + + return reAuthNeeded + } + + fun givenSignoutError(deviceIds: List, error: Throwable) { + coEvery { instance.execute(deviceIds, any()) } returns Result.failure(error) + } +} From 880ee4058c2fe50f6fa10edff53c6a892cb88ef9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 16:14:32 +0200 Subject: [PATCH 087/215] Adding unit tests about reAuth actions for devices view model --- .../settings/devices/v2/DevicesViewModel.kt | 1 - .../devices/v2/DevicesViewModelTest.kt | 214 ++++++++++++++---- 2 files changed, 170 insertions(+), 45 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index abe0e2719f..fe4d0dc838 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -113,7 +113,6 @@ class DevicesViewModel @AssistedInject constructor( } } - // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index bf06dd7329..71e9d609b7 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -19,16 +19,17 @@ package im.vector.app.features.settings.devices.v2 import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.R import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -36,20 +37,32 @@ import im.vector.app.test.testDispatcher import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every -import io.mockk.just +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.runs import io.mockk.unmockkAll import io.mockk.verify +import io.mockk.verifyAll import kotlinx.coroutines.flow.flowOf +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.net.ssl.HttpsURLConnection + +private const val A_CURRENT_DEVICE_ID = "current-device-id" +private const val A_DEVICE_ID_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" +private const val A_PASSWORD = "password" +private const val AUTH_ERROR_MESSAGE = "auth-error-message" +private const val AN_ERROR_MESSAGE = "error-message" class DevicesViewModelTest { @@ -60,9 +73,9 @@ class DevicesViewModelTest { private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() - private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk() + private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true) private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val fakeSignoutSessionsUseCase = mockk() + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxUnitFun = true) @@ -76,7 +89,7 @@ class DevicesViewModelTest { getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionsUseCase = fakeSignoutSessionsUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = refreshDevicesUseCase, @@ -88,6 +101,20 @@ class DevicesViewModelTest { // Needed for internal usage of Flow.throttleFirst() inside the ViewModel mockkStatic(SystemClock::class) every { SystemClock.elapsedRealtime() } returns 1234 + + givenVerificationService() + givenCurrentSessionCrossSigningInfo() + givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + } + + private fun givenVerificationService(): FakeVerificationService { + val fakeVerificationService = fakeActiveSessionHolder + .fakeSession + .fakeCryptoService + .fakeVerificationService + fakeVerificationService.givenAddListenerSucceeds() + fakeVerificationService.givenRemoveListenerSucceeds() + return fakeVerificationService } @After @@ -99,9 +126,6 @@ class DevicesViewModelTest { fun `given the viewModel when initializing it then verification listener is added`() { // Given val fakeVerificationService = givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModel = createViewModel() @@ -116,9 +140,6 @@ class DevicesViewModelTest { fun `given the viewModel when clearing it then verification listener is removed`() { // Given val fakeVerificationService = givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModel = createViewModel() @@ -133,10 +154,7 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then view state is updated with current session cross signing info`() { // Given - givenVerificationService() val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModelTest = createViewModel().test() @@ -149,10 +167,7 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then view state is updated with current device full info list`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - val deviceFullInfoList = givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) // When val viewModelTest = createViewModel().test() @@ -168,10 +183,6 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then devices are refreshed on crypto devices change`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When createViewModel() @@ -183,10 +194,6 @@ class DevicesViewModelTest { @Test fun `given current session can be verified when handling verify current session action then self verification event is posted`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true @@ -207,10 +214,6 @@ class DevicesViewModelTest { @Test fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false @@ -228,18 +231,128 @@ class DevicesViewModelTest { } } - private fun givenVerificationService(): FakeVerificationService { - val fakeVerificationService = fakeActiveSessionHolder - .fakeSession - .fakeCryptoService - .fakeVerificationService - fakeVerificationService.givenAddListenerSucceeds() - fakeVerificationService.givenRemoveListenerSucceeds() - return fakeVerificationService + @Test + fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { + // Given + val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) + val expectedViewState = givenInitialViewState() + fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val error = Exception() + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) + val expectedViewState = givenInitialViewState() + fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { + // Given + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertEvent { it == expectedReAuthEvent } + .finish() + fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth + fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation + } + + @Test + fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() { + // Given + justRun { fakePendingAuthHandler.instance.ssoAuthDone() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.SsoAuthDone) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.ssoAuthDone() + } + } + + @Test + fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() { + // Given + justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.PasswordAuthDone(A_PASSWORD)) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD) + } + } + + @Test + fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() { + // Given + justRun { fakePendingAuthHandler.instance.reAuthCancelled() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.ReAuthCancelled) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.reAuthCancelled() + } } private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo { val currentSessionCrossSigningInfo = mockk() + every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo) return currentSessionCrossSigningInfo } @@ -247,14 +360,19 @@ class DevicesViewModelTest { /** * Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active. */ - private fun givenDeviceFullInfoList(): List { + private fun givenDeviceFullInfoList(deviceId1: String, deviceId2: String): List { val verifiedCryptoDeviceInfo = mockk() every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) val unverifiedCryptoDeviceInfo = mockk() every { unverifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) + val deviceInfo1 = mockk() + every { deviceInfo1.deviceId } returns deviceId1 + val deviceInfo2 = mockk() + every { deviceInfo2.deviceId } returns deviceId2 + val deviceFullInfo1 = DeviceFullInfo( - deviceInfo = mockk(), + deviceInfo = deviceInfo1, cryptoDeviceInfo = verifiedCryptoDeviceInfo, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, isInactive = false, @@ -263,7 +381,7 @@ class DevicesViewModelTest { matrixClientInfo = MatrixClientInfoContent(), ) val deviceFullInfo2 = DeviceFullInfo( - deviceInfo = mockk(), + deviceInfo = deviceInfo2, cryptoDeviceInfo = unverifiedCryptoDeviceInfo, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, isInactive = true, @@ -277,7 +395,15 @@ class DevicesViewModelTest { return deviceFullInfoList } - private fun givenRefreshDevicesOnCryptoDevicesChange() { - coEvery { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } just runs + private fun givenInitialViewState(): DevicesViewState { + val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + return DevicesViewState( + currentSessionCrossSigningInfo = currentSessionCrossSigningInfo, + devices = Success(deviceFullInfoList), + unverifiedSessionsCount = 1, + inactiveSessionsCount = 1, + isLoading = false, + ) } } From a3df90ae3ec70e3e5427b16382855099d3db95b2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:03:13 +0200 Subject: [PATCH 088/215] Adding unit tests about multi signout action for devices view model --- .../devices/v2/DevicesViewModelTest.kt | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 71e9d609b7..7ece9cf877 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -78,7 +78,7 @@ class DevicesViewModelTest { private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private val refreshDevicesUseCase = mockk(relaxUnitFun = true) + private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) private fun createViewModel(): DevicesViewModel { return DevicesViewModel( @@ -92,7 +92,7 @@ class DevicesViewModelTest { signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, - refreshDevicesUseCase = refreshDevicesUseCase, + refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) } @@ -231,12 +231,38 @@ class DevicesViewModelTest { } } + @Test + fun `given no reAuth is needed when handling multiSignout other sessions action then signout process is performed`() { + // Given + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID) + // signout all devices except the current device + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1), fakeInterceptSignoutFlowResponseUseCase) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCase.execute() + } + } + @Test fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { // Given val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = givenInitialViewState() + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) // When @@ -260,7 +286,7 @@ class DevicesViewModelTest { // Given val error = Exception() fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) - val expectedViewState = givenInitialViewState() + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When @@ -395,9 +421,9 @@ class DevicesViewModelTest { return deviceFullInfoList } - private fun givenInitialViewState(): DevicesViewState { + private fun givenInitialViewState(deviceId1: String, deviceId2: String): DevicesViewState { val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() - val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1, deviceId2) return DevicesViewState( currentSessionCrossSigningInfo = currentSessionCrossSigningInfo, devices = Success(deviceFullInfoList), From e0d511a4880fb0a4c4aa305b02b60782dea2af99 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:05:01 +0200 Subject: [PATCH 089/215] Fixing a name of a mocked component --- .../v2/othersessions/OtherSessionsViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 28b97ed70d..f282e5ca82 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -71,7 +71,7 @@ class OtherSessionsViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() - private val fakeRefreshDevicesUseCaseUseCase = mockk(relaxed = true) + private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() @@ -85,7 +85,7 @@ class OtherSessionsViewModelTest { signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, - refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, + refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) @Before @@ -352,7 +352,7 @@ class OtherSessionsViewModelTest { .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } .finish() verify { - fakeRefreshDevicesUseCaseUseCase.execute() + fakeRefreshDevicesUseCase.execute() } } @@ -388,7 +388,7 @@ class OtherSessionsViewModelTest { .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } .finish() verify { - fakeRefreshDevicesUseCaseUseCase.execute() + fakeRefreshDevicesUseCase.execute() } } From 4b0b335a687e8ea9abe0ce5b2ae219020c03a39d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:06:32 +0200 Subject: [PATCH 090/215] Fixing code quality issues --- .../vector/app/features/settings/devices/v2/DevicesViewEvent.kt | 2 -- .../settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt index 770ffc2513..9f5257693e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt @@ -17,10 +17,8 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewEvents -import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewEvents import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo sealed class DevicesViewEvent : VectorViewEvents { data class RequestReAuth( diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt index 208ce8b334..08a9fa625b 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -80,4 +80,3 @@ class SignoutSessionsUseCaseTest { private fun givenAuthInterceptor() = mockk() } - From 76e2b6b39f5300944107e62ac3eb5cf27fe54005 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:09:51 +0200 Subject: [PATCH 091/215] Removing some TODOs --- .../matrix/android/sdk/internal/crypto/DefaultCryptoService.kt | 2 -- .../android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 032d649421..7862da1c17 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -241,12 +241,10 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } - // TODO add unit test override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback) } - // TODO add unit test override fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index fc6bc9b1bc..12b3fbd624 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -37,7 +37,6 @@ internal interface DeleteDeviceTask : Task { ) } -// TODO add unit tests internal class DefaultDeleteDeviceTask @Inject constructor( private val cryptoApi: CryptoApi, private val globalErrorReceiver: GlobalErrorReceiver From db42d1c01cd2b98c0027cc8c315a4d7f1bf0bad3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 12:31:57 +0200 Subject: [PATCH 092/215] Fix post rebase unit tests --- .../OtherSessionsViewModelTest.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index f282e5ca82..f899e3c657 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -324,8 +324,8 @@ class OtherSessionsViewModelTest { fun `given no reAuth is needed and in selectMode when handling multiSignout action then signout process is performed`() { // Given val isSelectModeEnabled = true - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout only selected devices @@ -360,8 +360,8 @@ class OtherSessionsViewModelTest { fun `given no reAuth is needed and NOT in selectMode when handling multiSignout action then signout process is performed`() { // Given val isSelectModeEnabled = false - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout all devices @@ -395,8 +395,8 @@ class OtherSessionsViewModelTest { @Test fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) @@ -427,8 +427,8 @@ class OtherSessionsViewModelTest { @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val error = Exception() @@ -459,8 +459,8 @@ class OtherSessionsViewModelTest { @Test fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) From ef5aaf752554385428a0b9f0c88e52fd19a1a6dc Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:10:31 +0200 Subject: [PATCH 093/215] Fix forbidden usage of AlertDialog --- .../BuildConfirmSignoutDialogUseCase.kt | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt index 9959bd1828..4edfc2febe 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt @@ -17,21 +17,19 @@ package im.vector.app.features.settings.devices.v2.signout import android.content.Context -import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import javax.inject.Inject class BuildConfirmSignoutDialogUseCase @Inject constructor() { - fun execute(context: Context, onConfirm: () -> Unit): AlertDialog { - return MaterialAlertDialogBuilder(context) - .setTitle(R.string.action_sign_out) - .setMessage(R.string.action_sign_out_confirmation_simple) - .setPositiveButton(R.string.action_sign_out) { _, _ -> - onConfirm() - } - .setNegativeButton(R.string.action_cancel, null) - .create() - } + fun execute(context: Context, onConfirm: () -> Unit) = + MaterialAlertDialogBuilder(context) + .setTitle(R.string.action_sign_out) + .setMessage(R.string.action_sign_out_confirmation_simple) + .setPositiveButton(R.string.action_sign_out) { _, _ -> + onConfirm() + } + .setNegativeButton(R.string.action_cancel, null) + .create() } From d2d9da3ef73584e9367ad443dd20851b52e6ed1c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:17:37 +0200 Subject: [PATCH 094/215] Exclude the current session from other sessions and security recommendation screens --- .../settings/devices/v2/VectorSettingsDevicesFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 98c7016d29..3a3c3463fb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -173,7 +173,7 @@ class VectorSettingsDevicesFragment : requireActivity(), R.string.device_manager_header_section_security_recommendations_title, DeviceManagerFilterType.UNVERIFIED, - excludeCurrentDevice = false + excludeCurrentDevice = true ) } } @@ -183,7 +183,7 @@ class VectorSettingsDevicesFragment : requireActivity(), R.string.device_manager_header_section_security_recommendations_title, DeviceManagerFilterType.INACTIVE, - excludeCurrentDevice = false + excludeCurrentDevice = true ) } } From 3c7ba85c2604f886def970c5aa6f13656cabe7b1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 16:03:22 +0200 Subject: [PATCH 095/215] Removing unused string --- library/ui-strings/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index e772748a41..cd7cb3f477 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3345,7 +3345,6 @@ No inactive sessions found. Clear Filter Select sessions - Sign out of these sessions Sign out Sign out of %1$d session From 5515cd379f5f0a76f3efa8e0ccdaeeb864dd01f1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 16:04:51 +0200 Subject: [PATCH 096/215] Use SHOW_AS_ACTION_IF_ROOM tag --- .../settings/devices/v2/othersessions/OtherSessionsFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index d2bb1d443b..487531646a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -102,7 +102,7 @@ class OtherSessionsFragment : } else { viewState.devices.invoke()?.isNotEmpty().orFalse() } - val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) changeTextColorOfDestructiveAction(multiSignoutItem) } From 1d2b8e76a289fcbc3996d16379136521566221e1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 7 Nov 2022 11:13:23 +0100 Subject: [PATCH 097/215] Adding min size annotation to task params --- .../android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 12b3fbd624..549122447e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.tasks +import androidx.annotation.Size import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.uia.UiaResult @@ -31,7 +32,7 @@ import javax.inject.Inject internal interface DeleteDeviceTask : Task { data class Params( - val deviceIds: List, + @Size(min = 1) val deviceIds: List, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?, val userAuthParam: UIABaseAuth? ) From 45050e821648b83e62a549abb182ce4ac981429f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 7 Nov 2022 11:34:04 +0100 Subject: [PATCH 098/215] Removing error formatting from ViewModel --- .../settings/devices/v2/DevicesViewModel.kt | 12 +---- .../othersessions/OtherSessionsViewModel.kt | 12 +---- .../v2/overview/SessionOverviewViewModel.kt | 12 +---- .../devices/v2/DevicesViewModelTest.kt | 35 +------------- .../OtherSessionsViewModelTest.kt | 43 +---------------- .../overview/SessionOverviewViewModelTest.kt | 46 +------------------ 6 files changed, 6 insertions(+), 154 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index fe4d0dc838..c714645b9a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -21,11 +21,9 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase @@ -40,16 +38,13 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, activeSessionHolder: ActiveSessionHolder, - private val stringProvider: StringProvider, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, @@ -195,12 +190,7 @@ class DevicesViewModel @AssistedInject constructor( private fun onSignoutFailure(failure: Throwable) { Timber.e("signout failure", failure) - val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { - stringProvider.getString(R.string.authentication_error) - } else { - stringProvider.getString(R.string.matrix_error) - } - _viewEvents.post(DevicesViewEvent.SignoutError(Exception(failureMessage))) + _viewEvents.post(DevicesViewEvent.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index a26187b797..c33490400b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -21,11 +21,9 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase @@ -39,16 +37,13 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, - private val stringProvider: StringProvider, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, @@ -223,12 +218,7 @@ class OtherSessionsViewModel @AssistedInject constructor( private fun onSignoutFailure(failure: Throwable) { Timber.e("signout failure", failure) - val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { - stringProvider.getString(R.string.authentication_error) - } else { - stringProvider.getString(R.string.matrix_error) - } - _viewEvents.post(OtherSessionsViewEvents.SignoutError(Exception(failureMessage))) + _viewEvents.post(OtherSessionsViewEvents.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index e6aa7c2747..59eeaaadb4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -21,11 +21,9 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel @@ -44,16 +42,13 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation class SessionOverviewViewModel @AssistedInject constructor( @Assisted val initialState: SessionOverviewViewState, - private val stringProvider: StringProvider, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val signoutSessionUseCase: SignoutSessionUseCase, @@ -196,12 +191,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun onSignoutFailure(failure: Throwable) { Timber.e("signout failure", failure) - val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { - stringProvider.getString(R.string.authentication_error) - } else { - stringProvider.getString(R.string.matrix_error) - } - _viewEvents.post(SessionOverviewViewEvent.SignoutError(Exception(failureMessage))) + _viewEvents.post(SessionOverviewViewEvent.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 7ece9cf877..852fc64fd5 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -19,7 +19,6 @@ package im.vector.app.features.settings.devices.v2 import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType @@ -30,7 +29,6 @@ import im.vector.app.features.settings.devices.v2.verification.GetCurrentSession import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase -import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test import im.vector.app.test.testDispatcher @@ -49,20 +47,16 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import javax.net.ssl.HttpsURLConnection private const val A_CURRENT_DEVICE_ID = "current-device-id" private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" private const val A_PASSWORD = "password" -private const val AUTH_ERROR_MESSAGE = "auth-error-message" -private const val AN_ERROR_MESSAGE = "error-message" class DevicesViewModelTest { @@ -70,7 +64,6 @@ class DevicesViewModelTest { val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true) @@ -84,7 +77,6 @@ class DevicesViewModelTest { return DevicesViewModel( initialState = DevicesViewState(), activeSessionHolder = fakeActiveSessionHolder.instance, - stringProvider = fakeStringProvider.instance, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, @@ -257,37 +249,12 @@ class DevicesViewModelTest { } } - @Test - fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { - // Given - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) - fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) - - // When - val viewModel = createViewModel() - val viewModelTest = viewModel.test() - viewModel.handle(DevicesAction.MultiSignoutOtherSessions) - - // Then - viewModelTest - .assertStatesChanges( - expectedViewState, - { copy(isLoading = true) }, - { copy(isLoading = false) } - ) - .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } - .finish() - } - @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given val error = Exception() fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -301,7 +268,7 @@ class DevicesViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is DevicesViewEvent.SignoutError && it.error == error } .finish() } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index f899e3c657..e01d6e058c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -19,7 +19,6 @@ package im.vector.app.features.settings.devices.v2.othersessions import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase @@ -28,7 +27,6 @@ import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowRe import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase -import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test @@ -46,16 +44,12 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import javax.net.ssl.HttpsURLConnection private const val A_TITLE_RES_ID = 1 private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" private const val A_PASSWORD = "password" -private const val AUTH_ERROR_MESSAGE = "auth-error-message" -private const val AN_ERROR_MESSAGE = "error-message" class OtherSessionsViewModelTest { @@ -69,7 +63,6 @@ class OtherSessionsViewModelTest { ) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() @@ -79,7 +72,6 @@ class OtherSessionsViewModelTest { private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( initialState = viewState, - stringProvider = fakeStringProvider.instance, activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, @@ -392,38 +384,6 @@ class OtherSessionsViewModelTest { } } - @Test - fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { - // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) - val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) - givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = OtherSessionsViewState( - devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), - currentFilter = defaultArgs.defaultFilter, - excludeCurrentDevice = defaultArgs.excludeCurrentDevice, - ) - fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) - - // When - val viewModel = createViewModel() - val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.MultiSignout) - - // Then - viewModelTest - .assertStatesChanges( - expectedViewState, - { copy(isLoading = true) }, - { copy(isLoading = false) } - ) - .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } - .finish() - } - @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given @@ -438,7 +398,6 @@ class OtherSessionsViewModelTest { currentFilter = defaultArgs.defaultFilter, excludeCurrentDevice = defaultArgs.excludeCurrentDevice, ) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -452,7 +411,7 @@ class OtherSessionsViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error == error } .finish() } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 289279b8f6..b2ab939bd1 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -20,7 +20,6 @@ import android.os.SystemClock import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase @@ -30,7 +29,6 @@ import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSes import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionUseCase -import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -51,15 +49,11 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import javax.net.ssl.HttpsURLConnection private const val A_SESSION_ID_1 = "session-id-1" private const val A_SESSION_ID_2 = "session-id-2" -private const val AUTH_ERROR_MESSAGE = "auth-error-message" -private const val AN_ERROR_MESSAGE = "error-message" private const val A_PASSWORD = "password" class SessionOverviewViewModelTest { @@ -75,7 +69,6 @@ class SessionOverviewViewModelTest { ) private val getDeviceFullInfoUseCase = mockk(relaxed = true) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() private val interceptSignoutFlowResponseUseCase = mockk() @@ -87,7 +80,6 @@ class SessionOverviewViewModelTest { private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), - stringProvider = fakeStringProvider.instance, getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, signoutSessionUseCase = fakeSignoutSessionUseCase.instance, @@ -286,41 +278,6 @@ class SessionOverviewViewModelTest { } } - @Test - fun `given another session and server error during signout when handling signout action then signout process is performed`() { - // Given - val deviceFullInfo = mockk() - every { deviceFullInfo.isCurrentDevice } returns false - every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, serverError) - val signoutAction = SessionOverviewAction.SignoutOtherSession - givenCurrentSessionIsTrusted() - val expectedViewState = SessionOverviewViewState( - deviceId = A_SESSION_ID_1, - isCurrentSessionTrusted = true, - deviceInfo = Success(deviceFullInfo), - isLoading = false, - notificationsStatus = notificationsStatus, - ) - fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) - - // When - val viewModel = createViewModel() - val viewModelTest = viewModel.test() - viewModel.handle(signoutAction) - - // Then - viewModelTest - .assertStatesChanges( - expectedViewState, - { copy(isLoading = true) }, - { copy(isLoading = false) } - ) - .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } - .finish() - } - @Test fun `given another session and unexpected error during signout when handling signout action then signout process is performed`() { // Given @@ -338,7 +295,6 @@ class SessionOverviewViewModelTest { isLoading = false, notificationsStatus = notificationsStatus, ) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -352,7 +308,7 @@ class SessionOverviewViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error == error } .finish() } From f0340d5cedf0be689885280a924b8718dd44b421 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 7 Nov 2022 11:58:34 +0100 Subject: [PATCH 099/215] When joining a room, the message composer should be visible once the room loads (#7510) --- changelog.d/7509.bugfix | 1 + .../vector/app/features/home/room/detail/TimelineFragment.kt | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/7509.bugfix diff --git a/changelog.d/7509.bugfix b/changelog.d/7509.bugfix new file mode 100644 index 0000000000..93ec812e0e --- /dev/null +++ b/changelog.d/7509.bugfix @@ -0,0 +1 @@ +When joining a room, the message composer is displayed once the room is loaded. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 120e5e22cb..60dd1320d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1169,6 +1169,9 @@ class TimelineFragment : lazyLoadedViews.inviteView(false)?.isVisible = false if (mainState.tombstoneEvent == null) { + views.composerContainer.isInvisible = !messageComposerState.isComposerVisible + views.voiceMessageRecorderContainer.isVisible = messageComposerState.isVoiceMessageRecorderVisible + when (messageComposerState.canSendMessage) { CanSendStatus.Allowed -> { NotificationAreaView.State.Hidden @@ -1224,6 +1227,7 @@ class TimelineFragment : private fun FragmentTimelineBinding.hideComposerViews() { composerContainer.isVisible = false + voiceMessageRecorderContainer.isVisible = false } private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) { From 44c0378de8b59a69a7d585bfb83cda8418273205 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 7 Nov 2022 14:46:32 +0300 Subject: [PATCH 100/215] Fix description of verified sessions. --- library/ui-strings/src/main/res/values/strings.xml | 2 ++ .../devices/v2/othersessions/OtherSessionsFragment.kt | 5 ++++- .../settings/devices/v2/overview/SessionOverviewFragment.kt | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 450eb64849..370005363d 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3373,7 +3373,9 @@ Unverified sessions Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account. Verified sessions + Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. + Verified sessions are anywhere you are using ${app_name} after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. Renaming sessions Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. Enable new session manager diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 4f1c8353f5..c5c7ab634c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -196,7 +196,10 @@ class OtherSessionsFragment : ) ) views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found) - updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_verified_title, R.string.device_manager_learn_more_sessions_verified) + updateSecurityLearnMoreButton( + R.string.device_manager_learn_more_sessions_verified_title, + R.string.device_manager_learn_more_sessions_verified_description + ) } DeviceManagerFilterType.UNVERIFIED -> { views.otherSessionsSecurityRecommendationView.render( diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index 620372f810..c1d332fd23 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -284,7 +284,7 @@ class SessionOverviewFragment : R.string.device_manager_verification_status_unverified } val descriptionResId = if (isVerified) { - R.string.device_manager_learn_more_sessions_verified + R.string.device_manager_learn_more_sessions_verified_description } else { R.string.device_manager_learn_more_sessions_unverified } From e30cbd5916c25916da438dd2afed5aff68f1edb9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 7 Nov 2022 14:51:18 +0300 Subject: [PATCH 101/215] Add changelog. --- changelog.d/7533.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7533.bugfix diff --git a/changelog.d/7533.bugfix b/changelog.d/7533.bugfix new file mode 100644 index 0000000000..5e603ece22 --- /dev/null +++ b/changelog.d/7533.bugfix @@ -0,0 +1 @@ +Fix description of verified sessions From d89ef6988b6e894040da5e447cb781903b565767 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 11:35:50 +0100 Subject: [PATCH 102/215] Improve player seek --- .../MessageVoiceBroadcastListeningItem.kt | 7 +- .../VoiceBroadcastExtensions.kt | 5 + .../voicebroadcast/VoiceBroadcastHelper.kt | 4 +- .../listening/VoiceBroadcastPlayer.kt | 4 +- .../listening/VoiceBroadcastPlayerImpl.kt | 228 +++++++++--------- .../GetLiveVoiceBroadcastChunksUseCase.kt | 5 +- .../usecase/GetVoiceBroadcastEventUseCase.kt | 35 ++- 7 files changed, 157 insertions(+), 131 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 5e3cc6fba8..19caf3d8ba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -71,18 +71,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) } - seekBar.isEnabled = true } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } - seekBar.isEnabled = false - } - VoiceBroadcastPlayer.State.BUFFERING -> { - seekBar.isEnabled = true } + VoiceBroadcastPlayer.State.BUFFERING -> Unit } } } @@ -112,6 +108,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } is AudioMessagePlaybackTracker.Listener.State.Playing -> { if (!isUserSeeking) { +// Timber.d("Voice Broadcast | AudioMessagePlaybackTracker.Listener.onUpdate - duration: $duration, playbackTime: ${state.playbackTime}") holder.seekBar.progress = state.playbackTime } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index 48554f51d0..a1328c0ba3 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -17,6 +17,8 @@ package im.vector.app.features.voicebroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -34,3 +36,6 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? { val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 + +val VoiceBroadcastEvent.isLive + get() = content?.voiceBroadcastState != null && content?.voiceBroadcastState != VoiceBroadcastState.STOPPED diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 6839056520..3661928fa5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -49,8 +49,6 @@ class VoiceBroadcastHelper @Inject constructor( fun stopPlayback() = voiceBroadcastPlayer.stop() fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { - if (voiceBroadcastPlayer.currentVoiceBroadcast == voiceBroadcast) { - voiceBroadcastPlayer.seekTo(positionMillis) - } + voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis) } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index b4806ba57d..36e75236ad 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -46,9 +46,9 @@ interface VoiceBroadcastPlayer { fun stop() /** - * Seek to the given playback position, is milliseconds. + * Seek the given voice broadcast playback to the given position, is milliseconds. */ - fun seekTo(positionMillis: Int) + fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) /** * Add a [Listener] to the given voice broadcast. diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index fc983e4112..773883d81a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -18,16 +18,18 @@ package im.vector.app.features.voicebroadcast.listening import android.media.AudioAttributes import android.media.MediaPlayer +import android.media.MediaPlayer.OnPreparedListener import androidx.annotation.MainThread import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voicebroadcast.duration +import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.model.VoiceBroadcast -import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer @@ -38,7 +40,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent @@ -47,6 +48,7 @@ import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.absoluteValue @Singleton class VoiceBroadcastPlayerImpl @Inject constructor( @@ -60,19 +62,20 @@ class VoiceBroadcastPlayerImpl @Inject constructor( get() = sessionHolder.getActiveSession() private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private var voiceBroadcastStateJob: Job? = null + private var fetchPlaylistTask: Job? = null + private var voiceBroadcastStateTask: Job? = null private val mediaPlayerListener = MediaPlayerListener() private val playbackTicker = PlaybackTicker() private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - private var currentSequence: Int? = null - private var fetchPlaylistJob: Job? = null private var playlist = emptyList() - - private var isLive: Boolean = false + private var currentSequence: Int? = null + private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null + private val isLive get() = currentVoiceBroadcastEvent?.isLive.orFalse() + private val lastSequence get() = currentVoiceBroadcastEvent?.content?.lastChunkSequence override var currentVoiceBroadcast: VoiceBroadcast? = null @@ -81,33 +84,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( set(value) { Timber.w("## VoiceBroadcastPlayer state: $field -> $value") field = value - // Notify state change to all the listeners attached to the current voice broadcast id - currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> - when (value) { - State.PLAYING -> { - playbackTracker.startPlayback(voiceBroadcastId) - playbackTicker.startPlaybackTicker(voiceBroadcastId) - } - State.PAUSED -> { - playbackTracker.pausePlayback(voiceBroadcastId) - playbackTicker.stopPlaybackTicker() - } - State.BUFFERING -> { - playbackTracker.pausePlayback(voiceBroadcastId) - playbackTicker.stopPlaybackTicker() - } - State.IDLE -> { - playbackTracker.stopPlayback(voiceBroadcastId) - playbackTicker.stopPlaybackTicker() - } - } - listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } - } + onPlayingStateChanged(value) } - /** - * Map voiceBroadcastId to listeners. - */ + /** Map voiceBroadcastId to listeners.*/ private val listeners: MutableMap> = mutableMapOf() override fun playOrResume(voiceBroadcast: VoiceBroadcast) { @@ -120,38 +100,28 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } override fun pause() { - playingState = State.PAUSED currentMediaPlayer?.pause() + playingState = State.PAUSED } override fun stop() { // Update state playingState = State.IDLE - // Stop playback - currentMediaPlayer?.stop() - isLive = false + // Stop and release media players + stopPlayer() - // Release current player - release(currentMediaPlayer) - currentMediaPlayer = null - - // Release next player - release(nextMediaPlayer) - nextMediaPlayer = null - - // Do not observe anymore voice broadcast state changes - voiceBroadcastStateJob?.cancel() - voiceBroadcastStateJob = null - - // Do not fetch the playlist anymore - fetchPlaylistJob?.cancel() - fetchPlaylistJob = null + // Do not observe anymore voice broadcast changes + fetchPlaylistTask?.cancel() + fetchPlaylistTask = null + voiceBroadcastStateTask?.cancel() + voiceBroadcastStateTask = null // Clear playlist playlist = emptyList() currentSequence = null + currentVoiceBroadcastEvent = null currentVoiceBroadcast = null } @@ -174,13 +144,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.BUFFERING - val voiceBroadcastState = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)?.content?.voiceBroadcastState - isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED + observeVoiceBroadcastLiveState(voiceBroadcast) fetchPlaylistAndStartPlayback(voiceBroadcast) } + private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { + voiceBroadcastStateTask = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) + .onEach { currentVoiceBroadcastEvent = it.getOrNull() } + .launchIn(coroutineScope) + } + private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { - fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) + fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) .onEach(this::updatePlaylist) .launchIn(coroutineScope) } @@ -204,40 +179,51 @@ class VoiceBroadcastPlayerImpl @Inject constructor( when (playingState) { State.PLAYING -> { if (nextMediaPlayer == null) { - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + prepareNextMediaPlayer() } } State.PAUSED -> { if (nextMediaPlayer == null) { - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + prepareNextMediaPlayer() } } State.BUFFERING -> { val newMediaContent = getNextAudioContent() - if (newMediaContent != null) startPlayback() + if (newMediaContent != null) { + val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } + startPlayback(savedPosition) + } + } + State.IDLE -> { + val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } + startPlayback(savedPosition) } - State.IDLE -> startPlayback() } } - private fun startPlayback(sequence: Int? = null, position: Int = 0) { + private fun startPlayback(position: Int? = null) { + stopPlayer() + val playlistItem = when { - sequence != null -> playlist.find { it.audioEvent.sequence == sequence } + position != null -> playlist.lastOrNull { it.startTime <= position } isLive -> playlist.lastOrNull() else -> playlist.firstOrNull() } val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val computedSequence = playlistItem.audioEvent.sequence + val sequence = playlistItem.audioEvent.sequence + val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 coroutineScope.launch { try { - currentMediaPlayer = prepareMediaPlayer(content) - currentMediaPlayer?.start() - if (position > 0) { - currentMediaPlayer?.seekTo(position) + prepareMediaPlayer(content) { mp -> + currentMediaPlayer = mp + currentSequence = sequence + mp.start() + if (sequencePosition > 0) { + mp.seekTo(sequencePosition) + } + playingState = State.PLAYING + prepareNextMediaPlayer() } - currentSequence = computedSequence - withContext(Dispatchers.Main) { playingState = State.PLAYING } - nextMediaPlayer = prepareNextMediaPlayer() } catch (failure: Throwable) { Timber.e(failure, "Unable to start playback") throw VoiceFailure.UnableToPlay(failure) @@ -250,20 +236,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.PLAYING } - override fun seekTo(positionMillis: Int) { - val duration = getVoiceBroadcastDuration() - val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return - val audioEvent = playlistItem.audioEvent - val eventPosition = positionMillis - playlistItem.startTime - - Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${audioEvent.sequence}, sequencePosition=$eventPosition") - - tryOrNull { currentMediaPlayer?.stop() } - release(currentMediaPlayer) - tryOrNull { nextMediaPlayer?.stop() } - release(nextMediaPlayer) - - startPlayback(audioEvent.sequence, eventPosition) + override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { + if (voiceBroadcast != currentVoiceBroadcast) { + playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, 0f) + } else { + startPlayback(positionMillis) + } } private fun getNextAudioContent(): MessageAudioContent? { @@ -273,12 +251,24 @@ class VoiceBroadcastPlayerImpl @Inject constructor( return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content } - private suspend fun prepareNextMediaPlayer(): MediaPlayer? { - val nextContent = getNextAudioContent() ?: return null - return prepareMediaPlayer(nextContent) + private fun prepareNextMediaPlayer() { + nextMediaPlayer = null + val nextContent = getNextAudioContent() + if (nextContent != null) { + coroutineScope.launch { + prepareMediaPlayer(nextContent) { mp -> + if (nextMediaPlayer == null) { + nextMediaPlayer = mp + currentMediaPlayer?.setNextMediaPlayer(mp) + } else { + mp.release() + } + } + } + } } - private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent): MediaPlayer { + private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent, onPreparedListener: OnPreparedListener): MediaPlayer { // Download can fail val audioFile = try { session.fileService().downloadFile(messageAudioContent) @@ -299,57 +289,55 @@ class VoiceBroadcastPlayerImpl @Inject constructor( setDataSource(fis.fd) setOnInfoListener(mediaPlayerListener) setOnErrorListener(mediaPlayerListener) + setOnPreparedListener(onPreparedListener) setOnCompletionListener(mediaPlayerListener) prepare() } } } - private fun release(mp: MediaPlayer?) { - mp?.apply { - release() - setOnInfoListener(null) - setOnCompletionListener(null) - setOnErrorListener(null) + private fun stopPlayer() { + tryOrNull { currentMediaPlayer?.stop() } + currentMediaPlayer?.release() + currentMediaPlayer = null + + nextMediaPlayer?.release() + nextMediaPlayer = null + } + + private fun onPlayingStateChanged(playingState: State) { + // Notify state change to all the listeners attached to the current voice broadcast id + currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> + when (playingState) { + State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId) + State.PAUSED, + State.BUFFERING, + State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) + } + listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) } } } private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, - MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { - release(currentMediaPlayer) - currentMediaPlayer = mp currentSequence = currentSequence?.plus(1) - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + currentMediaPlayer = mp + prepareNextMediaPlayer() } } return false } - override fun onPrepared(mp: MediaPlayer) { - when (mp) { - currentMediaPlayer -> { - nextMediaPlayer?.let { mp.setNextMediaPlayer(it) } - } - nextMediaPlayer -> { - tryOrNull { currentMediaPlayer?.setNextMediaPlayer(mp) } - } - } - } - override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return - val voiceBroadcast = currentVoiceBroadcast ?: return - val voiceBroadcastEventContent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)?.content ?: return - isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED - if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) { + if (!isLive && lastSequence == currentSequence) { // We'll not receive new chunks anymore so we can stop the live listening stop() } else { @@ -388,23 +376,31 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (currentMediaPlayer?.isPlaying.orFalse()) { val itemStartPosition = currentSequence?.let { seq -> playlist.find { it.audioEvent.sequence == seq } }?.startTime val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) + Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") if (currentVoiceBroadcastPosition != null) { val totalDuration = getVoiceBroadcastDuration() val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) } else { - playbackTracker.stopPlayback(id) - stopPlaybackTicker() + stopPlaybackTicker(id) } } else { - playbackTracker.stopPlayback(id) - stopPlaybackTicker() + stopPlaybackTicker(id) } } - fun stopPlaybackTicker() { + fun stopPlaybackTicker(id: String) { playbackTicker?.stop() playbackTicker = null + + val totalDuration = getVoiceBroadcastDuration() + val playbackTime = playbackTracker.getPlaybackTime(id) + val remainingTime = totalDuration - playbackTime + if (remainingTime < 1000) { + playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) + } else { + playbackTracker.pausePlayback(id) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 2e8fc31870..33e370e9bc 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -29,9 +29,12 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.lastOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningReduce +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent @@ -57,7 +60,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) .mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } } - val voiceBroadcastEvent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) + val voiceBroadcastEvent = runBlocking { getVoiceBroadcastEventUseCase.execute(voiceBroadcast).firstOrNull()?.getOrNull() } val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt index 26ba3209b7..7106322f06 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt @@ -16,12 +16,26 @@ package im.vector.app.features.voicebroadcast.usecase +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap import timber.log.Timber import javax.inject.Inject @@ -29,14 +43,27 @@ class GetVoiceBroadcastEventUseCase @Inject constructor( private val session: Session, ) { - fun execute(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { + fun execute(voiceBroadcast: VoiceBroadcast): Flow> { val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}") Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast") val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent() - val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) - .sortedBy { it.root.originServerTs } - return relatedEvents.mapNotNull { it.root.asVoiceBroadcastEvent() }.lastOrNull() ?: initialEvent + val latestEvent = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) + .mapNotNull { it.root.asVoiceBroadcastEvent() } + .maxByOrNull { it.root.originServerTs ?: 0 } + ?: initialEvent + + return when (latestEvent?.content?.voiceBroadcastState) { + null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional()) + else -> { + room.flow() + .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty())) + .unwrap() + .mapNotNull { it.asVoiceBroadcastEvent() } + .filter { it.reference?.eventId == voiceBroadcast.voiceBroadcastId } + .map { it.toOptional() } + } + } } } From 392fe6fa329d84805f553986b8a338018ee2c095 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 15:47:10 +0100 Subject: [PATCH 103/215] Transform TickListener to fun interface --- .../vector/lib/attachmentviewer/VideoViewHolder.kt | 14 ++++++-------- .../im/vector/lib/core/utils/timer/CountUpTimer.kt | 2 +- .../vector/app/features/call/webrtc/WebRtcCall.kt | 10 ++++------ .../room/detail/composer/AudioMessageHelper.kt | 12 ++---------- .../composer/voice/VoiceMessageRecorderView.kt | 8 +++----- .../location/live/map/LiveLocationUserItem.kt | 6 ++---- .../listening/VoiceBroadcastPlayerImpl.kt | 6 +----- 7 files changed, 19 insertions(+), 39 deletions(-) diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt index 92d28d26c9..07c7b4588f 100644 --- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt +++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt @@ -103,14 +103,12 @@ class VideoViewHolder constructor(itemView: View) : views.videoView.setOnPreparedListener { stopTimer() countUpTimer = CountUpTimer(100).also { - it.tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - val duration = views.videoView.duration - val progress = views.videoView.currentPosition - val isPlaying = views.videoView.isPlaying -// Log.v("FOO", "isPlaying $isPlaying $progress/$duration") - eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) - } + it.tickListener = CountUpTimer.TickListener { + val duration = views.videoView.duration + val progress = views.videoView.currentPosition + val isPlaying = views.videoView.isPlaying + // Log.v("FOO", "isPlaying $isPlaying $progress/$duration") + eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) } it.resume() } diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt index e9d311fe03..a4fd8bb4e1 100644 --- a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt @@ -66,7 +66,7 @@ class CountUpTimer(private val intervalInMs: Long = 1_000) { coroutineScope.cancel() } - interface TickListener { + fun interface TickListener { fun onTick(milliseconds: Long) } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 00b9a76de7..0bf70690ba 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -167,12 +167,10 @@ class WebRtcCall( private var screenSender: RtpSender? = null private val timer = CountUpTimer(1000L).apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) - listeners.forEach { - tryOrNull { it.onTick(formattedDuration) } - } + tickListener = CountUpTimer.TickListener { milliseconds -> + val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) + listeners.forEach { + tryOrNull { it.onTick(formattedDuration) } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index bede02c17f..eddfe500b3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -199,11 +199,7 @@ class AudioMessageHelper @Inject constructor( private fun startRecordingAmplitudes() { amplitudeTicker?.stop() amplitudeTicker = CountUpTimer(50).apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - onAmplitudeTick() - } - } + tickListener = CountUpTimer.TickListener { onAmplitudeTick() } resume() } } @@ -234,11 +230,7 @@ class AudioMessageHelper @Inject constructor( private fun startPlaybackTicker(id: String) { playbackTicker?.stop() playbackTicker = CountUpTimer().apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - onPlaybackTick(id) - } - } + tickListener = CountUpTimer.TickListener { onPlaybackTick(id) } resume() } onPlaybackTick(id) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt index 13e0477ab6..a7b926f29a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt @@ -189,11 +189,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor( val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0) recordingTicker?.stop() recordingTicker = CountUpTimer().apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked - onRecordingTick(isLocked, milliseconds + startMs) - } + tickListener = CountUpTimer.TickListener { milliseconds -> + val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked + onRecordingTick(isLocked, milliseconds + startMs) } resume() } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt index bab7f4c7f9..c108e83e76 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt @@ -79,10 +79,8 @@ abstract class LiveLocationUserItem : VectorEpoxyModel Date: Fri, 4 Nov 2022 15:54:28 +0100 Subject: [PATCH 104/215] VoiceBroadcastPlayerImpl - use session coroutine scope --- .../listening/VoiceBroadcastPlayerImpl.kt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index bf8ff11043..6eb9cbc735 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -22,6 +22,7 @@ import android.media.MediaPlayer.OnPreparedListener import androidx.annotation.MainThread import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker +import im.vector.app.features.session.coroutineScope import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.isLive @@ -33,10 +34,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -48,7 +46,6 @@ import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject import javax.inject.Singleton -import kotlin.math.absoluteValue @Singleton class VoiceBroadcastPlayerImpl @Inject constructor( @@ -58,10 +55,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase ) : VoiceBroadcastPlayer { - private val session - get() = sessionHolder.getActiveSession() + private val session get() = sessionHolder.getActiveSession() + private val sessionScope get() = session.coroutineScope - private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private var fetchPlaylistTask: Job? = null private var voiceBroadcastStateTask: Job? = null @@ -151,13 +147,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { voiceBroadcastStateTask = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) .onEach { currentVoiceBroadcastEvent = it.getOrNull() } - .launchIn(coroutineScope) + .launchIn(sessionScope) } private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) .onEach(this::updatePlaylist) - .launchIn(coroutineScope) + .launchIn(sessionScope) } private fun updatePlaylist(audioEvents: List) { @@ -212,7 +208,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } val sequence = playlistItem.audioEvent.sequence val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 - coroutineScope.launch { + sessionScope.launch { try { prepareMediaPlayer(content) { mp -> currentMediaPlayer = mp @@ -255,7 +251,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( nextMediaPlayer = null val nextContent = getNextAudioContent() if (nextContent != null) { - coroutineScope.launch { + sessionScope.launch { prepareMediaPlayer(nextContent) { mp -> if (nextMediaPlayer == null) { nextMediaPlayer = mp From c85b159952accaff1b7d65f027ced44fe790f0e4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 17:12:02 +0100 Subject: [PATCH 105/215] VoiceBroadcastPlayer - Extract some code to VoiceBroadcastPlaylist --- .../VoiceBroadcastExtensions.kt | 7 +- .../listening/VoiceBroadcastPlayerImpl.kt | 60 +++++------------ .../listening/VoiceBroadcastPlaylist.kt | 67 +++++++++++++++++++ 3 files changed, 91 insertions(+), 43 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index a1328c0ba3..fa8033a211 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -16,9 +16,11 @@ package im.vector.app.features.voicebroadcast +import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -38,4 +40,7 @@ val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 val VoiceBroadcastEvent.isLive - get() = content?.voiceBroadcastState != null && content?.voiceBroadcastState != VoiceBroadcastState.STOPPED + get() = content?.isLive.orFalse() + +val MessageVoiceBroadcastInfoContent.isLive + get() = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 6eb9cbc735..de4f965a59 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -24,7 +24,6 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.session.coroutineScope import im.vector.app.features.voice.VoiceFailure -import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State @@ -41,7 +40,6 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent -import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -67,11 +65,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - private var playlist = emptyList() - private var currentSequence: Int? = null + private val playlist = VoiceBroadcastPlaylist() private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null - private val isLive get() = currentVoiceBroadcastEvent?.isLive.orFalse() - private val lastSequence get() = currentVoiceBroadcastEvent?.content?.lastChunkSequence override var currentVoiceBroadcast: VoiceBroadcast? = null @@ -114,8 +109,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( voiceBroadcastStateTask = null // Clear playlist - playlist = emptyList() - currentSequence = null + playlist.reset() currentVoiceBroadcastEvent = null currentVoiceBroadcast = null @@ -152,25 +146,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) - .onEach(this::updatePlaylist) + .onEach { + playlist.setItems(it) + onPlaylistUpdated() + } .launchIn(sessionScope) } - private fun updatePlaylist(audioEvents: List) { - val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs } - val chunkPositions = sorted - .map { it.duration } - .runningFold(0) { acc, i -> acc + i } - .dropLast(1) - playlist = sorted.mapIndexed { index, messageAudioEvent -> - PlaylistItem( - audioEvent = messageAudioEvent, - startTime = chunkPositions.getOrNull(index) ?: 0 - ) - } - onPlaylistUpdated() - } - private fun onPlaylistUpdated() { when (playingState) { State.PLAYING -> { @@ -201,18 +183,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor( stopPlayer() val playlistItem = when { - position != null -> playlist.lastOrNull { it.startTime <= position } - isLive -> playlist.lastOrNull() + position != null -> playlist.findByPosition(position) + currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull() else -> playlist.firstOrNull() } val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = playlistItem.audioEvent.sequence + val sequence = playlistItem.audioEvent.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return } val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 sessionScope.launch { try { prepareMediaPlayer(content) { mp -> currentMediaPlayer = mp - currentSequence = sequence + playlist.currentSequence = sequence mp.start() if (sequencePosition > 0) { mp.seekTo(sequencePosition) @@ -241,10 +223,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun getNextAudioContent(): MessageAudioContent? { - val nextSequence = currentSequence?.plus(1) - ?: playlist.lastOrNull()?.audioEvent?.sequence - ?: 1 - return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content + return playlist.getNextItem()?.audioEvent?.content } private fun prepareNextMediaPlayer() { @@ -322,7 +301,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { - currentSequence = currentSequence?.plus(1) + playlist.currentSequence++ currentMediaPlayer = mp prepareNextMediaPlayer() } @@ -333,7 +312,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return - if (!isLive && lastSequence == currentSequence) { + val content = currentVoiceBroadcastEvent?.content + val isLive = content?.isLive.orFalse() + if (!isLive && content?.lastChunkSequence == playlist.currentSequence) { // We'll not receive new chunks anymore so we can stop the live listening stop() } else { @@ -347,10 +328,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 - - private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) - private inner class PlaybackTicker( private var playbackTicker: CountUpTimer? = null, ) { @@ -366,12 +343,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onPlaybackTick(id: String) { if (currentMediaPlayer?.isPlaying.orFalse()) { - val itemStartPosition = currentSequence?.let { seq -> playlist.find { it.audioEvent.sequence == seq } }?.startTime + val itemStartPosition = playlist.currentItem?.startTime val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") if (currentVoiceBroadcastPosition != null) { - val totalDuration = getVoiceBroadcastDuration() - val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration + val percentage = currentVoiceBroadcastPosition.toFloat() / playlist.duration playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) } else { stopPlaybackTicker(id) @@ -385,7 +361,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTicker?.stop() playbackTicker = null - val totalDuration = getVoiceBroadcastDuration() + val totalDuration = playlist.duration val playbackTime = playbackTracker.getPlaybackTime(id) val remainingTime = totalDuration - playbackTime if (remainingTime < 1000) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt new file mode 100644 index 0000000000..5cd6efc28a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 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.voicebroadcast.listening + +import im.vector.app.features.voicebroadcast.duration +import im.vector.app.features.voicebroadcast.sequence +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent + +class VoiceBroadcastPlaylist( + private val items: MutableList = mutableListOf(), +) : List by items { + + var currentSequence = 1 + val currentItem get() = findBySequence(currentSequence) + + val duration + get() = items.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 + + fun setItems(audioEvents: List) { + items.clear() + val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs } + val chunkPositions = sorted + .map { it.duration } + .runningFold(0) { acc, i -> acc + i } + .dropLast(1) + val newItems = sorted.mapIndexed { index, messageAudioEvent -> + PlaylistItem( + audioEvent = messageAudioEvent, + startTime = chunkPositions.getOrNull(index) ?: 0 + ) + } + items.addAll(newItems) + } + + fun reset() { + currentSequence = 1 + items.clear() + } + + fun findByPosition(positionMillis: Int): PlaylistItem? { + return items.lastOrNull { it.startTime <= positionMillis } + } + + fun findBySequence(sequenceNumber: Int): PlaylistItem? { + return items.find { it.audioEvent.sequence == sequenceNumber } + } + + fun getNextItem() = findBySequence(currentSequence.plus(1)) + + fun firstOrNull() = findBySequence(1) +} + +data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) From 37c75354bed4a884c03c2ca15a6f22e32d05f572 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:18:26 +0100 Subject: [PATCH 106/215] VoiceBroadcastPlayer - Reorganize some code --- .../listening/VoiceBroadcastPlayerImpl.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index de4f965a59..a38442a19a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -56,16 +56,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private val session get() = sessionHolder.getActiveSession() private val sessionScope get() = session.coroutineScope - private var fetchPlaylistTask: Job? = null - private var voiceBroadcastStateTask: Job? = null - private val mediaPlayerListener = MediaPlayerListener() private val playbackTicker = PlaybackTicker() + private val playlist = VoiceBroadcastPlaylist() + + private var fetchPlaylistTask: Job? = null + private var voiceBroadcastStateObserver: Job? = null private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - private val playlist = VoiceBroadcastPlaylist() private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null override var currentVoiceBroadcast: VoiceBroadcast? = null @@ -105,8 +105,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // Do not observe anymore voice broadcast changes fetchPlaylistTask?.cancel() fetchPlaylistTask = null - voiceBroadcastStateTask?.cancel() - voiceBroadcastStateTask = null + voiceBroadcastStateObserver?.cancel() + voiceBroadcastStateObserver = null // Clear playlist playlist.reset() @@ -139,7 +139,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { - voiceBroadcastStateTask = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) + voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) .onEach { currentVoiceBroadcastEvent = it.getOrNull() } .launchIn(sessionScope) } @@ -345,7 +345,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (currentMediaPlayer?.isPlaying.orFalse()) { val itemStartPosition = playlist.currentItem?.startTime val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) - Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") + Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: ${playlist.currentSequence}, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") if (currentVoiceBroadcastPosition != null) { val percentage = currentVoiceBroadcastPosition.toFloat() / playlist.duration playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) From b87b2cbb63aea881d753759482167e89f0c5ca0c Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:28:26 +0100 Subject: [PATCH 107/215] Remove useless method --- .../listening/VoiceBroadcastPlayerImpl.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index a38442a19a..de01661431 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -166,8 +166,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } State.BUFFERING -> { - val newMediaContent = getNextAudioContent() - if (newMediaContent != null) { + val nextItem = playlist.getNextItem() + if (nextItem != null) { val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } startPlayback(savedPosition) } @@ -222,14 +222,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun getNextAudioContent(): MessageAudioContent? { - return playlist.getNextItem()?.audioEvent?.content - } - private fun prepareNextMediaPlayer() { nextMediaPlayer = null - val nextContent = getNextAudioContent() - if (nextContent != null) { + val nextItem = playlist.getNextItem() + if (nextItem != null) { sessionScope.launch { prepareMediaPlayer(nextContent) { mp -> if (nextMediaPlayer == null) { From a3cd861e15d09ec29ae29f84b3586523647c22e6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:29:01 +0100 Subject: [PATCH 108/215] Add isPreparingNextPlayer flag --- .../listening/VoiceBroadcastPlayerImpl.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index de01661431..adc1912140 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -65,6 +65,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null + private var isPreparingNextPlayer: Boolean = false private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null @@ -156,12 +157,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onPlaylistUpdated() { when (playingState) { State.PLAYING -> { - if (nextMediaPlayer == null) { + if (nextMediaPlayer == null && !isPreparingNextPlayer) { prepareNextMediaPlayer() } } State.PAUSED -> { - if (nextMediaPlayer == null) { + if (nextMediaPlayer == null && !isPreparingNextPlayer) { prepareNextMediaPlayer() } } @@ -223,17 +224,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun prepareNextMediaPlayer() { - nextMediaPlayer = null val nextItem = playlist.getNextItem() if (nextItem != null) { + isPreparingNextPlayer = true sessionScope.launch { - prepareMediaPlayer(nextContent) { mp -> - if (nextMediaPlayer == null) { - nextMediaPlayer = mp - currentMediaPlayer?.setNextMediaPlayer(mp) - } else { - mp.release() - } + prepareMediaPlayer(nextItem.audioEvent.content) { mp -> + nextMediaPlayer = mp + currentMediaPlayer?.setNextMediaPlayer(mp) + isPreparingNextPlayer = false } } } @@ -274,6 +272,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( nextMediaPlayer?.release() nextMediaPlayer = null + isPreparingNextPlayer = false } private fun onPlayingStateChanged(playingState: State) { From a3201555462586c837f883216170668e2ed7840a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:36:37 +0100 Subject: [PATCH 109/215] reset nextMediaPlayer when item has changed --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index adc1912140..da3a9559a4 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -298,6 +298,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { playlist.currentSequence++ currentMediaPlayer = mp + nextMediaPlayer = null prepareNextMediaPlayer() } } From 43a112839f6bcd2bfb17dc3bf36c16f92cd3d58d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:49:10 +0100 Subject: [PATCH 110/215] Fix seek when playlist is not loaded --- .../listening/VoiceBroadcastPlayerImpl.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index da3a9559a4..7b8d8c9547 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -358,12 +358,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTicker = null val totalDuration = playlist.duration - val playbackTime = playbackTracker.getPlaybackTime(id) - val remainingTime = totalDuration - playbackTime - if (remainingTime < 1000) { - playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) - } else { - playbackTracker.pausePlayback(id) + if (totalDuration > 0) { + val playbackTime = playbackTracker.getPlaybackTime(id) + val remainingTime = totalDuration - playbackTime + if (remainingTime < 1000) { + playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) + } else { + playbackTracker.pausePlayback(id) + } } } } From 266236c1e5daee19bdb6e632ab74850a30ed3c7e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:20:22 +0100 Subject: [PATCH 111/215] set playlist.currentSequence null by default --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 2 +- .../voicebroadcast/listening/VoiceBroadcastPlaylist.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 7b8d8c9547..921e0e69ea 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -296,7 +296,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { - playlist.currentSequence++ + playlist.currentSequence = playlist.currentSequence?.inc() currentMediaPlayer = mp nextMediaPlayer = null prepareNextMediaPlayer() diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt index 5cd6efc28a..ff388c2313 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt @@ -24,8 +24,8 @@ class VoiceBroadcastPlaylist( private val items: MutableList = mutableListOf(), ) : List by items { - var currentSequence = 1 - val currentItem get() = findBySequence(currentSequence) + var currentSequence: Int? = null + val currentItem get() = currentSequence?.let { findBySequence(it) } val duration get() = items.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 @@ -47,7 +47,7 @@ class VoiceBroadcastPlaylist( } fun reset() { - currentSequence = 1 + currentSequence = null items.clear() } @@ -59,7 +59,7 @@ class VoiceBroadcastPlaylist( return items.find { it.audioEvent.sequence == sequenceNumber } } - fun getNextItem() = findBySequence(currentSequence.plus(1)) + fun getNextItem() = findBySequence(currentSequence?.plus(1) ?: 1) fun firstOrNull() = findBySequence(1) } From a47e3c1233999b8e5ebbd83974626b86d58488a0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:20:54 +0100 Subject: [PATCH 112/215] Improve playing state updates --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 921e0e69ea..bfbc53fd78 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -74,9 +74,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override var playingState = State.IDLE @MainThread set(value) { - Timber.w("## VoiceBroadcastPlayer state: $field -> $value") - field = value - onPlayingStateChanged(value) + if (field != value) { + Timber.w("## VoiceBroadcastPlayer state: $field -> $value") + field = value + onPlayingStateChanged(value) + } } /** Map voiceBroadcastId to listeners.*/ @@ -299,6 +301,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playlist.currentSequence = playlist.currentSequence?.inc() currentMediaPlayer = mp nextMediaPlayer = null + playingState = State.PLAYING prepareNextMediaPlayer() } } From b2f35fa1352f1b3771d8d3aa9231340560bebdab Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:21:18 +0100 Subject: [PATCH 113/215] Improve PlaybackTicker --- .../helper/AudioMessagePlaybackTracker.kt | 2 +- .../listening/VoiceBroadcastPlayerImpl.kt | 51 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index 6937cd3a46..7e40b92ac8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -127,7 +127,7 @@ class AudioMessagePlaybackTracker @Inject constructor() { } } - private fun getPercentage(id: String): Float { + fun getPercentage(id: String): Float { return when (val state = states[id]) { is Listener.State.Playing -> state.percentage is Listener.State.Paused -> state.percentage diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index bfbc53fd78..50a3002c0d 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -340,34 +340,37 @@ class VoiceBroadcastPlayerImpl @Inject constructor( onPlaybackTick(id) } - private fun onPlaybackTick(id: String) { - if (currentMediaPlayer?.isPlaying.orFalse()) { - val itemStartPosition = playlist.currentItem?.startTime - val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) - Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: ${playlist.currentSequence}, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") - if (currentVoiceBroadcastPosition != null) { - val percentage = currentVoiceBroadcastPosition.toFloat() / playlist.duration - playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) - } else { - stopPlaybackTicker(id) - } - } else { - stopPlaybackTicker(id) - } - } - fun stopPlaybackTicker(id: String) { playbackTicker?.stop() playbackTicker = null + onPlaybackTick(id) + } - val totalDuration = playlist.duration - if (totalDuration > 0) { - val playbackTime = playbackTracker.getPlaybackTime(id) - val remainingTime = totalDuration - playbackTime - if (remainingTime < 1000) { - playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) - } else { - playbackTracker.pausePlayback(id) + private fun onPlaybackTick(id: String) { + val currentItem = playlist.currentItem ?: return + val itemStartTime = currentItem.startTime + val duration = playlist.duration + when (playingState) { + State.PLAYING, + State.PAUSED -> { + Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: ${playlist.currentSequence}, itemStartTime $itemStartTime, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") + val position = itemStartTime + (currentMediaPlayer?.currentPosition ?: 0) + val percentage = position.toFloat() / playlist.duration + if (playingState == State.PLAYING) { + playbackTracker.updatePlayingAtPlaybackTime(id, position, percentage) + } else { + playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) + } + } + State.BUFFERING, + State.IDLE -> { + val playbackTime = playbackTracker.getPlaybackTime(id) + val percentage = playbackTracker.getPercentage(id) + if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 1000) { + playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) + } else { + playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + } } } } From 436e76c7563f3f529f395630c75dd59ef32436aa Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:42:02 +0100 Subject: [PATCH 114/215] Fix seek on paused state --- .../home/room/detail/RoomDetailAction.kt | 2 +- .../home/room/detail/TimelineViewModel.kt | 2 +- .../MessageVoiceBroadcastListeningItem.kt | 2 +- .../voicebroadcast/VoiceBroadcastHelper.kt | 4 +- .../listening/VoiceBroadcastPlayer.kt | 2 +- .../listening/VoiceBroadcastPlayerImpl.kt | 40 ++++++++++++++----- 6 files changed, 37 insertions(+), 15 deletions(-) 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 ba0f7dbdf8..faee8f652c 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 @@ -133,7 +133,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class PlayOrResume(val voiceBroadcast: VoiceBroadcast) : Listening() object Pause : Listening() object Stop : Listening() - data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int) : Listening() + data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int, val duration: Int) : Listening() } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 252823b2a6..ef238d56e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -637,7 +637,7 @@ class TimelineViewModel @AssistedInject constructor( is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast) VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() - is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis) + is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis, action.duration) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 19caf3d8ba..ebbfe13730 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -94,7 +94,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } override fun onStopTrackingTouch(seekBar: SeekBar) { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress)) + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration)) isUserSeeking = false } }) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 3661928fa5..38fb157748 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -48,7 +48,7 @@ class VoiceBroadcastHelper @Inject constructor( fun stopPlayback() = voiceBroadcastPlayer.stop() - fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { - voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis) + fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) { + voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis, duration) } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 36e75236ad..8c11db4f43 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -48,7 +48,7 @@ interface VoiceBroadcastPlayer { /** * Seek the given voice broadcast playback to the given position, is milliseconds. */ - fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) + fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) /** * Add a [Listener] to the given voice broadcast. diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 50a3002c0d..0937977b70 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -94,8 +94,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } override fun pause() { - currentMediaPlayer?.pause() - playingState = State.PAUSED + pausePlayback() } override fun stop() { @@ -212,16 +211,39 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun resumePlayback() { - currentMediaPlayer?.start() - playingState = State.PLAYING + private fun pausePlayback(positionMillis: Int? = null) { + if (positionMillis == null) { + currentMediaPlayer?.pause() + } else { + stopPlayer() + currentVoiceBroadcast?.voiceBroadcastId?.let { id -> + playbackTracker.updatePausedAtPlaybackTime(id, positionMillis, positionMillis.toFloat() / playlist.duration) + } + } + playingState = State.PAUSED } - override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { - if (voiceBroadcast != currentVoiceBroadcast) { - playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, 0f) + private fun resumePlayback() { + if (currentMediaPlayer != null) { + currentMediaPlayer?.start() + playingState = State.PLAYING } else { - startPlayback(positionMillis) + val position = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } + startPlayback(position) + } + } + + override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) { + when { + voiceBroadcast != currentVoiceBroadcast -> { + playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration) + } + playingState == State.PLAYING || playingState == State.BUFFERING -> { + startPlayback(positionMillis) + } + playingState == State.IDLE || playingState == State.PAUSED -> { + pausePlayback(positionMillis) + } } } From 7d51a265222c5bcabee7fe3e92a7a956e6c37377 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:45:38 +0100 Subject: [PATCH 115/215] Decrease tick interval --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 0937977b70..f7e296ffed 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -355,7 +355,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( fun startPlaybackTicker(id: String) { playbackTicker?.stop() - playbackTicker = CountUpTimer().apply { + playbackTicker = CountUpTimer(50L).apply { tickListener = CountUpTimer.TickListener { onPlaybackTick(id) } resume() } @@ -388,7 +388,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( State.IDLE -> { val playbackTime = playbackTracker.getPlaybackTime(id) val percentage = playbackTracker.getPercentage(id) - if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 1000) { + if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 100) { playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) } else { playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) From baa9cb39b0ff0755376b612e7d5c10fdcab439cb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Sat, 5 Nov 2022 00:06:00 +0100 Subject: [PATCH 116/215] Fix broken live listening --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index f7e296ffed..b613d99d33 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -171,12 +171,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val nextItem = playlist.getNextItem() if (nextItem != null) { val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } - startPlayback(savedPosition) + startPlayback(savedPosition?.takeIf { it > 0 }) } } State.IDLE -> { val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } - startPlayback(savedPosition) + startPlayback(savedPosition?.takeIf { it > 0 }) } } } @@ -389,7 +389,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val playbackTime = playbackTracker.getPlaybackTime(id) val percentage = playbackTracker.getPercentage(id) if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 100) { - playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) + playbackTracker.stopPlayback(id) } else { playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) } From c5e6eb0d0e131f86ba9da3af94fa8c0455ebbc6e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Sat, 5 Nov 2022 00:07:02 +0100 Subject: [PATCH 117/215] Remove some logs --- .../detail/timeline/item/MessageVoiceBroadcastListeningItem.kt | 1 - .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index ebbfe13730..bdd0670029 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -108,7 +108,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } is AudioMessagePlaybackTracker.Listener.State.Playing -> { if (!isUserSeeking) { -// Timber.d("Voice Broadcast | AudioMessagePlaybackTracker.Listener.onUpdate - duration: $duration, playbackTime: ${state.playbackTime}") holder.seekBar.progress = state.playbackTime } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index b613d99d33..9fb7c3ccb5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -375,7 +375,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( when (playingState) { State.PLAYING, State.PAUSED -> { - Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: ${playlist.currentSequence}, itemStartTime $itemStartTime, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") val position = itemStartTime + (currentMediaPlayer?.currentPosition ?: 0) val percentage = position.toFloat() / playlist.duration if (playingState == State.PLAYING) { From aa8eec221a1551b6d737e6a58feffe8f82fd3da7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Sat, 5 Nov 2022 00:29:11 +0100 Subject: [PATCH 118/215] Enable fast backward/forward buttons --- .../MessageVoiceBroadcastListeningItem.kt | 59 +++++++++++++------ .../listening/VoiceBroadcastPlayerImpl.kt | 13 ++-- ...e_event_voice_broadcast_listening_stub.xml | 4 +- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index bdd0670029..558f81b5fa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -28,6 +28,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker +import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @@ -48,6 +49,32 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } player.addListener(voiceBroadcast, playerListener) bindSeekBar(holder) + bindButtons(holder) + } + + private fun bindButtons(holder: Holder) { + with(holder) { + playPauseButton.onClick { + when (player.playingState) { + VoiceBroadcastPlayer.State.PLAYING -> { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) + } + VoiceBroadcastPlayer.State.PAUSED, + VoiceBroadcastPlayer.State.IDLE -> { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) + } + VoiceBroadcastPlayer.State.BUFFERING -> Unit + } + } + fastBackwardButton.onClick { + val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration) + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration)) + } + fastForwardButton.onClick { + val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration) + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration)) + } + } } override fun renderMetadata(holder: Holder) { @@ -63,20 +90,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING - fastBackwardButton.isInvisible = true - fastForwardButton.isInvisible = true - when (state) { VoiceBroadcastPlayer.State.PLAYING -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) - playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) } } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) - playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } } VoiceBroadcastPlayer.State.BUFFERING -> Unit } @@ -99,25 +121,24 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } }) playbackTracker.track(voiceBroadcast.voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { - override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { - when (state) { - is AudioMessagePlaybackTracker.Listener.State.Paused -> { - if (!isUserSeeking) { - holder.seekBar.progress = state.playbackTime - } - } - is AudioMessagePlaybackTracker.Listener.State.Playing -> { - if (!isUserSeeking) { - holder.seekBar.progress = state.playbackTime - } - } - AudioMessagePlaybackTracker.Listener.State.Idle -> Unit - is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit + override fun onUpdate(state: State) { + renderBackwardForwardButtons(holder, state) + if (!isUserSeeking) { + holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) } } }) } + private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) { + val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused + val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) + val canBackward = isPlayingOrPaused && playbackTime > 0 + val canForward = isPlayingOrPaused && playbackTime < duration + holder.fastBackwardButton.isInvisible = !canBackward + holder.fastForwardButton.isInvisible = !canForward + } + private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 9fb7c3ccb5..020edc283a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -371,7 +371,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onPlaybackTick(id: String) { val currentItem = playlist.currentItem ?: return val itemStartTime = currentItem.startTime - val duration = playlist.duration when (playingState) { State.PLAYING, State.PAUSED -> { @@ -383,15 +382,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) } } - State.BUFFERING, - State.IDLE -> { + State.BUFFERING -> { val playbackTime = playbackTracker.getPlaybackTime(id) val percentage = playbackTracker.getPercentage(id) - if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 100) { - playbackTracker.stopPlayback(id) - } else { - playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) - } + playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + } + State.IDLE -> { + playbackTracker.stopPlayback(id) } } } diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index bed9407dfa..150f1cb281 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -100,7 +100,7 @@ android:id="@+id/fastBackwardButton" android:layout_width="24dp" android:layout_height="24dp" - android:background="@android:color/transparent" + android:background="@drawable/bg_rounded_button" android:contentDescription="@string/a11y_voice_broadcast_fast_backward" android:src="@drawable/ic_player_backward_30" app:tint="?vctr_content_secondary" /> @@ -127,7 +127,7 @@ android:id="@+id/fastForwardButton" android:layout_width="24dp" android:layout_height="24dp" - android:background="@android:color/transparent" + android:background="@drawable/bg_rounded_button" android:contentDescription="@string/a11y_voice_broadcast_fast_forward" android:src="@drawable/ic_player_forward_30" app:tint="?vctr_content_secondary" /> From 1c40f9c5e82ca4128238a42c99652954c787d33e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 11:40:57 +0100 Subject: [PATCH 119/215] Minor cleanup --- .../MessageVoiceBroadcastListeningItem.kt | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 558f81b5fa..a43783a626 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -44,9 +44,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - playerListener = VoiceBroadcastPlayer.Listener { state -> - renderPlayingState(holder, state) - } + playerListener = VoiceBroadcastPlayer.Listener { renderPlayingState(holder, it) } player.addListener(voiceBroadcast, playerListener) bindSeekBar(holder) bindButtons(holder) @@ -56,13 +54,9 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem with(holder) { playPauseButton.onClick { when (player.playingState) { - VoiceBroadcastPlayer.State.PLAYING -> { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) - } + VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) VoiceBroadcastPlayer.State.PAUSED, - VoiceBroadcastPlayer.State.IDLE -> { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) - } + VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) VoiceBroadcastPlayer.State.BUFFERING -> Unit } } @@ -106,20 +100,22 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindSeekBar(holder: Holder) { - holder.durationView.text = formatPlaybackTime(duration) - holder.seekBar.max = duration - holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit + with(holder) { + durationView.text = formatPlaybackTime(duration) + seekBar.max = duration + seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit - override fun onStartTrackingTouch(seekBar: SeekBar) { - isUserSeeking = true - } + override fun onStartTrackingTouch(seekBar: SeekBar) { + isUserSeeking = true + } - override fun onStopTrackingTouch(seekBar: SeekBar) { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration)) - isUserSeeking = false - } - }) + override fun onStopTrackingTouch(seekBar: SeekBar) { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration)) + isUserSeeking = false + } + }) + } playbackTracker.track(voiceBroadcast.voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { override fun onUpdate(state: State) { renderBackwardForwardButtons(holder, state) From 226e2026a12452884b05434815afe609e671a98d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 11:42:04 +0100 Subject: [PATCH 120/215] Remove item listeners --- .../timeline/item/MessageVoiceBroadcastListeningItem.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index a43783a626..c83b1f6954 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -140,8 +140,13 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem override fun unbind(holder: Holder) { super.unbind(holder) player.removeListener(voiceBroadcast, playerListener) - holder.seekBar.setOnSeekBarChangeListener(null) playbackTracker.untrack(voiceBroadcast.voiceBroadcastId) + with(holder) { + seekBar.onClick(null) + playPauseButton.onClick(null) + fastForwardButton.onClick(null) + fastBackwardButton.onClick(null) + } } override fun getViewStubId() = STUB_ID From 6b57b1190cccbe8e73e8e5b3934c14ad399b57eb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 11:46:30 +0100 Subject: [PATCH 121/215] Make AudioMessagePlaybackTracker.Listener interface funny --- .../helper/AudioMessagePlaybackTracker.kt | 2 +- .../detail/timeline/item/MessageAudioItem.kt | 16 +++++++--------- .../item/MessageVoiceBroadcastListeningItem.kt | 12 +++++------- .../detail/timeline/item/MessageVoiceItem.kt | 16 +++++++--------- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index 7e40b92ac8..91f27ce5a8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -148,7 +148,7 @@ class AudioMessagePlaybackTracker @Inject constructor() { const val RECORDING_ID = "RECORDING_ID" } - interface Listener { + fun interface Listener { fun onUpdate(state: State) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt index fda9a1465f..3e8d6cb487 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt @@ -140,16 +140,14 @@ abstract class MessageAudioItem : AbsMessageItem() { } private fun renderStateBasedOnAudioPlayback(holder: Holder) { - audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener { - override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { - when (state) { - is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder) - is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state) - is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state) - is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit - } + audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state -> + when (state) { + is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder) + is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state) + is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state) + is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit } - }) + } } private fun renderIdleState(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index c83b1f6954..1076e06b7e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -116,14 +116,12 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } }) } - playbackTracker.track(voiceBroadcast.voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { - override fun onUpdate(state: State) { - renderBackwardForwardButtons(holder, state) - if (!isUserSeeking) { - holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) - } + playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState -> + renderBackwardForwardButtons(holder, playbackState) + if (!isUserSeeking) { + holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) } - }) + } } private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) { 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 e057950790..d3f320db7d 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 @@ -122,16 +122,14 @@ abstract class MessageVoiceItem : AbsMessageItem() { true } - audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener { - override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { - when (state) { - is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed) - is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed) - is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed) - is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit - } + audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state -> + when (state) { + is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed) + is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed) + is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed) + is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit } - }) + } } private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f) From 305a362e9ee15e6a915053a5457d70e6de087dc8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 12:04:30 +0100 Subject: [PATCH 122/215] Fix play action on other voice broadcast than the current one --- .../item/MessageVoiceBroadcastListeningItem.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 1076e06b7e..4b91bbfb0e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -27,7 +27,6 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction -import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @@ -53,11 +52,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem private fun bindButtons(holder: Holder) { with(holder) { playPauseButton.onClick { - when (player.playingState) { - VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) - VoiceBroadcastPlayer.State.PAUSED, - VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) - VoiceBroadcastPlayer.State.BUFFERING -> Unit + if (player.currentVoiceBroadcast == voiceBroadcast) { + when (player.playingState) { + VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) + VoiceBroadcastPlayer.State.PAUSED, + VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) + VoiceBroadcastPlayer.State.BUFFERING -> Unit + } + } else { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } } fastBackwardButton.onClick { From be18f4ec78af8bb77f3ff99f65157f3c12aaca35 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 14:09:15 +0100 Subject: [PATCH 123/215] remove unused imports --- .../listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt | 1 - .../voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 33e370e9bc..d12a329142 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.lastOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningReduce import kotlinx.coroutines.runBlocking diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt index 7106322f06..696d300fc3 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt @@ -22,9 +22,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -56,7 +54,7 @@ class GetVoiceBroadcastEventUseCase @Inject constructor( return when (latestEvent?.content?.voiceBroadcastState) { null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional()) - else -> { + else -> { room.flow() .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty())) .unwrap() From 9e83d88f08ae492a2f1271f0df255edf66c546c4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 14:52:53 +0100 Subject: [PATCH 124/215] Fix seek position when listening another voice broadcast --- .../listening/VoiceBroadcastPlayerImpl.kt | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 020edc283a..977b3906e0 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -216,8 +216,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( currentMediaPlayer?.pause() } else { stopPlayer() - currentVoiceBroadcast?.voiceBroadcastId?.let { id -> - playbackTracker.updatePausedAtPlaybackTime(id, positionMillis, positionMillis.toFloat() / playlist.duration) + val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId + val duration = playlist.duration.takeIf { it > 0 } + if (voiceBroadcastId != null && duration != null) { + playbackTracker.updatePausedAtPlaybackTime(voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration) } } playingState = State.PAUSED @@ -312,6 +314,22 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } + private fun getCurrentPlaybackPosition(): Int? { + val playlistPosition = playlist.currentItem?.startTime + val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition + val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } + return computedPosition ?: savedPosition + } + + private fun getCurrentPlaybackPercentage(): Float? { + val playlistPosition = playlist.currentItem?.startTime + val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition + val duration = playlist.duration.takeIf { it > 0 } + val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null + val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) } + return computedPercentage ?: savedPercentage + } + private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, @@ -369,26 +387,26 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun onPlaybackTick(id: String) { - val currentItem = playlist.currentItem ?: return - val itemStartTime = currentItem.startTime + val playbackTime = getCurrentPlaybackPosition() + val percentage = getCurrentPlaybackPercentage() when (playingState) { - State.PLAYING, - State.PAUSED -> { - val position = itemStartTime + (currentMediaPlayer?.currentPosition ?: 0) - val percentage = position.toFloat() / playlist.duration - if (playingState == State.PLAYING) { - playbackTracker.updatePlayingAtPlaybackTime(id, position, percentage) - } else { - playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) + State.PLAYING -> { + if (playbackTime != null && percentage != null) { + playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage) } } + State.PAUSED, State.BUFFERING -> { - val playbackTime = playbackTracker.getPlaybackTime(id) - val percentage = playbackTracker.getPercentage(id) - playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + if (playbackTime != null && percentage != null) { + playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + } } State.IDLE -> { - playbackTracker.stopPlayback(id) + if (playbackTime == null || percentage == null || playbackTime == playlist.duration) { + playbackTracker.stopPlayback(id) + } else { + playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + } } } } From 4e533667278c5a5f6900edebc58c5d6cad4c7725 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 15:46:52 +0100 Subject: [PATCH 125/215] Fix default visibility of fast backward/forward buttons --- ...timeline_event_voice_broadcast_listening_stub.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 150f1cb281..1d31afba99 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -103,7 +103,9 @@ android:background="@drawable/bg_rounded_button" android:contentDescription="@string/a11y_voice_broadcast_fast_backward" android:src="@drawable/ic_player_backward_30" - app:tint="?vctr_content_secondary" /> + android:visibility="invisible" + app:tint="?vctr_content_secondary" + tools:visibility="visible" /> + android:indeterminateTint="?vctr_content_secondary" + android:visibility="gone" + tools:visibility="visible" /> + android:visibility="invisible" + app:tint="?vctr_content_secondary" + tools:visibility="visible" /> Date: Mon, 7 Nov 2022 16:05:06 +0100 Subject: [PATCH 126/215] improve end of voice broadcast check --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 977b3906e0..6a6dc6a9e8 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -402,7 +402,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } State.IDLE -> { - if (playbackTime == null || percentage == null || playbackTime == playlist.duration) { + if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) { playbackTracker.stopPlayback(id) } else { playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) From 0c40acb186efad47fc19b924eae4320f88282c0a Mon Sep 17 00:00:00 2001 From: NIkita Fedrunov Date: Mon, 7 Nov 2022 16:16:51 +0100 Subject: [PATCH 127/215] temporary workaround for a failing sync due to unexpected `enableUnreadThreadNotifications` param --- .../android/sdk/internal/session/filter/FilterFactory.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt index 77c5649709..e0919c52e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt @@ -67,7 +67,9 @@ internal object FilterFactory { } private fun createElementTimelineFilter(): RoomEventFilter? { - return RoomEventFilter(enableUnreadThreadNotifications = true) +// we need to check if homeserver supports thread notifications before setting this param +// return RoomEventFilter(enableUnreadThreadNotifications = true) + return null } private fun createElementStateFilter(): RoomEventFilter { From 6d2620815cc3923759b3d17690aaaf15eb1f26d6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 7 Nov 2022 16:52:41 +0100 Subject: [PATCH 128/215] Moving UI auth interceptor into use case --- .../settings/devices/v2/DevicesViewModel.kt | 27 ++----- .../othersessions/OtherSessionsViewModel.kt | 29 ++----- .../v2/overview/SessionOverviewViewModel.kt | 31 +++----- .../InterceptSignoutFlowResponseUseCase.kt | 7 +- .../v2/signout/SignoutSessionUseCase.kt | 42 ---------- ...sult.kt => SignoutSessionsReAuthNeeded.kt} | 16 ++-- .../v2/signout/SignoutSessionsUseCase.kt | 39 ++++++--- .../devices/v2/DevicesViewModelTest.kt | 4 +- .../OtherSessionsViewModelTest.kt | 9 +-- .../overview/SessionOverviewViewModelTest.kt | 12 +-- ...InterceptSignoutFlowResponseUseCaseTest.kt | 12 +-- .../v2/signout/SignoutSessionUseCaseTest.kt | 79 ------------------- .../v2/signout/SignoutSessionsUseCaseTest.kt | 51 +++++++++--- .../app/test/fakes/FakeCryptoService.kt | 22 ++---- .../test/fakes/FakeSignoutSessionUseCase.kt | 77 ------------------ .../test/fakes/FakeSignoutSessionsUseCase.kt | 38 ++------- 16 files changed, 132 insertions(+), 363 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt rename vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/{SignoutSessionResult.kt => SignoutSessionsReAuthNeeded.kt} (71%) delete mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt delete mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index c714645b9a..cd97795b69 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -27,20 +27,16 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, @@ -141,16 +137,14 @@ class DevicesViewModel @AssistedInject constructor( if (deviceIds.isEmpty()) { return@launch } - val signoutResult = signout(deviceIds) + val result = signout(deviceIds) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } @@ -162,16 +156,9 @@ class DevicesViewModel @AssistedInject constructor( .orEmpty() } - private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index c33490400b..9b4c26ee4f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -29,24 +29,18 @@ import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, - private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase ) : VectorSessionsListViewModel( @@ -168,16 +162,14 @@ class OtherSessionsViewModel @AssistedInject constructor( if (deviceIds.isEmpty()) { return@launch } - val signoutResult = signout(deviceIds) + val result = signout(deviceIds) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } @@ -190,16 +182,9 @@ class OtherSessionsViewModel @AssistedInject constructor( }.mapNotNull { it.deviceInfo.deviceId } } - private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 59eeaaadb4..9c4ece7e02 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -30,28 +30,24 @@ import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class SessionOverviewViewModel @AssistedInject constructor( @Assisted val initialState: SessionOverviewViewState, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, - private val signoutSessionUseCase: SignoutSessionUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, @@ -149,30 +145,21 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleSignoutOtherSession(deviceId: String) { viewModelScope.launch { setLoading(true) - val signoutResult = signout(deviceId) + val result = signout(deviceId) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } - private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceId: String) = signoutSessionsUseCase.execute(listOf(deviceId), this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt index 4316995272..42ebd7782e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt @@ -37,17 +37,16 @@ class InterceptSignoutFlowResponseUseCase @Inject constructor( flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation - ): SignoutSessionResult { + ): SignoutSessionsReAuthNeeded? { return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) { UserPasswordAuth( session = null, user = activeSessionHolder.getActiveSession().myUserId, password = reAuthHelper.data ).let { promise.resume(it) } - - SignoutSessionResult.Completed + null } else { - SignoutSessionResult.ReAuthNeeded( + SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = flowResponse.session), uiaContinuation = promise, flowResponse = flowResponse, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt deleted file mode 100644 index bc6cff0d43..0000000000 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022 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.settings.devices.v2.signout - -import im.vector.app.core.di.ActiveSessionHolder -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.util.awaitCallback -import javax.inject.Inject - -/** - * Use case to signout a single session. - */ -class SignoutSessionUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, -) { - - suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { - return deleteDevice(deviceId, userInteractiveAuthInterceptor) - } - - private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { - awaitCallback { matrixCallback -> - activeSessionHolder.getActiveSession() - .cryptoService() - .deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt similarity index 71% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt index fa1fb31b66..56e3d17686 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt @@ -20,13 +20,9 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import kotlin.coroutines.Continuation -sealed class SignoutSessionResult { - data class ReAuthNeeded( - val pendingAuth: UIABaseAuth, - val uiaContinuation: Continuation, - val flowResponse: RegistrationFlowResponse, - val errCode: String? - ) : SignoutSessionResult() - - object Completed : SignoutSessionResult() -} +data class SignoutSessionsReAuthNeeded( + val pendingAuth: UIABaseAuth, + val uiaContinuation: Continuation, + val flowResponse: RegistrationFlowResponse, + val errCode: String? +) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt index b4fc78043e..1cf713a711 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -16,27 +16,42 @@ package im.vector.app.features.settings.devices.v2.signout +import androidx.annotation.Size import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.util.awaitCallback +import timber.log.Timber import javax.inject.Inject +import kotlin.coroutines.Continuation -/** - * Use case to signout several sessions. - */ class SignoutSessionsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, ) { - suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { - return deleteDevices(deviceIds, userInteractiveAuthInterceptor) + suspend fun execute( + @Size(min = 1) deviceIds: List, + onReAuthNeeded: (SignoutSessionsReAuthNeeded) -> Unit, + ): Result = runCatching { + Timber.d("start execute with ${deviceIds.size} deviceIds") + + val authInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise) + result?.let(onReAuthNeeded) + } + } + + deleteDevices(deviceIds, authInterceptor) + Timber.d("end execute") } - private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { - awaitCallback { matrixCallback -> - activeSessionHolder.getActiveSession() - .cryptoService() - .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) - } - } + private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) + } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 852fc64fd5..65da1a9385 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -228,7 +228,7 @@ class DevicesViewModelTest { // Given val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID) // signout all devices except the current device - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1)) // When val viewModel = createViewModel() @@ -275,7 +275,7 @@ class DevicesViewModelTest { @Test fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { // Given - val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index e01d6e058c..1e8c511c42 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -23,7 +23,6 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase @@ -66,7 +65,6 @@ class OtherSessionsViewModelTest { private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() - private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = @@ -75,7 +73,6 @@ class OtherSessionsViewModelTest { activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, - interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) @@ -321,7 +318,7 @@ class OtherSessionsViewModelTest { val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout only selected devices - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2)) val expectedViewState = OtherSessionsViewState( devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), currentFilter = defaultArgs.defaultFilter, @@ -357,7 +354,7 @@ class OtherSessionsViewModelTest { val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout all devices - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedViewState = OtherSessionsViewState( devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), currentFilter = defaultArgs.defaultFilter, @@ -422,7 +419,7 @@ class OtherSessionsViewModelTest { val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) - val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index b2ab939bd1..f26c818e1d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -28,7 +28,7 @@ import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowRe import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler -import im.vector.app.test.fakes.FakeSignoutSessionUseCase +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -70,7 +70,7 @@ class SessionOverviewViewModelTest { private val getDeviceFullInfoUseCase = mockk(relaxed = true) private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxed = true) @@ -82,7 +82,7 @@ class SessionOverviewViewModelTest { initialState = SessionOverviewViewState(args), getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionUseCase = fakeSignoutSessionUseCase.instance, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, @@ -248,7 +248,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - fakeSignoutSessionUseCase.givenSignoutSuccess(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_SESSION_ID_1)) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -285,7 +285,7 @@ class SessionOverviewViewModelTest { every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) val error = Exception() - fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, error) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_SESSION_ID_1), error) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -318,7 +318,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val reAuthNeeded = fakeSignoutSessionUseCase.givenSignoutReAuthNeeded(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_SESSION_ID_1)) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt index 35551ba36e..cd0575f2a0 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt @@ -24,8 +24,8 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs import io.mockk.unmockkAll +import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeInstanceOf import org.junit.After import org.junit.Before import org.junit.Test @@ -63,7 +63,7 @@ class InterceptSignoutFlowResponseUseCaseTest { } @Test - fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and success is returned`() { + fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and null is returned`() { // Given val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID) fakeReAuthHelper.givenStoredPassword(A_PASSWORD) @@ -84,7 +84,7 @@ class InterceptSignoutFlowResponseUseCaseTest { ) // Then - result shouldBeInstanceOf (SignoutSessionResult.Completed::class) + result shouldBe null every { promise.resume(expectedAuth) } @@ -97,7 +97,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(A_PASSWORD) val errorCode = AN_ERROR_CODE val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, @@ -122,7 +122,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(A_PASSWORD) val errorCode: String? = null val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, @@ -147,7 +147,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(null) val errorCode: String? = null val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt deleted file mode 100644 index 5af91c16ce..0000000000 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2022 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.settings.devices.v2.signout - -import im.vector.app.test.fakes.FakeActiveSessionHolder -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBe -import org.junit.Test -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor - -private const val A_DEVICE_ID = "device-id" - -class SignoutSessionUseCaseTest { - - private val fakeActiveSessionHolder = FakeActiveSessionHolder() - - private val signoutSessionUseCase = SignoutSessionUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance - ) - - @Test - fun `given a device id when signing out with success then success result is returned`() = runTest { - // Given - val interceptor = givenAuthInterceptor() - fakeActiveSessionHolder.fakeSession - .fakeCryptoService - .givenDeleteDeviceSucceeds(A_DEVICE_ID) - - // When - val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor) - - // Then - result.isSuccess shouldBe true - every { - fakeActiveSessionHolder.fakeSession - .fakeCryptoService - .deleteDevice(A_DEVICE_ID, interceptor, any()) - } - } - - @Test - fun `given a device id when signing out with error then failure result is returned`() = runTest { - // Given - val interceptor = givenAuthInterceptor() - val error = mockk() - fakeActiveSessionHolder.fakeSession - .fakeCryptoService - .givenDeleteDeviceFailsWithError(A_DEVICE_ID, error) - - // When - val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor) - - // Then - result.isFailure shouldBe true - every { - fakeActiveSessionHolder.fakeSession - .fakeCryptoService - .deleteDevice(A_DEVICE_ID, interceptor, any()) - } - } - - private fun givenAuthInterceptor() = mockk() -} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt index 08a9fa625b..70d2b4b039 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -19,10 +19,10 @@ package im.vector.app.features.settings.devices.v2.signout import im.vector.app.test.fakes.FakeActiveSessionHolder import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.junit.Test -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" @@ -30,36 +30,38 @@ private const val A_DEVICE_ID_2 = "device-id-2" class SignoutSessionsUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val signoutSessionsUseCase = SignoutSessionsUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance + activeSessionHolder = fakeActiveSessionHolder.instance, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, ) @Test fun `given a list of device ids when signing out with success then success result is returned`() = runTest { // Given - val interceptor = givenAuthInterceptor() + val callback = givenOnReAuthCallback() val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) fakeActiveSessionHolder.fakeSession .fakeCryptoService .givenDeleteDevicesSucceeds(deviceIds) // When - val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + val result = signoutSessionsUseCase.execute(deviceIds, callback) // Then result.isSuccess shouldBe true - every { + verify { fakeActiveSessionHolder.fakeSession .fakeCryptoService - .deleteDevices(deviceIds, interceptor, any()) + .deleteDevices(deviceIds, any(), any()) } } @Test fun `given a list of device ids when signing out with error then failure result is returned`() = runTest { // Given - val interceptor = givenAuthInterceptor() + val interceptor = givenOnReAuthCallback() val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) val error = mockk() fakeActiveSessionHolder.fakeSession @@ -71,12 +73,41 @@ class SignoutSessionsUseCaseTest { // Then result.isFailure shouldBe true - every { + verify { fakeActiveSessionHolder.fakeSession .fakeCryptoService - .deleteDevices(deviceIds, interceptor, any()) + .deleteDevices(deviceIds, any(), any()) } } - private fun givenAuthInterceptor() = mockk() + @Test + fun `given a list of device ids when signing out with reAuth needed then callback is called`() = runTest { + // Given + val callback = givenOnReAuthCallback() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesNeedsUIAuth(deviceIds) + val reAuthNeeded = SignoutSessionsReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = mockk(), + flowResponse = mockk(), + errCode = "errorCode" + ) + every { fakeInterceptSignoutFlowResponseUseCase.execute(any(), any(), any()) } returns reAuthNeeded + + // When + val result = signoutSessionsUseCase.execute(deviceIds, callback) + + // Then + result.isSuccess shouldBe true + verify { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, any(), any()) + callback(reAuthNeeded) + } + } + + private fun givenOnReAuthCallback(): (SignoutSessionsReAuthNeeded) -> Unit = {} } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index 5f34c45fa7..b23f018cf5 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -22,6 +22,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.slot import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo @@ -70,30 +71,21 @@ class FakeCryptoService( } } - fun givenDeleteDeviceSucceeds(deviceId: String) { - val matrixCallback = slot>() - every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers { + fun givenDeleteDevicesSucceeds(deviceIds: List) { + every { deleteDevices(deviceIds, any(), any()) } answers { thirdArg>().onSuccess(Unit) } } - fun givenDeleteDeviceFailsWithError(deviceId: String, error: Exception) { - val matrixCallback = slot>() - every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers { - thirdArg>().onFailure(error) - } - } - - fun givenDeleteDevicesSucceeds(deviceIds: List) { - val matrixCallback = slot>() - every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + fun givenDeleteDevicesNeedsUIAuth(deviceIds: List) { + every { deleteDevices(deviceIds, any(), any()) } answers { + secondArg().performStage(mockk(), "", mockk()) thirdArg>().onSuccess(Unit) } } fun givenDeleteDevicesFailsWithError(deviceIds: List, error: Exception) { - val matrixCallback = slot>() - every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + every { deleteDevices(deviceIds, any(), any()) } answers { thirdArg>().onFailure(error) } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt deleted file mode 100644 index 8a6b101ff6..0000000000 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2022 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.test.fakes - -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import kotlin.coroutines.Continuation - -class FakeSignoutSessionUseCase { - - val instance = mockk() - - fun givenSignoutSuccess( - deviceId: String, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - } - - fun givenSignoutReAuthNeeded( - deviceId: String, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() - val flowResponse = mockk() - every { flowResponse.session } returns "a-session-id" - val errorCode = "errorCode" - val promise = mockk>() - val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( - pendingAuth = mockk(), - uiaContinuation = promise, - flowResponse = flowResponse, - errCode = errorCode, - ) - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded - coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - - return reAuthNeeded - } - - fun givenSignoutError(deviceId: String, error: Throwable) { - coEvery { instance.execute(deviceId, any()) } returns Result.failure(error) - } -} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt index 04d05b1d8a..9eb3676475 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt @@ -16,55 +16,33 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import io.mockk.slot -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import kotlin.coroutines.Continuation class FakeSignoutSessionsUseCase { val instance = mockk() - fun givenSignoutSuccess( - deviceIds: List, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } + fun givenSignoutSuccess(deviceIds: List) { + coEvery { instance.execute(deviceIds, any()) } returns Result.success(Unit) } - fun givenSignoutReAuthNeeded( - deviceIds: List, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() + fun givenSignoutReAuthNeeded(deviceIds: List): SignoutSessionsReAuthNeeded { val flowResponse = mockk() every { flowResponse.session } returns "a-session-id" val errorCode = "errorCode" - val promise = mockk>() - val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + val reAuthNeeded = SignoutSessionsReAuthNeeded( pendingAuth = mockk(), - uiaContinuation = promise, + uiaContinuation = mockk(), flowResponse = flowResponse, errCode = errorCode, ) - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded - coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) + coEvery { instance.execute(deviceIds, any()) } coAnswers { + secondArg<(SignoutSessionsReAuthNeeded) -> Unit>().invoke(reAuthNeeded) Result.success(Unit) } From 3a5af934ccc68195a7041d45f2346762d60d6535 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 23:15:05 +0000 Subject: [PATCH 129/215] Bump play-services-location from 21.0.0 to 21.0.1 Bumps play-services-location from 21.0.0 to 21.0.1. --- updated-dependencies: - dependency-name: com.google.android.gms:play-services-location dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vector-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index f793fff2c8..5b8d5b5331 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -372,7 +372,7 @@ dependencies { debugImplementation 'com.facebook.soloader:soloader:0.10.4' debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" - gplayImplementation "com.google.android.gms:play-services-location:21.0.0" + gplayImplementation "com.google.android.gms:play-services-location:21.0.1" // UnifiedPush gplay flavor only gplayImplementation('com.google.firebase:firebase-messaging:23.1.0') { exclude group: 'com.google.firebase', module: 'firebase-core' From 49bf0e18fc33d597f1cfbf360543522eabbf530a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 23:20:06 +0000 Subject: [PATCH 130/215] Bump sentry-android from 6.6.0 to 6.7.0 Bumps [sentry-android](https://github.com/getsentry/sentry-java) from 6.6.0 to 6.7.0. - [Release notes](https://github.com/getsentry/sentry-java/releases) - [Changelog](https://github.com/getsentry/sentry-java/blob/6.7.0/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-java/compare/6.6.0...6.7.0) --- updated-dependencies: - dependency-name: io.sentry:sentry-android dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 47f1097446..751cba4d44 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -26,7 +26,7 @@ def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.6.0" +def sentry = "6.7.0" def fragment = "1.5.4" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 From e84e2a10fd3d5370923c8a50f55a1684957c7c6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 23:20:45 +0000 Subject: [PATCH 131/215] Bump libphonenumber from 8.12.57 to 8.13.0 Bumps [libphonenumber](https://github.com/google/libphonenumber) from 8.12.57 to 8.13.0. - [Release notes](https://github.com/google/libphonenumber/releases) - [Changelog](https://github.com/google/libphonenumber/blob/master/making-metadata-changes.md) - [Commits](https://github.com/google/libphonenumber/compare/v8.12.57...v8.13.0) --- updated-dependencies: - dependency-name: com.googlecode.libphonenumber:libphonenumber dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 47f1097446..101b769576 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -83,7 +83,7 @@ ext.libs = [ 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.57" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.0" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", From 001ab8cb4958a8a247dd840f97f6987c8dc0a02a Mon Sep 17 00:00:00 2001 From: "Auri B. P" Date: Sat, 5 Nov 2022 21:56:13 +0000 Subject: [PATCH 132/215] Translated using Weblate (Catalan) Currently translated at 99.7% (2532 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ca/ --- library/ui-strings/src/main/res/values-ca/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-ca/strings.xml b/library/ui-strings/src/main/res/values-ca/strings.xml index ce786fb87d..f9d7145b66 100644 --- a/library/ui-strings/src/main/res/values-ca/strings.xml +++ b/library/ui-strings/src/main/res/values-ca/strings.xml @@ -2836,4 +2836,5 @@ Adjunts Adhesius Galeria + Format de text \ No newline at end of file From 2652b7ce636bbc21ff066aca4c9f048ac1fe53b6 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Fri, 4 Nov 2022 14:49:46 +0000 Subject: [PATCH 133/215] Translated using Weblate (Czech) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- library/ui-strings/src/main/res/values-cs/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index 53599adce2..47caa52149 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2891,4 +2891,12 @@ %1$d vybrané %1$d vybraných + Přepnutí režimu celé obrazovky + Formátování textu + Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a zahajte nové. + Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a zahajte nové. + Nemáte potřebná oprávnění k zahájení hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění. + Nelze zahájit nové hlasové vysílání + Přetočení o 30 sekund zpět + Přetočení o 30 sekund dopředu \ No newline at end of file From dac544577285ee891d0649b4064edbd227588dc1 Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 4 Nov 2022 15:26:12 +0000 Subject: [PATCH 134/215] Translated using Weblate (German) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- library/ui-strings/src/main/res/values-de/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index 409fc564f4..cd215e175d 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2835,4 +2835,12 @@ %1$d ausgewählt %1$d ausgewählt + Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen. + Sprachübertragung kann nicht gestartet werden + Vollbildmodus umschalten + Textformatierung + Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen. + Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest. + 30 Sekunden vorspulen + 30 Sekunden zurückspulen \ No newline at end of file From 6e46cdded69525b8b9a844095e7552fd09e67f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 6 Nov 2022 07:18:57 +0000 Subject: [PATCH 135/215] Translated using Weblate (Estonian) Currently translated at 99.6% (2531 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/et/ --- library/ui-strings/src/main/res/values-et/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 9bfbbe8eeb..22572a0f36 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2827,4 +2827,12 @@ %1$d valitud %1$d valitud + Lülita täisekraanivaade sisse/välja + Tekstivorming + Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus. + Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud. + Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga. + Uue ringhäälingukõne alustamine pole võimalik + Keri tagasi 30 sekundi kaupa + Keri edasi 30 sekundi kaupa \ No newline at end of file From 546f391c577c27b53a2ee7dc45547693ac62b641 Mon Sep 17 00:00:00 2001 From: Glandos Date: Sat, 5 Nov 2022 13:36:12 +0000 Subject: [PATCH 136/215] Translated using Weblate (French) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- library/ui-strings/src/main/res/values-fr/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index 6c767fc350..a02b062596 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2836,4 +2836,12 @@ %1$d sélectionné %1$d sélectionnés + Basculer en mode plein écran + Formatage de texte + Vous êtes déjà en train de réaliser une diffusion audio. Veuillez terminer votre diffusion audio actuelle pour en démarrer une nouvelle. + Une autre personne est déjà en train de réaliser une diffusion audio. Attendez que sa diffusion audio soit terminée pour en démarrer une nouvelle. + Vous n’avez pas les permissions requises pour démarrer une nouvelle diffusion audio dans ce salon. Contactez un administrateur du salon pour mettre-à-jour vos permissions. + Impossible de commencer une nouvelle diffusion audio + Avance rapide de 30 secondes + Retour rapide de 30 secondes \ No newline at end of file From 4a00be4e8a2f5ec55eeb440b02bca43562789f00 Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 5 Nov 2022 10:26:39 +0000 Subject: [PATCH 137/215] Translated using Weblate (Indonesian) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/id/ --- library/ui-strings/src/main/res/values-in/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index 6ed423bb70..cde367faf9 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2783,4 +2783,12 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. %1$d dipilih + Ubah mode layar penuh + Format teks + Anda sedang merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru. + Orang lain sedang merekam sebuah siaran suara. Tunggu untuk siaran suara berakhir untuk memulai yang baru. + Anda tidak memiliki izin yang dibutuhkan untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda. + Tidak dapat memulai siaran suara baru + Maju cepat 30 detik + Mundur cepat 30 detik \ No newline at end of file From fd70e648c11b6e5b4c1c5a47ecbc180120de8b7d Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Fri, 4 Nov 2022 21:31:37 +0000 Subject: [PATCH 138/215] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/ --- library/ui-strings/src/main/res/values-pt-rBR/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml index 2ec5f394bd..d3061371fa 100644 --- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml @@ -2836,4 +2836,12 @@ %1$d selecionada(o) %1$d selecionadas(os) + Alguma outra pessoa já está gravando um broadcast de voz. Espere que o broadcast de voz dela termine para começar um novo. + Alternar modo de tela cheia + Formatação de texto + Você já está gravando um broadcast de voz. Por favor termine seu broadcast de voz atual para começar um novo. + Você não tem as permissões requeridas para começar um broadcast de voz nesta sala. Contacte um/uma administrador(a) para fazer upgrade de suas permissões. + Não dá pra começar um novo broadcast de voz + Avançar rápido 30 segundos + Retroceder 30 segundos \ No newline at end of file From 89bb2d1a9bfde0b9215055416cf52aafa3ee8bfe Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 4 Nov 2022 17:21:58 +0000 Subject: [PATCH 139/215] Translated using Weblate (Slovak) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- library/ui-strings/src/main/res/values-sk/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index a41aca05dc..9eac092a62 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2891,4 +2891,12 @@ %1$d vybraté %1$d vybraných + Prepnutie režimu na celú obrazovku + Formátovanie textu + Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové. + Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové. + Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia. + Nie je možné spustiť nové hlasové vysielanie + Rýchle posunutie dozadu o 30 sekúnd + Rýchle posunutie dopredu o 30 sekúnd \ No newline at end of file From 5d5ea81db897a816d5e3a2fa19e3f225c40c13c9 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sun, 6 Nov 2022 15:48:31 +0000 Subject: [PATCH 140/215] Translated using Weblate (Albanian) Currently translated at 99.0% (2516 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/ --- .../src/main/res/values-sq/strings.xml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index b1d8eb9564..773454c39f 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -896,7 +896,7 @@ Shfaq te rrjedha kohore akte të fshehura Përgjegjës integrimesh ID Aplikacioni: - + Emër Aplikacioni Në Ekran: Emër Sesioni Në Ekran: Mesazhe të Drejtpërdrejtë @@ -1275,7 +1275,7 @@ \n - Shërbyesi Home te i cili është lidhur përdoruesi që po verifikoni \n - Lidhja juaj internet ose ajo e përdoruesit tjetër \n - Pajisja juaj ose ajo e përdoruesit tjetër - + %s u anulua %s u pranua Skanojeni kodin me pajisjen e përdoruesit tjetër, për të verifikuar në mënyrë të sigurt njëri-tjetrin Nëse s’jeni vetë atje, krahasoni emoji-n @@ -2808,4 +2808,18 @@ %1$d i përzgjedhura %1$d të përzgjedhura + Shërbyesi Home nuk mbulon hyrje me kod QR. + U has një problem sigurie, kur ujdisej shkëmbim i siguruar mesazhesh. Mund të jetë komprometuar një nga sa vijon: shërbyesi juaj Home; lidhja(et) tuaja internet; pajisja(et) tuaja; + Lidhja s’u plotësua në kohën e duhur. + Kontrolloni pajisjen ku jeni i futur, duhet të shfaqet kodi më poshtë. Sigurohuni se kodi më poshtë përputhet me atë pajisje: + Skanoni kodin QR më poshtë me pajisjen tuaj prej nga është dalë nga llogaria. + Përdorni pajisjen tuaj ku jeni brenda llogarisë që të skanoni kodin QR më poshtë: + Përdorni kamerën në këtë pajisje që të skanoni kodin QR të shfaqur në pajisjen tuaj tjetër: + Mirato vetvetiu widget-e Thirrjesh Element Call dhe akordo përdorim kamere / mikfrofoni + MSC3061: Po jepen kyçe dhome për mesazhe të dikurshëm + Shfaq hollësitë më të reja të profileve (avatar dhe emër në ekran) për krejt mesazhet. + Kërko doemos që tastiera të mos përditësojë ndonjë të dhënë të personalizuar, bie fjala, historik shtypjeje në të dhe fjalor bazuar në ç’keni shtypur në biseda. Kini parasysh se disa tastiera mund të mos e respektojnë këtë rregullim. + Ky kod QR duket i formuar keq. Ju lutemi, provoni ta verifikoni me tjetër metodë. + 🔒 Keni aktivizuar fshehtëzim për sesionie të verifikuar vetëm për krejt dhomat, që nga Rregullime Sigurie. + Luaj figura të animuara te rrjedha kohora sapo zënë të duken \ No newline at end of file From 0904f9c6e0b1d54722af6d163b7a52f1632241f6 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 4 Nov 2022 19:04:33 +0000 Subject: [PATCH 141/215] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/ --- library/ui-strings/src/main/res/values-uk/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index f633a0ef2f..8cbfeca6ba 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -2946,4 +2946,12 @@ Вибрано %1$d Вибрати все + Перемкнути повноекранний режим + Форматування тексту + Ви вже записуєте голосове повідомлення. Завершіть поточну трансляцію, щоб розпочати нову. + Хтось інший вже записує голосове повідомлення. Зачекайте, поки закінчиться трансляція, щоб розпочати нову. + Ви не маєте необхідних дозволів для початку передавання голосового повідомлення в цю кімнату. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи. + Не вдалося розпочати передавання нового голосового повідомлення + Перемотати вперед на 30 секунд + Перемотати назад на 30 секунд \ No newline at end of file From 6161a9582e03cfe0ff4e1773866fefd0fde84dbc Mon Sep 17 00:00:00 2001 From: PotLice Date: Mon, 7 Nov 2022 09:06:07 +0000 Subject: [PATCH 142/215] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hans/ --- .../src/main/res/values-zh-rCN/strings.xml | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 112b900da7..5ab8a351d1 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -348,7 +348,7 @@ 显示系统设置中的应用程序信息。 通话请求 使用条款 - 其他 + 其它 通知目标 登录为 请检查你的电子邮件并点击里面包含的链接。完成时请点击继续。 @@ -434,7 +434,7 @@ 你添加了一个新会话“%s”,它正在请求加密密钥。 你的未验证会话“%s”正在请求加密密钥。 开始验证 - bug报告 + 错误报告 拍摄照片 拍摄视频 使用原生相机 @@ -1714,7 +1714,7 @@ 建议 已知用户 二维码 - 通过QR码添加 + 通过二维码添加 房间设置 话题 房间话题(可选) @@ -2099,7 +2099,7 @@ 我的用户名 我的显示名称 通知事项 - 其他 + 其它 提及和关键词 默认通知 可用视频通话 @@ -2303,7 +2303,7 @@ BETA 共享你的实时位置 缩放到当前位置 - 地图上选定位置的图钉 + 地图上选定位置的固定标记 无投票 验证你的电子邮件 @@ -2336,12 +2336,12 @@ 共享位置 您需要拥有正确的权限才能在此房间中共享实时位置。 你没有权限共享实时位置 - %1$s前已更新 + %1$s 前已更新 临时执行:地点在房间历史中持续存在 启用实时位置共享 位置共享正在进行中 - ${app_name}实时位置 - 剩余%1$s + ${app_name} 实时位置 + 剩余 %1$s 停止 实时共享直到 %1$s 查看实时位置 @@ -2350,14 +2350,14 @@ 启用实时位置 加载地图失败 打开,用 - ${app_name}无法访问你的位置。请稍后再试。 - ${app_name}无法访问你的位置 + ${app_name} 无法访问你的位置。请稍后再试。 + ${app_name} 无法访问你的位置 在房间中查看 MSC3061:为过去的消息共享房间密钥 在共享历史的加密房间中邀请时,加密历史将可见。 - 8小时 - 1小时 - 15分钟 + 8 小时 + 1 小时 + 15 分钟 共享此位置 共享此位置 共享实时位置 @@ -2539,7 +2539,7 @@ 自动允许 Element 通话小部件并授予相机/麦克风访问权限 启用 Element 通话权限快捷方式 实时位置 - 这个QR码看起来不正常。请尝试用另一个方法验证。 + 此二维码看起来格式不正确。请尝试使用其它方法进行验证。 你无法访问加密消息历史。重置你的安全消息备份和验证密钥以重新开始。 无法验证此设备 你的服务器地址是什么? @@ -2562,7 +2562,7 @@ A—Z 活动 排序方式 - 显示最近的 + 显示最近 显示过滤条件 布局偏好 探索房间 @@ -2622,7 +2622,7 @@ 你当前的会话已准备好安全地收发消息。 仅在首条消息创建私聊消息 启用延迟的私聊消息 - 简化的Element,带有可选的标签 + 简化的 Element,带有可选的标签 无痕键盘 请求键盘不要根据您在对话中输入的内容更新任何个性化数据,例如输入历史记录和字典。 请注意,某些键盘可能不遵守此设置。 ${app_name}需要权限来显示通知。通知可以显示消息、邀请等。 @@ -2762,9 +2762,31 @@ 停止语音广播录制 暂停语音广播录制 继续语音广播录制 - 扫描QR码 + 扫描二维码 语音广播 已启用: 会话ID: 出了点差错。请检查您的网络连接并重试。 + 联系人 + 切换全屏模式 + 选择会话 + 文本格式 + 相机 + 位置 + 投票 + 语音广播 + 附件 + 贴纸 + 照片库 + 您没有在此房间内开始语音广播所需的权限。联系房间管理员升级您的权限。 + 其他人已经在录制语音广播。等待他们的语音广播结束以开始新的广播。 + 您已经在录制语音广播。请结束您当前的语音广播以开始新的语音广播。 + 无法开始新的语音广播 + 快进 30 秒 + 快退 30 秒 + 取消全选 + 全选 + + 已选择 %1$d + \ No newline at end of file From 191034a7d384e4d06d035ab015de840feb64f9f9 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 7 Nov 2022 02:33:01 +0000 Subject: [PATCH 143/215] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/ --- library/ui-strings/src/main/res/values-zh-rTW/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index 739ea09755..91e08c803a 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -2781,4 +2781,12 @@ 已選取 %1$d + 切換全螢幕模式 + 文字格式化 + 您已在錄製語音廣播。請結束您目前的語音廣播以開始新的。 + 其他人已在錄製語音廣播。等待他們的語音廣播結束以開始新的。 + 您沒有在此聊天室中開始語音廣播的必要權限。請聯絡聊天室管理員以升級您的權限。 + 無法開始新的語音廣播 + 快轉30秒 + 快退30秒 \ No newline at end of file From 718545cd4d5da4e7a3ab18edcfc2e942e2d171f3 Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 4 Nov 2022 15:19:12 +0000 Subject: [PATCH 144/215] Translated using Weblate (German) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/de/ --- fastlane/metadata/android/de-DE/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/40105060.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/40105060.txt b/fastlane/metadata/android/de-DE/changelogs/40105060.txt new file mode 100644 index 0000000000..0b36faff1e --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Die wichtigste Änderung in dieser Version: Neues Anhangauswahl-UI. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases From ab1c720a19eb58df3bc6264fc3b0fe40fccf172a Mon Sep 17 00:00:00 2001 From: Glandos Date: Sat, 5 Nov 2022 13:34:45 +0000 Subject: [PATCH 145/215] Translated using Weblate (French) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fr/ --- fastlane/metadata/android/fr-FR/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40105060.txt diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105060.txt b/fastlane/metadata/android/fr-FR/changelogs/40105060.txt new file mode 100644 index 0000000000..b33f290d0d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : nouvelle interface de sélection d’une pièce jointe. +Intégralité des changements : https://github.com/vector-im/element-android/releases From 89554b7f9ece1610f7d0e89373b0aa45e4d188b9 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Fri, 4 Nov 2022 21:32:44 +0000 Subject: [PATCH 146/215] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/pt_BR/ --- fastlane/metadata/android/pt-BR/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40105060.txt diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105060.txt b/fastlane/metadata/android/pt-BR/changelogs/40105060.txt new file mode 100644 index 0000000000..108a8a88b4 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: novo UI para selecionar um anexo. +Changelog completo: https://github.com/vector-im/element-android/releases From 0d002dbd26e13187d87ff0ce93dfe0518bf8defc Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 4 Nov 2022 17:19:19 +0000 Subject: [PATCH 147/215] Translated using Weblate (Slovak) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/ --- fastlane/metadata/android/sk/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sk/changelogs/40105060.txt diff --git a/fastlane/metadata/android/sk/changelogs/40105060.txt b/fastlane/metadata/android/sk/changelogs/40105060.txt new file mode 100644 index 0000000000..0d1d4965ca --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: nové používateľské rozhranie na výber príloh. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases From cfbbfc6cb5f1d8c002bd084995f831e5fb165314 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 4 Nov 2022 19:19:22 +0000 Subject: [PATCH 148/215] Translated using Weblate (Ukrainian) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/ --- fastlane/metadata/android/uk/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/40105060.txt diff --git a/fastlane/metadata/android/uk/changelogs/40105060.txt b/fastlane/metadata/android/uk/changelogs/40105060.txt new file mode 100644 index 0000000000..4be635901f --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: новий інтерфейс для вибору вкладення. +Перелік усіх змін: https://github.com/vector-im/element-android/releases From 54259f2f40eb8b71e3e1e052d384894d8645a35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 6 Nov 2022 07:16:40 +0000 Subject: [PATCH 149/215] Translated using Weblate (Estonian) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/et/ --- fastlane/metadata/android/et/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/40105060.txt diff --git a/fastlane/metadata/android/et/changelogs/40105060.txt b/fastlane/metadata/android/et/changelogs/40105060.txt new file mode 100644 index 0000000000..d5606e24b3 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: uus liides manuste lisamiseks. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases From 9bb2157477a4213f9554bf315d58e727ae0c7a7d Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 7 Nov 2022 02:30:36 +0000 Subject: [PATCH 150/215] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/ --- fastlane/metadata/android/zh-TW/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-TW/changelogs/40105060.txt diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105060.txt b/fastlane/metadata/android/zh-TW/changelogs/40105060.txt new file mode 100644 index 0000000000..56667ccfc0 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105060.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:選取附件的新使用者介面。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases From 1d3e61aa53f2fc4dfb43d3583d158850ed4afafc Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Fri, 4 Nov 2022 14:46:17 +0000 Subject: [PATCH 151/215] Translated using Weblate (Czech) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/ --- fastlane/metadata/android/cs-CZ/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/40105060.txt diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt new file mode 100644 index 0000000000..e966dbbd92 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: nové uživatelské rozhraní pro výběr přílohy. +Úplný seznam změn: https://github.com/vector-im/element-android/releases From 8dbd170b760a1a48aaa4036d53788e73eb230c31 Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 5 Nov 2022 10:22:29 +0000 Subject: [PATCH 152/215] Translated using Weblate (Indonesian) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/id/ --- fastlane/metadata/android/id/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/id/changelogs/40105060.txt diff --git a/fastlane/metadata/android/id/changelogs/40105060.txt b/fastlane/metadata/android/id/changelogs/40105060.txt new file mode 100644 index 0000000000..32fb87563e --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Antarmuka baru untuk memilih sebuah lampiran. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases From 2796f1b0be0d6870561298b7dd4175a10db0ea30 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sun, 6 Nov 2022 16:36:25 +0000 Subject: [PATCH 153/215] Translated using Weblate (Albanian) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/ --- fastlane/metadata/android/sq/changelogs/40104120.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104130.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104140.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104160.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104180.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104190.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104200.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104220.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104230.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104240.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104250.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104260.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104270.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104280.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104300.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104310.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104320.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104340.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104360.txt | 3 +++ fastlane/metadata/android/sq/changelogs/40105000.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40105020.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40105040.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40105060.txt | 2 ++ 23 files changed, 47 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/40104120.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104130.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104140.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104160.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104180.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104190.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104200.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104220.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104230.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104240.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104250.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104260.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104270.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104280.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104300.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104310.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104320.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104340.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104360.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40105000.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40105020.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40105040.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40105060.txt diff --git a/fastlane/metadata/android/sq/changelogs/40104120.txt b/fastlane/metadata/android/sq/changelogs/40104120.txt new file mode 100644 index 0000000000..f93220235b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: U lejon përdoruesve të shfaqen si jo në linjë dhe shton një lojtës audio për bashkëngjitje audio +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104130.txt b/fastlane/metadata/android/sq/changelogs/40104130.txt new file mode 100644 index 0000000000..f93220235b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104130.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: U lejon përdoruesve të shfaqen si jo në linjë dhe shton një lojtës audio për bashkëngjitje audio +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104140.txt b/fastlane/metadata/android/sq/changelogs/40104140.txt new file mode 100644 index 0000000000..c8b2eb09ab --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Përmirësim i administrimit të përdoruesve të shpërfillur. Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104160.txt b/fastlane/metadata/android/sq/changelogs/40104160.txt new file mode 100644 index 0000000000..987197f0f6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104160.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Administrim më i mirë i mesazheve të fshehtëzuar. Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104180.txt b/fastlane/metadata/android/sq/changelogs/40104180.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104180.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104190.txt b/fastlane/metadata/android/sq/changelogs/40104190.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104190.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104200.txt b/fastlane/metadata/android/sq/changelogs/40104200.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104200.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104220.txt b/fastlane/metadata/android/sq/changelogs/40104220.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104230.txt b/fastlane/metadata/android/sq/changelogs/40104230.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104240.txt b/fastlane/metadata/android/sq/changelogs/40104240.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104250.txt b/fastlane/metadata/android/sq/changelogs/40104250.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104260.txt b/fastlane/metadata/android/sq/changelogs/40104260.txt new file mode 100644 index 0000000000..c5ffad38c9 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Përdorim i UnifiedPush dhe lejim i përdoruesve të kenë push pa FCM. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104270.txt b/fastlane/metadata/android/sq/changelogs/40104270.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104280.txt b/fastlane/metadata/android/sq/changelogs/40104280.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104280.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104300.txt b/fastlane/metadata/android/sq/changelogs/40104300.txt new file mode 100644 index 0000000000..6c1be8f556 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104300.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Bërje e mundur hapash të përmirësuar hyrje dhe dalje nga llogaria. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104310.txt b/fastlane/metadata/android/sq/changelogs/40104310.txt new file mode 100644 index 0000000000..6c1be8f556 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104310.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Bërje e mundur hapash të përmirësuar hyrje dhe dalje nga llogaria. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104320.txt b/fastlane/metadata/android/sq/changelogs/40104320.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104320.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104340.txt b/fastlane/metadata/android/sq/changelogs/40104340.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104340.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104360.txt b/fastlane/metadata/android/sq/changelogs/40104360.txt new file mode 100644 index 0000000000..ef9251a497 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Skema e re e Aplikacionit mund të aktivizohet që nga rregullimet Labs. Ju lutemi, provojeni! +Ndreqje problemesh me njoftim që mungon dhe njëkohësim i gjatë shtues. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105000.txt b/fastlane/metadata/android/sq/changelogs/40105000.txt new file mode 100644 index 0000000000..2ee2ded823 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105000.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Hedhje poshtë MD e aktivizuar, si parazgjedhje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105020.txt b/fastlane/metadata/android/sq/changelogs/40105020.txt new file mode 100644 index 0000000000..26647d519f --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105020.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Skema e re e aplikacionit e aktivizuar, si parazgjedhje! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105040.txt b/fastlane/metadata/android/sq/changelogs/40105040.txt new file mode 100644 index 0000000000..4e38434f89 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105040.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Veçori të reja nën rregullimet Labs: hartues teksti të pasur, administrim i ri pajisjesh, transmetim zanor. Ende nën zhvillim aktivt! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105060.txt b/fastlane/metadata/android/sq/changelogs/40105060.txt new file mode 100644 index 0000000000..eb300bafed --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndërfaqe e re UI për përzgjedhjen e një bashkëngjitjeje! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases From c74445cf5bb0c5ad67e1b0c20aeb4011abf5e0b4 Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Thu, 3 Nov 2022 10:13:05 +0000 Subject: [PATCH 154/215] Update PR automation Stop using deprecated ProjectNext API in favour of the new ProjectV2 one --- .github/workflows/triage-move-review-requests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml index 61f1f114dd..6aeba66ccc 100644 --- a/.github/workflows/triage-move-review-requests.yml +++ b/.github/workflows/triage-move-review-requests.yml @@ -60,8 +60,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -129,8 +129,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } From 06538276d9948dd467cd1bce88a7c11d598af6f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:04:44 +0000 Subject: [PATCH 155/215] Bump kotlin-gradle-plugin from 1.7.20 to 1.7.21 Bumps [kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) from 1.7.20 to 1.7.21. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/commits) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 751cba4d44..cc5cb73586 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -8,7 +8,7 @@ ext.versions = [ def gradle = "7.3.1" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.7.20" +def kotlin = "1.7.21" def kotlinCoroutines = "1.6.4" def dagger = "2.44" def appDistribution = "16.0.0-beta05" From 25d33e9b1aebf4c39e66cd89e8ff554bc7010d4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:11:03 +0000 Subject: [PATCH 156/215] Bump kotlin-reflect from 1.7.20 to 1.7.21 Bumps [kotlin-reflect](https://github.com/JetBrains/kotlin) from 1.7.20 to 1.7.21. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/commits) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vector-app/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index f793fff2c8..612bda56c0 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -412,7 +412,7 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.20" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21" debugImplementation libs.androidx.fragmentTesting debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' } diff --git a/vector/build.gradle b/vector/build.gradle index 1ce78520f2..c35f21163f 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -331,5 +331,5 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.20" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21" } From cd08b8134cd3153c6cef2f6621fa31abe25aefdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:12:24 +0000 Subject: [PATCH 157/215] Bump orchestrator from 1.4.1 to 1.4.2 Bumps orchestrator from 1.4.1 to 1.4.2. --- updated-dependencies: - dependency-name: androidx.test:orchestrator dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 751cba4d44..f3b9c5cc2e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -32,7 +32,7 @@ def fragment = "1.5.4" def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.4.0" def androidxTest = "1.4.0" -def androidxOrchestrator = "1.4.1" +def androidxOrchestrator = "1.4.2" def paparazzi = "1.1.0" ext.libs = [ From ba6d414f67483bc13001409e13d54f25e84dd8ba Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 9 Nov 2022 16:59:02 +0300 Subject: [PATCH 158/215] Code review fix. --- library/ui-strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 370005363d..5a37cc7f7c 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3375,7 +3375,7 @@ Verified sessions Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. - Verified sessions are anywhere you are using ${app_name} after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. + Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. Renaming sessions Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. Enable new session manager From 823e7bf212058540a31e61bc62a6b770d5b23392 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Nov 2022 15:26:39 +0100 Subject: [PATCH 159/215] Fix search tests. --- .../matrix/android/sdk/session/search/SearchMessagesTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index 6ef90193d8..66929dcf31 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -43,7 +43,7 @@ class SearchMessagesTest : InstrumentedTest { cryptoTestData.firstSession .searchService() .search( - searchTerm = "lore", + searchTerm = "lorem", limit = 10, includeProfile = true, afterLimit = 0, @@ -61,7 +61,7 @@ class SearchMessagesTest : InstrumentedTest { cryptoTestData.firstSession .searchService() .search( - searchTerm = "lore", + searchTerm = "lorem", roomId = cryptoTestData.roomId, limit = 10, includeProfile = true, From d07c6da3ac4b2ecb996b1e588af81b4392e1fd0a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Nov 2022 15:31:27 +0100 Subject: [PATCH 160/215] Add a test for incomplete word. --- .../sdk/session/search/SearchMessagesTest.kt | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index 66929dcf31..81351523e9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.session.search +import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert.assertTrue import org.junit.FixMethodOrder import org.junit.Test @@ -73,7 +74,28 @@ class SearchMessagesTest : InstrumentedTest { } } - private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + @Test + fun sendTextMessageAndSearchPartOfItIncompleteWord() { + doTest(expectedNumberOfResult = 0) { cryptoTestData -> + cryptoTestData.firstSession + .searchService() + .search( + searchTerm = "lore", /* incomplete word */ + roomId = cryptoTestData.roomId, + limit = 10, + includeProfile = true, + afterLimit = 0, + beforeLimit = 10, + orderByRecent = true, + nextBatch = null + ) + } + } + + private fun doTest( + expectedNumberOfResult: Int = 2, + block: suspend (CryptoTestData) -> SearchResult, + ) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -87,7 +109,7 @@ class SearchMessagesTest : InstrumentedTest { val data = block.invoke(cryptoTestData) - assertTrue(data.results?.size == 2) + data.results?.size shouldBeEqualTo expectedNumberOfResult assertTrue( data.results ?.all { From 40e960f19e23201fc83e11b24f800e38daa5ccd6 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 9 Nov 2022 20:41:53 +0300 Subject: [PATCH 161/215] Lint fix. --- library/ui-strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index b8fc5e4fbd..f05a2a11e6 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3379,7 +3379,7 @@ Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account. Verified sessions - Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. + Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. Renaming sessions Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. From 02c16d30f41ddc3fa43ff31cf36de19b6ef44e6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Nov 2022 23:03:07 +0000 Subject: [PATCH 162/215] Bump com.google.devtools.ksp from 1.7.20-1.0.7 to 1.7.21-1.0.8 Bumps [com.google.devtools.ksp](https://github.com/google/ksp) from 1.7.20-1.0.7 to 1.7.21-1.0.8. - [Release notes](https://github.com/google/ksp/releases) - [Commits](https://github.com/google/ksp/compare/1.7.20-1.0.7...1.7.21-1.0.8) --- updated-dependencies: - dependency-name: com.google.devtools.ksp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7e7da48295..5e5db64834 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ plugins { // Detekt id "io.gitlab.arturbosch.detekt" version "1.21.0" // Ksp - id "com.google.devtools.ksp" version "1.7.20-1.0.7" + id "com.google.devtools.ksp" version "1.7.21-1.0.8" // Dependency Analysis id 'com.autonomousapps.dependency-analysis' version "1.13.1" From e84c68495ff721c3e850ee970d3f9196e6cfa597 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Nov 2022 23:03:47 +0000 Subject: [PATCH 163/215] Bump posthog from 1.1.2 to 2.0.0 Bumps [posthog](https://github.com/PostHog/posthog-android) from 1.1.2 to 2.0.0. - [Release notes](https://github.com/PostHog/posthog-android/releases) - [Changelog](https://github.com/PostHog/posthog-android/blob/master/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-android/compare/1.1.2...2.0.0) --- updated-dependencies: - dependency-name: com.posthog.android:posthog dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 1ce78520f2..76518b4380 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -233,7 +233,7 @@ dependencies { kapt libs.dagger.hiltCompiler // Analytics - implementation('com.posthog.android:posthog:1.1.2') { + implementation('com.posthog.android:posthog:2.0.0') { exclude group: 'com.android.support', module: 'support-annotations' } implementation libs.sentry.sentryAndroid From 41ab29d4c008c7abd61db0f6cdc860cf01e894e9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 2 Nov 2022 17:17:44 +0100 Subject: [PATCH 164/215] Adding changelog entry --- changelog.d/7512.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7512.feature diff --git a/changelog.d/7512.feature b/changelog.d/7512.feature new file mode 100644 index 0000000000..00411a75ad --- /dev/null +++ b/changelog.d/7512.feature @@ -0,0 +1 @@ +Push notifications toggle: align implementation for current session From 2941cfa329519de3248e437f7e92de3c5d7ab802 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 11:39:56 +0100 Subject: [PATCH 165/215] Adding use cases to handle toggle of push notifications for current session --- .../src/main/res/values/strings.xml | 3 +- .../vector/app/core/pushers/PushersManager.kt | 6 -- ...leNotificationsForCurrentSessionUseCase.kt | 44 ++++++++++++ ...leNotificationsForCurrentSessionUseCase.kt | 72 +++++++++++++++++++ ...rSettingsNotificationPreferenceFragment.kt | 32 +++------ 5 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f05a2a11e6..372692770e 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -1679,7 +1679,8 @@ Create New Room Create New Space No network. Please check your Internet connection. - Something went wrong. Please check your network connection and try again. + + Something went wrong. Please check your network connection and try again. "Change network" "Please wait…" Updating your data… diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index cda6f5bae8..6f186262fc 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -97,12 +97,6 @@ class PushersManager @Inject constructor( return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } } - suspend fun togglePusherForCurrentSession(enable: Boolean) { - val session = activeSessionHolder.getSafeActiveSession() ?: return - val pusher = getPusherForCurrentSession() ?: return - session.pushersService().togglePusher(pusher, enable) - } - suspend fun unregisterEmailPusher(email: String) { val currentSession = activeSessionHolder.getSafeActiveSession() ?: return currentSession.pushersService().removeEmailPusher(email) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt new file mode 100644 index 0000000000..8962e8d67d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.settings.notifications + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import javax.inject.Inject + +class DisableNotificationsForCurrentSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val unifiedPushHelper: UnifiedPushHelper, + private val pushersManager: PushersManager, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val togglePushNotificationUseCase: TogglePushNotificationUseCase, +) { + + // TODO add unit tests + suspend fun execute() { + val session = activeSessionHolder.getSafeActiveSession() ?: return + val deviceId = session.sessionParams.deviceId ?: return + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + togglePushNotificationUseCase.execute(deviceId, enabled = false) + } else { + unifiedPushHelper.unregister(pushersManager) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt new file mode 100644 index 0000000000..ef37f67bef --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 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.settings.notifications + +import androidx.fragment.app.FragmentActivity +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.pushers.FcmHelper +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class EnableNotificationsForCurrentSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val unifiedPushHelper: UnifiedPushHelper, + private val pushersManager: PushersManager, + private val fcmHelper: FcmHelper, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val togglePushNotificationUseCase: TogglePushNotificationUseCase, +) { + + // TODO add unit tests + suspend fun execute(fragmentActivity: FragmentActivity) { + val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() + if (pusherForCurrentSession == null) { + registerPusher(fragmentActivity) + } + + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + val session = activeSessionHolder.getSafeActiveSession() ?: return + val deviceId = session.sessionParams.deviceId ?: return + togglePushNotificationUseCase.execute(deviceId, enabled = true) + } + } + + private suspend fun registerPusher(fragmentActivity: FragmentActivity) { + suspendCoroutine { continuation -> + try { + unifiedPushHelper.register(fragmentActivity) { + if (unifiedPushHelper.isEmbeddedDistributor()) { + fcmHelper.ensureFcmTokenIsRetrieved( + fragmentActivity, + pushersManager, + registerPusher = true + ) + } + continuation.resume(Unit) + } + } catch (error: Exception) { + continuation.resumeWithException(error) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index f800c518f3..4a43a20de3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -57,7 +57,6 @@ import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session @@ -81,6 +80,8 @@ class VectorSettingsNotificationPreferenceFragment : @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var vectorFeatures: VectorFeatures @Inject lateinit var notificationPermissionManager: NotificationPermissionManager + @Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase + @Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase override var titleRes: Int = R.string.settings_notifications override val preferenceXmlRes = R.xml.vector_settings_notifications @@ -126,28 +127,12 @@ class VectorSettingsNotificationPreferenceFragment : it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { - unifiedPushHelper.register(requireActivity()) { - // Update the summary - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved( - requireActivity(), - pushersManager, - vectorPreferences.areNotificationEnabledForDevice() - ) - } - findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) - ?.summary = unifiedPushHelper.getCurrentDistributorName() - lifecycleScope.launch { - val result = runCatching { - pushersManager.togglePusherForCurrentSession(true) - } + enableNotificationsForCurrentSessionUseCase.execute(requireActivity()) - result.exceptionOrNull()?.let { _ -> - Toast.makeText(context, R.string.error_check_network, Toast.LENGTH_SHORT).show() - it.isChecked = false - } - } - } + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) + ?.summary = unifiedPushHelper.getCurrentDistributorName() + + // TODO test with API 33 notificationPermissionManager.eventuallyRequestPermission( requireActivity(), postPermissionLauncher, @@ -155,8 +140,7 @@ class VectorSettingsNotificationPreferenceFragment : ignorePreference = true ) } else { - unifiedPushHelper.unregister(pushersManager) - session.pushersService().refreshPushers() + disableNotificationsForCurrentSessionUseCase.execute() notificationPermissionManager.eventuallyRevokePermission(requireActivity()) } } From 67d2a6faab0979c48e1b9d9f43de8565d06e0635 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 16:31:41 +0100 Subject: [PATCH 166/215] Use the preference value to render the push notifications toggle --- ...rSettingsNotificationPreferenceFragment.kt | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 4a43a20de3..58f86bc949 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -120,31 +120,25 @@ class VectorSettingsNotificationPreferenceFragment : (pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel } - findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { - pushersManager.getPusherForCurrentSession()?.let { pusher -> - it.isChecked = pusher.enabled - } + findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + ?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> + if (isChecked) { + enableNotificationsForCurrentSessionUseCase.execute(requireActivity()) - it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> - if (isChecked) { - enableNotificationsForCurrentSessionUseCase.execute(requireActivity()) + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) + ?.summary = unifiedPushHelper.getCurrentDistributorName() - findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) - ?.summary = unifiedPushHelper.getCurrentDistributorName() - - // TODO test with API 33 - notificationPermissionManager.eventuallyRequestPermission( - requireActivity(), - postPermissionLauncher, - showRationale = false, - ignorePreference = true - ) - } else { - disableNotificationsForCurrentSessionUseCase.execute() - notificationPermissionManager.eventuallyRevokePermission(requireActivity()) + notificationPermissionManager.eventuallyRequestPermission( + requireActivity(), + postPermissionLauncher, + showRationale = false, + ignorePreference = true + ) + } else { + disableNotificationsForCurrentSessionUseCase.execute() + notificationPermissionManager.eventuallyRevokePermission(requireActivity()) + } } - } - } findPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { From 24a5cfa9e54fed6b9d9aaebd5f9471222e883786 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 16:33:16 +0100 Subject: [PATCH 167/215] Listen for pusher or account data changes to update the local setting --- .../HomeServerCapabilitiesService.kt | 8 +++ .../DefaultHomeServerCapabilitiesService.kt | 6 +++ .../HomeServerCapabilitiesDataSource.kt | 17 +++++- .../vector/app/core/di/ActiveSessionHolder.kt | 1 + .../EnableNotificationsSettingUpdater.kt | 41 ++++++++++++++ ...ableNotificationsSettingOnChangeUseCase.kt | 53 +++++++++++++++++++ .../ConfigureAndStartSessionUseCase.kt | 4 ++ ...TogglePushNotificationsViaPusherUseCase.kt | 36 +++++++++++++ ...ePushNotificationsViaAccountDataUseCase.kt | 15 +++--- ...TogglePushNotificationsViaPusherUseCase.kt | 19 +++---- .../GetNotificationsStatusUseCase.kt | 34 ++++++------ .../TogglePushNotificationUseCase.kt | 4 +- .../v2/overview/SessionOverviewViewModel.kt | 8 +-- ...leNotificationsForCurrentSessionUseCase.kt | 2 +- ...leNotificationsForCurrentSessionUseCase.kt | 4 +- 15 files changed, 206 insertions(+), 46 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt create mode 100644 vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt index 9d2c48e194..c65a5382fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.api.session.homeserver +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.util.Optional + /** * This interface defines a method to retrieve the homeserver capabilities. */ @@ -30,4 +33,9 @@ interface HomeServerCapabilitiesService { * Get the HomeServer capabilities. */ fun getHomeServerCapabilities(): HomeServerCapabilities + + /** + * Get a LiveData on the HomeServer capabilities. + */ + fun getHomeServerCapabilitiesLive(): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt index 4c755b54b5..eb9e862de2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.internal.session.homeserver +import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.util.Optional import javax.inject.Inject internal class DefaultHomeServerCapabilitiesService @Inject constructor( @@ -33,4 +35,8 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor( return homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities() } + + override fun getHomeServerCapabilitiesLive(): LiveData> { + return homeServerCapabilitiesDataSource.getHomeServerCapabilitiesLive() + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt index 6c913fa41e..beb1e67e40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt @@ -16,9 +16,14 @@ package org.matrix.android.sdk.internal.session.homeserver +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import io.realm.Realm +import io.realm.kotlin.where import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity import org.matrix.android.sdk.internal.database.query.get @@ -26,7 +31,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import javax.inject.Inject internal class HomeServerCapabilitiesDataSource @Inject constructor( - @SessionDatabase private val monarchy: Monarchy + @SessionDatabase private val monarchy: Monarchy, ) { fun getHomeServerCapabilities(): HomeServerCapabilities? { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> @@ -35,4 +40,14 @@ internal class HomeServerCapabilitiesDataSource @Inject constructor( } } } + + fun getHomeServerCapabilitiesLive(): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> realm.where() }, + { HomeServerCapabilitiesMapper.map(it) } + ) + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } } diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 7e4f73e7a5..1e9f080303 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -19,6 +19,7 @@ package im.vector.app.core.di import android.content.Context import im.vector.app.ActiveSessionDataSource import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.notification.EnableNotificationsSettingUpdater import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.session.ConfigureAndStartSessionUseCase diff --git a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt new file mode 100644 index 0000000000..21febaee9d --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 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.core.notification + +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class EnableNotificationsSettingUpdater @Inject constructor( + private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase, +) { + + private var job: Job? = null + + fun onSessionsStarted(session: Session) { + job?.cancel() + job = session.coroutineScope.launch { + updateEnableNotificationsSettingOnChangeUseCase.execute(session) + .launchIn(this) + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt new file mode 100644 index 0000000000..a6bcf17b5c --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 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.core.notification + +import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +/** + * Listen for changes in either Pusher or Account data to update the local enable notifications + * setting for the current device. + */ +class UpdateEnableNotificationsSettingOnChangeUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, +) { + + // TODO add unit tests + fun execute(session: Session): Flow { + val deviceId = session.sessionParams.deviceId ?: return emptyFlow() + return getNotificationsStatusUseCase.execute(session, deviceId) + .onEach(::updatePreference) + } + + private fun updatePreference(notificationStatus: NotificationsStatus) { + Timber.d("updatePreference with status=$notificationStatus") + when (notificationStatus) { + NotificationsStatus.ENABLED -> vectorPreferences.setNotificationEnabledForDevice(true) + NotificationsStatus.DISABLED -> vectorPreferences.setNotificationEnabledForDevice(false) + else -> Unit + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index a5e1fe68bd..00dc1ab5f9 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -19,6 +19,7 @@ package im.vector.app.core.session import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.notification.EnableNotificationsSettingUpdater import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.settings.VectorPreferences @@ -32,8 +33,10 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val webRtcCallManager: WebRtcCallManager, private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase, private val vectorPreferences: VectorPreferences, + private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, ) { + // TODO update unit tests suspend fun execute(session: Session, startSyncing: Boolean = true) { Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing") session.open() @@ -46,5 +49,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor( if (vectorPreferences.isClientInfoRecordingEnabled()) { updateMatrixClientInfoUseCase.execute(session) } + enableNotificationsSettingUpdater.onSessionsStarted(session) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt new file mode 100644 index 0000000000..963768ca04 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +import androidx.lifecycle.asFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.flow.unwrap +import javax.inject.Inject + +class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { + + fun execute(session: Session): Flow { + return session + .homeServerCapabilitiesService() + .getHomeServerCapabilitiesLive() + .asFlow() + .unwrap() + .map { it.canRemotelyTogglePushNotificationsOfDevices } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt index dbf9adca14..194a2aebbf 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -16,18 +16,15 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import javax.inject.Inject -class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, -) { +class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { - fun execute(deviceId: String): Boolean { - return activeSessionHolder - .getSafeActiveSession() - ?.accountDataService() - ?.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null + fun execute(session: Session, deviceId: String): Boolean { + return session + .accountDataService() + .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt index 0d5bce663a..ca314bf145 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt @@ -16,20 +16,15 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.core.di.ActiveSessionHolder -import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, -) { +class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { - fun execute(): Boolean { - return activeSessionHolder - .getSafeActiveSession() - ?.homeServerCapabilitiesService() - ?.getHomeServerCapabilities() - ?.canRemotelyTogglePushNotificationsOfDevices - .orFalse() + fun execute(session: Session): Boolean { + return session + .homeServerCapabilitiesService() + .getHomeServerCapabilities() + .canRemotelyTogglePushNotificationsOfDevices } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 69659bf23f..03e4e31f2e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -16,12 +16,13 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.flow.flow @@ -29,16 +30,13 @@ import org.matrix.android.sdk.flow.unwrap import javax.inject.Inject class GetNotificationsStatusUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, - private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase, private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - fun execute(deviceId: String): Flow { - val session = activeSessionHolder.getSafeActiveSession() + fun execute(session: Session, deviceId: String): Flow { return when { - session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED) - checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { + checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { session.flow() .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) .unwrap() @@ -46,15 +44,19 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } .distinctUntilChanged() } - checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { - session.flow() - .livePushers() - .map { it.filter { pusher -> pusher.deviceId == deviceId } } - .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } - .distinctUntilChanged() - } - else -> flowOf(NotificationsStatus.NOT_SUPPORTED) + else -> canTogglePushNotificationsViaPusherUseCase.execute(session) + .flatMapLatest { canToggle -> + if (canToggle) { + session.flow() + .livePushers() + .map { it.filter { pusher -> pusher.deviceId == deviceId } } + .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() + } else { + flowOf(NotificationsStatus.NOT_SUPPORTED) + } + } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index be9012e9f1..7969bbbe9b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -31,14 +31,14 @@ class TogglePushNotificationUseCase @Inject constructor( suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } devicePusher?.let { pusher -> session.pushersService().togglePusher(pusher, enabled) } } - if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId)) { + if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) session.accountDataService().updateUserAccountData( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 9c4ece7e02..a56872e648 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -96,9 +96,11 @@ class SessionOverviewViewModel @AssistedInject constructor( } private fun observeNotificationsStatus(deviceId: String) { - getNotificationsStatusUseCase.execute(deviceId) - .onEach { setState { copy(notificationsStatus = it) } } - .launchIn(viewModelScope) + activeSessionHolder.getSafeActiveSession()?.let { session -> + getNotificationsStatusUseCase.execute(session, deviceId) + .onEach { setState { copy(notificationsStatus = it) } } + .launchIn(viewModelScope) + } } override fun handle(action: SessionOverviewAction) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 8962e8d67d..d66bf8b789 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -35,7 +35,7 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor( suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { togglePushNotificationUseCase.execute(deviceId, enabled = false) } else { unifiedPushHelper.unregister(pushersManager) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index ef37f67bef..cee653380a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -44,8 +44,8 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( registerPusher(fragmentActivity) } - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { - val session = activeSessionHolder.getSafeActiveSession() ?: return + val session = activeSessionHolder.getSafeActiveSession() ?: return + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { val deviceId = session.sessionParams.deviceId ?: return togglePushNotificationUseCase.execute(deviceId, enabled = true) } From 6239b3e68618cbbdc9cf18f6afa8cbdda483ec30 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 17:48:49 +0100 Subject: [PATCH 168/215] Adding some TODOs --- .../notification/CanTogglePushNotificationsViaPusherUseCase.kt | 1 + .../CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt | 1 + .../CheckIfCanTogglePushNotificationsViaPusherUseCase.kt | 1 + .../devices/v2/notification/GetNotificationsStatusUseCase.kt | 1 + .../devices/v2/notification/TogglePushNotificationUseCase.kt | 1 + 5 files changed, 5 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt index 963768ca04..af15e2f349 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt @@ -25,6 +25,7 @@ import javax.inject.Inject class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { + // TODO add unit tests fun execute(session: Session): Flow { return session .homeServerCapabilitiesService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt index 194a2aebbf..85abd7cd35 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -22,6 +22,7 @@ import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { + // TODO update unit tests fun execute(session: Session, deviceId: String): Boolean { return session .accountDataService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt index ca314bf145..9c2a471120 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt @@ -21,6 +21,7 @@ import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { + // TODO update unit tests fun execute(session: Session): Boolean { return session .homeServerCapabilitiesService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 03e4e31f2e..ed1d54e118 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -34,6 +34,7 @@ class GetNotificationsStatusUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { + // TODO update unit tests fun execute(session: Session, deviceId: String): Flow { return when { checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index 7969bbbe9b..28018f9687 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -28,6 +28,7 @@ class TogglePushNotificationUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { + // TODO update unit tests suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return From 18929324fee53d6fc81a4c252a65534c073c25b4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 10:28:24 +0100 Subject: [PATCH 169/215] Updating existing unit tests --- .../vector/app/core/di/ActiveSessionHolder.kt | 1 - .../ConfigureAndStartSessionUseCase.kt | 1 - ...ePushNotificationsViaAccountDataUseCase.kt | 1 - ...TogglePushNotificationsViaPusherUseCase.kt | 1 - .../GetNotificationsStatusUseCase.kt | 1 - .../TogglePushNotificationUseCase.kt | 1 - .../app/core/pushers/PushersManagerTest.kt | 16 ------ .../ConfigureAndStartSessionUseCaseTest.kt | 6 +++ ...hNotificationsViaAccountDataUseCaseTest.kt | 18 +++---- ...lePushNotificationsViaPusherUseCaseTest.kt | 25 ++------- .../GetNotificationsStatusUseCaseTest.kt | 51 +++++++------------ .../TogglePushNotificationUseCaseTest.kt | 18 ++++--- .../overview/SessionOverviewViewModelTest.kt | 9 ++-- .../FakeEnableNotificationsSettingUpdater.kt | 31 +++++++++++ 14 files changed, 82 insertions(+), 98 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 1e9f080303..7e4f73e7a5 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -19,7 +19,6 @@ package im.vector.app.core.di import android.content.Context import im.vector.app.ActiveSessionDataSource import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.notification.EnableNotificationsSettingUpdater import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.session.ConfigureAndStartSessionUseCase diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index 00dc1ab5f9..71863b8642 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -36,7 +36,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, ) { - // TODO update unit tests suspend fun execute(session: Session, startSyncing: Boolean = true) { Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing") session.open() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt index 85abd7cd35..194a2aebbf 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -22,7 +22,6 @@ import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { - // TODO update unit tests fun execute(session: Session, deviceId: String): Boolean { return session .accountDataService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt index 9c2a471120..ca314bf145 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt @@ -21,7 +21,6 @@ import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { - // TODO update unit tests fun execute(session: Session): Boolean { return session .homeServerCapabilitiesService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index ed1d54e118..03e4e31f2e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -34,7 +34,6 @@ class GetNotificationsStatusUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - // TODO update unit tests fun execute(session: Session, deviceId: String): Flow { return when { checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index 28018f9687..7969bbbe9b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -28,7 +28,6 @@ class TogglePushNotificationUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - // TODO update unit tests suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt index 113a810ac2..7a1833e057 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt @@ -29,7 +29,6 @@ import im.vector.app.test.fixtures.CryptoDeviceInfoFixture.aCryptoDeviceInfo import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.fixtures.SessionParamsFixture import io.mockk.mockk -import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo @@ -101,19 +100,4 @@ class PushersManagerTest { pusher shouldBeEqualTo expectedPusher } - - @Test - fun `when togglePusherForCurrentSession, then do service toggle pusher`() = runTest { - val deviceId = "device_id" - val sessionParams = SessionParamsFixture.aSessionParams( - credentials = CredentialsFixture.aCredentials(deviceId = deviceId) - ) - session.givenSessionParams(sessionParams) - val pusher = PusherFixture.aPusher(deviceId = deviceId) - pushersService.givenGetPushers(listOf(pusher)) - - pushersManager.togglePusherForCurrentSession(true) - - pushersService.verifyTogglePusherCalled(pusher, true) - } } diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt index 8d4507e85d..861e59e0f1 100644 --- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt @@ -19,6 +19,7 @@ package im.vector.app.core.session import im.vector.app.core.extensions.startSyncing import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeWebRtcCallManager @@ -43,12 +44,14 @@ class ConfigureAndStartSessionUseCaseTest { private val fakeWebRtcCallManager = FakeWebRtcCallManager() private val fakeUpdateMatrixClientInfoUseCase = mockk() private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater() private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase( context = fakeContext.instance, webRtcCallManager = fakeWebRtcCallManager.instance, updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase, vectorPreferences = fakeVectorPreferences.instance, + enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance, ) @Before @@ -68,6 +71,7 @@ class ConfigureAndStartSessionUseCaseTest { fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) + fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) @@ -87,6 +91,7 @@ class ConfigureAndStartSessionUseCaseTest { fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false) + fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) @@ -106,6 +111,7 @@ class ConfigureAndStartSessionUseCaseTest { fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) + fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt index 0303444605..37433364e8 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt @@ -16,7 +16,7 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeSession import io.mockk.mockk import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -26,18 +26,15 @@ private const val A_DEVICE_ID = "device-id" class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { - private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeSession = FakeSession() private val checkIfCanTogglePushNotificationsViaAccountDataUseCase = - CheckIfCanTogglePushNotificationsViaAccountDataUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, - ) + CheckIfCanTogglePushNotificationsViaAccountDataUseCase() @Test fun `given current session and an account data for the device id when execute then result is true`() { // Given - fakeActiveSessionHolder - .fakeSession + fakeSession .accountDataService() .givenGetUserAccountDataEventReturns( type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, @@ -45,7 +42,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { ) // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) // Then result shouldBeEqualTo true @@ -54,8 +51,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { @Test fun `given current session and NO account data for the device id when execute then result is false`() { // Given - fakeActiveSessionHolder - .fakeSession + fakeSession .accountDataService() .givenGetUserAccountDataEventReturns( type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, @@ -63,7 +59,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { ) // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) // Then result shouldBeEqualTo false diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt index 51874be1bc..508a05acd6 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt @@ -16,7 +16,7 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fixtures.aHomeServerCapabilities import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -25,37 +25,22 @@ private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyToggl class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { - private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeSession = FakeSession() private val checkIfCanTogglePushNotificationsViaPusherUseCase = - CheckIfCanTogglePushNotificationsViaPusherUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, - ) + CheckIfCanTogglePushNotificationsViaPusherUseCase() @Test fun `given current session when execute then toggle capability is returned`() { // Given - fakeActiveSessionHolder - .fakeSession + fakeSession .fakeHomeServerCapabilitiesService .givenCapabilities(A_HOMESERVER_CAPABILITIES) // When - val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() + val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) // Then result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices } - - @Test - fun `given no current session when execute then false is returned`() { - // Given - fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) - - // When - val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() - - // Then - result shouldBeEqualTo false - } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index b13018a20d..b38367b098 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -17,7 +17,7 @@ package im.vector.app.features.settings.devices.v2.notification import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.testDispatcher import io.mockk.every @@ -25,6 +25,7 @@ import io.mockk.mockk import io.mockk.verifyOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain @@ -44,17 +45,16 @@ class GetNotificationsStatusUseCaseTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() - private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = - mockk() + private val fakeSession = FakeSession() private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = mockk() + private val fakeCanTogglePushNotificationsViaPusherUseCase = + mockk() private val getNotificationsStatusUseCase = GetNotificationsStatusUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + canTogglePushNotificationsViaPusherUseCase = fakeCanTogglePushNotificationsViaPusherUseCase, ) @Before @@ -67,33 +67,21 @@ class GetNotificationsStatusUseCaseTest { Dispatchers.resetMain() } - @Test - fun `given NO current session when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { - // Given - fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) - - // When - val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) - - // Then - result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED - } - @Test fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { // Given - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When - val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED verifyOrder { // we should first check account data - fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) - fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() + fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } } @@ -106,12 +94,12 @@ class GetNotificationsStatusUseCaseTest { enabled = true, ) ) - fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + fakeSession.pushersService().givenPushersLive(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) // When - val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED @@ -120,8 +108,7 @@ class GetNotificationsStatusUseCaseTest { @Test fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest { // Given - fakeActiveSessionHolder - .fakeSession + fakeSession .accountDataService() .givenGetUserAccountDataEventReturns( type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, @@ -129,11 +116,11 @@ class GetNotificationsStatusUseCaseTest { isSilenced = false ).toContent(), ) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true + every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When - val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt index 0a649354f9..35c5979e53 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt @@ -49,10 +49,11 @@ class TogglePushNotificationUseCaseTest { PusherFixture.aPusher(deviceId = sessionId, enabled = false), PusherFixture.aPusher(deviceId = "another id", enabled = false) ) - activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) - activeSessionHolder.fakeSession.pushersService().givenGetPushers(pushers) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns false + val fakeSession = activeSessionHolder.fakeSession + fakeSession.pushersService().givenPushersLive(pushers) + fakeSession.pushersService().givenGetPushers(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false // When togglePushNotificationUseCase.execute(sessionId, true) @@ -69,13 +70,14 @@ class TogglePushNotificationUseCaseTest { PusherFixture.aPusher(deviceId = sessionId, enabled = false), PusherFixture.aPusher(deviceId = "another id", enabled = false) ) - activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) - activeSessionHolder.fakeSession.accountDataService().givenGetUserAccountDataEventReturns( + val fakeSession = activeSessionHolder.fakeSession + fakeSession.pushersService().givenPushersLive(pushers) + fakeSession.accountDataService().givenGetUserAccountDataEventReturns( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, LocalNotificationSettingsContent(isSilenced = true).toContent() ) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns true + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true // When togglePushNotificationUseCase.execute(sessionId, true) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index f26c818e1d..444e14ec0e 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -37,6 +37,8 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.just +import io.mockk.justRun +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs @@ -98,7 +100,7 @@ class SessionOverviewViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() - every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) + every { fakeGetNotificationsStatusUseCase.execute(fakeActiveSessionHolder.fakeSession, A_SESSION_ID_1) } returns flowOf(notificationsStatus) } private fun givenVerificationService(): FakeVerificationService { @@ -412,13 +414,10 @@ class SessionOverviewViewModelTest { @Test fun `when viewModel init, then observe pushers and emit to state`() { - val notificationStatus = NotificationsStatus.ENABLED - every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationStatus) - val viewModel = createViewModel() viewModel.test() - .assertLatestState { state -> state.notificationsStatus == notificationStatus } + .assertLatestState { state -> state.notificationsStatus == notificationsStatus } .finish() } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt new file mode 100644 index 0000000000..a78dd1a34b --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import im.vector.app.core.notification.EnableNotificationsSettingUpdater +import io.mockk.justRun +import io.mockk.mockk +import org.matrix.android.sdk.api.session.Session + +class FakeEnableNotificationsSettingUpdater { + + val instance = mockk() + + fun givenOnSessionsStarted(session: Session) { + justRun { instance.onSessionsStarted(session) } + } +} From e5e971683b6faa9a85049080f743177b944cfe67 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 11:47:23 +0100 Subject: [PATCH 170/215] Adding unit tests on CanTogglePushNotificationsViaPusherUseCase --- ...TogglePushNotificationsViaPusherUseCase.kt | 1 - ...lePushNotificationsViaPusherUseCaseTest.kt | 65 +++++++++++++++++++ .../FakeHomeServerCapabilitiesService.kt | 10 +++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt index af15e2f349..963768ca04 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt @@ -25,7 +25,6 @@ import javax.inject.Inject class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { - // TODO add unit tests fun execute(session: Session): Flow { return session .homeServerCapabilitiesService() diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt new file mode 100644 index 0000000000..997fa827f5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 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.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeFlowLiveDataConversions +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.givenAsFlow +import im.vector.app.test.fixtures.aHomeServerCapabilities +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test + +private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) + +class CanTogglePushNotificationsViaPusherUseCaseTest { + + private val fakeSession = FakeSession() + private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() + + private val canTogglePushNotificationsViaPusherUseCase = + CanTogglePushNotificationsViaPusherUseCase() + + @Before + fun setUp() { + fakeFlowLiveDataConversions.setup() + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given current session when execute then flow of the toggle capability is returned`() = runTest { + // Given + fakeSession + .fakeHomeServerCapabilitiesService + .givenCapabilitiesLiveReturns(A_HOMESERVER_CAPABILITIES) + .givenAsFlow() + + // When + val result = canTogglePushNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull() + + // Then + result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt index 006789f62b..c816c51c0f 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt @@ -16,14 +16,24 @@ package im.vector.app.test.fakes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional class FakeHomeServerCapabilitiesService : HomeServerCapabilitiesService by mockk() { fun givenCapabilities(homeServerCapabilities: HomeServerCapabilities) { every { getHomeServerCapabilities() } returns homeServerCapabilities } + + fun givenCapabilitiesLiveReturns(homeServerCapabilities: HomeServerCapabilities): LiveData> { + return MutableLiveData(homeServerCapabilities.toOptional()).also { + every { getHomeServerCapabilitiesLive() } returns it + } + } } From 2eeb04426b49ab5496e5e5fce8a493795fd8505c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 14:22:50 +0100 Subject: [PATCH 171/215] Adding unit tests on DisableNotificationsForCurrentSessionUseCase --- ...leNotificationsForCurrentSessionUseCase.kt | 1 - ...tificationsForCurrentSessionUseCaseTest.kt | 78 +++++++++++++++++++ .../app/test/fakes/FakePushersManager.kt | 25 ++++++ .../app/test/fakes/FakeUnifiedPushHelper.kt | 36 +++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index d66bf8b789..61c884f0bc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -31,7 +31,6 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor( private val togglePushNotificationUseCase: TogglePushNotificationUseCase, ) { - // TODO add unit tests suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt new file mode 100644 index 0000000000..e460413a39 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022 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.settings.notifications + +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_SESSION_ID = "session-id" + +class DisableNotificationsForCurrentSessionUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakePushersManager = FakePushersManager() + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() + private val fakeTogglePushNotificationUseCase = mockk() + + private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + pushersManager = fakePushersManager.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + ) + + @Test + fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest { + // Given + val fakeSession = fakeActiveSessionHolder.fakeSession + fakeSession.givenSessionId(A_SESSION_ID) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true + coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + + // When + disableNotificationsForCurrentSessionUseCase.execute() + + // Then + coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) } + } + + @Test + fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest { + // Given + val fakeSession = fakeActiveSessionHolder.fakeSession + fakeSession.givenSessionId(A_SESSION_ID) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false + fakeUnifiedPushHelper.givenUnregister(fakePushersManager.instance) + + // When + disableNotificationsForCurrentSessionUseCase.execute() + + // Then + fakeUnifiedPushHelper.verifyUnregister(fakePushersManager.instance) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt new file mode 100644 index 0000000000..245662b0c6 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import im.vector.app.core.pushers.PushersManager +import io.mockk.mockk + +class FakePushersManager { + + val instance = mockk() +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt new file mode 100644 index 0000000000..d7985d9757 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk + +class FakeUnifiedPushHelper { + + val instance = mockk() + + fun givenUnregister(pushersManager: PushersManager) { + coJustRun { instance.unregister(pushersManager) } + } + + fun verifyUnregister(pushersManager: PushersManager) { + coVerify { instance.unregister(pushersManager) } + } +} From b43c3a8502e6a18df0f2198cbb9ebacb3f487ac6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 14:42:52 +0100 Subject: [PATCH 172/215] Adding unit tests on UpdateEnableNotificationsSettingOnChangeUseCase --- .../EnableNotificationsSettingUpdater.kt | 2 - ...ableNotificationsSettingOnChangeUseCase.kt | 11 ++- ...NotificationsSettingOnChangeUseCaseTest.kt | 90 +++++++++++++++++++ .../overview/SessionOverviewViewModelTest.kt | 12 ++- .../FakeGetNotificationsStatusUseCase.kt | 37 ++++++++ .../app/test/fakes/FakeVectorPreferences.kt | 11 ++- 6 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt diff --git a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt index 21febaee9d..81b524cde9 100644 --- a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt +++ b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt @@ -18,7 +18,6 @@ package im.vector.app.core.notification import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import javax.inject.Inject @@ -35,7 +34,6 @@ class EnableNotificationsSettingUpdater @Inject constructor( job?.cancel() job = session.coroutineScope.launch { updateEnableNotificationsSettingOnChangeUseCase.execute(session) - .launchIn(this) } } } diff --git a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt index a6bcf17b5c..55a2cfdc64 100644 --- a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt @@ -19,8 +19,7 @@ package im.vector.app.core.notification import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -35,11 +34,11 @@ class UpdateEnableNotificationsSettingOnChangeUseCase @Inject constructor( private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, ) { - // TODO add unit tests - fun execute(session: Session): Flow { - val deviceId = session.sessionParams.deviceId ?: return emptyFlow() - return getNotificationsStatusUseCase.execute(session, deviceId) + suspend fun execute(session: Session) { + val deviceId = session.sessionParams.deviceId ?: return + getNotificationsStatusUseCase.execute(session, deviceId) .onEach(::updatePreference) + .collect() } private fun updatePreference(notificationStatus: NotificationsStatus) { diff --git a/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt new file mode 100644 index 0000000000..5cced75735 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 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.core.notification + +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeVectorPreferences +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_SESSION_ID = "session-id" + +class UpdateEnableNotificationsSettingOnChangeUseCaseTest { + + private val fakeSession = FakeSession().also { it.givenSessionId(A_SESSION_ID) } + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() + + private val updateEnableNotificationsSettingOnChangeUseCase = UpdateEnableNotificationsSettingOnChangeUseCase( + vectorPreferences = fakeVectorPreferences.instance, + getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, + ) + + @Test + fun `given notifications are enabled when execute then setting is updated to true`() = runTest { + // Given + fakeGetNotificationsStatusUseCase.givenExecuteReturns( + fakeSession, + A_SESSION_ID, + NotificationsStatus.ENABLED, + ) + fakeVectorPreferences.givenSetNotificationEnabledForDevice() + + // When + updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession) + + // Then + fakeVectorPreferences.verifySetNotificationEnabledForDevice(true) + } + + @Test + fun `given notifications are disabled when execute then setting is updated to false`() = runTest { + // Given + fakeGetNotificationsStatusUseCase.givenExecuteReturns( + fakeSession, + A_SESSION_ID, + NotificationsStatus.DISABLED, + ) + fakeVectorPreferences.givenSetNotificationEnabledForDevice() + + // When + updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession) + + // Then + fakeVectorPreferences.verifySetNotificationEnabledForDevice(false) + } + + @Test + fun `given notifications toggle is not supported when execute then nothing is done`() = runTest { + // Given + fakeGetNotificationsStatusUseCase.givenExecuteReturns( + fakeSession, + A_SESSION_ID, + NotificationsStatus.NOT_SUPPORTED, + ) + fakeVectorPreferences.givenSetNotificationEnabledForDevice() + + // When + updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession) + + // Then + fakeVectorPreferences.verifySetNotificationEnabledForDevice(true, inverse = true) + fakeVectorPreferences.verifySetNotificationEnabledForDevice(false, inverse = true) + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 444e14ec0e..86a9969a6a 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -22,11 +22,11 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase -import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase @@ -77,7 +77,7 @@ class SessionOverviewViewModelTest { private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxed = true) private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() - private val fakeGetNotificationsStatusUseCase = mockk() + private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() private val notificationsStatus = NotificationsStatus.ENABLED private fun createViewModel() = SessionOverviewViewModel( @@ -90,7 +90,7 @@ class SessionOverviewViewModelTest { activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, togglePushNotificationUseCase = togglePushNotificationUseCase.instance, - getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase, + getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, ) @Before @@ -100,7 +100,11 @@ class SessionOverviewViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() - every { fakeGetNotificationsStatusUseCase.execute(fakeActiveSessionHolder.fakeSession, A_SESSION_ID_1) } returns flowOf(notificationsStatus) + fakeGetNotificationsStatusUseCase.givenExecuteReturns( + fakeActiveSessionHolder.fakeSession, + A_SESSION_ID_1, + notificationsStatus + ) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt new file mode 100644 index 0000000000..a9c1b37d69 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import org.matrix.android.sdk.api.session.Session + +class FakeGetNotificationsStatusUseCase { + + val instance = mockk() + + fun givenExecuteReturns( + session: Session, + sessionId: String, + notificationsStatus: NotificationsStatus + ) { + every { instance.execute(session, sessionId) } returns flowOf(notificationsStatus) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index cd4f70bf63..4baa7e2b90 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -18,6 +18,7 @@ package im.vector.app.test.fakes import im.vector.app.features.settings.VectorPreferences import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.verify @@ -42,5 +43,13 @@ class FakeVectorPreferences { } fun givenTextFormatting(isEnabled: Boolean) = - every { instance.isTextFormattingEnabled() } returns isEnabled + every { instance.isTextFormattingEnabled() } returns isEnabled + + fun givenSetNotificationEnabledForDevice() { + justRun { instance.setNotificationEnabledForDevice(any()) } + } + + fun verifySetNotificationEnabledForDevice(enabled: Boolean, inverse: Boolean = false) { + verify(inverse = inverse) { instance.setNotificationEnabledForDevice(enabled) } + } } From ced4bf3573a51b13ac5211980415978c686ed770 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 15:10:19 +0100 Subject: [PATCH 173/215] Adding unit tests on EnableNotificationsForCurrentSessionUseCase --- ...leNotificationsForCurrentSessionUseCase.kt | 1 - ...tificationsForCurrentSessionUseCaseTest.kt | 87 +++++++++++++++++++ .../im/vector/app/test/fakes/FakeFcmHelper.kt | 44 ++++++++++ .../app/test/fakes/FakePushersManager.kt | 6 ++ .../app/test/fakes/FakeUnifiedPushHelper.kt | 17 ++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index cee653380a..180627a15f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -37,7 +37,6 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( private val togglePushNotificationUseCase: TogglePushNotificationUseCase, ) { - // TODO add unit tests suspend fun execute(fragmentActivity: FragmentActivity) { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt new file mode 100644 index 0000000000..eb6629cb13 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 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.settings.notifications + +import androidx.fragment.app.FragmentActivity +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeFcmHelper +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_SESSION_ID = "session-id" + +class EnableNotificationsForCurrentSessionUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakePushersManager = FakePushersManager() + private val fakeFcmHelper = FakeFcmHelper() + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() + private val fakeTogglePushNotificationUseCase = mockk() + + private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + pushersManager = fakePushersManager.instance, + fcmHelper = fakeFcmHelper.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + ) + + @Test + fun `given no existing pusher for current session when execute then a new pusher is registered`() = runTest { + // Given + val fragmentActivity = mockk() + fakePushersManager.givenGetPusherForCurrentSessionReturns(null) + fakeUnifiedPushHelper.givenRegister(fragmentActivity) + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) + fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns false + + // When + enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity) + + // Then + fakeUnifiedPushHelper.verifyRegister(fragmentActivity) + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance, registerPusher = true) + } + + @Test + fun `given toggle via Pusher is possible when execute then current pusher is toggled to true`() = runTest { + // Given + val fragmentActivity = mockk() + fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk()) + val fakeSession = fakeActiveSessionHolder.fakeSession + fakeSession.givenSessionId(A_SESSION_ID) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns true + coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + + // When + enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity) + + // Then + coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, true) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt new file mode 100644 index 0000000000..11abf18794 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import androidx.fragment.app.FragmentActivity +import im.vector.app.core.pushers.FcmHelper +import im.vector.app.core.pushers.PushersManager +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify + +class FakeFcmHelper { + + val instance = mockk() + + fun givenEnsureFcmTokenIsRetrieved( + fragmentActivity: FragmentActivity, + pushersManager: PushersManager, + ) { + justRun { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, any()) } + } + + fun verifyEnsureFcmTokenIsRetrieved( + fragmentActivity: FragmentActivity, + pushersManager: PushersManager, + registerPusher: Boolean, + ) { + verify { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, registerPusher) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt index 245662b0c6..46d852f4f8 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt @@ -17,9 +17,15 @@ package im.vector.app.test.fakes import im.vector.app.core.pushers.PushersManager +import io.mockk.every import io.mockk.mockk +import org.matrix.android.sdk.api.session.pushers.Pusher class FakePushersManager { val instance = mockk() + + fun givenGetPusherForCurrentSessionReturns(pusher: Pusher?) { + every { instance.getPusherForCurrentSession() } returns pusher + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt index d7985d9757..1f2cc8a1ce 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt @@ -16,16 +16,29 @@ package im.vector.app.test.fakes +import androidx.fragment.app.FragmentActivity import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper import io.mockk.coJustRun import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk +import io.mockk.verify class FakeUnifiedPushHelper { val instance = mockk() + fun givenRegister(fragmentActivity: FragmentActivity) { + every { instance.register(fragmentActivity, any()) } answers { + secondArg().run() + } + } + + fun verifyRegister(fragmentActivity: FragmentActivity) { + verify { instance.register(fragmentActivity, any()) } + } + fun givenUnregister(pushersManager: PushersManager) { coJustRun { instance.unregister(pushersManager) } } @@ -33,4 +46,8 @@ class FakeUnifiedPushHelper { fun verifyUnregister(pushersManager: PushersManager) { coVerify { instance.unregister(pushersManager) } } + + fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) { + every { instance.isEmbeddedDistributor() } returns isEmbedded + } } From 163bf57fdaf001bd7f8366b850acad8e43fb0f4b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 8 Nov 2022 17:14:57 +0100 Subject: [PATCH 174/215] Removing non necessary debug log --- .../UpdateEnableNotificationsSettingOnChangeUseCase.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt index 55a2cfdc64..36df939bad 100644 --- a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt @@ -22,7 +22,6 @@ import im.vector.app.features.settings.devices.v2.notification.NotificationsStat import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.Session -import timber.log.Timber import javax.inject.Inject /** @@ -42,7 +41,6 @@ class UpdateEnableNotificationsSettingOnChangeUseCase @Inject constructor( } private fun updatePreference(notificationStatus: NotificationsStatus) { - Timber.d("updatePreference with status=$notificationStatus") when (notificationStatus) { NotificationsStatus.ENABLED -> vectorPreferences.setNotificationEnabledForDevice(true) NotificationsStatus.DISABLED -> vectorPreferences.setNotificationEnabledForDevice(false) From ba5a433cafc8b6ed6cdace2a526bead13c08bf34 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 8 Nov 2022 17:19:06 +0100 Subject: [PATCH 175/215] Adding distinctUntilChanged for flow of remote toggle via Pusher capability --- .../notification/CanTogglePushNotificationsViaPusherUseCase.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt index 963768ca04..0125d92ba6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt @@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2.notification import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.flow.unwrap @@ -32,5 +33,6 @@ class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { .asFlow() .unwrap() .map { it.canRemotelyTogglePushNotificationsOfDevices } + .distinctUntilChanged() } } From 6ec33f1264ecc59b26a8db8844dafacaa9f83a18 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 9 Nov 2022 09:33:39 +0100 Subject: [PATCH 176/215] Removing unused imports --- .../devices/v2/overview/SessionOverviewViewModelTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 86a9969a6a..1a57b76020 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -37,8 +37,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.just -import io.mockk.justRun -import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs From 8814117f26c8e6c099464c942f20036d9ad1d39b Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Tue, 8 Nov 2022 13:45:46 +0000 Subject: [PATCH 177/215] Translated using Weblate (Persian) Currently translated at 99.6% (2530 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/ --- .../src/main/res/values-fa/strings.xml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index f2701519e7..313734290f 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -2802,4 +2802,27 @@ ۱ گزیده %1$d گزیده + اجازه‌های لازم برای آغاز پخش صوتی در این اتاق را ندارید. برای ارتقای اجازه‌هایتان با یک مدیر اتاق تماس بگیرید. + فرد دیگری در حال ضبط یک پخش صوتی است. برای آغاز یک پخش جدید، منتظر پایان پخشش بمانید. + با بررسی افزاره‌های وارد شده‌تان باید کد زیر را ببینید. تأیید کنید که این کد با آن افزاره مطابق است: + دارید یک پخش صوتی ضبط می‌کنید. لطفاً برای آغاز یک پخش جدید، به پخش کنونی پایان دهید. + ⚠ افزاره‌های تأییدنشده‌ای در این اتاق وجود دارند. آن‌ها قادر به رمزگشایی پیام‌هایی که فرستاده‌اید نیستند. + استفاده از دوربین روی این افزاره برای پویش کد QR نشان داده شده روی افزارهٔ دیگرتان: + ضبط نام کارخواه، نگارش و نشانی برای بازشناسی آسان‌تر نشست‌ها در مدیر نشست. + 🔒 رمزگذاری به نشست‌های تأیید شده را فقط برای تمامی اتاق‌ها در تنظیمات امنیت به کار انداخته‌اید. + + خارج شدن از نشست‌های قدیمی (۱ روز یا بیش‌تر) که دیگر استفاده نمی‌کنید را در نظر داشته باشید. + خارج شدن از نشست‌های قدیمی (%1$d روز یا بیش‌تر) که دیگر استفاده نمی‌کنید را در نظر داشته باشید. + + توانایی ضبط و فرستادن پخش صدا در خط زمانی اتاق. + پویش کد QR زیر با افزاره‌ای که خارج شده. + استفاده از افزارهٔ وارد شده‌تان برای پویش کد QR زیر: + چیزی اشتباه پیش رفت. لطفاً اتّصال شبکه‌تان را بررسی و دوباره تلاش کنید. + ${app_name} برای نمایش آگاهی‌ها نیازمند اجازه است. +\nلطفاً اجازه را اعطا کنید. + نمی‌توان پخش صدایی جدید را آغاز کرد + تغییر حالت تمام‌صفحه + ۳۰ ثانیه پیش‌روی + ۳۰ ثانیه پس‌روی + قالب‌بندی متن \ No newline at end of file From 46615082a9801473626868fde5b336a707ced23b Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 9 Nov 2022 03:15:24 +0000 Subject: [PATCH 178/215] Translated using Weblate (Japanese) Currently translated at 88.4% (2246 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- .../ui-strings/src/main/res/values-ja/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/library/ui-strings/src/main/res/values-ja/strings.xml b/library/ui-strings/src/main/res/values-ja/strings.xml index 37c0bca52f..11ab6ee857 100644 --- a/library/ui-strings/src/main/res/values-ja/strings.xml +++ b/library/ui-strings/src/main/res/values-ja/strings.xml @@ -2459,4 +2459,18 @@ ルームを作成 チャットを開始 全ての会話 + ${app_name}にようこそ、 +\n%s。 + 認証済のセッション + QRコードでサインイン + 新しいセッションマネージャーを有効にする + QRコードでサインイン + 3 + 2 + 1 + リクエストが失敗しました。 + QRコードをスキャン + QRコードをスキャン + QRコードをスキャン + QRコードが不正です。 \ No newline at end of file From 218026f5df2bf2bc4247d77408ea7c1d6d0df8df Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 8 Nov 2022 03:38:31 +0000 Subject: [PATCH 179/215] Translated using Weblate (German) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/de/ --- fastlane/metadata/android/de-DE/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/40105070.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/40105070.txt b/fastlane/metadata/android/de-DE/changelogs/40105070.txt new file mode 100644 index 0000000000..3141cea7cb --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Die wichtigste Änderung in dieser Version: Neue Anhangauswahl-Oberfläche. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases From 15f946c62cc8b7a48bbd784649449bc27d8b926c Mon Sep 17 00:00:00 2001 From: Glandos Date: Wed, 9 Nov 2022 09:53:47 +0000 Subject: [PATCH 180/215] Translated using Weblate (French) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fr/ --- fastlane/metadata/android/fr-FR/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40105070.txt diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105070.txt b/fastlane/metadata/android/fr-FR/changelogs/40105070.txt new file mode 100644 index 0000000000..b33f290d0d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : nouvelle interface de sélection d’une pièce jointe. +Intégralité des changements : https://github.com/vector-im/element-android/releases From 6faec3d9bd9a13434ae9b691c3746dcd78c55e71 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Tue, 8 Nov 2022 04:05:32 +0000 Subject: [PATCH 181/215] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/pt_BR/ --- fastlane/metadata/android/pt-BR/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40105070.txt diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105070.txt b/fastlane/metadata/android/pt-BR/changelogs/40105070.txt new file mode 100644 index 0000000000..108a8a88b4 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: novo UI para selecionar um anexo. +Changelog completo: https://github.com/vector-im/element-android/releases From ba2fbf10e5d02723c87e21df68b56ec9cadf347f Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 8 Nov 2022 10:11:06 +0000 Subject: [PATCH 182/215] Translated using Weblate (Slovak) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/ --- fastlane/metadata/android/sk/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sk/changelogs/40105070.txt diff --git a/fastlane/metadata/android/sk/changelogs/40105070.txt b/fastlane/metadata/android/sk/changelogs/40105070.txt new file mode 100644 index 0000000000..0d1d4965ca --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: nové používateľské rozhranie na výber príloh. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases From ae150a262370a380c7201bde7fe3baba44e6ce02 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 8 Nov 2022 09:17:39 +0000 Subject: [PATCH 183/215] Translated using Weblate (Ukrainian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/ --- fastlane/metadata/android/uk/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/40105070.txt diff --git a/fastlane/metadata/android/uk/changelogs/40105070.txt b/fastlane/metadata/android/uk/changelogs/40105070.txt new file mode 100644 index 0000000000..65254059c5 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: новий інтерфейс для вибору вкладень. +Перелік усіх змін: https://github.com/vector-im/element-android/releases From ab396c5f7fff36d61ff11d8e3c9ed12f092ec0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 8 Nov 2022 07:21:52 +0000 Subject: [PATCH 184/215] Translated using Weblate (Estonian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/et/ --- fastlane/metadata/android/et/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/40105070.txt diff --git a/fastlane/metadata/android/et/changelogs/40105070.txt b/fastlane/metadata/android/et/changelogs/40105070.txt new file mode 100644 index 0000000000..061e09814d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: uus liides manuste valimiseks. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases From 48972a1a18e672d2138d567eda340efb9c4bd2c0 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Tue, 8 Nov 2022 13:32:49 +0000 Subject: [PATCH 185/215] Translated using Weblate (Persian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fa/ --- fastlane/metadata/android/fa/changelogs/40105060.txt | 2 ++ fastlane/metadata/android/fa/changelogs/40105070.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/fa/changelogs/40105060.txt create mode 100644 fastlane/metadata/android/fa/changelogs/40105070.txt diff --git a/fastlane/metadata/android/fa/changelogs/40105060.txt b/fastlane/metadata/android/fa/changelogs/40105060.txt new file mode 100644 index 0000000000..b677c05c89 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105060.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رابط کاربری جدید برای گزینش پیوست. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105070.txt b/fastlane/metadata/android/fa/changelogs/40105070.txt new file mode 100644 index 0000000000..b677c05c89 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105070.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رابط کاربری جدید برای گزینش پیوست. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases From 4d27d568aa66e87850e5669a2021897d29dc2d1e Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 8 Nov 2022 03:32:34 +0000 Subject: [PATCH 186/215] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/ --- fastlane/metadata/android/zh-TW/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-TW/changelogs/40105070.txt diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105070.txt b/fastlane/metadata/android/zh-TW/changelogs/40105070.txt new file mode 100644 index 0000000000..56667ccfc0 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105070.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:選取附件的新使用者介面。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases From 26b16fca187af18c1a0b0291cf95f75520b2822c Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Tue, 8 Nov 2022 06:13:17 +0000 Subject: [PATCH 187/215] Translated using Weblate (Czech) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/ --- fastlane/metadata/android/cs-CZ/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/40105070.txt diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt new file mode 100644 index 0000000000..e966dbbd92 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: nové uživatelské rozhraní pro výběr přílohy. +Úplný seznam změn: https://github.com/vector-im/element-android/releases From f39e3538a1fc2039ad8d5b083a228beafa30ebbe Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 8 Nov 2022 05:58:49 +0000 Subject: [PATCH 188/215] Translated using Weblate (Indonesian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/id/ --- fastlane/metadata/android/id/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/id/changelogs/40105070.txt diff --git a/fastlane/metadata/android/id/changelogs/40105070.txt b/fastlane/metadata/android/id/changelogs/40105070.txt new file mode 100644 index 0000000000..32fb87563e --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Antarmuka baru untuk memilih sebuah lampiran. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases From 6783b11a63cfa8f6ab71454ffc1596e94ba7da4d Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 8 Nov 2022 21:10:13 +0000 Subject: [PATCH 189/215] Translated using Weblate (Albanian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/ --- fastlane/metadata/android/sq/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/40105070.txt diff --git a/fastlane/metadata/android/sq/changelogs/40105070.txt b/fastlane/metadata/android/sq/changelogs/40105070.txt new file mode 100644 index 0000000000..f4beb912a5 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë verson: ndërfaqe UI e re për përzgjedhje të një bashkëngjitjeje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases From c07b110b99c93ab59b8944b688405ee79dc9a95c Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 10 Nov 2022 16:13:09 +0530 Subject: [PATCH 190/215] Add spannable tracking around SyncResponseHandler (#7514) * Add spannable tracking around SyncResponseHandler * Update LICENSE header * Refactor handleResponse and MetricsExtensions * Update changelog.d * Improve code docs and comments * Check if Sentry is enabled before tracking --- changelog.d/7514.sdk | 1 + .../sdk/api/extensions/MetricsExtensions.kt | 34 +++- .../sdk/api/metrics/SpannableMetricPlugin.kt | 36 ++++ .../api/metrics/SyncDurationMetricPlugin.kt | 32 ++++ .../sdk/internal/crypto/DeviceListManager.kt | 2 +- .../session/sync/SyncResponseHandler.kt | 175 +++++++++++++----- .../analytics/metrics/VectorPlugins.kt | 4 +- .../sentry/SentryDownloadDeviceKeysMetrics.kt | 6 +- .../sentry/SentrySyncDurationMetrics.kt | 89 +++++++++ 9 files changed, 323 insertions(+), 56 deletions(-) create mode 100644 changelog.d/7514.sdk create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt diff --git a/changelog.d/7514.sdk b/changelog.d/7514.sdk new file mode 100644 index 0000000000..f335156a49 --- /dev/null +++ b/changelog.d/7514.sdk @@ -0,0 +1 @@ +[Metrics] Add `SpannableMetricPlugin` to support spans within transactions. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt index 9487a27086..7f0e828f62 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt @@ -17,25 +17,51 @@ package org.matrix.android.sdk.api.extensions import org.matrix.android.sdk.api.metrics.MetricPlugin +import org.matrix.android.sdk.api.metrics.SpannableMetricPlugin import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** * Executes the given [block] while measuring the transaction. + * + * @param block Action/Task to be executed within this span. */ @OptIn(ExperimentalContracts::class) -inline fun measureMetric(metricMeasurementPlugins: List, block: () -> Unit) { +inline fun List.measureMetric(block: () -> Unit) { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } try { - metricMeasurementPlugins.forEach { plugin -> plugin.startTransaction() } // Start the transaction. + this.forEach { plugin -> plugin.startTransaction() } // Start the transaction. block() } catch (throwable: Throwable) { - metricMeasurementPlugins.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown. + this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown. throw throwable } finally { - metricMeasurementPlugins.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction. + this.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction. + } +} + +/** + * Executes the given [block] while measuring a span. + * + * @param operation Name of the new span. + * @param description Description of the new span. + * @param block Action/Task to be executed within this span. + */ +@OptIn(ExperimentalContracts::class) +inline fun List.measureSpan(operation: String, description: String, block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + try { + this.forEach { plugin -> plugin.startSpan(operation, description) } // Start the transaction. + block() + } catch (throwable: Throwable) { + this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown. + throw throwable + } finally { + this.forEach { plugin -> plugin.finishSpan() } // Finally, finish this transaction. } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt new file mode 100644 index 0000000000..54aa21877e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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.api.metrics + +/** + * A plugin that tracks span along with transactions. + */ +interface SpannableMetricPlugin : MetricPlugin { + + /** + * Starts the span for a sub-task. + * + * @param operation Name of the new span. + * @param description Description of the new span. + */ + fun startSpan(operation: String, description: String) + + /** + * Finish the span when sub-task is completed. + */ + fun finishSpan() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt new file mode 100644 index 0000000000..79ece002e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 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.api.metrics + +import org.matrix.android.sdk.api.logger.LoggerTag +import timber.log.Timber + +private val loggerTag = LoggerTag("SyncDurationMetricPlugin", LoggerTag.CRYPTO) + +/** + * An spannable metric plugin for sync response handling task. + */ +interface SyncDurationMetricPlugin : SpannableMetricPlugin { + + override fun logTransaction(message: String?) { + Timber.tag(loggerTag.value).v("## syncResponseHandler() : $message") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 2ac6b8c854..7e9e156003 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -355,7 +355,7 @@ internal class DeviceListManager @Inject constructor( val relevantPlugins = metricPlugins.filterIsInstance() val response: KeysQueryResponse - measureMetric(relevantPlugins) { + relevantPlugins.measureMetric { response = try { downloadKeysForUsersTask.execute(params) } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 05216d1de1..05d50d9595 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -17,6 +17,11 @@ package org.matrix.android.sdk.internal.session.sync import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.extensions.measureMetric +import org.matrix.android.sdk.api.extensions.measureSpan +import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin import org.matrix.android.sdk.api.session.pushrules.PushRuleService import org.matrix.android.sdk.api.session.pushrules.RuleScope import org.matrix.android.sdk.api.session.sync.InitialSyncStep @@ -52,9 +57,12 @@ internal class SyncResponseHandler @Inject constructor( private val tokenStore: SyncTokenStore, private val processEventForPushTask: ProcessEventForPushTask, private val pushRuleService: PushRuleService, - private val presenceSyncHandler: PresenceSyncHandler + private val presenceSyncHandler: PresenceSyncHandler, + matrixConfiguration: MatrixConfiguration, ) { + private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance() + suspend fun handleResponse( syncResponse: SyncResponse, fromToken: String?, @@ -63,39 +71,91 @@ internal class SyncResponseHandler @Inject constructor( val isInitialSync = fromToken == null Timber.v("Start handling sync, is InitialSync: $isInitialSync") - measureTimeMillis { - if (!cryptoService.isStarted()) { - Timber.v("Should start cryptoService") - cryptoService.start() - } - cryptoService.onSyncWillProcess(isInitialSync) - }.also { - Timber.v("Finish handling start cryptoService in $it ms") - } + relevantPlugins.measureMetric { + startCryptoService(isInitialSync) - // Handle the to device events before the room ones - // to ensure to decrypt them properly - measureTimeMillis { - Timber.v("Handle toDevice") - reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) { - if (syncResponse.toDevice != null) { - cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + // Handle the to device events before the room ones + // to ensure to decrypt them properly + handleToDevice(syncResponse, reporter) + + val aggregator = SyncResponsePostTreatmentAggregator() + + // Prerequisite for thread events handling in RoomSyncHandler + // Disabled due to the new fallback + // if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { + // threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) + // } + + startMonarchyTransaction(syncResponse, isInitialSync, reporter, aggregator) + + aggregateSyncResponse(aggregator) + + postTreatmentSyncResponse(syncResponse, isInitialSync) + + markCryptoSyncCompleted(syncResponse) + + handlePostSync() + + Timber.v("On sync completed") + } + } + + private fun startCryptoService(isInitialSync: Boolean) { + relevantPlugins.measureSpan("task", "start_crypto_service") { + measureTimeMillis { + if (!cryptoService.isStarted()) { + Timber.v("Should start cryptoService") + cryptoService.start() } + cryptoService.onSyncWillProcess(isInitialSync) + }.also { + Timber.v("Finish handling start cryptoService in $it ms") } - }.also { - Timber.v("Finish handling toDevice in $it ms") } - val aggregator = SyncResponsePostTreatmentAggregator() + } - // Prerequisite for thread events handling in RoomSyncHandler -// Disabled due to the new fallback -// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { -// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) -// } + private suspend fun handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) { + relevantPlugins.measureSpan("task", "handle_to_device") { + measureTimeMillis { + Timber.v("Handle toDevice") + reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) { + if (syncResponse.toDevice != null) { + cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + } + } + }.also { + Timber.v("Finish handling toDevice in $it ms") + } + } + } + private suspend fun startMonarchyTransaction( + syncResponse: SyncResponse, + isInitialSync: Boolean, + reporter: ProgressReporter?, + aggregator: SyncResponsePostTreatmentAggregator + ) { // Start one big transaction - monarchy.awaitTransaction { realm -> - // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local) + relevantPlugins.measureSpan("task", "monarchy_transaction") { + monarchy.awaitTransaction { realm -> + // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local) + handleRooms(reporter, syncResponse, realm, isInitialSync, aggregator) + handleAccountData(reporter, realm, syncResponse) + handlePresence(realm, syncResponse) + + tokenStore.saveToken(realm, syncResponse.nextBatch) + } + } + } + + private fun handleRooms( + reporter: ProgressReporter?, + syncResponse: SyncResponse, + realm: Realm, + isInitialSync: Boolean, + aggregator: SyncResponsePostTreatmentAggregator + ) { + relevantPlugins.measureSpan("task", "handle_rooms") { measureTimeMillis { Timber.v("Handle rooms") reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) { @@ -106,7 +166,11 @@ internal class SyncResponseHandler @Inject constructor( }.also { Timber.v("Finish handling rooms in $it ms") } + } + } + private fun handleAccountData(reporter: ProgressReporter?, realm: Realm, syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "handle_account_data") { measureTimeMillis { reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) { Timber.v("Handle accountData") @@ -115,44 +179,59 @@ internal class SyncResponseHandler @Inject constructor( }.also { Timber.v("Finish handling accountData in $it ms") } + } + } + private fun handlePresence(realm: Realm, syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "handle_presence") { measureTimeMillis { Timber.v("Handle Presence") presenceSyncHandler.handle(realm, syncResponse.presence) }.also { Timber.v("Finish handling Presence in $it ms") } - tokenStore.saveToken(realm, syncResponse.nextBatch) } + } - // Everything else we need to do outside the transaction - measureTimeMillis { - aggregatorHandler.handle(aggregator) - }.also { - Timber.v("Aggregator management took $it ms") - } - - measureTimeMillis { - syncResponse.rooms?.let { - checkPushRules(it, isInitialSync) - userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) - dispatchInvitedRoom(it) + private suspend fun aggregateSyncResponse(aggregator: SyncResponsePostTreatmentAggregator) { + relevantPlugins.measureSpan("task", "aggregator_management") { + // Everything else we need to do outside the transaction + measureTimeMillis { + aggregatorHandler.handle(aggregator) + }.also { + Timber.v("Aggregator management took $it ms") } - }.also { - Timber.v("SyncResponse.rooms post treatment took $it ms") } + } - measureTimeMillis { - cryptoSyncHandler.onSyncCompleted(syncResponse) - }.also { - Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") + private suspend fun postTreatmentSyncResponse(syncResponse: SyncResponse, isInitialSync: Boolean) { + relevantPlugins.measureSpan("task", "sync_response_post_treatment") { + measureTimeMillis { + syncResponse.rooms?.let { + checkPushRules(it, isInitialSync) + userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) + dispatchInvitedRoom(it) + } + }.also { + Timber.v("SyncResponse.rooms post treatment took $it ms") + } } + } - // post sync stuffs + private fun markCryptoSyncCompleted(syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") { + measureTimeMillis { + cryptoSyncHandler.onSyncCompleted(syncResponse) + }.also { + Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") + } + } + } + + private fun handlePostSync() { monarchy.writeAsync { roomSyncHandler.postSyncSpaceHierarchyHandle(it) } - Timber.v("On sync completed") } private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) { diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt index 64f143a2fd..4278c1011b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt @@ -17,6 +17,7 @@ package im.vector.app.features.analytics.metrics import im.vector.app.features.analytics.metrics.sentry.SentryDownloadDeviceKeysMetrics +import im.vector.app.features.analytics.metrics.sentry.SentrySyncDurationMetrics import org.matrix.android.sdk.api.metrics.MetricPlugin import javax.inject.Inject import javax.inject.Singleton @@ -27,9 +28,10 @@ import javax.inject.Singleton @Singleton data class VectorPlugins @Inject constructor( val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics, + val sentrySyncDurationMetrics: SentrySyncDurationMetrics, ) { /** * Returns [List] of all [MetricPlugin] hold by this class. */ - fun plugins(): List = listOf(sentryDownloadDeviceKeysMetrics) + fun plugins(): List = listOf(sentryDownloadDeviceKeysMetrics, sentrySyncDurationMetrics) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt index 92213d380c..488b72bfd9 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt @@ -26,8 +26,10 @@ class SentryDownloadDeviceKeysMetrics @Inject constructor() : DownloadDeviceKeys private var transaction: ITransaction? = null override fun startTransaction() { - transaction = Sentry.startTransaction("download_device_keys", "task") - logTransaction("Sentry transaction started") + if (Sentry.isEnabled()) { + transaction = Sentry.startTransaction("download_device_keys", "task") + logTransaction("Sentry transaction started") + } } override fun finishTransaction() { diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt new file mode 100644 index 0000000000..d69ed01526 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 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.analytics.metrics.sentry + +import io.sentry.ISpan +import io.sentry.ITransaction +import io.sentry.Sentry +import io.sentry.SpanStatus +import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin +import java.util.EmptyStackException +import java.util.Stack +import javax.inject.Inject + +/** + * Sentry based implementation of SyncDurationMetricPlugin. + */ +class SentrySyncDurationMetrics @Inject constructor() : SyncDurationMetricPlugin { + private var transaction: ITransaction? = null + + // Stacks to keep spans in LIFO order. + private var spans: Stack = Stack() + + /** + * Starts the span for a sub-task. + * + * @param operation Name of the new span. + * @param description Description of the new span. + * + * @throws IllegalStateException if this is called without starting a transaction ie. `measureSpan` must be called within `measureMetric`. + */ + override fun startSpan(operation: String, description: String) { + if (Sentry.isEnabled()) { + val span = Sentry.getSpan() ?: throw IllegalStateException("measureSpan block must be called within measureMetric") + val innerSpan = span.startChild(operation, description) + spans.push(innerSpan) + logTransaction("Sentry span started: operation=[$operation], description=[$description]") + } + } + + override fun finishSpan() { + try { + spans.pop() + } catch (e: EmptyStackException) { + null + }?.finish() + logTransaction("Sentry span finished") + } + + override fun startTransaction() { + if (Sentry.isEnabled()) { + transaction = Sentry.startTransaction("sync_response_handler", "task", true) + logTransaction("Sentry transaction started") + } + } + + override fun finishTransaction() { + transaction?.finish() + logTransaction("Sentry transaction finished") + } + + override fun onError(throwable: Throwable) { + try { + spans.peek() + } catch (e: EmptyStackException) { + null + }?.apply { + this.throwable = throwable + this.status = SpanStatus.INTERNAL_ERROR + } ?: transaction?.apply { + this.throwable = throwable + this.status = SpanStatus.INTERNAL_ERROR + } + logTransaction("Sentry transaction encountered error ${throwable.message}") + } +} From 008432af3687baf53f0dc32824dee27fd57b8d69 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 10 Nov 2022 18:28:03 +0100 Subject: [PATCH 191/215] Move TypingView into the timeline as another item (#7565) * Typing view as item in list * Don't show TypingItem if we're showing a forward loader --- changelog.d/7496.feature | 1 + .../app/core/ui/views/TypingMessageView.kt | 5 -- .../home/room/detail/TimelineFragment.kt | 12 --- .../timeline/TimelineEventController.kt | 15 +++- .../room/detail/timeline/item/TypingItem.kt | 76 +++++++++++++++++++ .../src/main/res/layout/fragment_timeline.xml | 14 +--- .../src/main/res/layout/item_typing_users.xml | 8 ++ 7 files changed, 98 insertions(+), 33 deletions(-) create mode 100644 changelog.d/7496.feature create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt create mode 100644 vector/src/main/res/layout/item_typing_users.xml diff --git a/changelog.d/7496.feature b/changelog.d/7496.feature new file mode 100644 index 0000000000..721164ee06 --- /dev/null +++ b/changelog.d/7496.feature @@ -0,0 +1 @@ +Move TypingView inside the timeline items. diff --git a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt index 263f043fad..b6dc404d01 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt @@ -48,9 +48,4 @@ class TypingMessageView @JvmOverloads constructor( views.typingUserText.text = typingHelper.getNotificationTypingMessage(typingUsers) views.typingUserAvatars.render(typingUsers, avatarRenderer) } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - removeAllViews() - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 60dd1320d3..e1392b7580 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1154,7 +1154,6 @@ class TimelineFragment : } val summary = mainState.asyncRoomSummary() renderToolbar(summary) - renderTypingMessageNotification(summary, mainState) views.removeJitsiWidgetView.render(mainState) if (mainState.hasFailedSending) { lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true @@ -1230,17 +1229,6 @@ class TimelineFragment : voiceMessageRecorderContainer.isVisible = false } - private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) { - if (!isThreadTimeLine() && roomSummary != null) { - views.typingMessageView.isInvisible = state.typingUsers.isNullOrEmpty() - state.typingUsers - ?.take(MAX_TYPING_MESSAGE_USERS_COUNT) - ?.let { senders -> views.typingMessageView.render(senders, avatarRenderer) } - } else { - views.typingMessageView.isInvisible = true - } - } - private fun renderToolbar(roomSummary: RoomSummary?) { when { isLocalRoom() -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 18c626bda8..57ad4331ce 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -32,6 +32,7 @@ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull import im.vector.app.core.time.Clock +import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.JitsiState import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState @@ -57,6 +58,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem +import im.vector.app.features.home.room.detail.timeline.item.TypingItem_ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.ImageContentRenderer @@ -94,6 +96,7 @@ class TimelineEventController @Inject constructor( private val readReceiptsItemFactory: ReadReceiptsItemFactory, private val reactionListFactory: ReactionsSummaryFactory, private val clock: Clock, + private val avatarRenderer: AvatarRenderer, ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { /** @@ -104,7 +107,7 @@ class TimelineEventController @Inject constructor( val highlightedEventId: String? = null, val jitsiState: JitsiState = JitsiState(), val roomSummary: RoomSummary? = null, - val rootThreadEventId: String? = null + val rootThreadEventId: String? = null, ) { constructor(state: RoomDetailViewState) : this( @@ -112,7 +115,7 @@ class TimelineEventController @Inject constructor( highlightedEventId = state.highlightedEventId, jitsiState = state.jitsiState, roomSummary = state.asyncRoomSummary(), - rootThreadEventId = state.rootThreadEventId + rootThreadEventId = state.rootThreadEventId, ) fun isFromThreadTimeline(): Boolean = rootThreadEventId != null @@ -286,7 +289,7 @@ class TimelineEventController @Inject constructor( private val interceptorHelper = TimelineControllerInterceptorHelper( ::positionOfReadMarker, - adapterPositionMapping + adapterPositionMapping, ) init { @@ -334,6 +337,12 @@ class TimelineEventController @Inject constructor( .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) .addWhenLoading(Timeline.Direction.FORWARDS) + if (!showingForwardLoader) { + val typingUsers = partialState.roomSummary?.typingUsers.orEmpty() + val typingItem = TypingItem_().id("typing_view").avatarRenderer(avatarRenderer).users(typingUsers) + add(typingItem) + } + val timelineModels = getModels() add(timelineModels) if (hasReachedInvite && hasUTD) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt new file mode 100644 index 0000000000..2ca0ebea48 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 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 androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.ui.views.TypingMessageView +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +@EpoxyModelClass +abstract class TypingItem : EpoxyModelWithHolder() { + + companion object { + private const val MAX_TYPING_MESSAGE_USERS_COUNT = 4 + } + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var users: List = emptyList() + + override fun getDefaultLayout(): Int = R.layout.item_typing_users + + override fun bind(holder: TypingHolder) { + super.bind(holder) + + val typingUsers = users.take(MAX_TYPING_MESSAGE_USERS_COUNT) + holder.typingView.apply { + animate().cancel() + val duration = 100L + if (typingUsers.isEmpty()) { + animate().translationY(height.toFloat()) + .alpha(0f) + .setDuration(duration) + .withEndAction { + isInvisible = true + }.start() + } else { + isVisible = true + + translationY = height.toFloat() + alpha = 0f + render(typingUsers, avatarRenderer) + animate().translationY(0f) + .alpha(1f) + .setDuration(duration) + .start() + } + } + } + + class TypingHolder : VectorEpoxyHolder() { + val typingView by bind(R.id.typingMessageView) + } +} diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml index 100cf694e0..2d07464e89 100644 --- a/vector/src/main/res/layout/fragment_timeline.xml +++ b/vector/src/main/res/layout/fragment_timeline.xml @@ -85,7 +85,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:overScrollMode="always" - app:layout_constraintBottom_toTopOf="@id/typingMessageView" + app:layout_constraintBottom_toTopOf="@id/bottomBarrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" @@ -107,18 +107,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" /> - - + From 8278ae61e57562aa689c006b8c6dc33066c3056a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 23:11:00 +0000 Subject: [PATCH 192/215] Bump flipper from 0.173.0 to 0.174.0 Bumps `flipper` from 0.173.0 to 0.174.0. Updates `flipper` from 0.173.0 to 0.174.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.173.0...v0.174.0) Updates `flipper-network-plugin` from 0.173.0 to 0.174.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.173.0...v0.174.0) --- updated-dependencies: - dependency-name: com.facebook.flipper:flipper dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.facebook.flipper:flipper-network-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index af3ff72446..dc66de43ea 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,7 +17,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.173.0" +def flipper = "0.174.0" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" From 6ee1e869513c7b2237d9e7e93119adc86142a508 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 00:44:05 +0100 Subject: [PATCH 193/215] Improve live indicator --- .../item/AbsMessageVoiceBroadcastItem.kt | 31 ++++++++++--------- .../MessageVoiceBroadcastListeningItem.kt | 12 +++++++ .../MessageVoiceBroadcastRecordingItem.kt | 9 ++++++ .../listening/VoiceBroadcastPlayer.kt | 5 +++ .../listening/VoiceBroadcastPlayerImpl.kt | 25 ++++++++++++--- .../listening/VoiceBroadcastPlaylist.kt | 7 +++-- 6 files changed, 68 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 0329adf12b..c6b90cdabe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -68,25 +68,26 @@ abstract class AbsMessageVoiceBroadcastItem { - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) - liveIndicator.isVisible = true - } - VoiceBroadcastState.PAUSED -> { - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary)) - liveIndicator.isVisible = true - } - VoiceBroadcastState.STOPPED, null -> { - liveIndicator.isVisible = false - } - } + liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) + liveIndicator.isVisible = true } } + protected fun renderPausedLiveIndicator(holder: H) { + with(holder) { + liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary)) + liveIndicator.isVisible = true + } + } + + protected fun renderNoLiveIndicator(holder: H) { + holder.liveIndicator.isVisible = false + } + abstract fun renderMetadata(holder: H) abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 4b91bbfb0e..b114f95f97 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -29,6 +29,7 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass @@ -82,6 +83,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } } + override fun renderLiveIndicator(holder: Holder) { + when { + voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED -> renderNoLiveIndicator(holder) + voiceBroadcastState == VoiceBroadcastState.PAUSED || !player.isLiveListening -> renderPausedLiveIndicator(holder) + else -> renderPlayingLiveIndicator(holder) + } + } + private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) { with(holder) { bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING @@ -99,6 +108,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } VoiceBroadcastPlayer.State.BUFFERING -> Unit } + + renderLiveIndicator(holder) } } @@ -121,6 +132,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState -> renderBackwardForwardButtons(holder, playbackState) + renderLiveIndicator(holder) if (!isUserSeeking) { holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index 17aa1543c0..ed77452382 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -48,6 +48,15 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } } + override fun renderLiveIndicator(holder: Holder) { + when (voiceBroadcastState) { + VoiceBroadcastState.STARTED, + VoiceBroadcastState.RESUMED -> renderPlayingLiveIndicator(holder) + VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder) + VoiceBroadcastState.STOPPED, null -> renderNoLiveIndicator(holder) + } + } + override fun renderMetadata(holder: Holder) { with(holder) { listenersCountMetadata.isVisible = false diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 8c11db4f43..02e843965f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -30,6 +30,11 @@ interface VoiceBroadcastPlayer { */ val playingState: State + /** + * Tells whether the player is listening a live voice broadcast in "live" position. + */ + val isLiveListening: Boolean + /** * Start playback of the given voice broadcast. */ diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 6a6dc6a9e8..573a178c78 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -30,7 +30,6 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.Job @@ -70,6 +69,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null override var currentVoiceBroadcast: VoiceBroadcast? = null + override var isLiveListening: Boolean = false override var playingState = State.IDLE @MainThread @@ -142,7 +142,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) - .onEach { currentVoiceBroadcastEvent = it.getOrNull() } + .onEach { + currentVoiceBroadcastEvent = it.getOrNull() + updateLiveListeningMode() + } .launchIn(sessionScope) } @@ -190,7 +193,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( else -> playlist.firstOrNull() } val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = playlistItem.audioEvent.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return } + val sequence = playlistItem.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return } val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 sessionScope.launch { try { @@ -241,6 +244,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration) } playingState == State.PLAYING || playingState == State.BUFFERING -> { + updateLiveListeningMode(positionMillis) startPlayback(positionMillis) } playingState == State.IDLE || playingState == State.PAUSED -> { @@ -302,18 +306,31 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun onPlayingStateChanged(playingState: State) { - // Notify state change to all the listeners attached to the current voice broadcast id + // Update live playback flag + updateLiveListeningMode() + currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> + // Start or stop playback ticker when (playingState) { State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId) State.PAUSED, State.BUFFERING, State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) } + // Notify state change to all the listeners attached to the current voice broadcast id listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) } } } + private fun updateLiveListeningMode(playbackPosition: Int? = null) { + isLiveListening = when { + !currentVoiceBroadcastEvent?.isLive.orFalse() -> false + playingState == State.IDLE || playingState == State.PAUSED -> false + playbackPosition != null -> playlist.findByPosition(playbackPosition)?.sequence == playlist.lastOrNull()?.sequence + else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence + } + } + private fun getCurrentPlaybackPosition(): Int? { val playlistPosition = playlist.currentItem?.startTime val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt index ff388c2313..36b737f23f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt @@ -56,7 +56,7 @@ class VoiceBroadcastPlaylist( } fun findBySequence(sequenceNumber: Int): PlaylistItem? { - return items.find { it.audioEvent.sequence == sequenceNumber } + return items.find { it.sequence == sequenceNumber } } fun getNextItem() = findBySequence(currentSequence?.plus(1) ?: 1) @@ -64,4 +64,7 @@ class VoiceBroadcastPlaylist( fun firstOrNull() = findBySequence(1) } -data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) +data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) { + val sequence: Int? + get() = audioEvent.sequence +} From 5eb260e674384cbcfa7e2771d6948a8b010ffb5e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 14:11:34 +0100 Subject: [PATCH 194/215] Unregister listeners on recording tile --- .../timeline/item/MessageVoiceBroadcastRecordingItem.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index ed77452382..9bd6fc45ec 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -113,6 +113,10 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem super.unbind(holder) recorderListener?.let { recorder?.removeListener(it) } recorderListener = null + with(holder) { + recordButton.onClick(null) + stopRecordButton.onClick(null) + } } override fun getViewStubId() = STUB_ID From 2d006f87256644087409bbc102156e7b1894055e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 15:59:28 +0100 Subject: [PATCH 195/215] Bind listener to live playback flag --- .../MessageVoiceBroadcastListeningItem.kt | 10 +++++- .../listening/VoiceBroadcastPlayer.kt | 9 +++-- .../listening/VoiceBroadcastPlayerImpl.kt | 33 ++++++++++++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index b114f95f97..7c7e69f320 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -44,7 +44,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - playerListener = VoiceBroadcastPlayer.Listener { renderPlayingState(holder, it) } + playerListener = object : VoiceBroadcastPlayer.Listener { + override fun onPlayingStateChanged(state: VoiceBroadcastPlayer.State) { + renderPlayingState(holder, state) + } + + override fun onLiveModeChanged(isLive: Boolean) { + renderLiveIndicator(holder) + } + } player.addListener(voiceBroadcast, playerListener) bindSeekBar(holder) bindButtons(holder) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 02e843965f..0de88e9992 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -78,10 +78,15 @@ interface VoiceBroadcastPlayer { /** * Listener related to [VoiceBroadcastPlayer]. */ - fun interface Listener { + interface Listener { /** * Notify about [VoiceBroadcastPlayer.playingState] changes. */ - fun onStateChanged(state: State) + fun onPlayingStateChanged(state: State) = Unit + + /** + * Notify about [VoiceBroadcastPlayer.isLiveListening] changes. + */ + fun onLiveModeChanged(isLive: Boolean) = Unit } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 573a178c78..56c80ddfb1 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -121,7 +121,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run { listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } - listener.onStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) + listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) + listener.onLiveModeChanged(if (voiceBroadcast == currentVoiceBroadcast) isLiveListening else false) } override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { @@ -318,17 +319,41 @@ class VoiceBroadcastPlayerImpl @Inject constructor( State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) } // Notify state change to all the listeners attached to the current voice broadcast id - listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) } + listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) } } } - private fun updateLiveListeningMode(playbackPosition: Int? = null) { + /** + * Update the live listening state according to: + * - the voice broadcast state, + * - the playing state, + * - the potential seek position. + */ + private fun updateLiveListeningMode(seekPosition: Int? = null) { isLiveListening = when { + // the current voice broadcast is not live (ended) !currentVoiceBroadcastEvent?.isLive.orFalse() -> false + // the player is stopped or paused playingState == State.IDLE || playingState == State.PAUSED -> false - playbackPosition != null -> playlist.findByPosition(playbackPosition)?.sequence == playlist.lastOrNull()?.sequence + // the user has sought + seekPosition != null -> { + val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0) + when { + // backward + seekDirection < 0 -> false + // forward: check if new sequence is the last one + else -> playlist.findByPosition(seekPosition)?.sequence == playlist.lastOrNull()?.sequence + } + } + // otherwise, stay in live or go in live if we reached the last sequence else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence } + + currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> + // Notify live mode change to all the listeners attached to the current voice broadcast id + listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) } + + } } private fun getCurrentPlaybackPosition(): Int? { From a3cd0ee790d30bae0f2a5360119f0f3f7edadfce Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 16:34:43 +0100 Subject: [PATCH 196/215] Fix fetch playlist task getting stopped event from other voice broadcast --- .../voicebroadcast/VoiceBroadcastExtensions.kt | 3 +++ .../usecase/GetLiveVoiceBroadcastChunksUseCase.kt | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index fa8033a211..6faec5a262 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -39,6 +39,9 @@ val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 +val VoiceBroadcastEvent.voiceBroadcastId + get() = reference?.eventId + val VoiceBroadcastEvent.isLive get() = content?.isLive.orFalse() diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index d12a329142..16b15b9a77 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -25,6 +25,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase +import im.vector.app.features.voicebroadcast.voiceBroadcastId import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -73,14 +74,15 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( // Observe new timeline events val listener = object : Timeline.Listener { - private var lastEventId: String? = null + private var latestEventId: String? = null private var lastSequence: Int? = null override fun onTimelineUpdated(snapshot: List) { - val newEvents = lastEventId?.let { eventId -> snapshot.subList(0, snapshot.indexOfFirst { it.eventId == eventId }) } ?: snapshot + val latestEventIndex = latestEventId?.let { eventId -> snapshot.indexOfFirst { it.eventId == eventId } } + val newEvents = if (latestEventIndex != null) snapshot.subList(0, latestEventIndex) else snapshot // Detect a potential stopped voice broadcast state event - val stopEvent = newEvents.findStopEvent() + val stopEvent = newEvents.findStopEvent(voiceBroadcast) if (stopEvent != null) { lastSequence = stopEvent.content?.lastChunkSequence } @@ -98,7 +100,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( timeline.dispose() } - lastEventId = snapshot.firstOrNull()?.eventId + latestEventId = snapshot.firstOrNull()?.eventId } } @@ -117,8 +119,8 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( /** * Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state. */ - private fun List.findStopEvent(): VoiceBroadcastEvent? = - this.mapNotNull { it.root.asVoiceBroadcastEvent() } + private fun List.findStopEvent(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? = + this.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeIf { it.voiceBroadcastId == voiceBroadcast.voiceBroadcastId } } .find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } /** From 73d62c944c5d53eedcdee09d6cc2ba3ce7b25380 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 18:13:00 +0100 Subject: [PATCH 197/215] Emit first event on voice broadcast event flow --- .../usecase/GetVoiceBroadcastEventUseCase.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt index 696d300fc3..94eca2b54e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt @@ -21,11 +21,12 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.voiceBroadcastId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onStart import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType @@ -33,7 +34,7 @@ import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.unwrap +import org.matrix.android.sdk.flow.mapOptional import timber.log.Timber import javax.inject.Inject @@ -57,10 +58,10 @@ class GetVoiceBroadcastEventUseCase @Inject constructor( else -> { room.flow() .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty())) - .unwrap() - .mapNotNull { it.asVoiceBroadcastEvent() } - .filter { it.reference?.eventId == voiceBroadcast.voiceBroadcastId } - .map { it.toOptional() } + .onStart { emit(latestEvent.root.toOptional()) } + .distinctUntilChanged() + .filter { !it.hasValue() || it.getOrNull()?.asVoiceBroadcastEvent()?.voiceBroadcastId == voiceBroadcast.voiceBroadcastId } + .mapOptional { it.asVoiceBroadcastEvent() } } } } From 44608f080c6fd8777fd9ccad2b6877c0746a313e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 10:24:22 +0100 Subject: [PATCH 198/215] Improve logs --- .../listening/VoiceBroadcastPlayerImpl.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 56c80ddfb1..9199de79cf 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -70,18 +70,26 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override var currentVoiceBroadcast: VoiceBroadcast? = null override var isLiveListening: Boolean = false + @MainThread + set(value) { + if (field != value) { + Timber.w("isLiveListening: $field -> $value") + field = value + onLiveListeningChanged(value) + } + } override var playingState = State.IDLE @MainThread set(value) { if (field != value) { - Timber.w("## VoiceBroadcastPlayer state: $field -> $value") + Timber.w("playingState: $field -> $value") field = value onPlayingStateChanged(value) } } - /** Map voiceBroadcastId to listeners.*/ + /** Map voiceBroadcastId to listeners. */ private val listeners: MutableMap> = mutableMapOf() override fun playOrResume(voiceBroadcast: VoiceBroadcast) { @@ -325,9 +333,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( /** * Update the live listening state according to: - * - the voice broadcast state, - * - the playing state, - * - the potential seek position. + * - the voice broadcast state (started/paused/resumed/stopped), + * - the playing state (IDLE, PLAYING, PAUSED, BUFFERING), + * - the potential seek position (backward/forward). */ private fun updateLiveListeningMode(seekPosition: Int? = null) { isLiveListening = when { @@ -348,11 +356,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // otherwise, stay in live or go in live if we reached the last sequence else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence } + } + private fun onLiveListeningChanged(isLiveListening: Boolean) { currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> // Notify live mode change to all the listeners attached to the current voice broadcast id listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) } - } } From 288fc354878638ca71a658a91122feb2b3daaef0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 10:46:40 +0100 Subject: [PATCH 199/215] Changelog --- changelog.d/7579.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7579.wip diff --git a/changelog.d/7579.wip b/changelog.d/7579.wip new file mode 100644 index 0000000000..08e6c2cdca --- /dev/null +++ b/changelog.d/7579.wip @@ -0,0 +1 @@ +[Voice Broadcast] Improve the live indicator icon rendering in the timeline From 403fd9260ee299f6142a55e4eb993d4e766a80fd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 11:57:05 +0100 Subject: [PATCH 200/215] improve boolean condition --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 9199de79cf..d04b46b842 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -130,7 +130,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) - listener.onLiveModeChanged(if (voiceBroadcast == currentVoiceBroadcast) isLiveListening else false) + listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast && isLiveListening) } override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { From a476544761b427f7388ab5fbbbbf3e0b105a59be Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 14 Nov 2022 12:01:29 +0100 Subject: [PATCH 201/215] Fix some quoted messages having 'null' message bodies (#7532) * Fix some quoted messages having 'null' message bodies --- changelog.d/7530.sdk | 1 + .../room/send/LocalEchoEventFactory.kt | 65 +++-- .../room/send/LocalEchoEventFactoryTests.kt | 241 ++++++++++++++++++ .../sdk/test/fakes/FakeClipboardManager.kt | 37 +++ .../sdk/test/fakes/FakeConnectivityManager.kt | 44 ++++ .../android/sdk/test/fakes/FakeContext.kt | 84 ++++++ .../sdk/test/fakes/FakeNetworkCapabilities.kt | 32 +++ .../session/content/FakeThumbnailExtractor.kt | 24 ++ .../permalinks/FakePermalinkFactory.kt | 24 ++ .../room/send/FakeLocalEchoRepository.kt | 24 ++ .../session/room/send/FakeMarkdownParser.kt | 32 +++ .../room/send/FakeWaveFormSanitizer.kt | 24 ++ .../room/send/pills/FakeTextPillsUtils.kt | 24 ++ 13 files changed, 631 insertions(+), 25 deletions(-) create mode 100644 changelog.d/7530.sdk create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt diff --git a/changelog.d/7530.sdk b/changelog.d/7530.sdk new file mode 100644 index 0000000000..4cea35f44b --- /dev/null +++ b/changelog.d/7530.sdk @@ -0,0 +1 @@ +Fix a bug that caused messages with no formatted text to be quoted as "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 7d8605c2bd..55ba78c2a5 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 @@ -804,20 +804,12 @@ internal class LocalEchoEventFactory @Inject constructor( additionalContent: Content? = null, ): Event { val messageContent = quotedEvent.getLastMessageContent() - val textMsg = if (messageContent is MessageContentWithFormattedBody) { - messageContent.formattedBody - } else { - messageContent?.body - } - val quoteText = legacyRiotQuoteText(textMsg, text) - val quoteFormattedText = "
$textMsg
$formattedText" - + val formattedQuotedText = (messageContent as? MessageContentWithFormattedBody)?.formattedBody + val textContent = createQuoteTextContent(messageContent?.body, formattedQuotedText, text, formattedText, autoMarkdown) return if (rootThreadEventId != null) { createMessageEvent( roomId, - markdownParser - .parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText) - .toThreadTextContent( + textContent.toThreadTextContent( rootThreadEventId = rootThreadEventId, latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), msgType = MessageType.MSGTYPE_TEXT @@ -827,31 +819,54 @@ internal class LocalEchoEventFactory @Inject constructor( } else { createFormattedTextEvent( roomId, - markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText), + textContent, MessageType.MSGTYPE_TEXT, additionalContent, ) } } - private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { - val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() - return buildString { - if (messageParagraphs != null) { - for (i in messageParagraphs.indices) { - if (messageParagraphs[i].isNotBlank()) { - append("> ") - append(messageParagraphs[i]) - } + private fun createQuoteTextContent( + quotedText: String?, + formattedQuotedText: String?, + text: String, + formattedText: String?, + autoMarkdown: Boolean + ): TextContent { + val currentFormattedText = formattedText ?: if (autoMarkdown) { + val parsed = markdownParser.parse(text, force = true, advanced = true) + // If formattedText == text, formattedText is returned as null + parsed.formattedText ?: parsed.text + } else { + text + } + val processedFormattedQuotedText = formattedQuotedText ?: quotedText - if (i != messageParagraphs.lastIndex) { - append("\n\n") - } + val plainTextBody = buildString { + val plainMessageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray().orEmpty() + plainMessageParagraphs.forEachIndexed { index, paragraph -> + if (paragraph.isNotBlank()) { + append("> ") + append(paragraph) + } + + if (index != plainMessageParagraphs.lastIndex) { + append("\n\n") } } append("\n\n") - append(myText) + append(text) } + val formattedTextBody = buildString { + if (!processedFormattedQuotedText.isNullOrBlank()) { + append("
") + append(processedFormattedQuotedText) + append("
") + } + append("
") + append(currentFormattedText) + } + return TextContent(plainTextBody, formattedTextBody) } companion object { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt new file mode 100644 index 0000000000..b30428e5e1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2022 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.internal.assertEquals +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.TextContent +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeContext +import org.matrix.android.sdk.test.fakes.internal.session.content.FakeThumbnailExtractor +import org.matrix.android.sdk.test.fakes.internal.session.permalinks.FakePermalinkFactory +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeLocalEchoRepository +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeMarkdownParser +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeWaveFormSanitizer +import org.matrix.android.sdk.test.fakes.internal.session.room.send.pills.FakeTextPillsUtils + +@Suppress("MaxLineLength") +class LocalEchoEventFactoryTests { + + companion object { + internal const val A_USER_ID_1 = "@user_1:matrix.org" + internal const val A_ROOM_ID = "!sUeOGZKsBValPTUMax:matrix.org" + internal const val AN_EVENT_ID = "\$vApgexcL8Vfh-WxYKsFKCDooo67ttbjm3TiVKXaWijU" + internal const val AN_EPOCH = 1655210176L + + val A_START_EVENT = Event( + type = EventType.STATE_ROOM_CREATE, + eventId = AN_EVENT_ID, + originServerTs = 1652435922563, + senderId = A_USER_ID_1, + roomId = A_ROOM_ID + ) + } + + private val fakeContext = FakeContext() + private val fakeMarkdownParser = FakeMarkdownParser() + private val fakeTextPillsUtils = FakeTextPillsUtils() + private val fakeThumbnailExtractor = FakeThumbnailExtractor() + private val fakeWaveFormSanitizer = FakeWaveFormSanitizer() + private val fakeLocalEchoRepository = FakeLocalEchoRepository() + private val fakePermalinkFactory = FakePermalinkFactory() + private val fakeClock = FakeClock() + + private val localEchoEventFactory = LocalEchoEventFactory( + context = fakeContext.instance, + userId = A_USER_ID_1, + markdownParser = fakeMarkdownParser.instance, + textPillsUtils = fakeTextPillsUtils.instance, + thumbnailExtractor = fakeThumbnailExtractor.instance, + waveformSanitizer = fakeWaveFormSanitizer.instance, + localEchoRepository = fakeLocalEchoRepository.instance, + permalinkFactory = fakePermalinkFactory.instance, + clock = fakeClock + ) + + @Before + fun setup() { + fakeClock.givenEpoch(AN_EPOCH) + fakeMarkdownParser.givenBoldMarkdown() + } + + @Test + fun `given a null quotedText, when a quote event is created, then the result message should only contain the new text after new lines`() { + val event = createTimelineEvent(null, null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + assertEquals("\n\nText", quotedContent?.body) + assertEquals("
Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given a plain text quoted message, when a quote event is created, then the result message should contain both the quoted and new text`() { + val event = createTimelineEvent("Quoted", null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + assertEquals("> Quoted\n\nText", quotedContent?.body) + assertEquals("
Quoted

Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given a formatted text quoted message, when a quote event is created, then the result message should contain both the formatted quote and new text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals("
Quoted

Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given formatted text quoted message and new message, when a quote event is created, then the result message should contain both the formatted quote and new formatted text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = "Formatted text", + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Formatted text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given formatted text quoted message and new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new formatted text, not the markdown processed text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = "Formatted text", + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Formatted text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given a formatted text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new processed formatted text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "**Text**", + formattedText = null, + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the markdown text version + assertEquals("> Quoted\n\n**Text**", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given a plain text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should the plain text quote and new processed formatted text`() { + val event = createTimelineEvent("Quoted", null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "**Text**", + formattedText = null, + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the markdown text version + assertEquals("> Quoted\n\n**Text**", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + private fun createTimelineEvent(quotedText: String?, formattedQuotedText: String?): TimelineEvent { + val textContent = quotedText?.let { + TextContent( + quotedText, + formattedQuotedText + ).toMessageTextContent().toContent() + } + return TimelineEvent( + root = A_START_EVENT, + localId = 1234, + eventId = AN_EVENT_ID, + displayIndex = 0, + senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null), + annotations = if (textContent != null) { + EventAnnotationsSummary( + editSummary = EditAggregatedSummary(latestContent = textContent, emptyList(), emptyList()) + ) + } else null + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt new file mode 100644 index 0000000000..bce8b41aa9 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import android.content.ClipData +import android.content.ClipboardManager +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify + +class FakeClipboardManager { + val instance = mockk() + + fun givenSetPrimaryClip() { + every { instance.setPrimaryClip(any()) } just runs + } + + fun verifySetPrimaryClip(clipData: ClipData) { + verify { instance.setPrimaryClip(clipData) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt new file mode 100644 index 0000000000..5c3a245c51 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk + +class FakeConnectivityManager { + val instance = mockk() + + fun givenNoActiveConnection() { + every { instance.activeNetwork } returns null + } + + fun givenHasActiveConnection() { + val network = mockk() + every { instance.activeNetwork } returns network + + val networkCapabilities = FakeNetworkCapabilities() + networkCapabilities.givenTransports( + NetworkCapabilities.TRANSPORT_CELLULAR, + NetworkCapabilities.TRANSPORT_WIFI, + NetworkCapabilities.TRANSPORT_VPN + ) + every { instance.getNetworkCapabilities(network) } returns networkCapabilities.instance + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt new file mode 100644 index 0000000000..966c6a1bb2 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import android.content.ClipboardManager +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.Uri +import android.os.ParcelFileDescriptor +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import java.io.OutputStream + +class FakeContext( + private val contentResolver: ContentResolver = mockk() +) { + + val instance = mockk() + + init { + every { instance.contentResolver } returns contentResolver + every { instance.applicationContext } returns instance + } + + fun givenFileDescriptor(uri: Uri, mode: String, factory: () -> ParcelFileDescriptor?) { + val fileDescriptor = factory() + every { contentResolver.openFileDescriptor(uri, mode, null) } returns fileDescriptor + } + + fun givenSafeOutputStreamFor(uri: Uri): OutputStream { + val outputStream = mockk(relaxed = true) + every { contentResolver.openOutputStream(uri, "wt") } returns outputStream + return outputStream + } + + fun givenMissingSafeOutputStreamFor(uri: Uri) { + every { contentResolver.openOutputStream(uri, "wt") } returns null + } + + fun givenNoConnection() { + val connectivityManager = FakeConnectivityManager() + connectivityManager.givenNoActiveConnection() + givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) + } + + fun givenService(name: String, klass: Class, service: T) { + every { instance.getSystemService(name) } returns service + every { instance.getSystemService(klass) } returns service + } + + fun givenHasConnection() { + val connectivityManager = FakeConnectivityManager() + connectivityManager.givenHasActiveConnection() + givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) + } + + fun givenStartActivity(intent: Intent) { + every { instance.startActivity(intent) } just runs + } + + fun givenClipboardManager(): FakeClipboardManager { + val fakeClipboardManager = FakeClipboardManager() + givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance) + return fakeClipboardManager + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt new file mode 100644 index 0000000000..c630b94d47 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk + +class FakeNetworkCapabilities { + val instance = mockk() + + fun givenTransports(vararg type: Int) { + every { instance.hasTransport(any()) } answers { + val input = it.invocation.args.first() as Int + type.contains(input) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt new file mode 100644 index 0000000000..b541d24161 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.test.fakes.internal.session.content + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor + +class FakeThumbnailExtractor { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt new file mode 100644 index 0000000000..3d7e85424e --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.test.fakes.internal.session.permalinks + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory + +class FakePermalinkFactory { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt new file mode 100644 index 0000000000..b10d13824b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.test.fakes.internal.session.room.send + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository + +class FakeLocalEchoRepository { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt new file mode 100644 index 0000000000..a27c9284e7 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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.test.fakes.internal.session.room.send + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.util.TextContent +import org.matrix.android.sdk.internal.session.room.send.MarkdownParser + +class FakeMarkdownParser { + internal val instance = mockk() + fun givenBoldMarkdown() { + every { instance.parse(any(), any(), any()) } answers { + val text = arg(0) + TextContent(text, "${text.replace("*", "")}") + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt new file mode 100644 index 0000000000..052ddf7831 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.test.fakes.internal.session.room.send + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.WaveFormSanitizer + +class FakeWaveFormSanitizer { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt new file mode 100644 index 0000000000..0d783d6628 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.test.fakes.internal.session.room.send.pills + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils + +class FakeTextPillsUtils { + internal val instance = mockk() +} From b85fcf9a005300ebf0c4116b2692bf0c982b3ca2 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 00:11:16 +0100 Subject: [PATCH 202/215] Remove debounce on player buttons --- .../timeline/item/MessageVoiceBroadcastListeningItem.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 7c7e69f320..e5cb677763 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -60,7 +60,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem private fun bindButtons(holder: Holder) { with(holder) { - playPauseButton.onClick { + playPauseButton.setOnClickListener { if (player.currentVoiceBroadcast == voiceBroadcast) { when (player.playingState) { VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) @@ -72,11 +72,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } } - fastBackwardButton.onClick { + fastBackwardButton.setOnClickListener { val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration) callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration)) } - fastForwardButton.onClick { + fastForwardButton.setOnClickListener { val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration) callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration)) } @@ -163,7 +163,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem player.removeListener(voiceBroadcast, playerListener) playbackTracker.untrack(voiceBroadcast.voiceBroadcastId) with(holder) { - seekBar.onClick(null) + seekBar.setOnSeekBarChangeListener(null) playPauseButton.onClick(null) fastForwardButton.onClick(null) fastBackwardButton.onClick(null) From d9454af63ed8b35a98fa4075fdc1082ef4a99120 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 00:30:55 +0100 Subject: [PATCH 203/215] Stay in live when moving playback position in the same chunk --- .../listening/VoiceBroadcastPlayerImpl.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index d04b46b842..f065ac4e44 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -343,17 +343,21 @@ class VoiceBroadcastPlayerImpl @Inject constructor( !currentVoiceBroadcastEvent?.isLive.orFalse() -> false // the player is stopped or paused playingState == State.IDLE || playingState == State.PAUSED -> false - // the user has sought seekPosition != null -> { val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0) - when { - // backward - seekDirection < 0 -> false - // forward: check if new sequence is the last one - else -> playlist.findByPosition(seekPosition)?.sequence == playlist.lastOrNull()?.sequence + val newSequence = playlist.findByPosition(seekPosition)?.sequence + // the user has sought forward + if (seekDirection >= 0) { + // stay in live or latest sequence reached + isLiveListening || newSequence == playlist.lastOrNull()?.sequence + } + // the user has sought backward + else { + // was in live and stay in the same sequence + isLiveListening && newSequence == playlist.currentSequence } } - // otherwise, stay in live or go in live if we reached the last sequence + // otherwise, stay in live or go in live if we reached the latest sequence else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence } } From dca379b80fe09184cf730151b0125710bb6a16f1 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 9 Nov 2022 11:29:12 +0100 Subject: [PATCH 204/215] Persist the playback state of voice messages across different screens --- .../features/home/room/detail/composer/AudioMessageHelper.kt | 4 ++-- .../home/room/detail/composer/MessageComposerViewModel.kt | 2 +- .../detail/timeline/helper/AudioMessagePlaybackTracker.kt | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index eddfe500b3..07d7ad4d0e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -253,8 +253,8 @@ class AudioMessageHelper @Inject constructor( playbackTicker = null } - fun clearTracker() { - playbackTracker.clear() + fun stopTracking() { + playbackTracker.unregisterListeners() } fun stopAllVoiceActions(deleteRecord: Boolean = true): MultiPickerAudioType? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 23d6e71114..a8be2be5e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -960,7 +960,7 @@ class MessageComposerViewModel @AssistedInject constructor( } fun endAllVoiceActions(deleteRecord: Boolean = true) { - audioMessageHelper.clearTracker() + audioMessageHelper.stopTracking() audioMessageHelper.stopAllVoiceActions(deleteRecord) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index 91f27ce5a8..b7b3846a10 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -136,12 +136,11 @@ class AudioMessagePlaybackTracker @Inject constructor() { } } - fun clear() { + fun unregisterListeners() { listeners.forEach { it.value.onUpdate(Listener.State.Idle) } listeners.clear() - states.clear() } companion object { From a73e707f33b492964c272145229c188f001df7f1 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 15:19:22 +0100 Subject: [PATCH 205/215] Changelog --- changelog.d/7582.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7582.feature diff --git a/changelog.d/7582.feature b/changelog.d/7582.feature new file mode 100644 index 0000000000..3aae4759ee --- /dev/null +++ b/changelog.d/7582.feature @@ -0,0 +1 @@ +Voice messages - Persist the playback position across different screens From 7349bc90c07235118b2d1a99f4dcd5252f9211fa Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 15:34:07 +0100 Subject: [PATCH 206/215] Pause playback instead of reset when recording a new voice message --- .../home/room/detail/composer/AudioMessageHelper.kt | 2 +- .../timeline/helper/AudioMessagePlaybackTracker.kt | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index 07d7ad4d0e..b5ea528bd7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -66,7 +66,7 @@ class AudioMessageHelper @Inject constructor( fun startRecording(roomId: String) { stopPlayback() - playbackTracker.makeAllPlaybacksIdle() + playbackTracker.pauseAllPlaybacks() amplitudeList.clear() try { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index b7b3846a10..90fd66f9ab 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -51,15 +51,7 @@ class AudioMessagePlaybackTracker @Inject constructor() { } fun pauseAllPlaybacks() { - listeners.keys.forEach { key -> - pausePlayback(key) - } - } - - fun makeAllPlaybacksIdle() { - listeners.keys.forEach { key -> - setState(key, Listener.State.Idle) - } + listeners.keys.forEach(::pausePlayback) } /** From 361538254b198597d897e0df847e8a7d1917e13b Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 17:30:03 +0100 Subject: [PATCH 207/215] Voice Broadcast - Add maximum length for recording --- .../src/main/res/values/strings.xml | 2 + .../MessageVoiceBroadcastRecordingItem.kt | 24 ++++- .../voicebroadcast/VoiceBroadcastConstants.kt | 3 + .../recording/VoiceBroadcastRecorder.kt | 11 ++- .../recording/VoiceBroadcastRecorderQ.kt | 88 +++++++++++++++++-- .../usecase/StartVoiceBroadcastUseCase.kt | 20 +++-- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 3 +- 7 files changed, 132 insertions(+), 19 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 372692770e..e503cb3fe7 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3101,6 +3101,8 @@ You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. + + %1$s left Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index 9bd6fc45ec..39d2d73c68 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -21,10 +21,12 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick +import im.vector.app.core.utils.TextUtils import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView +import org.threeten.bp.Duration @EpoxyModelClass abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem() { @@ -37,11 +39,15 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) { + if (recorder != null && recorder?.recordingState != VoiceBroadcastRecorder.State.Idle) { recorderListener = object : VoiceBroadcastRecorder.Listener { override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { renderRecordingState(holder, state) } + + override fun onRemainingTimeUpdated(remainingTime: Long?) { + renderRemainingTime(holder, remainingTime) + } }.also { recorder?.addListener(it) } } else { renderVoiceBroadcastState(holder) @@ -58,9 +64,19 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } override fun renderMetadata(holder: Holder) { - with(holder) { - listenersCountMetadata.isVisible = false - remainingTimeMetadata.isVisible = false + holder.listenersCountMetadata.isVisible = false + } + + private fun renderRemainingTime(holder: Holder, remainingTime: Long?) { + if (remainingTime != null) { + val formattedDuration = TextUtils.formatDurationWithUnits( + holder.view.context, + Duration.ofSeconds(remainingTime.coerceAtLeast(0L)) + ) + holder.remainingTimeMetadata.value = holder.view.resources.getString(R.string.voice_broadcast_recording_time_left, formattedDuration) + holder.remainingTimeMetadata.isVisible = true + } else { + holder.remainingTimeMetadata.isVisible = false } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt index 551eaa4dac..11b4f50d2f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt @@ -28,4 +28,7 @@ object VoiceBroadcastConstants { /** Default voice broadcast chunk duration, in seconds. */ const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 120 + + /** Maximum length of the voice broadcast in seconds. */ + const val MAX_VOICE_BROADCAST_LENGTH_IN_SECONDS = 14_400 // 4 hours } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt index 8bc33ed769..bc13d1fea8 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt @@ -22,16 +22,23 @@ import java.io.File interface VoiceBroadcastRecorder : VoiceRecorder { + /** The current chunk number. */ val currentSequence: Int - val state: State - fun startRecord(roomId: String, chunkLength: Int) + /** Current state of the recorder. */ + val recordingState: State + + /** Current remaining time of recording, in seconds, if any. */ + val currentRemainingTime: Long? + + fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) fun addListener(listener: Listener) fun removeListener(listener: Listener) interface Listener { fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int) = Unit fun onStateUpdated(state: State) = Unit + fun onRemainingTimeUpdated(remainingTime: Long?) = Unit } enum class State { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index 519f1f24aa..c5408b768b 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -21,9 +21,11 @@ import android.media.MediaRecorder import android.os.Build import androidx.annotation.RequiresApi import im.vector.app.features.voice.AbstractVoiceRecorderQ +import im.vector.lib.core.utils.timer.CountUpTimer import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentAttachmentData import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.TimeUnit @RequiresApi(Build.VERSION_CODES.Q) class VoiceBroadcastRecorderQ( @@ -32,13 +34,21 @@ class VoiceBroadcastRecorderQ( private var maxFileSize = 0L // zero or negative for no limit private var currentRoomId: String? = null + private var currentMaxLength: Int = 0 + override var currentSequence = 0 - override var state = VoiceBroadcastRecorder.State.Idle + override var recordingState = VoiceBroadcastRecorder.State.Idle set(value) { field = value listeners.forEach { it.onStateUpdated(value) } } + override var currentRemainingTime: Long? = null + set(value) { + field = value + listeners.forEach { it.onRemainingTimeUpdated(value) } + } + private val recordingTicker = RecordingTicker() private val listeners = CopyOnWriteArrayList() override val outputFormat = MediaRecorder.OutputFormat.MPEG_4 @@ -58,33 +68,47 @@ class VoiceBroadcastRecorderQ( } } - override fun startRecord(roomId: String, chunkLength: Int) { + override fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) { currentRoomId = roomId maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong() + currentMaxLength = maxLength currentSequence = 1 startRecord(roomId) - state = VoiceBroadcastRecorder.State.Recording + recordingState = VoiceBroadcastRecorder.State.Recording + recordingTicker.start() } override fun pauseRecord() { tryOrNull { mediaRecorder?.stop() } mediaRecorder?.reset() + recordingState = VoiceBroadcastRecorder.State.Paused + recordingTicker.pause() notifyOutputFileCreated() - state = VoiceBroadcastRecorder.State.Paused } override fun resumeRecord() { currentSequence++ currentRoomId?.let { startRecord(it) } - state = VoiceBroadcastRecorder.State.Recording + recordingState = VoiceBroadcastRecorder.State.Recording + recordingTicker.resume() } override fun stopRecord() { super.stopRecord() + + // Stop recording + recordingState = VoiceBroadcastRecorder.State.Idle + recordingTicker.stop() notifyOutputFileCreated() + + // Remove listeners listeners.clear() + + // Reset data currentSequence = 0 - state = VoiceBroadcastRecorder.State.Idle + currentMaxLength = 0 + currentRemainingTime = null + currentRoomId = null } override fun release() { @@ -94,7 +118,8 @@ class VoiceBroadcastRecorderQ( override fun addListener(listener: VoiceBroadcastRecorder.Listener) { listeners.add(listener) - listener.onStateUpdated(state) + listener.onStateUpdated(recordingState) + listener.onRemainingTimeUpdated(currentRemainingTime) } override fun removeListener(listener: VoiceBroadcastRecorder.Listener) { @@ -117,4 +142,53 @@ class VoiceBroadcastRecorderQ( nextOutputFile = null } } + + private fun onElapsedTimeUpdated(elapsedTimeMillis: Long) { + currentRemainingTime = if (currentMaxLength > 0 && recordingState != VoiceBroadcastRecorder.State.Idle) { + val currentMaxLengthMillis = TimeUnit.SECONDS.toMillis(currentMaxLength.toLong()) + val remainingTimeMillis = currentMaxLengthMillis - elapsedTimeMillis + TimeUnit.MILLISECONDS.toSeconds(remainingTimeMillis) + } else { + null + } + } + + private inner class RecordingTicker( + private var recordingTicker: CountUpTimer? = null, + ) { + fun start() { + recordingTicker?.stop() + recordingTicker = CountUpTimer().apply { + tickListener = CountUpTimer.TickListener { onTick(elapsedTime()) } + resume() + onTick(elapsedTime()) + } + } + + fun pause() { + recordingTicker?.apply { + pause() + onTick(elapsedTime()) + } + } + + fun resume() { + recordingTicker?.apply { + resume() + onTick(elapsedTime()) + } + } + + fun stop() { + recordingTicker?.apply { + stop() + onTick(elapsedTime()) + recordingTicker = null + } + } + + private fun onTick(elapsedTimeMillis: Long) { + onElapsedTimeUpdated(elapsedTimeMillis) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 85f72c09da..67c7f602b7 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.core.content.FileProvider import im.vector.app.core.resources.BuildMeta import im.vector.app.features.attachments.toContentAttachmentData +import im.vector.app.features.session.coroutineScope import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent @@ -28,6 +29,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -51,6 +53,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( private val context: Context, private val buildMeta: BuildMeta, private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, + private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase, ) { suspend fun execute(roomId: String): Result = runCatching { @@ -64,7 +67,8 @@ class StartVoiceBroadcastUseCase @Inject constructor( private suspend fun startVoiceBroadcast(room: Room) { Timber.d("## StartVoiceBroadcastUseCase: Send new voice broadcast info state event") - val chunkLength = VoiceBroadcastConstants.DEFAULT_CHUNK_LENGTH_IN_SECONDS // Todo Get the length from the room settings + val chunkLength = VoiceBroadcastConstants.DEFAULT_CHUNK_LENGTH_IN_SECONDS // Todo Get the chunk length from the room settings + val maxLength = VoiceBroadcastConstants.MAX_VOICE_BROADCAST_LENGTH_IN_SECONDS // Todo Get the max length from the room settings val eventId = room.stateService().sendStateEvent( eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, stateKey = session.myUserId, @@ -75,16 +79,22 @@ class StartVoiceBroadcastUseCase @Inject constructor( ).toContent() ) - startRecording(room, eventId, chunkLength) + startRecording(room, eventId, chunkLength, maxLength) } - private fun startRecording(room: Room, eventId: String, chunkLength: Int) { + private fun startRecording(room: Room, eventId: String, chunkLength: Int, maxLength: Int) { voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener { override fun onVoiceMessageCreated(file: File, sequence: Int) { sendVoiceFile(room, file, eventId, sequence) } + + override fun onRemainingTimeUpdated(remainingTime: Long?) { + if (remainingTime != null && remainingTime <= 0) { + session.coroutineScope.launch { stopVoiceBroadcastUseCase.execute(room.roomId) } + } + } }) - voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength) + voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength, maxLength) } private fun sendVoiceFile(room: Room, voiceMessageFile: File, referenceEventId: String, sequence: Int) { @@ -127,7 +137,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( @VisibleForTesting fun assertNoOngoingVoiceBroadcast(room: Room) { when { - voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> { + voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Paused -> { Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting } diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index ef78f1c80d..5b4076378c 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -60,6 +60,7 @@ class StartVoiceBroadcastUseCaseTest { context = FakeContext().instance, buildMeta = mockk(), getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, + stopVoiceBroadcastUseCase = mockk() ) ) @@ -67,7 +68,7 @@ class StartVoiceBroadcastUseCaseTest { fun setup() { every { fakeRoom.roomId } returns A_ROOM_ID justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) } - every { fakeVoiceBroadcastRecorder.state } returns VoiceBroadcastRecorder.State.Idle + every { fakeVoiceBroadcastRecorder.recordingState } returns VoiceBroadcastRecorder.State.Idle } @Test From c3090fa45a4258e1d17d43c38daee7819affc6fb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 10:43:50 +0100 Subject: [PATCH 208/215] Changelog --- changelog.d/7588.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7588.wip diff --git a/changelog.d/7588.wip b/changelog.d/7588.wip new file mode 100644 index 0000000000..b3fdda55fc --- /dev/null +++ b/changelog.d/7588.wip @@ -0,0 +1 @@ +Voice Broadcast - Add maximum length From 8ea909970dd873b97ae618f2e0e78d9159303820 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 10:47:39 +0100 Subject: [PATCH 209/215] Fix line length --- .../recording/usecase/StartVoiceBroadcastUseCase.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 67c7f602b7..45f622ad92 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -137,7 +137,8 @@ class StartVoiceBroadcastUseCase @Inject constructor( @VisibleForTesting fun assertNoOngoingVoiceBroadcast(room: Room) { when { - voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Paused -> { + voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Recording || + voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Paused -> { Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting } From 3239ec5d1f6e0f676c98533a8e6e464e44208aad Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 10:52:09 +0100 Subject: [PATCH 210/215] replace negation "!" with ".not()" --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index f065ac4e44..5b0e5b2b1c 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -340,7 +340,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun updateLiveListeningMode(seekPosition: Int? = null) { isLiveListening = when { // the current voice broadcast is not live (ended) - !currentVoiceBroadcastEvent?.isLive.orFalse() -> false + currentVoiceBroadcastEvent?.isLive?.not().orFalse() -> false // the player is stopped or paused playingState == State.IDLE || playingState == State.PAUSED -> false seekPosition != null -> { From 10775ab2f386ec964e14bcbd32a0fece77c7a863 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 16 Nov 2022 13:13:07 +0100 Subject: [PATCH 211/215] Editing: default to `MessageContent.body` when no `formattedBody` is present (#7592) * Editing: default to `MessageContent.body` when no `formattedBody` is present * Update docs --- changelog.d/7574.sdk | 1 + .../android/sdk/api/session/room/timeline/TimelineEvent.kt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7574.sdk diff --git a/changelog.d/7574.sdk b/changelog.d/7574.sdk new file mode 100644 index 0000000000..3757334138 --- /dev/null +++ b/changelog.d/7574.sdk @@ -0,0 +1 @@ +If message content has no `formattedBody`, default to `body` when editing. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 223acd1b9c..6f4049de36 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -180,11 +180,13 @@ fun TimelineEvent.isRootThread(): Boolean { /** * Get the latest message body, after a possible edition, stripping the reply prefix if necessary. + * @param formatted Indicates whether the formatted HTML body of the message should be retrieved of the plain text one. + * @return If [formatted] is `true`, the HTML body of the message will be retrieved if available. Otherwise, the plain text/markdown version will be returned. */ fun TimelineEvent.getTextEditableContent(formatted: Boolean): String { val lastMessageContent = getLastMessageContent() val lastContentBody = if (formatted && lastMessageContent is MessageContentWithFormattedBody) { - lastMessageContent.formattedBody + lastMessageContent.formattedBody ?: lastMessageContent.body } else { lastMessageContent?.body } ?: return "" From 33b7294bbf7e79a5a4788144eb9befb95b7c0718 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 10:25:32 +0100 Subject: [PATCH 212/215] Update the recipe to speed up the release process. --- .github/ISSUE_TEMPLATE/release.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index b41188a920..a84f4dfd3b 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -24,8 +24,7 @@ body: ### Do the release - - [ ] Make sure `develop` and `main` are up to date (git pull) - - [ ] Checkout develop and create a release with gitflow, branch name `release/1.2.3` + - [ ] Make sure `develop` and `main` are up to date and create a release with gitflow: `git checkout main; git pull; git checkout develop; git pull; git flow release start '1.2.3'` - [ ] Check the crashes from the PlayStore - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev - [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` @@ -34,12 +33,12 @@ body: - [ ] Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things - [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs - [ ] (optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. - - [ ] Finish release with gitflow, delete the draft PR (if created) - - [ ] Push `main` and the new tag `v1.2.3` to origin - - [ ] Checkout `develop` + - [ ] Finish release with gitflow, delete the draft PR (if created): `git flow release finish '1.2.3'` + - [ ] Push `main` and the new tag `v1.2.3` to origin: `git push origin main; git push origin 'v1.2.3'` + - [ ] Checkout `develop`: `git checkout develop` - [ ] Increase version (versionPatch + 2) in `./vector/build.gradle` - [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle` - - [ ] Commit and push `develop` + - [ ] Commit and push `develop`: `git commit -m 'version++'; git push origin develop` - [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch. - [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them. - [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.) From 5d3228d97bf701821983f133deb3ac56ed646569 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 12:28:58 +0100 Subject: [PATCH 213/215] `toModel` was not catching com.squareup.moshi.JsonDataException properly (discovered when joining a Jitsi conf added as a Widget) --- .../org/matrix/android/sdk/api/session/events/model/Event.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 1f16041b54..6ae585a273 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -53,7 +53,7 @@ inline fun Content?.toModel(catchError: Boolean = true): T? { val moshiAdapter = moshi.adapter(T::class.java) return try { moshiAdapter.fromJsonValue(this) - } catch (e: Exception) { + } catch (e: Throwable) { if (catchError) { Timber.e(e, "To model failed : $e") null From f7c3e62206fcd87ef1f5843fc96129d403527d75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 12:39:08 +0100 Subject: [PATCH 214/215] Changelog for version 1.5.8 --- CHANGES.md | 40 ++++++++++++++++++++++++++++++++++++++++ changelog.d/7418.feature | 1 - changelog.d/7431.bugfix | 1 - changelog.d/7436.feature | 1 - changelog.d/7448.wip | 1 - changelog.d/7450.wip | 1 - changelog.d/7452.feature | 1 - changelog.d/7457.bugfix | 1 - changelog.d/7478.wip | 1 - changelog.d/7485.wip | 1 - changelog.d/7491.bugfix | 1 - changelog.d/7496.feature | 1 - changelog.d/7496.wip | 1 - changelog.d/7501.bugfix | 1 - changelog.d/7502.bugfix | 1 - changelog.d/7509.bugfix | 1 - changelog.d/7512.feature | 1 - changelog.d/7514.sdk | 1 - changelog.d/7519.bugfix | 1 - changelog.d/7530.sdk | 1 - changelog.d/7533.bugfix | 1 - changelog.d/7574.sdk | 1 - changelog.d/7579.wip | 1 - changelog.d/7582.feature | 1 - changelog.d/7588.wip | 1 - 25 files changed, 40 insertions(+), 24 deletions(-) delete mode 100644 changelog.d/7418.feature delete mode 100644 changelog.d/7431.bugfix delete mode 100644 changelog.d/7436.feature delete mode 100644 changelog.d/7448.wip delete mode 100644 changelog.d/7450.wip delete mode 100644 changelog.d/7452.feature delete mode 100644 changelog.d/7457.bugfix delete mode 100644 changelog.d/7478.wip delete mode 100644 changelog.d/7485.wip delete mode 100644 changelog.d/7491.bugfix delete mode 100644 changelog.d/7496.feature delete mode 100644 changelog.d/7496.wip delete mode 100644 changelog.d/7501.bugfix delete mode 100644 changelog.d/7502.bugfix delete mode 100644 changelog.d/7509.bugfix delete mode 100644 changelog.d/7512.feature delete mode 100644 changelog.d/7514.sdk delete mode 100644 changelog.d/7519.bugfix delete mode 100644 changelog.d/7530.sdk delete mode 100644 changelog.d/7533.bugfix delete mode 100644 changelog.d/7574.sdk delete mode 100644 changelog.d/7579.wip delete mode 100644 changelog.d/7582.feature delete mode 100644 changelog.d/7588.wip diff --git a/CHANGES.md b/CHANGES.md index 18bb2480c3..442d3641dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,43 @@ +Changes in Element v1.5.8 (2022-11-17) +====================================== + +Features ✨ +---------- + - [Session manager] Multi-session signout ([#7418](https://github.com/vector-im/element-android/issues/7418)) + - Rich text editor: add full screen mode. ([#7436](https://github.com/vector-im/element-android/issues/7436)) + - [Rich text editor] Add plain text mode ([#7452](https://github.com/vector-im/element-android/issues/7452)) + - Move TypingView inside the timeline items. ([#7496](https://github.com/vector-im/element-android/issues/7496)) + - Push notifications toggle: align implementation for current session ([#7512](https://github.com/vector-im/element-android/issues/7512)) + - Voice messages - Persist the playback position across different screens ([#7582](https://github.com/vector-im/element-android/issues/7582)) + +Bugfixes 🐛 +---------- + - [Voice Broadcast] Do not display the recorder view for a live broadcast started from another session ([#7431](https://github.com/vector-im/element-android/issues/7431)) + - [Session manager] Hide push notification toggle when there is no server support ([#7457](https://github.com/vector-im/element-android/issues/7457)) + - Fix rich text editor textfield not growing to fill parent on full screen. ([#7491](https://github.com/vector-im/element-android/issues/7491)) + - Fix duplicated mention pills in some cases ([#7501](https://github.com/vector-im/element-android/issues/7501)) + - Voice Broadcast - Fix duplicated voice messages in the internal playlist ([#7502](https://github.com/vector-im/element-android/issues/7502)) + - When joining a room, the message composer is displayed once the room is loaded. ([#7509](https://github.com/vector-im/element-android/issues/7509)) + - Voice Broadcast - Fix error on voice messages in unencrypted rooms ([#7519](https://github.com/vector-im/element-android/issues/7519)) + - Fix description of verified sessions ([#7533](https://github.com/vector-im/element-android/issues/7533)) + +In development 🚧 +---------------- + - [Voice Broadcast] Improve timeline items factory and handle bad recording state display ([#7448](https://github.com/vector-im/element-android/issues/7448)) + - [Voice Broadcast] Stop recording when opening the room after an app restart ([#7450](https://github.com/vector-im/element-android/issues/7450)) + - [Voice Broadcast] Improve playlist fetching and player codebase ([#7478](https://github.com/vector-im/element-android/issues/7478)) + - [Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast ([#7485](https://github.com/vector-im/element-android/issues/7485)) + - [Voice Broadcast] Add seekbar in listening tile ([#7496](https://github.com/vector-im/element-android/issues/7496)) + - [Voice Broadcast] Improve the live indicator icon rendering in the timeline ([#7579](https://github.com/vector-im/element-android/issues/7579)) + - Voice Broadcast - Add maximum length ([#7588](https://github.com/vector-im/element-android/issues/7588)) + +SDK API changes ⚠️ +------------------ + - [Metrics] Add `SpannableMetricPlugin` to support spans within transactions. ([#7514](https://github.com/vector-im/element-android/issues/7514)) + - Fix a bug that caused messages with no formatted text to be quoted as "null". ([#7530](https://github.com/vector-im/element-android/issues/7530)) + - If message content has no `formattedBody`, default to `body` when editing. ([#7574](https://github.com/vector-im/element-android/issues/7574)) + + Changes in Element v1.5.7 (2022-11-07) ====================================== diff --git a/changelog.d/7418.feature b/changelog.d/7418.feature deleted file mode 100644 index b68ef700da..0000000000 --- a/changelog.d/7418.feature +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Multi-session signout diff --git a/changelog.d/7431.bugfix b/changelog.d/7431.bugfix deleted file mode 100644 index 681a1e9aa5..0000000000 --- a/changelog.d/7431.bugfix +++ /dev/null @@ -1 +0,0 @@ - [Voice Broadcast] Do not display the recorder view for a live broadcast started from another session \ No newline at end of file diff --git a/changelog.d/7436.feature b/changelog.d/7436.feature deleted file mode 100644 index b038c975e1..0000000000 --- a/changelog.d/7436.feature +++ /dev/null @@ -1 +0,0 @@ -Rich text editor: add full screen mode. diff --git a/changelog.d/7448.wip b/changelog.d/7448.wip deleted file mode 100644 index a99e5bbcfa..0000000000 --- a/changelog.d/7448.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Improve timeline items factory and handle bad recording state display diff --git a/changelog.d/7450.wip b/changelog.d/7450.wip deleted file mode 100644 index de4d3dc5e1..0000000000 --- a/changelog.d/7450.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Stop recording when opening the room after an app restart diff --git a/changelog.d/7452.feature b/changelog.d/7452.feature deleted file mode 100644 index a811f87c84..0000000000 --- a/changelog.d/7452.feature +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Add plain text mode diff --git a/changelog.d/7457.bugfix b/changelog.d/7457.bugfix deleted file mode 100644 index 9dfbc53329..0000000000 --- a/changelog.d/7457.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Hide push notification toggle when there is no server support diff --git a/changelog.d/7478.wip b/changelog.d/7478.wip deleted file mode 100644 index 2e6602b16d..0000000000 --- a/changelog.d/7478.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Improve playlist fetching and player codebase diff --git a/changelog.d/7485.wip b/changelog.d/7485.wip deleted file mode 100644 index 30cab45d9c..0000000000 --- a/changelog.d/7485.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast diff --git a/changelog.d/7491.bugfix b/changelog.d/7491.bugfix deleted file mode 100644 index 1a87bd03bd..0000000000 --- a/changelog.d/7491.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix rich text editor textfield not growing to fill parent on full screen. diff --git a/changelog.d/7496.feature b/changelog.d/7496.feature deleted file mode 100644 index 721164ee06..0000000000 --- a/changelog.d/7496.feature +++ /dev/null @@ -1 +0,0 @@ -Move TypingView inside the timeline items. diff --git a/changelog.d/7496.wip b/changelog.d/7496.wip deleted file mode 100644 index 49d15d084f..0000000000 --- a/changelog.d/7496.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Add seekbar in listening tile diff --git a/changelog.d/7501.bugfix b/changelog.d/7501.bugfix deleted file mode 100644 index b86258d427..0000000000 --- a/changelog.d/7501.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix duplicated mention pills in some cases diff --git a/changelog.d/7502.bugfix b/changelog.d/7502.bugfix deleted file mode 100644 index 8785310498..0000000000 --- a/changelog.d/7502.bugfix +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast - Fix duplicated voice messages in the internal playlist diff --git a/changelog.d/7509.bugfix b/changelog.d/7509.bugfix deleted file mode 100644 index 93ec812e0e..0000000000 --- a/changelog.d/7509.bugfix +++ /dev/null @@ -1 +0,0 @@ -When joining a room, the message composer is displayed once the room is loaded. diff --git a/changelog.d/7512.feature b/changelog.d/7512.feature deleted file mode 100644 index 00411a75ad..0000000000 --- a/changelog.d/7512.feature +++ /dev/null @@ -1 +0,0 @@ -Push notifications toggle: align implementation for current session diff --git a/changelog.d/7514.sdk b/changelog.d/7514.sdk deleted file mode 100644 index f335156a49..0000000000 --- a/changelog.d/7514.sdk +++ /dev/null @@ -1 +0,0 @@ -[Metrics] Add `SpannableMetricPlugin` to support spans within transactions. diff --git a/changelog.d/7519.bugfix b/changelog.d/7519.bugfix deleted file mode 100644 index c687bded49..0000000000 --- a/changelog.d/7519.bugfix +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast - Fix error on voice messages in unencrypted rooms diff --git a/changelog.d/7530.sdk b/changelog.d/7530.sdk deleted file mode 100644 index 4cea35f44b..0000000000 --- a/changelog.d/7530.sdk +++ /dev/null @@ -1 +0,0 @@ -Fix a bug that caused messages with no formatted text to be quoted as "null". diff --git a/changelog.d/7533.bugfix b/changelog.d/7533.bugfix deleted file mode 100644 index 5e603ece22..0000000000 --- a/changelog.d/7533.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix description of verified sessions diff --git a/changelog.d/7574.sdk b/changelog.d/7574.sdk deleted file mode 100644 index 3757334138..0000000000 --- a/changelog.d/7574.sdk +++ /dev/null @@ -1 +0,0 @@ -If message content has no `formattedBody`, default to `body` when editing. diff --git a/changelog.d/7579.wip b/changelog.d/7579.wip deleted file mode 100644 index 08e6c2cdca..0000000000 --- a/changelog.d/7579.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Improve the live indicator icon rendering in the timeline diff --git a/changelog.d/7582.feature b/changelog.d/7582.feature deleted file mode 100644 index 3aae4759ee..0000000000 --- a/changelog.d/7582.feature +++ /dev/null @@ -1 +0,0 @@ -Voice messages - Persist the playback position across different screens diff --git a/changelog.d/7588.wip b/changelog.d/7588.wip deleted file mode 100644 index b3fdda55fc..0000000000 --- a/changelog.d/7588.wip +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast - Add maximum length From 1b957073d9d7c4bcb34151ea5aff59bb20213942 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 12:41:21 +0100 Subject: [PATCH 215/215] Adding fastlane file --- fastlane/metadata/android/en-US/changelogs/40105080.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40105080.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40105080.txt b/fastlane/metadata/android/en-US/changelogs/40105080.txt new file mode 100644 index 0000000000..f9ca8cdd7c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105080.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/vector-im/element-android/releases