Overview
The card review process is the core of AnkiDroid Companion’s functionality. It involves querying AnkiDroid for scheduled cards, displaying them in notifications, and submitting review responses with ease ratings.
Querying Scheduled Cards
The queryCurrentScheduledCard() method retrieves the next due card from AnkiDroid:
@ SuppressLint ( "Range,DirectSystemCurrentTimeMillisUsage" )
public CardInfo queryCurrentScheduledCard ( long deckID) {
String [] deckArguments = new String [deckID == - 1 ? 1 : 2 ];
String deckSelector = "limit=?" ;
deckArguments[ 0 ] = "" + 1 ;
if (deckID != - 1 ) {
deckSelector += ",deckID=?" ;
deckArguments[ 1 ] = "" + deckID;
}
// Permission check
if ( ! isPermissionGranted ()) {
uiHandler . post (() -> Toast . makeText (mContext,
R . string . permission_not_granted ,
Toast . LENGTH_SHORT ). show ());
} else {
// Query AnkiDroid's content provider
Cursor reviewInfoCursor =
mContext . getContentResolver (). query (
FlashCardsContract . ReviewInfo . CONTENT_URI ,
null ,
deckSelector,
deckArguments,
null );
if (reviewInfoCursor == null || ! reviewInfoCursor . moveToFirst ()) {
if (reviewInfoCursor != null ) {
reviewInfoCursor . close ();
}
} else {
// Process cursor results...
}
}
return null ;
}
From AnkiDroidHelper.java:189-271
Query Parameters
limit=1 : Only fetch one card at a time
deckID : Optional parameter to filter by specific deck (if -1, queries all decks)
Content Provider Integration
AnkiDroid exposes card data through Android’s ContentProvider API using the FlashCardsContract.ReviewInfo.CONTENT_URI.
This operation requires com.ichi2.anki.permission.READ_WRITE_DATABASE permission.
Building CardInfo Objects
When a card is found, the cursor data is mapped to a CardInfo object:
do {
CardInfo card = new CardInfo ();
card . cardOrd = reviewInfoCursor . getInt (
reviewInfoCursor . getColumnIndex ( FlashCardsContract . ReviewInfo . CARD_ORD ));
card . noteID = reviewInfoCursor . getLong (
reviewInfoCursor . getColumnIndex ( FlashCardsContract . ReviewInfo . NOTE_ID ));
card . buttonCount = reviewInfoCursor . getInt (
reviewInfoCursor . getColumnIndex ( FlashCardsContract . ReviewInfo . BUTTON_COUNT ));
try {
card . fileNames = new JSONArray ( reviewInfoCursor . getString (
reviewInfoCursor . getColumnIndex ( FlashCardsContract . ReviewInfo . MEDIA_FILES )));
card . nextReviewTexts = new JSONArray ( reviewInfoCursor . getString (
reviewInfoCursor . getColumnIndex ( FlashCardsContract . ReviewInfo . NEXT_REVIEW_TIMES )));
} catch ( JSONException e ) {
e . printStackTrace ();
}
card . cardStartTime = System . currentTimeMillis ();
cards . add (card);
} while ( reviewInfoCursor . moveToNext ());
From AnkiDroidHelper.java:222-238
CardInfo Fields
cardOrd : Card order within its note template
noteID : Unique identifier for the note
buttonCount : Number of ease buttons (typically 4)
fileNames : Media files attached to the card (JSON array)
nextReviewTexts : Scheduled review times for each ease button
cardStartTime : Timestamp when card was first displayed (for time tracking)
Fetching Card Content
After getting review metadata, the app fetches the actual question and answer:
for ( CardInfo card : cards) {
Uri noteUri = Uri . withAppendedPath (
FlashCardsContract . Note . CONTENT_URI ,
Long . toString ( card . noteID ));
Uri cardsUri = Uri . withAppendedPath (noteUri, "cards" );
Uri specificCardUri = Uri . withAppendedPath (cardsUri, Integer . toString ( card . cardOrd ));
final Cursor specificCardCursor = mContext . getContentResolver (). query (
specificCardUri,
SIMPLE_CARD_PROJECTION, // [ANSWER_PURE, QUESTION_SIMPLE]
null ,
null ,
null
);
if (specificCardCursor == null || ! specificCardCursor . moveToFirst ()) {
if (specificCardCursor != null ) {
specificCardCursor . close ();
}
return null ;
} else {
card . a = specificCardCursor . getString (
specificCardCursor . getColumnIndex ( FlashCardsContract . Card . ANSWER_PURE ));
card . q = specificCardCursor . getString (
specificCardCursor . getColumnIndex ( FlashCardsContract . Card . QUESTION_SIMPLE ));
specificCardCursor . close ();
}
}
return cards . get ( 0 );
From AnkiDroidHelper.java:243-267
Content URI Structure
content://com.ichi2.anki.flashcards/notes/{noteID}/cards/{cardOrd}
This hierarchical URI structure maps to:
Note collection
Specific note by ID
Cards within that note
Specific card by ordinal
Submitting Reviews
When a user taps an ease button, the review is submitted with time tracking:
public void reviewCard ( long noteID, int cardOrd, long cardStartTime, int ease) {
long timeTaken = System . currentTimeMillis () - cardStartTime;
ContentResolver cr = mContext . getContentResolver ();
Uri reviewInfoUri = FlashCardsContract . ReviewInfo . CONTENT_URI ;
ContentValues values = new ContentValues ();
values . put ( FlashCardsContract . ReviewInfo . NOTE_ID , noteID);
values . put ( FlashCardsContract . ReviewInfo . CARD_ORD , cardOrd);
values . put ( FlashCardsContract . ReviewInfo . EASE , ease);
values . put ( FlashCardsContract . ReviewInfo . TIME_TAKEN , timeTaken);
cr . update (reviewInfoUri, values, null , null );
}
From AnkiDroidHelper.java:273-284
Review Parameters
noteID : Identifies which note was reviewed
cardOrd : Identifies which card within the note
ease : The difficulty rating (1-4)
timeTaken : Milliseconds spent reviewing the card
Ease Ratings
public static final int EASE_1 = 1 ; // Again
public static final int EASE_2 = 2 ; // Hard
public static final int EASE_3 = 3 ; // Good
public static final int EASE_4 = 4 ; // Easy
From AnkiDroidHelper.java:32-35
Moving to Next Card
After submitting a review, the app automatically moves to the next card:
private fun respondCard (context: Context , ease: Int ) {
var mAnkiDroid = AnkiDroidHelper (context)
val localState = mAnkiDroid.storedState
if (localState != null ) {
// Submit the review
mAnkiDroid. reviewCard (
localState.noteID,
localState.cardOrd,
localState.cardStartTime,
ease)
}
// Move to next card
val nextCard = mAnkiDroid. queryCurrentScheduledCard (localState.deckId)
if (nextCard != null ) {
mAnkiDroid. storeState (localState.deckId, nextCard)
Notifications. create (). showNotification (context, nextCard, mAnkiDroid.currentDeckName, true )
} else {
// No more cards - show completion notification
val emptyCard = CardInfo ()
emptyCard.cardOrd = - 1
emptyCard.noteID = - 1
mAnkiDroid. storeState (localState.deckId, emptyCard)
Notifications. create (). showNotification (context, null , "" , true )
}
}
From NotificationReceiver.kt:22-47
Review Flow
Retrieve State : Get the current card info from SharedPreferences
Submit Review : Send the ease rating and time to AnkiDroid
Query Next Card : Fetch the next scheduled card
Update State : Store the new card info
Update Notification : Show the new card or completion message
Time Tracking
The app tracks how long users spend on each card:
// When card is first shown
card . cardStartTime = System . currentTimeMillis ();
// When review is submitted
long timeTaken = System . currentTimeMillis () - cardStartTime;
This timing data helps AnkiDroid’s spaced repetition algorithm make better scheduling decisions.
Periodic Card Checking
The PeriodicWorker checks for new cards at regular intervals:
private fun checkNotifications () {
var mAnkiDroid = AnkiDroidHelper (applicationContext)
val localState = mAnkiDroid.storedState
if (localState == null ) {
return
}
// Only check if current card is empty (completed deck)
if (localState.cardOrd != - 1 || localState.noteID != ( - 1 ). toLong ()) {
return
}
// Try to get next scheduled card
val nextCard = mAnkiDroid. queryCurrentScheduledCard (localState.deckId)
if (nextCard != null ) {
mAnkiDroid. storeState (localState.deckId, nextCard)
Notifications. create (). showNotification (
applicationContext,
nextCard,
mAnkiDroid.currentDeckName,
false )
}
}
From PeriodicWorker.kt:23-54
Empty Card Detection
An “empty card” state (cardOrd = -1, noteID = -1) indicates the deck is complete. The worker only queries for new cards when in this state.
The periodic check uses isSilent = false to notify users when new cards become available.
Permission Requirements
All card review operations require the dangerous permission: com.ichi2.anki.permission.READ_WRITE_DATABASE
The app must request this at runtime on Android 6.0+.
Error Handling
The card query process includes several error checks:
Null cursor : AnkiDroid may return null if the database is unavailable
Empty cursor : No cards are currently scheduled
Permission denied : User hasn’t granted database access
Content not found : Specific card data may be missing
In all error cases, the method returns null, and the caller should handle this gracefully.
Notification System Learn how cards are displayed
Deck Management Understand deck selection