mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 03:48:12 +03:00
Merge branch 'develop' into feature/bca/rust_flavor
This commit is contained in:
commit
4ae93d5a2c
28 changed files with 333 additions and 107 deletions
1
changelog.d/4025.bugfix
Normal file
1
changelog.d/4025.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix can't get out of a verification dialog
|
1
changelog.d/7829.bugfix
Normal file
1
changelog.d/7829.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Handle exceptions when listening a voice broadcast
|
1
changelog.d/7845.wip
Normal file
1
changelog.d/7845.wip
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Voice Broadcast] Only display a notification on the first voice chunk
|
1
changelog.d/7938.bugfix
Normal file
1
changelog.d/7938.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix rendering of edited polls
|
|
@ -50,7 +50,7 @@ ext.libs = [
|
||||||
],
|
],
|
||||||
androidx : [
|
androidx : [
|
||||||
'activity' : "androidx.activity:activity-ktx:1.6.1",
|
'activity' : "androidx.activity:activity-ktx:1.6.1",
|
||||||
'appCompat' : "androidx.appcompat:appcompat:1.5.1",
|
'appCompat' : "androidx.appcompat:appcompat:1.6.0",
|
||||||
'biometric' : "androidx.biometric:biometric:1.1.0",
|
'biometric' : "androidx.biometric:biometric:1.1.0",
|
||||||
'core' : "androidx.core:core-ktx:1.9.0",
|
'core' : "androidx.core:core-ktx:1.9.0",
|
||||||
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
||||||
|
@ -103,7 +103,7 @@ ext.libs = [
|
||||||
],
|
],
|
||||||
element : [
|
element : [
|
||||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
||||||
'wysiwyg' : "io.element.android:wysiwyg:0.15.0"
|
'wysiwyg' : "io.element.android:wysiwyg:0.17.0"
|
||||||
],
|
],
|
||||||
squareup : [
|
squareup : [
|
||||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||||
|
|
|
@ -2860,7 +2860,7 @@
|
||||||
<string name="device_manager_sessions_sign_in_with_qr_code_title">Přihlásit se pomocí QR kódu</string>
|
<string name="device_manager_sessions_sign_in_with_qr_code_title">Přihlásit se pomocí QR kódu</string>
|
||||||
<string name="login_scan_qr_code">Naskenovat QR kód</string>
|
<string name="login_scan_qr_code">Naskenovat QR kód</string>
|
||||||
<string name="labs_enable_voice_broadcast_summary">Možnost nahrávat a odesílat hlasové vysílání na časové ose místnosti.</string>
|
<string name="labs_enable_voice_broadcast_summary">Možnost nahrávat a odesílat hlasové vysílání na časové ose místnosti.</string>
|
||||||
<string name="labs_enable_voice_broadcast_title">Povolit hlasové vysílání (v aktivním vývoji)</string>
|
<string name="labs_enable_voice_broadcast_title">Povolit hlasové vysílání</string>
|
||||||
<string name="qr_code_login_header_failed_homeserver_is_not_supported_description">Domovský server nepodporuje přihlášení pomocí QR kódu.</string>
|
<string name="qr_code_login_header_failed_homeserver_is_not_supported_description">Domovský server nepodporuje přihlášení pomocí QR kódu.</string>
|
||||||
<string name="qr_code_login_header_failed_user_cancelled_description">Přihlášení bylo na druhém zařízení zrušeno.</string>
|
<string name="qr_code_login_header_failed_user_cancelled_description">Přihlášení bylo na druhém zařízení zrušeno.</string>
|
||||||
<string name="qr_code_login_header_failed_invalid_qr_code_description">Tento QR kód je neplatný.</string>
|
<string name="qr_code_login_header_failed_invalid_qr_code_description">Tento QR kód je neplatný.</string>
|
||||||
|
@ -2946,4 +2946,13 @@
|
||||||
<string name="set_link_link">Odkaz</string>
|
<string name="set_link_link">Odkaz</string>
|
||||||
<string name="set_link_text">Text</string>
|
<string name="set_link_text">Text</string>
|
||||||
<string name="rich_text_editor_link">Nastavit odkaz</string>
|
<string name="rich_text_editor_link">Nastavit odkaz</string>
|
||||||
|
<string name="settings_access_token_summary">Přístupový token umožňuje plný přístup k účtu. Nikomu ho nesdělujte.</string>
|
||||||
|
<string name="settings_access_token">Přístupový token</string>
|
||||||
|
<string name="rich_text_editor_bullet_list">Přepnout na odrážky</string>
|
||||||
|
<string name="rich_text_editor_numbered_list">Přepnout na číslovaný seznam</string>
|
||||||
|
<string name="room_polls_ended_no_item">V této místnosti nejsou žádné předchozí hlasování</string>
|
||||||
|
<string name="room_polls_ended">Předchozí hlasování</string>
|
||||||
|
<string name="room_polls_active_no_item">V této místnosti nejsou žádné aktivní hlasování</string>
|
||||||
|
<string name="room_polls_active">Aktivní hlasování</string>
|
||||||
|
<string name="room_profile_section_more_polls">Historie hlasování</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1123,9 +1123,7 @@
|
||||||
<string name="settings_select_country">Elektu landon</string>
|
<string name="settings_select_country">Elektu landon</string>
|
||||||
<string name="settings_emails_and_phone_numbers_summary">Administri retpoŝtadresojn kaj telefonnumerojn ligitajn al via konto de Matrix</string>
|
<string name="settings_emails_and_phone_numbers_summary">Administri retpoŝtadresojn kaj telefonnumerojn ligitajn al via konto de Matrix</string>
|
||||||
<string name="settings_emails_and_phone_numbers_title">Retpoŝtadresoj kaj telefonnumeroj</string>
|
<string name="settings_emails_and_phone_numbers_title">Retpoŝtadresoj kaj telefonnumeroj</string>
|
||||||
<string name="settings_unignore_user">Ĉu montri ĉiujn mesaĝojn de %s\?
|
<string name="settings_unignore_user">Ĉu montri ĉiujn mesaĝojn de %s\?</string>
|
||||||
\n
|
|
||||||
\nSciu ke tiu ĉi ago reekigos la aplikaĵon, kaj tio povas daŭri iom da tempo.</string>
|
|
||||||
<string name="settings_password_updated">Via pasvorto ĝisdatiĝis</string>
|
<string name="settings_password_updated">Via pasvorto ĝisdatiĝis</string>
|
||||||
<string name="settings_fail_to_update_password_invalid_current_password">La pasvorto ne validas</string>
|
<string name="settings_fail_to_update_password_invalid_current_password">La pasvorto ne validas</string>
|
||||||
<string name="settings_fail_to_update_password">Malsukcesis ĝisdatigi pasvorton</string>
|
<string name="settings_fail_to_update_password">Malsukcesis ĝisdatigi pasvorton</string>
|
||||||
|
@ -2201,4 +2199,5 @@
|
||||||
<string name="call_ringing">Sonorante…</string>
|
<string name="call_ringing">Sonorante…</string>
|
||||||
<string name="spaces">Aroj</string>
|
<string name="spaces">Aroj</string>
|
||||||
<string name="initial_sync_request_reason_unignored_users">- Iom uzantoj reatentita</string>
|
<string name="initial_sync_request_reason_unignored_users">- Iom uzantoj reatentita</string>
|
||||||
|
<string name="settings_mentions_at_room">\@room</string>
|
||||||
</resources>
|
</resources>
|
|
@ -2890,4 +2890,13 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
|
||||||
<string name="set_link_link">Hivatkozás</string>
|
<string name="set_link_link">Hivatkozás</string>
|
||||||
<string name="set_link_text">Szöveg</string>
|
<string name="set_link_text">Szöveg</string>
|
||||||
<string name="rich_text_editor_link">Hivatkozás beállítása</string>
|
<string name="rich_text_editor_link">Hivatkozás beállítása</string>
|
||||||
|
<string name="settings_access_token_summary">A hozzáférési kulcs teljes elérést biztosít a fiókhoz. Soha ne ossza meg mással.</string>
|
||||||
|
<string name="settings_access_token">Elérési kulcs</string>
|
||||||
|
<string name="rich_text_editor_bullet_list">Lista ki-,bekapcsolása</string>
|
||||||
|
<string name="rich_text_editor_numbered_list">Számozott lista ki-,bekapcsolása</string>
|
||||||
|
<string name="room_polls_ended_no_item">Nincsenek régi szavazások ebben a szobában</string>
|
||||||
|
<string name="room_polls_ended">Régi szavazások</string>
|
||||||
|
<string name="room_polls_active_no_item">Nincsenek aktív szavazások ebben a szobában</string>
|
||||||
|
<string name="room_polls_active">Aktív szavazások</string>
|
||||||
|
<string name="room_profile_section_more_polls">Szavazás alakulása</string>
|
||||||
</resources>
|
</resources>
|
|
@ -2296,6 +2296,7 @@
|
||||||
<string name="sent_verification_conclusion">Verification Conclusion</string>
|
<string name="sent_verification_conclusion">Verification Conclusion</string>
|
||||||
<string name="sent_location">Shared their location</string>
|
<string name="sent_location">Shared their location</string>
|
||||||
<string name="sent_live_location">Shared their live location</string>
|
<string name="sent_live_location">Shared their live location</string>
|
||||||
|
<string name="started_a_voice_broadcast">Started a voice broadcast</string>
|
||||||
|
|
||||||
<string name="verification_request_waiting">Waiting…</string>
|
<string name="verification_request_waiting">Waiting…</string>
|
||||||
<string name="verification_request_other_cancelled">%s canceled</string>
|
<string name="verification_request_other_cancelled">%s canceled</string>
|
||||||
|
@ -3132,6 +3133,7 @@
|
||||||
<string name="error_voice_broadcast_permission_denied_message">You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
|
<string name="error_voice_broadcast_permission_denied_message">You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
|
||||||
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
|
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
|
||||||
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
|
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
|
||||||
|
<string name="error_voice_broadcast_unable_to_play">Unable to play this voice broadcast.</string>
|
||||||
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
|
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
|
||||||
<string name="voice_broadcast_recording_time_left">%1$s left</string>
|
<string name="voice_broadcast_recording_time_left">%1$s left</string>
|
||||||
<string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string>
|
<string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string>
|
||||||
|
|
|
@ -148,8 +148,8 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||||
// Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
|
// Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
|
||||||
// so toModel<MessageContent> won't parse them correctly
|
// so toModel<MessageContent> won't parse them correctly
|
||||||
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
|
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
|
||||||
in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>()
|
in EventType.POLL_START.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||||
in EventType.POLL_END.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageEndPollContent>()
|
in EventType.POLL_END.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel<MessageEndPollContent>()
|
||||||
in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
|
in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
|
||||||
in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
|
in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
|
||||||
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
|
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
|
||||||
|
@ -160,6 +160,10 @@ fun TimelineEvent.getLastEditNewContent(): Content? {
|
||||||
return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>()?.newContent
|
return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>()?.newContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun TimelineEvent.getLastPollEditNewContent(): Content? {
|
||||||
|
return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessagePollContent>()?.newContent
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if it's a reply.
|
* Returns true if it's a reply.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -16,13 +16,16 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room
|
package org.matrix.android.sdk.internal.session.room
|
||||||
|
|
||||||
|
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.Event
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
|
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -101,7 +104,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
|
||||||
if (originalDecrypted.type != replaceDecrypted.type) {
|
if (originalDecrypted.type != replaceDecrypted.type) {
|
||||||
return EditValidity.Invalid("replacement and original events must have the same type")
|
return EditValidity.Invalid("replacement and original events must have the same type")
|
||||||
}
|
}
|
||||||
if (replaceDecrypted.clearContent.toModel<MessageContent>()?.newContent == null) {
|
if (!hasNewContent(replaceDecrypted.type, replaceDecrypted.clearContent)) {
|
||||||
return EditValidity.Invalid("replacement event must have an m.new_content property")
|
return EditValidity.Invalid("replacement event must have an m.new_content property")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -116,11 +119,18 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
|
||||||
if (originalEvent.type != replaceEvent.type) {
|
if (originalEvent.type != replaceEvent.type) {
|
||||||
return EditValidity.Invalid("replacement and original events must have the same type")
|
return EditValidity.Invalid("replacement and original events must have the same type")
|
||||||
}
|
}
|
||||||
if (replaceEvent.content.toModel<MessageContent>()?.newContent == null) {
|
if (!hasNewContent(replaceEvent.type, replaceEvent.content)) {
|
||||||
return EditValidity.Invalid("replacement event must have an m.new_content property")
|
return EditValidity.Invalid("replacement event must have an m.new_content property")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return EditValidity.Valid
|
return EditValidity.Valid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasNewContent(eventType: String?, content: Content?): Boolean {
|
||||||
|
return when (eventType) {
|
||||||
|
in EventType.POLL_START.values -> content.toModel<MessagePollContent>()?.newContent != null
|
||||||
|
else -> content.toModel<MessageContent>()?.newContent != null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,6 +157,8 @@ class DefaultErrorFormatter @Inject constructor(
|
||||||
RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message)
|
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.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
|
||||||
RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
|
RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
|
||||||
|
is VoiceBroadcastFailure.ListeningError.UnableToPlay,
|
||||||
|
is VoiceBroadcastFailure.ListeningError.DownloadError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -229,6 +229,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||||
voiceMessageViews.renderPlaying(state)
|
voiceMessageViews.renderPlaying(state)
|
||||||
}
|
}
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Paused,
|
is AudioMessagePlaybackTracker.Listener.State.Paused,
|
||||||
|
is AudioMessagePlaybackTracker.Listener.State.Error,
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> {
|
is AudioMessagePlaybackTracker.Listener.State.Idle -> {
|
||||||
voiceMessageViews.renderIdle()
|
voiceMessageViews.renderIdle()
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,8 +217,8 @@ class MessageActionsViewModel @AssistedInject constructor(
|
||||||
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
|
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
|
||||||
}
|
}
|
||||||
in EventType.POLL_START.values -> {
|
in EventType.POLL_START.values -> {
|
||||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)
|
(timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||||
?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
|
?: stringProvider.getString(R.string.message_reply_to_poll_preview)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,9 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.home.room.detail.timeline.factory
|
package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
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.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
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.helper.VoiceBroadcastEventsGroup
|
||||||
|
@ -36,7 +36,6 @@ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.getUserOrDefault
|
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -45,6 +44,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
||||||
private val avatarSizeProvider: AvatarSizeProvider,
|
private val avatarSizeProvider: AvatarSizeProvider,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
private val drawableProvider: DrawableProvider,
|
private val drawableProvider: DrawableProvider,
|
||||||
|
private val errorFormatter: ErrorFormatter,
|
||||||
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||||
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
|
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
|
||||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||||
|
@ -75,13 +75,14 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
||||||
voiceBroadcast = voiceBroadcast,
|
voiceBroadcast = voiceBroadcast,
|
||||||
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
||||||
duration = voiceBroadcastEventsGroup.getDuration(),
|
duration = voiceBroadcastEventsGroup.getDuration(),
|
||||||
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
|
recorderName = params.event.senderInfo.disambiguatedDisplayName,
|
||||||
recorder = voiceBroadcastRecorder,
|
recorder = voiceBroadcastRecorder,
|
||||||
player = voiceBroadcastPlayer,
|
player = voiceBroadcastPlayer,
|
||||||
playbackTracker = playbackTracker,
|
playbackTracker = playbackTracker,
|
||||||
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
||||||
colorProvider = colorProvider,
|
colorProvider = colorProvider,
|
||||||
drawableProvider = drawableProvider,
|
drawableProvider = drawableProvider,
|
||||||
|
errorFormatter = errorFormatter,
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (isRecording) {
|
return if (isRecording) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.html.EventHtmlRenderer
|
import im.vector.app.features.html.EventHtmlRenderer
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.isLive
|
import im.vector.app.features.voicebroadcast.isLive
|
||||||
|
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import me.gujun.android.span.image
|
import me.gujun.android.span.image
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
|
@ -39,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent
|
import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent
|
||||||
|
@ -86,10 +88,16 @@ class DisplayableEventFormatter @Inject constructor(
|
||||||
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
|
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
|
||||||
}
|
}
|
||||||
MessageType.MSGTYPE_AUDIO -> {
|
MessageType.MSGTYPE_AUDIO -> {
|
||||||
if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) {
|
when {
|
||||||
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
|
(messageContent as? MessageAudioContent)?.voiceMessageIndicator == null -> {
|
||||||
} else {
|
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
|
||||||
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
|
}
|
||||||
|
timelineEvent.root.asMessageAudioEvent().isVoiceBroadcast() -> {
|
||||||
|
simpleFormat(senderName, stringProvider.getString(R.string.started_a_voice_broadcast), appendAuthor)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageType.MSGTYPE_VIDEO -> {
|
MessageType.MSGTYPE_VIDEO -> {
|
||||||
|
@ -130,7 +138,7 @@ class DisplayableEventFormatter @Inject constructor(
|
||||||
span { }
|
span { }
|
||||||
}
|
}
|
||||||
in EventType.POLL_START.values -> {
|
in EventType.POLL_START.values -> {
|
||||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
(timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||||
?: stringProvider.getString(R.string.sent_a_poll)
|
?: stringProvider.getString(R.string.sent_a_poll)
|
||||||
}
|
}
|
||||||
in EventType.POLL_RESPONSE.values -> {
|
in EventType.POLL_RESPONSE.values -> {
|
||||||
|
|
|
@ -50,8 +50,11 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
listeners.remove(id)
|
listeners.remove(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseAllPlaybacks() {
|
fun unregisterListeners() {
|
||||||
listeners.keys.forEach(::pausePlayback)
|
listeners.forEach {
|
||||||
|
it.value.onUpdate(Listener.State.Idle)
|
||||||
|
}
|
||||||
|
listeners.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,6 +87,10 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pauseAllPlaybacks() {
|
||||||
|
listeners.keys.forEach(::pausePlayback)
|
||||||
|
}
|
||||||
|
|
||||||
fun pausePlayback(id: String) {
|
fun pausePlayback(id: String) {
|
||||||
val state = getPlaybackState(id)
|
val state = getPlaybackState(id)
|
||||||
if (state is Listener.State.Playing) {
|
if (state is Listener.State.Playing) {
|
||||||
|
@ -94,7 +101,14 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopPlayback(id: String) {
|
fun stopPlayback(id: String) {
|
||||||
setState(id, Listener.State.Idle)
|
val state = getPlaybackState(id)
|
||||||
|
if (state !is Listener.State.Error) {
|
||||||
|
setState(id, Listener.State.Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onError(id: String, error: Throwable) {
|
||||||
|
setState(id, Listener.State.Error(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
|
fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
|
||||||
|
@ -116,6 +130,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
is Listener.State.Playing -> state.playbackTime
|
is Listener.State.Playing -> state.playbackTime
|
||||||
is Listener.State.Paused -> state.playbackTime
|
is Listener.State.Paused -> state.playbackTime
|
||||||
is Listener.State.Recording,
|
is Listener.State.Recording,
|
||||||
|
is Listener.State.Error,
|
||||||
Listener.State.Idle,
|
Listener.State.Idle,
|
||||||
null -> null
|
null -> null
|
||||||
}
|
}
|
||||||
|
@ -126,18 +141,12 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
is Listener.State.Playing -> state.percentage
|
is Listener.State.Playing -> state.percentage
|
||||||
is Listener.State.Paused -> state.percentage
|
is Listener.State.Paused -> state.percentage
|
||||||
is Listener.State.Recording,
|
is Listener.State.Recording,
|
||||||
|
is Listener.State.Error,
|
||||||
Listener.State.Idle,
|
Listener.State.Idle,
|
||||||
null -> null
|
null -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterListeners() {
|
|
||||||
listeners.forEach {
|
|
||||||
it.value.onUpdate(Listener.State.Idle)
|
|
||||||
}
|
|
||||||
listeners.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val RECORDING_ID = "RECORDING_ID"
|
const val RECORDING_ID = "RECORDING_ID"
|
||||||
}
|
}
|
||||||
|
@ -148,6 +157,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
|
|
||||||
sealed class State {
|
sealed class State {
|
||||||
object Idle : State()
|
object Idle : State()
|
||||||
|
data class Error(val failure: Throwable) : State()
|
||||||
data class Playing(val playbackTime: Int, val percentage: Float) : State()
|
data class Playing(val playbackTime: Int, val percentage: Float) : State()
|
||||||
data class Paused(val playbackTime: Int, val percentage: Float) : State()
|
data class Paused(val playbackTime: Int, val percentage: Float) : State()
|
||||||
data class Recording(val amplitudeList: List<Int>) : State()
|
data class Recording(val amplitudeList: List<Int>) : State()
|
||||||
|
|
|
@ -22,6 +22,7 @@ import androidx.annotation.IdRes
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.extensions.tintBackground
|
import im.vector.app.core.extensions.tintBackground
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
import im.vector.app.core.resources.DrawableProvider
|
||||||
|
@ -48,6 +49,7 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
||||||
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
|
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
|
||||||
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
||||||
protected val avatarRenderer get() = attributes.avatarRenderer
|
protected val avatarRenderer get() = attributes.avatarRenderer
|
||||||
|
protected val errorFormatter get() = voiceBroadcastAttributes.errorFormatter
|
||||||
protected val callback get() = attributes.callback
|
protected val callback get() = attributes.callback
|
||||||
|
|
||||||
override fun isCacheable(): Boolean = false
|
override fun isCacheable(): Boolean = false
|
||||||
|
@ -107,5 +109,6 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
||||||
val roomItem: MatrixItem?,
|
val roomItem: MatrixItem?,
|
||||||
val colorProvider: ColorProvider,
|
val colorProvider: ColorProvider,
|
||||||
val drawableProvider: DrawableProvider,
|
val drawableProvider: DrawableProvider,
|
||||||
|
val errorFormatter: ErrorFormatter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,7 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||||
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
||||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
|
is AudioMessagePlaybackTracker.Listener.State.Error,
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||||
|
|
|
@ -20,11 +20,13 @@ import android.text.format.DateUtils
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.Group
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
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.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||||
|
@ -54,6 +56,16 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.addListener(voiceBroadcast, playerListener)
|
player.addListener(voiceBroadcast, playerListener)
|
||||||
|
|
||||||
|
playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
|
||||||
|
renderBackwardForwardButtons(holder, playbackState)
|
||||||
|
renderPlaybackError(holder, playbackState)
|
||||||
|
renderLiveIndicator(holder)
|
||||||
|
if (!isUserSeeking) {
|
||||||
|
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bindSeekBar(holder)
|
bindSeekBar(holder)
|
||||||
bindButtons(holder)
|
bindButtons(holder)
|
||||||
}
|
}
|
||||||
|
@ -63,10 +75,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
playPauseButton.setOnClickListener {
|
playPauseButton.setOnClickListener {
|
||||||
if (player.currentVoiceBroadcast == voiceBroadcast) {
|
if (player.currentVoiceBroadcast == voiceBroadcast) {
|
||||||
when (player.playingState) {
|
when (player.playingState) {
|
||||||
VoiceBroadcastPlayer.State.PLAYING,
|
VoiceBroadcastPlayer.State.Playing,
|
||||||
VoiceBroadcastPlayer.State.BUFFERING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
|
VoiceBroadcastPlayer.State.Buffering -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
|
||||||
VoiceBroadcastPlayer.State.PAUSED,
|
VoiceBroadcastPlayer.State.Paused,
|
||||||
VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
is VoiceBroadcastPlayer.State.Error,
|
||||||
|
VoiceBroadcastPlayer.State.Idle -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||||
|
@ -100,17 +113,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
|
|
||||||
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.Buffering
|
||||||
voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.Buffering
|
||||||
|
|
||||||
when (state) {
|
when (state) {
|
||||||
VoiceBroadcastPlayer.State.PLAYING,
|
VoiceBroadcastPlayer.State.Playing,
|
||||||
VoiceBroadcastPlayer.State.BUFFERING -> {
|
VoiceBroadcastPlayer.State.Buffering -> {
|
||||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
||||||
}
|
}
|
||||||
VoiceBroadcastPlayer.State.IDLE,
|
is VoiceBroadcastPlayer.State.Error,
|
||||||
VoiceBroadcastPlayer.State.PAUSED -> {
|
VoiceBroadcastPlayer.State.Idle,
|
||||||
|
VoiceBroadcastPlayer.State.Paused -> {
|
||||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
||||||
}
|
}
|
||||||
|
@ -120,6 +134,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun renderPlaybackError(holder: Holder, playbackState: State) {
|
||||||
|
with(holder) {
|
||||||
|
if (playbackState is State.Error) {
|
||||||
|
controlsGroup.isVisible = false
|
||||||
|
errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure))
|
||||||
|
} else {
|
||||||
|
errorView.isVisible = false
|
||||||
|
controlsGroup.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun bindSeekBar(holder: Holder) {
|
private fun bindSeekBar(holder: Holder) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
remainingTimeView.text = formatRemainingTime(duration)
|
remainingTimeView.text = formatRemainingTime(duration)
|
||||||
|
@ -141,13 +167,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
|
|
||||||
renderBackwardForwardButtons(holder, playbackState)
|
|
||||||
renderLiveIndicator(holder)
|
|
||||||
if (!isUserSeeking) {
|
|
||||||
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
|
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
|
||||||
|
@ -187,6 +206,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
|
val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
|
||||||
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
|
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
|
||||||
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
|
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
|
||||||
|
val errorView by bind<TextView>(R.id.errorView)
|
||||||
|
val controlsGroup by bind<Group>(R.id.controlsGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -124,6 +124,7 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
||||||
|
|
||||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
|
is AudioMessagePlaybackTracker.Listener.State.Error,
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
||||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, 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.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.notifications
|
||||||
|
|
||||||
|
import im.vector.app.ActiveSessionDataSource
|
||||||
|
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||||
|
import im.vector.app.features.voicebroadcast.sequence
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
|
||||||
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
|
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FilteredEventDetector @Inject constructor(
|
||||||
|
private val activeSessionDataSource: ActiveSessionDataSource
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given event should be ignored.
|
||||||
|
* Used to skip notifications if a non expected message is received.
|
||||||
|
*/
|
||||||
|
fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean {
|
||||||
|
val session = activeSessionDataSource.currentValue?.orNull() ?: return false
|
||||||
|
|
||||||
|
if (notifiableEvent is NotifiableMessageEvent) {
|
||||||
|
val room = session.getRoom(notifiableEvent.roomId) ?: return false
|
||||||
|
val timelineEvent = room.getTimelineEvent(notifiableEvent.eventId) ?: return false
|
||||||
|
return timelineEvent.shouldBeIgnored()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the timeline event should be ignored.
|
||||||
|
*/
|
||||||
|
private fun TimelineEvent.shouldBeIgnored(): Boolean {
|
||||||
|
if (root.isVoiceMessage()) {
|
||||||
|
val audioEvent = root.asMessageAudioEvent()
|
||||||
|
// if the event is a voice message related to a voice broadcast, only show the event on the first chunk.
|
||||||
|
return audioEvent.isVoiceBroadcast() && audioEvent?.sequence != 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,7 @@ class NotificationDrawerManager @Inject constructor(
|
||||||
private val notifiableEventProcessor: NotifiableEventProcessor,
|
private val notifiableEventProcessor: NotifiableEventProcessor,
|
||||||
private val notificationRenderer: NotificationRenderer,
|
private val notificationRenderer: NotificationRenderer,
|
||||||
private val notificationEventPersistence: NotificationEventPersistence,
|
private val notificationEventPersistence: NotificationEventPersistence,
|
||||||
|
private val filteredEventDetector: FilteredEventDetector,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -100,6 +101,11 @@ class NotificationDrawerManager @Inject constructor(
|
||||||
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
|
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) {
|
||||||
|
Timber.d("onNotifiableEventReceived(): ignore the event")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
add(notifiableEvent)
|
add(notifiableEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,21 @@
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast
|
package im.vector.app.features.voicebroadcast
|
||||||
|
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
|
||||||
sealed class VoiceBroadcastFailure : Throwable() {
|
sealed class VoiceBroadcastFailure : Throwable() {
|
||||||
sealed class RecordingError : VoiceBroadcastFailure() {
|
sealed class RecordingError : VoiceBroadcastFailure() {
|
||||||
object NoPermission : RecordingError()
|
object NoPermission : RecordingError()
|
||||||
object BlockedBySomeoneElse : RecordingError()
|
object BlockedBySomeoneElse : RecordingError()
|
||||||
object UserAlreadyBroadcasting : RecordingError()
|
object UserAlreadyBroadcasting : RecordingError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class ListeningError : VoiceBroadcastFailure() {
|
||||||
|
/**
|
||||||
|
* @property what the type of error that has occurred, see [MediaPlayer.OnErrorListener.onError].
|
||||||
|
* @property extra an extra code, specific to the error, see [MediaPlayer.OnErrorListener.onError].
|
||||||
|
*/
|
||||||
|
data class UnableToPlay(val what: Int, val extra: Int) : ListeningError()
|
||||||
|
data class DownloadError(override val cause: Throwable?) : ListeningError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.voicebroadcast.listening
|
package im.vector.app.features.voicebroadcast.listening
|
||||||
|
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
|
|
||||||
interface VoiceBroadcastPlayer {
|
interface VoiceBroadcastPlayer {
|
||||||
|
@ -26,7 +27,7 @@ interface VoiceBroadcastPlayer {
|
||||||
val currentVoiceBroadcast: VoiceBroadcast?
|
val currentVoiceBroadcast: VoiceBroadcast?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current playing [State], [State.IDLE] by default.
|
* The current playing [State], [State.Idle] by default.
|
||||||
*/
|
*/
|
||||||
val playingState: State
|
val playingState: State
|
||||||
|
|
||||||
|
@ -68,11 +69,12 @@ interface VoiceBroadcastPlayer {
|
||||||
/**
|
/**
|
||||||
* Player states.
|
* Player states.
|
||||||
*/
|
*/
|
||||||
enum class State {
|
sealed interface State {
|
||||||
PLAYING,
|
object Playing : State
|
||||||
PAUSED,
|
object Paused : State
|
||||||
BUFFERING,
|
object Buffering : State
|
||||||
IDLE
|
data class Error(val failure: VoiceBroadcastFailure.ListeningError) : State
|
||||||
|
object Idle : State
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,7 +24,7 @@ import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.extensions.onFirst
|
import im.vector.app.core.extensions.onFirst
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.voice.VoiceFailure
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||||
import im.vector.app.features.voicebroadcast.isLive
|
import im.vector.app.features.voicebroadcast.isLive
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
|
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.VoiceBroadcastPlayer.State
|
||||||
|
@ -79,7 +79,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override var playingState = State.IDLE
|
override var playingState: State = State.Idle
|
||||||
@MainThread
|
@MainThread
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field != value) {
|
if (field != value) {
|
||||||
|
@ -96,7 +96,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
val hasChanged = currentVoiceBroadcast != voiceBroadcast
|
val hasChanged = currentVoiceBroadcast != voiceBroadcast
|
||||||
when {
|
when {
|
||||||
hasChanged -> startPlayback(voiceBroadcast)
|
hasChanged -> startPlayback(voiceBroadcast)
|
||||||
playingState == State.PAUSED -> resumePlayback()
|
playingState == State.Paused -> resumePlayback()
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
// Update state
|
// Update state
|
||||||
playingState = State.IDLE
|
playingState = State.Idle
|
||||||
|
|
||||||
// Stop and release media players
|
// Stop and release media players
|
||||||
stopPlayer()
|
stopPlayer()
|
||||||
|
@ -129,7 +129,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
|
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
|
||||||
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||||
}
|
}
|
||||||
listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
|
listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.Idle)
|
||||||
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast)
|
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,11 +139,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
|
|
||||||
private fun startPlayback(voiceBroadcast: VoiceBroadcast) {
|
private fun startPlayback(voiceBroadcast: VoiceBroadcast) {
|
||||||
// Stop listening previous voice broadcast if any
|
// Stop listening previous voice broadcast if any
|
||||||
if (playingState != State.IDLE) stop()
|
if (playingState != State.Idle) stop()
|
||||||
|
|
||||||
currentVoiceBroadcast = voiceBroadcast
|
currentVoiceBroadcast = voiceBroadcast
|
||||||
|
|
||||||
playingState = State.BUFFERING
|
playingState = State.Buffering
|
||||||
|
|
||||||
observeVoiceBroadcastStateEvent(voiceBroadcast)
|
observeVoiceBroadcastStateEvent(voiceBroadcast)
|
||||||
}
|
}
|
||||||
|
@ -175,13 +175,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
|
|
||||||
private fun onPlaylistUpdated() {
|
private fun onPlaylistUpdated() {
|
||||||
when (playingState) {
|
when (playingState) {
|
||||||
State.PLAYING,
|
State.Playing,
|
||||||
State.PAUSED -> {
|
State.Paused -> {
|
||||||
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
||||||
prepareNextMediaPlayer()
|
prepareNextMediaPlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State.BUFFERING -> {
|
State.Buffering -> {
|
||||||
val nextItem = if (isLiveListening && playlist.currentSequence == null) {
|
val nextItem = if (isLiveListening && playlist.currentSequence == null) {
|
||||||
// live listening, jump to the last item if playback has not started
|
// live listening, jump to the last item if playback has not started
|
||||||
playlist.lastOrNull()
|
playlist.lastOrNull()
|
||||||
|
@ -193,7 +193,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
startPlayback(nextItem.startTime)
|
startPlayback(nextItem.startTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State.IDLE -> Unit // Should not happen
|
is State.Error -> Unit
|
||||||
|
State.Idle -> Unit // Should not happen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,18 +214,17 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
if (sequencePosition > 0) {
|
if (sequencePosition > 0) {
|
||||||
mp.seekTo(sequencePosition)
|
mp.seekTo(sequencePosition)
|
||||||
}
|
}
|
||||||
playingState = State.PLAYING
|
playingState = State.Playing
|
||||||
prepareNextMediaPlayer()
|
prepareNextMediaPlayer()
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) {
|
||||||
Timber.e(failure, "## Voice Broadcast | Unable to start playback: $failure")
|
playingState = State.Error(failure)
|
||||||
throw VoiceFailure.UnableToPlay(failure)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pausePlayback() {
|
private fun pausePlayback() {
|
||||||
playingState = State.PAUSED // This will trigger a playing state update and save the current position
|
playingState = State.Paused // This will trigger a playing state update and save the current position
|
||||||
if (currentMediaPlayer != null) {
|
if (currentMediaPlayer != null) {
|
||||||
currentMediaPlayer?.pause()
|
currentMediaPlayer?.pause()
|
||||||
} else {
|
} else {
|
||||||
|
@ -234,7 +234,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
|
|
||||||
private fun resumePlayback() {
|
private fun resumePlayback() {
|
||||||
if (currentMediaPlayer != null) {
|
if (currentMediaPlayer != null) {
|
||||||
playingState = State.PLAYING
|
playingState = State.Playing
|
||||||
currentMediaPlayer?.start()
|
currentMediaPlayer?.start()
|
||||||
} else {
|
} else {
|
||||||
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
|
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
|
||||||
|
@ -247,11 +247,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
voiceBroadcast != currentVoiceBroadcast -> {
|
voiceBroadcast != currentVoiceBroadcast -> {
|
||||||
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||||
}
|
}
|
||||||
playingState == State.PLAYING || playingState == State.BUFFERING -> {
|
playingState == State.Playing || playingState == State.Buffering -> {
|
||||||
updateLiveListeningMode(positionMillis)
|
updateLiveListeningMode(positionMillis)
|
||||||
startPlayback(positionMillis)
|
startPlayback(positionMillis)
|
||||||
}
|
}
|
||||||
playingState == State.IDLE || playingState == State.PAUSED -> {
|
playingState == State.Idle || playingState == State.Paused -> {
|
||||||
stopPlayer()
|
stopPlayer()
|
||||||
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||||
}
|
}
|
||||||
|
@ -263,19 +263,29 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
if (nextItem != null) {
|
if (nextItem != null) {
|
||||||
isPreparingNextPlayer = true
|
isPreparingNextPlayer = true
|
||||||
sessionScope.launch {
|
sessionScope.launch {
|
||||||
prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
|
try {
|
||||||
|
prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
|
||||||
|
isPreparingNextPlayer = false
|
||||||
|
nextMediaPlayer = mp
|
||||||
|
when (playingState) {
|
||||||
|
State.Playing,
|
||||||
|
State.Paused -> {
|
||||||
|
currentMediaPlayer?.setNextMediaPlayer(mp)
|
||||||
|
}
|
||||||
|
State.Buffering -> {
|
||||||
|
mp.start()
|
||||||
|
onNextMediaPlayerStarted(mp)
|
||||||
|
}
|
||||||
|
is State.Error,
|
||||||
|
State.Idle -> stopPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) {
|
||||||
isPreparingNextPlayer = false
|
isPreparingNextPlayer = false
|
||||||
nextMediaPlayer = mp
|
// Do not change the playingState if the current player is still valid,
|
||||||
when (playingState) {
|
// the error will be thrown again when switching to the next player
|
||||||
State.PLAYING,
|
if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) {
|
||||||
State.PAUSED -> {
|
playingState = State.Error(failure)
|
||||||
currentMediaPlayer?.setNextMediaPlayer(mp)
|
|
||||||
}
|
|
||||||
State.BUFFERING -> {
|
|
||||||
mp.start()
|
|
||||||
onNextMediaPlayerStarted(mp)
|
|
||||||
}
|
|
||||||
State.IDLE -> stopPlayer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -288,11 +298,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
session.fileService().downloadFile(messageAudioContent)
|
session.fileService().downloadFile(messageAudioContent)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Voice Broadcast | Download has failed: $failure")
|
Timber.e(failure, "Voice Broadcast | Download has failed: $failure")
|
||||||
throw VoiceFailure.UnableToPlay(failure)
|
throw VoiceBroadcastFailure.ListeningError.DownloadError(failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
return audioFile.inputStream().use { fis ->
|
return audioFile.inputStream().use { fis ->
|
||||||
MediaPlayer().apply {
|
MediaPlayer().apply {
|
||||||
|
setOnErrorListener(mediaPlayerListener)
|
||||||
setAudioAttributes(
|
setAudioAttributes(
|
||||||
AudioAttributes.Builder()
|
AudioAttributes.Builder()
|
||||||
// Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here
|
// Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here
|
||||||
|
@ -302,10 +313,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
setDataSource(fis.fd)
|
setDataSource(fis.fd)
|
||||||
setOnInfoListener(mediaPlayerListener)
|
setOnInfoListener(mediaPlayerListener)
|
||||||
setOnErrorListener(mediaPlayerListener)
|
|
||||||
setOnPreparedListener(onPreparedListener)
|
setOnPreparedListener(onPreparedListener)
|
||||||
setOnCompletionListener(mediaPlayerListener)
|
setOnCompletionListener(mediaPlayerListener)
|
||||||
prepare()
|
prepareAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,11 +337,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
||||||
// Start or stop playback ticker
|
// Start or stop playback ticker
|
||||||
when (playingState) {
|
when (playingState) {
|
||||||
State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
|
State.Playing -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
|
||||||
State.PAUSED,
|
State.Paused,
|
||||||
State.BUFFERING,
|
State.Buffering,
|
||||||
State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
|
is State.Error,
|
||||||
|
State.Idle -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify playback tracker about error
|
||||||
|
if (playingState is State.Error) {
|
||||||
|
playbackTracker.onError(voiceBroadcastId, playingState.failure)
|
||||||
|
}
|
||||||
|
|
||||||
// Notify state change to all the listeners attached to the current voice broadcast id
|
// Notify state change to all the listeners attached to the current voice broadcast id
|
||||||
listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) }
|
listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) }
|
||||||
}
|
}
|
||||||
|
@ -348,7 +365,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
// the current voice broadcast is not live (ended)
|
// the current voice broadcast is not live (ended)
|
||||||
mostRecentVoiceBroadcastEvent?.isLive != true -> false
|
mostRecentVoiceBroadcastEvent?.isLive != true -> false
|
||||||
// the player is stopped or paused
|
// the player is stopped or paused
|
||||||
playingState == State.IDLE || playingState == State.PAUSED -> false
|
playingState == State.Idle || playingState == State.Paused -> false
|
||||||
seekPosition != null -> {
|
seekPosition != null -> {
|
||||||
val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0)
|
val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0)
|
||||||
val newSequence = playlist.findByPosition(seekPosition)?.sequence
|
val newSequence = playlist.findByPosition(seekPosition)?.sequence
|
||||||
|
@ -374,13 +391,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
|
|
||||||
private fun onLiveListeningChanged(isLiveListening: Boolean) {
|
private fun onLiveListeningChanged(isLiveListening: Boolean) {
|
||||||
// Live has ended and last chunk has been reached, we can stop the playback
|
// Live has ended and last chunk has been reached, we can stop the playback
|
||||||
if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) {
|
val hasReachedLastChunk = playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence
|
||||||
|
if (!isLiveListening && playingState == State.Buffering && hasReachedLastChunk) {
|
||||||
stop()
|
stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNextMediaPlayerStarted(mp: MediaPlayer) {
|
private fun onNextMediaPlayerStarted(mp: MediaPlayer) {
|
||||||
playingState = State.PLAYING
|
playingState = State.Playing
|
||||||
playlist.currentSequence = playlist.currentSequence?.inc()
|
playlist.currentSequence = playlist.currentSequence?.inc()
|
||||||
currentMediaPlayer = mp
|
currentMediaPlayer = mp
|
||||||
nextMediaPlayer = null
|
nextMediaPlayer = null
|
||||||
|
@ -389,16 +407,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
|
|
||||||
private fun getCurrentPlaybackPosition(): Int? {
|
private fun getCurrentPlaybackPosition(): Int? {
|
||||||
val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null
|
val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null
|
||||||
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlist.currentItem?.startTime?.plus(it) }
|
val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlist.currentItem?.startTime?.plus(it) }
|
||||||
val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId)
|
val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId)
|
||||||
return computedPosition ?: savedPosition
|
return computedPosition ?: savedPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCurrentPlaybackPercentage(): Float? {
|
private fun getCurrentPlaybackPercentage(): Float? {
|
||||||
val playlistPosition = playlist.currentItem?.startTime
|
val playlistPosition = playlist.currentItem?.startTime
|
||||||
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
|
val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlistPosition?.plus(it) } ?: playlistPosition
|
||||||
val duration = playlist.duration.takeIf { it > 0 }
|
val duration = playlist.duration
|
||||||
val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null
|
val computedPercentage = if (computedPosition != null && duration > 0) computedPosition.toFloat() / duration else null
|
||||||
val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) }
|
val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) }
|
||||||
return computedPercentage ?: savedPercentage
|
return computedPercentage ?: savedPercentage
|
||||||
}
|
}
|
||||||
|
@ -416,6 +434,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCompletion(mp: MediaPlayer) {
|
override fun onCompletion(mp: MediaPlayer) {
|
||||||
|
// Release media player as soon as it completed
|
||||||
|
mp.release()
|
||||||
|
if (currentMediaPlayer == mp) {
|
||||||
|
currentMediaPlayer = null
|
||||||
|
} else {
|
||||||
|
error("The media player which has completed mismatches the current media player instance.")
|
||||||
|
}
|
||||||
|
|
||||||
// Next media player is already attached to this player and will start playing automatically
|
// Next media player is already attached to this player and will start playing automatically
|
||||||
if (nextMediaPlayer != null) return
|
if (nextMediaPlayer != null) return
|
||||||
|
|
||||||
|
@ -426,15 +452,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
// We'll not receive new chunks anymore so we can stop the live listening
|
// We'll not receive new chunks anymore so we can stop the live listening
|
||||||
stop()
|
stop()
|
||||||
} else {
|
} else {
|
||||||
// Enter in buffering mode and release current media player
|
playingState = State.Buffering
|
||||||
playingState = State.BUFFERING
|
prepareNextMediaPlayer()
|
||||||
currentMediaPlayer?.release()
|
|
||||||
currentMediaPlayer = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||||
stop()
|
Timber.d("## Voice Broadcast | onError: what=$what, extra=$extra")
|
||||||
|
// Do not change the playingState if the current player is still valid,
|
||||||
|
// the error will be thrown again when switching to the next player
|
||||||
|
if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) {
|
||||||
|
playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToPlay(what, extra))
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -462,24 +491,25 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
val playbackTime = getCurrentPlaybackPosition()
|
val playbackTime = getCurrentPlaybackPosition()
|
||||||
val percentage = getCurrentPlaybackPercentage()
|
val percentage = getCurrentPlaybackPercentage()
|
||||||
when (playingState) {
|
when (playingState) {
|
||||||
State.PLAYING -> {
|
State.Playing -> {
|
||||||
if (playbackTime != null && percentage != null) {
|
if (playbackTime != null && percentage != null) {
|
||||||
playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage)
|
playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State.PAUSED,
|
State.Paused,
|
||||||
State.BUFFERING -> {
|
State.Buffering -> {
|
||||||
if (playbackTime != null && percentage != null) {
|
if (playbackTime != null && percentage != null) {
|
||||||
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State.IDLE -> {
|
State.Idle -> {
|
||||||
if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) {
|
if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) {
|
||||||
playbackTracker.stopPlayback(id)
|
playbackTracker.stopPlayback(id)
|
||||||
} else {
|
} else {
|
||||||
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is State.Error -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
vector/src/main/res/drawable/ic_voice_broadcast_error.xml
Normal file
10
vector/src/main/res/drawable/ic_voice_broadcast_error.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M8,16C12.418,16 16,12.418 16,8C16,3.582 12.418,0 8,0C3.582,0 0,3.582 0,8C0,12.418 3.582,16 8,16ZM6.777,4.135C6.717,3.451 7.221,2.851 7.905,2.803C8.577,2.755 9.177,3.259 9.249,3.943V4.135L8.865,8.935C8.829,9.379 8.457,9.715 8.013,9.715H7.941C7.521,9.679 7.197,9.355 7.161,8.935L6.777,4.135ZM9.056,12.067C9.056,12.651 8.583,13.123 8,13.123C7.417,13.123 6.944,12.651 6.944,12.067C6.944,11.484 7.417,11.011 8,11.011C8.583,11.011 9.056,11.484 9.056,12.067Z"
|
||||||
|
android:fillColor="#FF5B55"
|
||||||
|
android:fillType="evenOdd" />
|
||||||
|
</vector>
|
|
@ -176,4 +176,27 @@
|
||||||
tools:ignore="NegativeMargin"
|
tools:ignore="NegativeMargin"
|
||||||
tools:text="-0:12" />
|
tools:text="-0:12" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/controlsGroup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:constraint_referenced_ids="controllerButtonsFlow,seekBar,elapsedTime,remainingTime" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/errorView"
|
||||||
|
style="@style/Widget.Vector.TextView.Caption"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:text="@string/error_voice_broadcast_unable_to_play"
|
||||||
|
android:textColor="?colorError"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:drawableStartCompat="@drawable/ic_voice_broadcast_error"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
Loading…
Reference in a new issue