Skip to main content

Overview

AnkiDroid Companion is built with a modular architecture that integrates with the AnkiDroid API to provide flashcard study sessions through persistent notifications. The app uses Android’s WorkManager for background task scheduling and SharedPreferences for state management.

Core Components

The application consists of five main components working together to deliver flashcard notifications:

MainActivity

Location: MainActivity.kt:22 The main entry point of the application, responsible for:
  • Initialization: Sets up notification channels and checks API availability
  • Permission Management: Requests AnkiDroid API read/write permissions
  • Deck Selection: Provides a UI for users to select their study deck
  • Worker Scheduling: Initiates the periodic background worker
class MainActivity : ComponentActivity() {
    private lateinit var mAnkiDroid:AnkiDroidHelper

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_layout)
        createNotificationChannel()
        setup()
    }
}
Key Responsibilities:
  1. Creates notification channel on app startup (MainActivity.kt:32-43)
  2. Validates AnkiDroid API availability (MainActivity.kt:60-63)
  3. Handles permission requests (MainActivity.kt:97-115)
  4. Populates deck spinner with available decks (MainActivity.kt:117-145)
  5. Triggers initial card notification on refresh (MainActivity.kt:147-168)

AnkiDroidHelper

Location: AnkiDroidHelper.java:31 The core integration layer that communicates with AnkiDroid’s Content Provider API.
public class AnkiDroidHelper {
    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
    
    private AddContentApi mApi;
    private Context mContext;
}
Key Responsibilities:
  1. API Access Management: Checks API availability and permissions
  2. Card Queries: Retrieves scheduled cards from AnkiDroid (AnkiDroidHelper.java:190-271)
  3. Review Submission: Submits user responses back to AnkiDroid (AnkiDroidHelper.java:273-284)
  4. State Persistence: Stores and retrieves current card state (AnkiDroidHelper.java:151-187)
  5. Deck Management: Maps deck names to IDs and handles deck lookups

Notifications

Location: Notifications.kt:15 Handles notification creation and display with custom views.
class Notifications {
    fun showNotification(context: Context, card: CardInfo?, 
                        deckName: String, isSilent: Boolean) {
        // Creates collapsed and expanded notification views
        // Attaches action buttons for card responses
    }
}
Key Features:
  1. Dual Notification States:
    • Active Card (Notifications.kt:26-62): Shows question/answer with 4 response buttons
    • Deck Complete (Notifications.kt:63-80): Congratulations message when no cards remain
  2. Custom Views:
    • Collapsed view with question header
    • Expanded view with question and answer
    • Four action buttons (Again, Hard, Good, Easy)
  3. HTML Stripping: Converts HTML card content to plain text (Notifications.kt:28-40)

NotificationReceiver

Location: NotificationReceiver.kt:8 A BroadcastReceiver that handles user interactions with notification buttons.
class NotificationReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        when (intent?.action) {
            "ACTION_BUTTON_1" -> respondCard(context, AnkiDroidHelper.EASE_1)
            "ACTION_BUTTON_2" -> respondCard(context, AnkiDroidHelper.EASE_2)
            "ACTION_BUTTON_3" -> respondCard(context, AnkiDroidHelper.EASE_3)
            "ACTION_BUTTON_4" -> respondCard(context, AnkiDroidHelper.EASE_4)
        }
    }
}
Response Flow (NotificationReceiver.kt:22-47):
  1. Retrieves stored card state
  2. Submits review to AnkiDroid with selected ease
  3. Queries for next scheduled card
  4. Updates notification with next card or completion message

PeriodicWorker

Location: PeriodicWorker.kt:10 A background worker that checks for new cards every 8 hours.
class PeriodicWorker(context: Context, params: WorkerParameters) 
    : Worker(context, params) {
    
    override fun doWork(): Result {
        mainThreadHandler.post {
            checkNotifications()
        }
        return Result.success()
    }
}
Scheduling (MainActivity.kt:45-56):
val periodicWorkRequest = PeriodicWorkRequest.Builder(
    PeriodicWorker::class.java,
    8, TimeUnit.HOURS
).build()

WorkManager.getInstance(this).enqueueUniquePeriodicWork(
    "WORKER_ANKI",
    ExistingPeriodicWorkPolicy.REPLACE,
    periodicWorkRequest
)
Logic (PeriodicWorker.kt:23-54):
  1. Only runs if current stored state shows no active card (cardOrd = -1)
  2. Queries AnkiDroid for next scheduled card
  3. Shows notification if a new card is available
  4. Stays silent if no cards are due

Data Flow

Initial Card Load

Card Response Flow

Background Worker Flow

State Management

The app uses Android’s SharedPreferences for persistent storage across two databases:

Deck Reference Database

Key: com.ichi2.anki.api.decks (AnkiDroidHelper.java:40) Purpose: Maps deck names to deck IDs to handle deck renaming
public void storeDeckReference(String deckName, long deckId) {
    final SharedPreferences decksDb = 
        mContext.getSharedPreferences(DECK_REF_DB, Context.MODE_PRIVATE);
    decksDb.edit().putLong(deckName, deckId).apply();
}

State Database

Key: com.ichi2.anki.api.state (AnkiDroidHelper.java:41) Purpose: Stores current card state as JSON Stored Fields (AnkiDroidHelper.java:154-161):
  • deck_id: Currently selected deck
  • note_id: Current card’s note ID
  • card_ord: Card ordinal number
  • start_time: When the card was first displayed
Map<String, Object> message = new HashMap<>();
message.put("deck_id", deckId);
message.put("note_id", card.noteID);
message.put("card_ord", card.cardOrd);
message.put("start_time", card.cardStartTime);
JSONObject js = new JSONObject(message);
cardsDb.edit().putString(KEY_CURRENT_STATE, js.toString()).apply();

Data Models

CardInfo

Location: CardInfo.java:7 Represents a flashcard with all necessary data for display and review.
public class CardInfo {
    String q = "", a = "";              // Question and answer
    int cardOrd;                        // Card ordinal in note
    long noteID;                        // Parent note ID
    int buttonCount;                    // Number of response buttons
    JSONArray nextReviewTexts = null;   // Time until next review
    JSONArray fileNames;                // Associated media files
    ArrayList<Uri> soundUris = null;    // Audio file URIs
    long cardStartTime;                 // Display timestamp
}

StoredState

Location: StoredState.java:3 Minimal state representation for persistence.
public class StoredState {
    long deckId;         // Selected deck
    int cardOrd;         // Current card ordinal (-1 if none)
    long noteID;         // Current note ID (-1 if none)
    long cardStartTime;  // When card was displayed
}

Dependencies

From build.gradle.kts:52-70:
dependencies {
    // Core Android
    implementation("androidx.core:core-ktx:1.10.1")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
    
    // AnkiDroid API
    implementation("com.github.ankidroid:Anki-Android:v2.17alpha8")
    
    // Background Work
    implementation("androidx.work:work-runtime:2.7.0")
    
    // Compose UI
    implementation(platform("androidx.compose:compose-bom:2023.08.00"))
    implementation("androidx.compose.material3:material3")
}
The app uses WorkManager for reliable background execution and Compose for UI, targeting Android SDK 26+ (Android 8.0 Oreo).

Thread Safety

The app handles threading carefully:
  • Main Thread: UI updates, notification display
  • Worker Thread: AnkiDroid API queries run in WorkManager’s background thread
  • Handler: PeriodicWorker uses Handler(Looper.getMainLooper()) to post results back to main thread (PeriodicWorker.kt:11,15)
AnkiDroid Content Provider queries must run on a thread with permission context. The app handles this by running queries in the worker thread and posting UI updates to the main thread.

Notification Channel

Setup (MainActivity.kt:32-43):
private fun createNotificationChannel() {
    val name = "AnkiNotificationChannel"
    val descriptionText = "Channel for anki notifications"
    val importance = NotificationManager.IMPORTANCE_DEFAULT
    val channel = NotificationChannel("channel_id", name, importance).apply {
        description = descriptionText
    }
    
    val notificationManager: NotificationManager =
        getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    notificationManager.createNotificationChannel(channel)
}
Properties:
  • Channel ID: channel_id
  • Importance: Default (makes sound, appears in notification tray)
  • Persistence: Active card notifications are ongoing (setOngoing(true)), completion notifications can be dismissed

Summary

The architecture follows a clear separation of concerns:
  1. MainActivity: User interface and initialization
  2. AnkiDroidHelper: Business logic and API integration
  3. Notifications: Presentation layer for card display
  4. NotificationReceiver: User input handling
  5. PeriodicWorker: Automated card scheduling
All components communicate through AnkiDroidHelper, which maintains state in SharedPreferences and coordinates with the AnkiDroid API. This design ensures that card data never leaves AnkiDroid’s database while providing a seamless notification-based study experience.

Build docs developers (and LLMs) love