Auf den Kalender zugreifen: CalendarProvider, Events und Berechtigungen
Android bietet mit dem CalendarProvider eine standardisierte Schnittstelle zu Terminen. Sensible Kalender-Daten verlangen explizite Laufzeit-Berechtigungen.
Der Android-Kalender ist mehr als eine isolierte App – er ist ein geteilter Datenspeicher, auf den mehrere Apps gleichzeitig zugreifen können. Wer Termine lesen oder eintragen möchte, arbeitet mit dem CalendarProvider, dem offiziellen ContentProvider des Betriebssystems. Dabei ist Sorgfalt gefragt: Termindaten verraten Gewohnheiten, Beziehungen und vertrauliche Meetings, weshalb Android den Zugriff mit strengen Berechtigungsregeln absichert.
Was ist das?
Der CalendarProvider ist ein systemweiter ContentProvider, der sämtliche Kalender und Ereignisse auf dem Gerät verwaltet. Sein Datenmodell steckt in der Klasse CalendarContract und besteht aus mehreren Tabellen: Calendars listet die verfügbaren Kalender auf (zum Beispiel Google Calendar, Exchange oder lokale Kalender), Events enthält die einzelnen Termine, und Hilfstabellen wie Reminders und Attendees ergänzen das Modell um Erinnerungen und Teilnehmer.
Im Android-Ökosystem ist dieser Provider das Bindeglied zwischen System-Apps und Drittanbieter-Apps. Eine Aufgaben-App kann darüber Deadlines importieren, eine CRM-App kann Kundenmeetings eintragen, ein Widget kann den nächsten Termin anzeigen – alles über dieselbe einheitliche Schnittstelle. Weil diese Daten tiefe Einblicke in das Privatleben ermöglichen, hat Google den Zugriff in die Kategorie der sensitiven Berechtigungen eingestuft. Du musst die Berechtigung im Manifest deklarieren und sie zur Laufzeit explizit vom Nutzer anfordern.
Wie funktioniert es?
Berechtigungen
Zuerst deklarierst du die benötigten Berechtigungen im Manifest:
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
Zur Laufzeit fragst du sie mit dem Activity Result API an – direkt vor dem ersten Zugriff. ActivityResultContracts.RequestMultiplePermissions eignet sich, wenn du beide Richtungen gleichzeitig benötigst. Hat der Nutzer die Berechtigung dauerhaft verweigert, leite ihn mit einem Intent zu den App-Einstellungen weiter, anstatt stumm zu scheitern.
ContentResolver und CalendarContract
Der Datenzugriff läuft vollständig über ContentResolver. Jede Tabelle ist über eine URI ansprechbar:
CalendarContract.Calendars.CONTENT_URI– alle KalenderCalendarContract.Events.CONTENT_URI– alle Ereignisse
Abfragen folgen dem Standard-Muster mit query(), insert(), update() und delete(). Die Spalten-Konstanten liegen direkt in den inneren Klassen von CalendarContract, zum Beispiel Events.TITLE, Events.DTSTART, Events.DTEND oder Events.CALENDAR_ID. Für eine neue Einfüge-Operation packst du alle Felder in ein ContentValues-Objekt und übergibst es an contentResolver.insert().
Architektur-Einordnung
Da ContentResolver-Aufrufe blockierend sind, gehören sie in einen Repository-Layer, der mit Coroutines auf Dispatchers.IO ausgeführt wird. Die offizielle Data-Layer-Architektur von Android empfiehlt, alle Datenzugriffe hinter einem Repository zu kapseln, damit ViewModel und UI-Schicht entkoppelt bleiben. In einem Compose-basierten Projekt gibt das Repository einen Flow zurück; das ViewModel hält den Zustand und deine Composables lesen ihn über collectAsState().
In der Praxis
Das folgende Beispiel liest die nächsten fünf Termine aus allen Kalendern des Geräts und gibt deren Titel zurück:
suspend fun fetchUpcomingEvents(context: Context): List<String> =
withContext(Dispatchers.IO) {
val now = System.currentTimeMillis()
val projection = arrayOf(
CalendarContract.Events._ID,
CalendarContract.Events.TITLE,
CalendarContract.Events.DTSTART
)
val selection = "${CalendarContract.Events.DTSTART} >= ?"
val selectionArgs = arrayOf(now.toString())
val sortOrder = "${CalendarContract.Events.DTSTART} ASC"
val cursor = context.contentResolver.query(
CalendarContract.Events.CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)
val titles = mutableListOf<String>()
cursor?.use {
var count = 0
while (it.moveToNext() && count < 5) {
val title = it.getString(
it.getColumnIndexOrThrow(CalendarContract.Events.TITLE)
)
titles.add(title)
count++
}
}
titles
}
Typische Stolperfalle: fehlende Berechtigungsprüfung
Der häufigste Fehler ist, contentResolver.query() aufzurufen, ohne vorher mit ContextCompat.checkSelfPermission() zu prüfen, ob READ_CALENDAR tatsächlich erteilt wurde. Das Ergebnis ist eine SecurityException, die deine App zum Absturz bringt. Prüfe die Berechtigung deshalb immer als ersten Schritt – auch wenn du sie kurz zuvor angefragt hast, denn der Nutzer kann sie in den Systemeinstellungen jederzeit widerrufen. Ein zweiter Fallstrick: Lasse niemals einen Cursor offen. Verwende immer cursor?.use { ... }, damit der Cursor garantiert geschlossen wird, selbst wenn innerhalb des Blocks eine Exception auftritt.
Plane außerdem ein, dass auf Geräten ohne Google-Konto oder in reinen Arbeitsprofil-Konfigurationen möglicherweise gar keine Kalender vorhanden sind. Fange den leeren Cursor-Fall explizit ab, anstatt stillschweigend eine leere Liste zurückzugeben, ohne den Nutzer zu informieren.
Fazit
Der CalendarProvider gibt dir ein mächtiges Werkzeug an die Hand, um Termindaten systemweit zu lesen und zu schreiben – aber mit großer Macht kommt große Verantwortung. Fordere READ_CALENDAR und WRITE_CALENDAR nur an, wenn deine App sie wirklich benötigt, erkläre dem Nutzer klar den Zweck, und kapsle alle Zugriffe in einem Repository hinter Coroutines. Zur Übung: Erstelle auf deinem Testgerät einen Termin mit einem eindeutigen Titel, schreibe einen Integrations-Test, der diesen Titel über den ContentResolver ausliest, und vergleiche ihn gegen den erwarteten Wert. So merkst du sofort, ob deine Berechtigungslogik, dein Repository und dein ViewModel korrekt zusammenspielen.