Skip to content

Commit acaa29e

Browse files
fix #19481: stop audio on previewer navigation
1 parent 0ef7450 commit acaa29e

3 files changed

Lines changed: 46 additions & 5 deletions

File tree

AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,14 @@ abstract class CardViewerViewModel(
5656

5757
val showingAnswer = savedStateHandle.getMutableStateFlow(KEY_SHOWING_ANSWER, false)
5858

59-
protected val cardMediaPlayer =
59+
protected open val cardMediaPlayer by lazy {
6060
CardMediaPlayer(
6161
javascriptEvaluator = { launchCatchingIO { eval.emit(it) } },
6262
mediaErrorListener = mediaErrorHandler,
6363
).also {
6464
addCloseable(it)
6565
}
66+
}
6667
abstract var currentCard: Deferred<Card>
6768

6869
abstract val server: AnkiServer

AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
4343
import kotlinx.coroutines.flow.collectLatest
4444
import kotlinx.coroutines.flow.combine
4545
import kotlinx.coroutines.flow.update
46+
import kotlinx.coroutines.launch
4647
import timber.log.Timber
4748

48-
class PreviewerViewModel(
49+
open class PreviewerViewModel(
4950
savedStateHandle: SavedStateHandle,
5051
) : CardViewerViewModel(savedStateHandle),
5152
ChangeManager.Subscriber {
@@ -115,9 +116,11 @@ class PreviewerViewModel(
115116
launchCatchingIO {
116117
backSideOnly.emit(!backSideOnly.value)
117118
if (!backSideOnly.value && showingAnswer.value) {
119+
stopAudio()
118120
showQuestion()
119121
cardMediaPlayer.autoplayAllForSide(CardSide.QUESTION)
120122
} else if (backSideOnly.value && !showingAnswer.value) {
123+
stopAudio()
121124
showAnswer()
122125
cardMediaPlayer.autoplayAllForSide(CardSide.ANSWER)
123126
}
@@ -158,6 +161,7 @@ class PreviewerViewModel(
158161
fun onNextButtonClick() {
159162
launchCatchingIO {
160163
if (!showingAnswer.value && !backSideOnly.value) {
164+
stopAudio()
161165
showAnswer()
162166
cardMediaPlayer.autoplayAllForSide(CardSide.ANSWER)
163167
} else {
@@ -175,6 +179,7 @@ class PreviewerViewModel(
175179
if (currentIndex.value > 0) {
176180
currentIndex.update { it - 1 }
177181
} else if (showingAnswer.value && !backSideOnly.value) {
182+
stopAudio()
178183
showQuestion()
179184
}
180185
}
@@ -271,6 +276,10 @@ class PreviewerViewModel(
271276
}
272277
}
273278

279+
private fun stopAudio() {
280+
viewModelScope.launch { cardMediaPlayer.stop() }
281+
}
282+
274283
companion object {
275284
private const val KEY_BACKSIDE_ONLY = "backsideOnly"
276285
private const val KEY_CURRENT_INDEX = "currentIndex"

AnkiDroid/src/test/java/com/ichi2/anki/previewer/PreviewerViewModelTest.kt

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ import androidx.lifecycle.SavedStateHandle
1919
import androidx.test.ext.junit.runners.AndroidJUnit4
2020
import com.ichi2.anki.Flag
2121
import com.ichi2.anki.browser.IdsFile
22+
import com.ichi2.anki.cardviewer.CardMediaPlayer
2223
import com.ichi2.anki.servicelayer.NoteService
2324
import com.ichi2.anki.utils.ext.flag
2425
import com.ichi2.testutils.JvmTest
2526
import com.ichi2.testutils.common.Flaky
2627
import com.ichi2.testutils.common.OS
2728
import io.mockk.coEvery
29+
import io.mockk.coVerify
2830
import io.mockk.every
2931
import io.mockk.mockk
3032
import io.mockk.spyk
33+
import io.mockk.verify
3134
import kotlinx.coroutines.flow.first
3235
import kotlinx.coroutines.test.TestScope
3336
import kotlinx.coroutines.test.advanceUntilIdle
@@ -42,7 +45,13 @@ import org.junit.runner.RunWith
4245
class PreviewerViewModelTest : JvmTest() {
4346
private val idsFile: IdsFile = mockk()
4447

48+
private class InstrumentedPreviewerViewModel(
49+
savedStateHandle: SavedStateHandle,
50+
override val cardMediaPlayer: CardMediaPlayer,
51+
) : PreviewerViewModel(savedStateHandle)
52+
4553
private lateinit var viewModel: PreviewerViewModel
54+
private lateinit var mockMediaPlayer: CardMediaPlayer
4655

4756
private fun TestScope.onNextButtonClick() {
4857
viewModel.onNextButtonClick()
@@ -79,9 +88,9 @@ class PreviewerViewModelTest : JvmTest() {
7988
set(PreviewerFragment.CARD_IDS_FILE_ARG, idsFile)
8089
}
8190

82-
viewModel = spyk(PreviewerViewModel(savedStateHandle))
83-
// the default implementation requires the Collection media directory,
84-
// which needs Robolectric with CollectionStorageMode.IN_MEMORY_WITH_MEDIA or ON_DISK
91+
mockMediaPlayer = mockk(relaxed = true)
92+
viewModel = spyk(InstrumentedPreviewerViewModel(savedStateHandle, mockMediaPlayer))
93+
8594
coEvery { viewModel.prepareCardTextForDisplay(any()) } answers { firstArg() }
8695
}
8796

@@ -270,4 +279,26 @@ class PreviewerViewModelTest : JvmTest() {
270279
onSliderChange(sliderPosition = 2)
271280
assertEquals("Index should update for valid input", 1, viewModel.currentIndex.value)
272281
}
282+
283+
@Test
284+
fun `audio stops when changing sides`() =
285+
runTest {
286+
viewModel.onPageFinished(false)
287+
viewModel.showingAnswer.value = false
288+
289+
// 1. Next Button (Question -> Answer)
290+
onNextButtonClick()
291+
coVerify(atLeast = 1) { mockMediaPlayer.stop() }
292+
293+
// Reset mocks
294+
io.mockk.clearMocks(mockMediaPlayer, answers = false, recordedCalls = true, childMocks = false)
295+
296+
onPreviousButtonClick()
297+
coVerify(atLeast = 1) { mockMediaPlayer.stop() }
298+
299+
io.mockk.clearMocks(mockMediaPlayer, answers = false, recordedCalls = true, childMocks = false)
300+
301+
toggleBackSideOnly()
302+
coVerify(atLeast = 1) { mockMediaPlayer.stop() }
303+
}
273304
}

0 commit comments

Comments
 (0)