Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.ichi2.anki.browser

import android.content.Context
import android.content.Intent
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.CardBrowser
import com.ichi2.anki.libanki.CardId
import com.ichi2.anki.libanki.DeckId
Expand All @@ -31,29 +30,24 @@ sealed interface BrowserDestination : Destination {
data class ToDeck(
val deckId: DeckId,
) : BrowserDestination {
override fun toIntent(context: Context): Intent {
AnkiDroidApp.instance.sharedPrefsLastDeckIdRepository.lastDeckId = deckId
return Intent(context, CardBrowser::class.java)
}
override fun toIntent(context: Context): Intent =
Intent(context, CardBrowser::class.java).apply {
putExtra(CardBrowserViewModel.EXTRA_DECK_ID, deckId)
}
}

/**
* Opens the [CardBrowser] with the specified deck selected,
* and automatically scrolls to [cardId] if the card is present on the deck.
* Opens the [CardBrowser] scoped to [deckId], auto-scrolling to [cardId]
* if the card is present on the deck.
*/
data class ToCard(
data class ScrollToCard(
val deckId: DeckId,
val cardId: CardId,
) : BrowserDestination {
override fun toIntent(context: Context): Intent {
AnkiDroidApp.instance.sharedPrefsLastDeckIdRepository.lastDeckId = deckId
return Intent(context, CardBrowser::class.java).apply {
putExtra(EXTRA_CARD_ID_KEY, cardId)
override fun toIntent(context: Context): Intent =
Intent(context, CardBrowser::class.java).apply {
putExtra(CardBrowserViewModel.EXTRA_DECK_ID, deckId)
putExtra(CardBrowserViewModel.EXTRA_CARD_ID_KEY, cardId)
}
}

companion object {
const val EXTRA_CARD_ID_KEY = "cardId"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fun Intent.toCardBrowserLaunchOptions(): CardBrowserLaunchOptions? {
return CardBrowserLaunchOptions.SearchQueryJs(it, getBooleanExtra("all_decks", false))
}

getLongExtra(BrowserDestination.ToCard.EXTRA_CARD_ID_KEY)?.let { cardId ->
getLongExtra(CardBrowserViewModel.EXTRA_CARD_ID_KEY)?.let { cardId ->
return CardBrowserLaunchOptions.ScrollToCard(cardId)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,17 @@ class CardBrowserViewModel(
suspend fun queryDataForCardEdit(id: CardOrNoteId): CardId? = id.toCardId(cardsOrNotes)

private suspend fun getInitialDeck(): SelectableDeck {
// TODO: Handle the launch intent
suspend fun consumeIntentDeck(): SelectableDeck.Deck? {
if (savedStateHandle.get<Boolean>(STATE_LAUNCH_INTENT_CONSUMED) == true) return null
savedStateHandle[STATE_LAUNCH_INTENT_CONSUMED] = true
val deckId = savedStateHandle.get<Long>(EXTRA_DECK_ID) ?: return null
val name = withCol { decks.nameIfExists(deckId) } ?: return null
return SelectableDeck.Deck(deckId = deckId, name = name)
}

// Intent-supplied deck takes precedence, but only on the first launch
consumeIntentDeck()?.let { deck -> return deck }

val lastDeckId = lastDeckId
if (lastDeckId == ALL_DECKS_ID) {
return SelectableDeck.AllDecks
Expand Down Expand Up @@ -1424,6 +1434,15 @@ class CardBrowserViewModel(
suspend fun getAvailableDecks(): List<SelectableDeck.Deck> = SelectableDeck.fromCollection(includeFiltered = false)

companion object {
/** Intent extra carrying the [DeckId] the browser should open scoped to. */
const val EXTRA_DECK_ID = "deckId"

/** Intent extra carrying a [CardId] to auto-scroll to once the browser opens. */
const val EXTRA_CARD_ID_KEY = "cardId"

/** Prevents one-shot extras from being re-applied after process death. */
private const val STATE_LAUNCH_INTENT_CONSUMED = "launchIntentConsumed"

const val STATE_MULTISELECT = "multiselect"
const val STATE_MULTISELECT_VALUES = "multiselect_values"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ class ReviewerViewModel(
private suspend fun emitBrowseDestination() {
val deckId = withCol { decks.getCurrentId() }
val cardId = currentCard.await().id
val destination = BrowserDestination.ToCard(deckId, cardId)
val destination = BrowserDestination.ScrollToCard(deckId, cardId)
Timber.i("Launching 'browse options' for deck %d", deckId)
destinationFlow.emit(destination)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,77 @@ class CardBrowserViewModelTest : JvmTest() {
}
}

@Test
fun `EXTRA_DECK_ID intent opens the specified deck`() =
runTest {
val deckId = addDeck("New")
val savedState = SavedStateHandle(mapOf(CardBrowserViewModel.EXTRA_DECK_ID to deckId))
viewModel(savedStateHandle = savedState).apply {
assertThat("intent deck is selected", deckId, equalTo(this.deckId))
}
}

@Test
fun `EXTRA_DECK_ID intent persists deck to lastDeckIdRepository`() =
runTest {
val deckId = addDeck("New")
val savedState = SavedStateHandle(mapOf(CardBrowserViewModel.EXTRA_DECK_ID to deckId))
viewModel(savedStateHandle = savedState).apply {
assertThat("deck persisted for next launch", lastDeckId, equalTo(deckId))
}
}

@Test
fun `no EXTRA_DECK_ID falls back to lastDeckIdRepository`() =
runTest {
val deckId = addDeck("Persisted")
viewModel(lastDeckId = deckId).apply {
assertThat("repository value is used", deckId, equalTo(this.deckId))
}
}

@Test
fun `EXTRA_DECK_ID for unknown deck falls back to lastDeckIdRepository`() =
runTest {
val persisted = addDeck("Persisted")
val unknownDeckId: DeckId = 9_999_999_999L
val savedState = SavedStateHandle(mapOf(CardBrowserViewModel.EXTRA_DECK_ID to unknownDeckId))
viewModel(lastDeckId = persisted, savedStateHandle = savedState).apply {
assertThat("unknown intent deck is ignored", persisted, equalTo(this.deckId))
}
}

@Test
fun `user deck change survives process death`() =
runTest {
val intentDeckId = addDeck("From intent")
val userDeckId = addDeck("User selection")
val savedState = SavedStateHandle(mapOf(CardBrowserViewModel.EXTRA_DECK_ID to intentDeckId))

val persistentRepo =
object : LastDeckIdRepository {
override var lastDeckId: DeckId? = null
}

// setup: initial launch + select new deck
viewModel(
savedStateHandle = savedState,
lastDeckIdRepository = persistentRepo,
).apply {
assertThat("intent honored on first launch", deckId, equalTo(intentDeckId))
setSelectedDeck(SelectableDeck.Deck(deckId = userDeckId, name = "User selection"))
assertThat("user choice persisted to repository", persistentRepo.lastDeckId, equalTo(userDeckId))
}

// intent does not override user selection
viewModel(
savedStateHandle = savedState,
lastDeckIdRepository = persistentRepo,
).apply {
assertThat("user choice survives recreation", deckId, equalTo(userDeckId))
}
}

@Test
fun `sort order from notes is selected - 16514`() {
col.config.set("sortType", "noteCrt")
Expand Down Expand Up @@ -1710,12 +1781,12 @@ class CardBrowserViewModelTest : JvmTest() {
lastDeckId: DeckId? = null,
intent: CardBrowserLaunchOptions? = null,
mode: CardsOrNotes = CardsOrNotes.CARDS,
): CardBrowserViewModel {
val lastDeckIdRepository =
savedStateHandle: SavedStateHandle = SavedStateHandle(),
lastDeckIdRepository: LastDeckIdRepository =
object : LastDeckIdRepository {
override var lastDeckId: DeckId? = lastDeckId
}

},
): CardBrowserViewModel {
// default is CARDS, do nothing in this case
if (mode == CardsOrNotes.NOTES) {
CollectionManager.withCol { mode.saveToCollection(this@withCol) }
Expand All @@ -1728,7 +1799,7 @@ class CardBrowserViewModelTest : JvmTest() {
options = intent,
isFragmented = false,
preferences = AnkiDroidApp.sharedPreferencesProvider,
savedStateHandle = SavedStateHandle(),
savedStateHandle = savedStateHandle,
).apply {
invokeInitialSearch()
}
Expand Down
Loading