mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Fix voice note playback and wave form generation in CFv2.
This commit is contained in:
@@ -7,6 +7,7 @@ import androidx.annotation.AnyThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
@@ -29,6 +30,7 @@ object AudioWaveForms {
|
||||
private val TAG = Log.tag(AudioWaveForms::class.java)
|
||||
|
||||
private val cache = ThreadSafeLruCache(200)
|
||||
private val pending = hashMapOf<String, SingleSubject<AudioFileInfo>>()
|
||||
|
||||
@AnyThread
|
||||
@JvmStatic
|
||||
@@ -43,39 +45,47 @@ object AudioWaveForms {
|
||||
val cachedInfo = cache.get(cacheKey)
|
||||
if (cachedInfo != null) {
|
||||
Log.i(TAG, "Loaded wave form from cache $cacheKey")
|
||||
synchronized(pending) {
|
||||
pending.remove(cacheKey)
|
||||
}
|
||||
return Single.just(cachedInfo)
|
||||
}
|
||||
|
||||
val databaseCache = Single.fromCallable {
|
||||
val audioHash = attachment.audioHash
|
||||
return@fromCallable if (audioHash != null) {
|
||||
checkDatabaseCache(cacheKey, audioHash.audioWaveForm)
|
||||
val pendingSubject = synchronized(pending) {
|
||||
if (pending.containsKey(cacheKey)) {
|
||||
Log.i(TAG, "Wave currently generating, returning existing subject")
|
||||
return pending[cacheKey]!!
|
||||
} else {
|
||||
Miss
|
||||
pending[cacheKey] = SingleSubject.create()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
|
||||
val generateWaveForm: Single<CacheCheckResult> = if (attachment is DatabaseAttachment) {
|
||||
Single.fromCallable { generateWaveForm(context, uri, cacheKey, attachment.attachmentId) }
|
||||
} else {
|
||||
Single.fromCallable { generateWaveForm(context, uri, cacheKey) }
|
||||
}.subscribeOn(Schedulers.io())
|
||||
pending[cacheKey]!!
|
||||
}
|
||||
|
||||
return databaseCache
|
||||
.flatMap { r ->
|
||||
if (r is Miss) {
|
||||
generateWaveForm
|
||||
Single.fromCallable { attachment.audioHash?.let { checkDatabaseCache(cacheKey, it.audioWaveForm) } ?: Miss }
|
||||
.flatMap { result ->
|
||||
if (result !is Success) {
|
||||
if (attachment is DatabaseAttachment) {
|
||||
Single.fromCallable { generateWaveForm(context, uri, cacheKey, attachment.attachmentId) }
|
||||
} else {
|
||||
Single.fromCallable { generateWaveForm(context, uri, cacheKey) }
|
||||
}
|
||||
} else {
|
||||
Single.just(r)
|
||||
Single.just(result)
|
||||
}
|
||||
}
|
||||
.map { r ->
|
||||
if (r is Success) {
|
||||
r.audioFileInfo
|
||||
.map { result ->
|
||||
if (result is Success) {
|
||||
result.audioFileInfo
|
||||
} else {
|
||||
throw IOException("Unable to generate wave form")
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(pendingSubject)
|
||||
|
||||
return pendingSubject
|
||||
}
|
||||
|
||||
private fun checkDatabaseCache(cacheKey: String, audioWaveForm: AudioWaveFormData): CacheCheckResult {
|
||||
|
||||
@@ -176,7 +176,6 @@ public final class AudioView extends FrameLayout {
|
||||
final boolean showControls,
|
||||
final boolean forceHideDuration)
|
||||
{
|
||||
this.disposable.dispose();
|
||||
this.callbacks = callbacks;
|
||||
|
||||
if (duration != null) {
|
||||
@@ -213,25 +212,26 @@ public final class AudioView extends FrameLayout {
|
||||
showPlayButton();
|
||||
}
|
||||
|
||||
this.audioSlide = audio;
|
||||
|
||||
if (seekBar instanceof WaveFormSeekBarView) {
|
||||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor, waveFormThumbTint);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
disposable = AudioWaveForms.getWaveForm(getContext(), audioSlide.asAttachment())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
data -> {
|
||||
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||
updateProgress(0, 0);
|
||||
if (!forceHideDuration && duration != null) {
|
||||
duration.setVisibility(VISIBLE);
|
||||
}
|
||||
waveFormView.setWaveData(data.getWaveForm());
|
||||
},
|
||||
t -> waveFormView.setWaveMode(false)
|
||||
);
|
||||
if (audioSlide == null || !Objects.equals(audioSlide.getUri(), audio.getUri())) {
|
||||
disposable.dispose();
|
||||
disposable = AudioWaveForms.getWaveForm(getContext(), audio.asAttachment())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
data -> {
|
||||
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||
updateProgress(0, 0);
|
||||
if (!forceHideDuration && duration != null) {
|
||||
duration.setVisibility(VISIBLE);
|
||||
}
|
||||
waveFormView.setWaveData(data.getWaveForm());
|
||||
},
|
||||
t -> waveFormView.setWaveMode(false)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
waveFormView.setWaveMode(false);
|
||||
if (duration != null) {
|
||||
@@ -243,6 +243,8 @@ public final class AudioView extends FrameLayout {
|
||||
if (forceHideDuration && duration != null) {
|
||||
duration.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
this.audioSlide = audio;
|
||||
}
|
||||
|
||||
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
@@ -52,6 +53,7 @@ class ConversationBannerView @JvmOverloads constructor(
|
||||
private val unverifiedBannerStub: Stub<UnverifiedBannerView> by lazy { ViewUtil.findStubById(this, R.id.unverified_banner_stub) }
|
||||
private val reminderStub: Stub<ReminderView> by lazy { ViewUtil.findStubById(this, R.id.reminder_stub) }
|
||||
private val reviewBannerStub: Stub<ReviewBannerView> by lazy { ViewUtil.findStubById(this, R.id.review_banner_stub) }
|
||||
private val voiceNotePlayerStub: Stub<View> by lazy { ViewUtil.findStubById(this, R.id.voice_note_player_stub) }
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
@@ -140,6 +142,20 @@ class ConversationBannerView @JvmOverloads constructor(
|
||||
hide(reviewBannerStub)
|
||||
}
|
||||
|
||||
fun showVoiceNotePlayer(state: VoiceNotePlayerView.State, voiceNotePlayerViewListener: VoiceNotePlayerView.Listener) {
|
||||
show(
|
||||
stub = voiceNotePlayerStub
|
||||
) {
|
||||
val playerView: VoiceNotePlayerView = findViewById(R.id.voice_note_player_view)
|
||||
playerView.listener = voiceNotePlayerViewListener
|
||||
playerView.setState(state)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearVoiceNotePlayer() {
|
||||
hide(voiceNotePlayerStub)
|
||||
}
|
||||
|
||||
private fun <V : View> show(stub: Stub<V>, bind: V.() -> Unit = {}) {
|
||||
TransitionManager.beginDelayedTransition(this, Slide(Gravity.TOP))
|
||||
stub.get().bind()
|
||||
|
||||
@@ -125,6 +125,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.ConversationS
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey.RecipientSearchKey
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil
|
||||
@@ -416,6 +417,10 @@ class ConversationFragment :
|
||||
)
|
||||
}
|
||||
|
||||
private val voiceNotePlayerListener: VoiceNotePlayerView.Listener by lazy {
|
||||
VoiceNotePlayerViewListener()
|
||||
}
|
||||
|
||||
private val conversationTooltips = ConversationTooltips(this)
|
||||
private val colorizer = Colorizer()
|
||||
private val textDraftSaveDebouncer = Debouncer(500)
|
||||
@@ -930,6 +935,18 @@ class ConversationFragment :
|
||||
.addTo(disposables)
|
||||
|
||||
presentTypingIndicator()
|
||||
|
||||
getVoiceNoteMediaController().finishPostpone()
|
||||
|
||||
getVoiceNoteMediaController()
|
||||
.voiceNotePlayerViewState
|
||||
.observe(viewLifecycleOwner) { state: Optional<VoiceNotePlayerView.State> ->
|
||||
if (state.isPresent) {
|
||||
binding.conversationBanner.showVoiceNotePlayer(state.get(), voiceNotePlayerListener)
|
||||
} else {
|
||||
binding.conversationBanner.clearVoiceNotePlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeInlineSearch() {
|
||||
@@ -3793,4 +3810,39 @@ class ConversationFragment :
|
||||
draftViewModel.saveEphemeralVoiceNoteDraft(draft.asDraft())
|
||||
}
|
||||
}
|
||||
|
||||
private inner class VoiceNotePlayerViewListener : VoiceNotePlayerView.Listener {
|
||||
override fun onCloseRequested(uri: Uri) {
|
||||
getVoiceNoteMediaController().stopPlaybackAndReset(uri)
|
||||
}
|
||||
|
||||
override fun onSpeedChangeRequested(uri: Uri, speed: Float) {
|
||||
getVoiceNoteMediaController().setPlaybackSpeed(uri, speed)
|
||||
}
|
||||
|
||||
override fun onPlay(uri: Uri, messageId: Long, position: Double) {
|
||||
getVoiceNoteMediaController().startSinglePlayback(uri, messageId, position)
|
||||
}
|
||||
|
||||
override fun onPause(uri: Uri) {
|
||||
getVoiceNoteMediaController().pausePlayback(uri)
|
||||
}
|
||||
|
||||
override fun onNavigateToMessage(threadId: Long, threadRecipientId: RecipientId, senderId: RecipientId, messageTimestamp: Long, messagePositionInThread: Long) {
|
||||
if (threadId != viewModel.threadId) {
|
||||
startActivity(
|
||||
ConversationIntents.createBuilderSync(requireActivity(), threadRecipientId, threadId)
|
||||
.withStartingPosition(messagePositionInThread.toInt())
|
||||
.build()
|
||||
)
|
||||
} else {
|
||||
viewModel
|
||||
.moveToMessage(messageTimestamp, senderId)
|
||||
.subscribeBy {
|
||||
moveToPosition(it)
|
||||
}
|
||||
.addTo(disposables)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,9 +320,9 @@ class ConversationRepository(
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getMessagePosition(threadId: Long, messageRecord: MessageRecord): Single<Int> {
|
||||
fun getMessagePosition(threadId: Long, dateReceived: Long, authorId: RecipientId): Single<Int> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.messages.getMessagePositionInConversation(threadId, messageRecord.dateReceived)
|
||||
SignalDatabase.messages.getMessagePositionInConversation(threadId, dateReceived, authorId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
|
||||
@@ -262,8 +262,13 @@ class ConversationViewModel(
|
||||
return repository.getNextMentionPosition(threadId)
|
||||
}
|
||||
|
||||
fun moveToMessage(dateReceived: Long, author: RecipientId): Single<Int> {
|
||||
return repository.getMessagePosition(threadId, dateReceived, author)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun moveToMessage(messageRecord: MessageRecord): Single<Int> {
|
||||
return repository.getMessagePosition(threadId, messageRecord)
|
||||
return repository.getMessagePosition(threadId, messageRecord.dateReceived, messageRecord.fromRecipient.id)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
app:animateKeyboardChanges="true">
|
||||
|
||||
<include layout="@layout/system_ui_guidelines" />
|
||||
@@ -138,6 +139,13 @@
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/voice_note_player_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/voice_note_player"
|
||||
android:layout="@layout/voice_note_player_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/unverified_banner_stub"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -58,3 +58,18 @@ fun <S : Subject<T>, T : Any> Observable<T>.subscribeWithSubject(
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
fun <S : Subject<T>, T : Any> Single<T>.subscribeWithSubject(
|
||||
subject: S,
|
||||
disposables: CompositeDisposable
|
||||
): S {
|
||||
subscribeBy(
|
||||
onSuccess = {
|
||||
subject.onNext(it)
|
||||
subject.onComplete()
|
||||
},
|
||||
onError = subject::onError
|
||||
).addTo(disposables)
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user