Интеграция Calendar Provider (системный календарь) в Android-приложение
Системный календарь Android доступен через CalendarProvider — стандартный Content Provider, доступный с API 14. Большинство приложений используют один из двух сценариев: читать существующие события или создавать новые. Оба требуют рантайм-разрешений и корректной работы с URI.
Разрешения
Для чтения — READ_CALENDAR, для записи — WRITE_CALENDAR. Оба разрешения относятся к группе dangerous permissions, запрашиваются через ActivityCompat.requestPermissions() или ActivityResultContracts.RequestPermission(). Без явного запроса в рантайме SecurityException на Android 6+.
Чтение событий
События хранятся в таблице CalendarContract.Events. Запрос через ContentResolver:
val projection = arrayOf(
CalendarContract.Events._ID,
CalendarContract.Events.TITLE,
CalendarContract.Events.DTSTART,
CalendarContract.Events.DTEND,
CalendarContract.Events.CALENDAR_ID
)
val selection = "${CalendarContract.Events.DTSTART} >= ? AND ${CalendarContract.Events.DTEND} <= ?"
val selectionArgs = arrayOf(
startMillis.toString(),
endMillis.toString()
)
val cursor = context.contentResolver.query(
CalendarContract.Events.CONTENT_URI,
projection,
selection,
selectionArgs,
"${CalendarContract.Events.DTSTART} ASC"
)
cursor?.use {
while (it.moveToNext()) {
val title = it.getString(it.getColumnIndexOrThrow(CalendarContract.Events.TITLE))
val dtStart = it.getLong(it.getColumnIndexOrThrow(CalendarContract.Events.DTSTART))
// обработка
}
}
Важно: getColumnIndexOrThrow() вместо getColumnIndex() — при отсутствии колонки в проекции сразу падает с понятным исключением, а не с ArrayIndexOutOfBoundsException где-то в бизнес-логике.
Создание события
val values = ContentValues().apply {
put(CalendarContract.Events.CALENDAR_ID, calendarId)
put(CalendarContract.Events.TITLE, "Встреча с командой")
put(CalendarContract.Events.DTSTART, startMillis)
put(CalendarContract.Events.DTEND, endMillis)
put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
put(CalendarContract.Events.DESCRIPTION, "Обсуждение релиза v2.1")
}
val uri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, values)
val eventId = uri?.lastPathSegment?.toLong()
EVENT_TIMEZONE — обязательное поле. Без него событие создаётся в UTC, и пользователь видит некорректное время после смены часового пояса. Классическая ошибка, которая уходит в production и проявляется у пользователей из других регионов.
Добавление напоминания
val reminderValues = ContentValues().apply {
put(CalendarContract.Reminders.EVENT_ID, eventId)
put(CalendarContract.Reminders.MINUTES, 15)
put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
}
context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues)
Открытие системного UI
Если приложению не нужен прямой доступ к данным, а только открыть стандартный интерфейс добавления события — Intent без разрешений:
val intent = Intent(Intent.ACTION_INSERT).apply {
data = CalendarContract.Events.CONTENT_URI
putExtra(CalendarContract.Events.TITLE, "Название события")
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis)
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis)
}
startActivity(intent)
Это проще, безопаснее и не требует разрешений. Подходит для большинства случаев, когда приложение не ведёт собственный список событий.
Интеграция CalendarProvider занимает 1-3 дня в зависимости от объёма функциональности: чтение событий, создание, синхронизация нескольких аккаунтов. Стоимость рассчитывается индивидуально.







