Fix voice note playback and wave form generation in CFv2.

This commit is contained in:
Cody Henthorne
2023-07-18 12:53:20 -04:00
committed by Nicholas
parent b8effba497
commit e6c9449e3c
8 changed files with 146 additions and 38 deletions

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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)
}
}
}
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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"

View File

@@ -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
}