Skip to content

Commit 570c05a

Browse files
lonewolf2208mikehardy
authored andcommitted
feat(api): added fields to cards endpoints
Refine card field docs and tests test: fix nullable cursor handling in provider tests docs: refine card field KDoc in FlashCardsContract docs: clarify FlashCardsContract.Card.TYPE KDoc
1 parent 4152002 commit 570c05a

3 files changed

Lines changed: 210 additions & 0 deletions

File tree

AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,158 @@ class ContentProviderTest : InstrumentedTest() {
391391
}
392392
}
393393

394+
@Test
395+
fun testQueryCardReps() {
396+
val noteId = createdNotes.first().lastPathSegment!!.toLong()
397+
val note = col.getNote(noteId)
398+
val card = note.cards(col).single()
399+
val expectedReps = 7
400+
401+
card.update {
402+
reps = expectedReps
403+
}
404+
405+
val cursor =
406+
checkNotNull(
407+
contentResolver.query(
408+
FlashCardsContract.Card.CONTENT_URI,
409+
arrayOf(FlashCardsContract.Card.REPS),
410+
"cid:${card.id}",
411+
null,
412+
null,
413+
),
414+
) { "cursor from /cards" }
415+
416+
cursor.use {
417+
assertEquals(1, it.count)
418+
assertTrue(it.moveToFirst())
419+
assertEquals(listOf(FlashCardsContract.Card.REPS), it.columnNames.toList())
420+
assertEquals(expectedReps, it.getInt(it.getColumnIndex(FlashCardsContract.Card.REPS)))
421+
}
422+
}
423+
424+
@Test
425+
fun testQueryCardLapses() {
426+
val noteId = createdNotes.first().lastPathSegment!!.toLong()
427+
val noteUri = Uri.withAppendedPath(FlashCardsContract.Note.CONTENT_URI, noteId.toString())
428+
val noteCardsUri = Uri.withAppendedPath(noteUri, "cards")
429+
val card = col.getNote(noteId).cards(col).single()
430+
val expectedLapses = 3
431+
432+
card.update {
433+
lapses = expectedLapses
434+
}
435+
436+
val cursor =
437+
checkNotNull(
438+
contentResolver.query(
439+
noteCardsUri,
440+
arrayOf(FlashCardsContract.Card.LAPSES),
441+
null,
442+
null,
443+
null,
444+
),
445+
) { "cursor from /notes/#/cards" }
446+
447+
cursor.use {
448+
assertEquals(1, it.count)
449+
assertTrue(it.moveToFirst())
450+
assertEquals(listOf(FlashCardsContract.Card.LAPSES), it.columnNames.toList())
451+
assertEquals(expectedLapses, it.getInt(it.getColumnIndex(FlashCardsContract.Card.LAPSES)))
452+
}
453+
}
454+
455+
@Test
456+
fun testQueryCardType() {
457+
val noteId = createdNotes.first().lastPathSegment!!.toLong()
458+
val card = col.getNote(noteId).cards(col).single()
459+
val expectedType = 2
460+
val cardUri =
461+
Uri.withAppendedPath(
462+
FlashCardsContract.Card.CONTENT_URI,
463+
card.id.toString(),
464+
)
465+
466+
card.moveToReviewQueue()
467+
468+
val cursor =
469+
checkNotNull(
470+
contentResolver.query(
471+
cardUri,
472+
arrayOf(FlashCardsContract.Card.TYPE),
473+
null,
474+
null,
475+
null,
476+
),
477+
) { "cursor from /cards/#" }
478+
479+
cursor.use {
480+
assertEquals(1, it.count)
481+
assertTrue(it.moveToFirst())
482+
assertEquals(listOf(FlashCardsContract.Card.TYPE), it.columnNames.toList())
483+
assertEquals(expectedType, it.getInt(it.getColumnIndex(FlashCardsContract.Card.TYPE)))
484+
}
485+
}
486+
487+
@Test
488+
fun testQueryCardOriginalDeckId() {
489+
val noteId = createdNotes.first().lastPathSegment!!.toLong()
490+
val noteUri = Uri.withAppendedPath(FlashCardsContract.Note.CONTENT_URI, noteId.toString())
491+
val noteCardsUri = Uri.withAppendedPath(noteUri, "cards")
492+
val card = col.getNote(noteId).cards(col).single()
493+
val expectedOriginalDeckId = testDeckIds[0]
494+
val noteCardUri = Uri.withAppendedPath(noteCardsUri, card.ord.toString())
495+
496+
card.update {
497+
oDid = expectedOriginalDeckId
498+
}
499+
500+
val cursor =
501+
checkNotNull(
502+
contentResolver.query(
503+
noteCardUri,
504+
arrayOf(FlashCardsContract.Card.ORIGINAL_DECK_ID),
505+
null,
506+
null,
507+
null,
508+
),
509+
) { "cursor from /notes/#/cards/#" }
510+
511+
cursor.use {
512+
assertEquals(1, it.count)
513+
assertTrue(it.moveToFirst())
514+
assertEquals(listOf(FlashCardsContract.Card.ORIGINAL_DECK_ID), it.columnNames.toList())
515+
assertEquals(
516+
expectedOriginalDeckId,
517+
it.getLong(it.getColumnIndex(FlashCardsContract.Card.ORIGINAL_DECK_ID)),
518+
)
519+
}
520+
}
521+
522+
@Test
523+
fun testQueryCardDefaultProjectionOmitsRawProperties() {
524+
val noteId = createdNotes.first().lastPathSegment!!.toLong()
525+
val card = col.getNote(noteId).cards(col).single()
526+
val cardUri =
527+
Uri.withAppendedPath(
528+
FlashCardsContract.Card.CONTENT_URI,
529+
card.id.toString(),
530+
)
531+
532+
val cursor =
533+
checkNotNull(contentResolver.query(cardUri, null, null, null, null)) {
534+
"cursor from default projection query"
535+
}
536+
537+
cursor.use {
538+
assertTrue(it.moveToFirst())
539+
assertEquals(-1, it.getColumnIndex(FlashCardsContract.Card.TYPE))
540+
assertEquals(-1, it.getColumnIndex(FlashCardsContract.Card.REPS))
541+
assertEquals(-1, it.getColumnIndex(FlashCardsContract.Card.LAPSES))
542+
assertEquals(-1, it.getColumnIndex(FlashCardsContract.Card.ORIGINAL_DECK_ID))
543+
}
544+
}
545+
394546
@Test
395547
fun testQueryCardsRoot_returnsCards() {
396548
val cursor =

AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,10 @@ class CardContentProvider : ContentProvider() {
11781178
FlashCardsContract.Card.CARD_ORD -> rb.add(currentCard.ord)
11791179
FlashCardsContract.Card.CARD_NAME -> rb.add(cardName)
11801180
FlashCardsContract.Card.DECK_ID -> rb.add(currentCard.did)
1181+
FlashCardsContract.Card.REPS -> rb.add(currentCard.reps)
1182+
FlashCardsContract.Card.LAPSES -> rb.add(currentCard.lapses)
1183+
FlashCardsContract.Card.TYPE -> rb.add(currentCard.type.code)
1184+
FlashCardsContract.Card.ORIGINAL_DECK_ID -> rb.add(currentCard.oDid)
11811185
FlashCardsContract.Card.QUESTION -> rb.add(question)
11821186
FlashCardsContract.Card.ANSWER -> rb.add(answer)
11831187
FlashCardsContract.Card.QUESTION_SIMPLE -> rb.add(currentCard.renderOutput(col).questionText)

api/src/main/java/com/ichi2/anki/FlashCardsContract.kt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,60 @@ public object FlashCardsContract {
608608
*/
609609
public const val DECK_ID: String = "deck_id"
610610

611+
/**
612+
* The stored Anki repetition count for this card.
613+
*
614+
* Counts how many times Anki has recorded this card as answered during study. The Anki
615+
* manual's
616+
* [`prop:reps`](https://docs.ankiweb.net/searching.html#card-properties) search uses the
617+
* same stored counter. New cards typically start at 0.
618+
*
619+
* This provider exposes the backend value as-is; exact effects of manual operations such as
620+
* rescheduling depend on Anki's scheduler/backend behavior.
621+
*/
622+
public const val REPS: String = "reps"
623+
624+
/**
625+
* The stored Anki lapse count for this card.
626+
*
627+
* Counts how many times Anki has recorded this card as lapsed. In Anki's manual, a
628+
* [lapse](https://docs.ankiweb.net/deck-options.html#lapses) is pressing Again on a review
629+
* card, and [`prop:lapses`](https://docs.ankiweb.net/searching.html#card-properties)
630+
* searches this same stored counter.
631+
*
632+
* This provider exposes the backend value as-is.
633+
*/
634+
public const val LAPSES: String = "lapses"
635+
636+
/**
637+
* The stored Anki card type code: the card's learning or review stage.
638+
*
639+
* See also [Anki's card states](https://docs.ankiweb.net/getting-started.html#card-states).
640+
*
641+
* Think of this as a simple state machine that affects how the card is scheduled when it
642+
* is answered. A card typically moves `0 -> 1 -> 2`; if a review card lapses, it
643+
* typically moves `2 -> 3 -> 2`.
644+
*
645+
* * `0` = new
646+
* * `1` = learning
647+
* * `2` = review
648+
* * `3` = relearning
649+
*
650+
* Other values should be treated as unknown.
651+
*/
652+
public const val TYPE: String = "type"
653+
654+
/**
655+
* The stored original deck id for this card.
656+
*
657+
* For cards in filtered decks, Anki keeps a link to the card's
658+
* [home deck](https://docs.ankiweb.net/filtered-decks.html#home-decks).
659+
*
660+
* * If the card is currently in a filtered deck, this is the deck id the card came from.
661+
* * If the card is not currently in a filtered deck, this value is 0.
662+
*/
663+
public const val ORIGINAL_DECK_ID: String = "original_deck_id"
664+
611665
/**
612666
* The question for this card.
613667
*/

0 commit comments

Comments
 (0)