Skip to content

Conversation

@sanjaysargam
Copy link
Member

Purpose / Description

Expand CardContentProvider to return card IDs and cardinfo details

Fixes

Approach

I copy-pasted the proposed solution by @joaquintoral. And understood how it works on external app like AnkiConnectAndroid and test it properly.

How Has This Been Tested?

  1. Run AnkiConnectAndroid locally - https://github.com/sanjaysargam/AnkiconnectAndroid/tree/card
  2. Run AnkiDroid.

FindCards - /cards

curl -X POST http://192.168.0.101:8765 \
  -H "Content-Type: application/json" \
  -d '{
    "action": "findCards",
    "version": 6,
    "params": {
      "query": "deck:current"
    }
  }'

response in logcat:

2026-01-20 13:50:07.435  3211-8481  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  received json:
{"action":"findCards","version":6,"params":{"query":"deck:current"}}

2026-01-20 13:50:07.437  3211-8481  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  Processing findCards with query: deck:current

2026-01-20 13:50:07.437  3211-8481  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  findCards query: deck:current

2026-01-20 13:50:07.484  3211-8481  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  findCards found 1 cards for query: deck:current

2026-01-20 13:50:07.486  3211-8481  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  response json:
{"result":[1768897186292],"error":null}

FindCardByID - /cards/#

curl -X POST http://192.168.0.101:8765 \
  -H "Content-Type: application/json" \
  -d '{
    "action": "cardsInfo",
    "version": 6,
    "params": {
      "notes": [1768897186292]
    }
  }'

response in logcat:

2026-01-20 13:59:09.410  3211-9024  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  received json:
{"action":"cardsInfo","version":6,"params":{"notes":[1768897186292]}}

2026-01-20 13:59:09.412  3211-9024  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  Processing cardsInfo for IDs: [1768897186292]

2026-01-20 13:59:09.413  3211-9024  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  cardsInfo requested for card IDs: [1768897186292]

2026-01-20 13:59:09.425  3211-9024  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  Processed card ID: 1768897186292

2026-01-20 13:59:09.425  3211-9024  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  cardsInfo returning 1 card(s)

2026-01-20 13:59:09.428  3211-9024  AnkiConnectAndroid  com.kamwithk.ankiconnectandroid  D  response json:
{"result":[{"id":1768897186292,"noteId":1768897186292,"deckId":0,"templateIndex":0,"question":"Card 1768897186292 question","answer":"Card 1768897186292 answer","flags":0,"tags":[]}],"error":null}

Learning (optional, can help others)

This was completely new for me. I haven’t worked on scenarios where an external app consumes our APIs before, so exploring this is really interesting

Checklist

Please, go through these checks before submitting the PR.

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ContentProvider needs heavy testing as it's our public API

Please add tests for all additional functionality


I'd strongly suggest splitting this PR into two parts:

  1. Adding the public /cards/ endpoints: easy
  2. Adding additional properties to /cards: harder

Specifically for (2), I would like to see the source/docs of upstream Anki Desktop AnkiConnect, and how the data is modelled there. This is how people are used to consuming our data.

/**
* Due date for this card (day for review, timestamp for learning).
*/
public const val DUE: String = "due"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bad abstraction in the database, we probably want to expose something more useable publicly

/**
* Interval in days since last review.
*/
public const val INTERVAL: String = "interval"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto: this is optional

public const val INTERVAL: String = "interval"

/**
* Ease factor (0-2500, divide by 1000 for actual ease).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An output of 2500 is confusing

No explanation of what this does under FSRS

val cardIds = col.findCards(query)

// Fast path: Only if _id is requested
val onlyRequestingId = columns.size == 1 && columns[0] == FlashCardsContract.Card._ID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.singleOrNull

// Search for cards using Anki browser syntax
val columns = projection ?: FlashCardsContract.Card.DEFAULT_PROJECTION
val query = selection ?: ""
val cardIds = col.findCards(query)
Copy link
Member

@david-allison david-allison Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can throw on bad syntax: and throws (I believe).

Ensure we throw consistent exceptions from the API, and that exceptions are appropriately tested and documented

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returns empty list on a bad query

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't documented in the tests.

I also don't think it's correct, looking at the implementation of findCards

If it is correct, it doesn't appear to be consistent with our other endpoints.

Regardless

  • add a test, with a search of and
  • update the KDoc on findCards

@lukstbit lukstbit added Needs Author Reply Waiting for a reply from the original author and removed Needs Review labels Jan 20, 2026
@sanjaysargam
Copy link
Member Author

The ContentProvider needs heavy testing as it's our public API

For this, separate PR would be better

@david-allison
Copy link
Member

Not sure how to read the comment, but just to confirm:

Content Provider PRs need to include tests. It's not okay to submit them later

@sanjaysargam

This comment was marked as resolved.

@david-allison
Copy link
Member

Two PRs

@sanjaysargam sanjaysargam added Needs Review and removed Needs Author Reply Waiting for a reply from the original author labels Jan 23, 2026
Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Much easier to get through.

The returned columns aren't asserted on

// Search for cards using Anki browser syntax
val columns = projection ?: FlashCardsContract.Card.DEFAULT_PROJECTION
val query = selection ?: ""
val cardIds = col.findCards(query)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't documented in the tests.

I also don't think it's correct, looking at the implementation of findCards

If it is correct, it doesn't appear to be consistent with our other endpoints.

Regardless

  • add a test, with a search of and
  • update the KDoc on findCards

"999999999",
)

assertThrows<BackendNotFoundException> {
Copy link
Member

@david-allison david-allison Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is asserted in other places

We'll probably want to document it, and tighten this down later. RuntimeException was previously asserted

}

@Test
fun testSearchCards_returnsMatchingCards() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the intention of this test is unclear:

  • Are you adding a note with one, or two cards?

The test would pass with both, you might want to disambiguate this into two tests


/**
* The content:// style URI for cards. Can be used to search for cards or access specific cards.
* For examples on how to use the URI for queries see class description.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider linking the class description

*/
public object Card {
/**
* This is the ID of the card in the Anki database.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This is the ID of the card in the Anki database.
* The ID of the card in the Anki database.

val cardId = uri.pathSegments[1].toLong()
val columns = projection ?: FlashCardsContract.Card.DEFAULT_PROJECTION
val rv = MatrixCursor(columns, 1)
val card = col.getCard(cardId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the documentation on getCard: @throws

Comment on lines +114 to +115
private const val CARDS = 1100
private const val CARD_ID = 1101
Copy link
Member

@david-allison david-allison Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you reusing the 1 prefix?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expand CardContentProvider to return card IDs and cardinfo details

3 participants