基本原理
在 Android 里,日程数据其实不是直接由"日历 App"管理,而是由 日历内容提供器(CalendarProvider) 管理的。不同厂商的 ROM(比如小米、华为、OPPO)可能自研了日历 App,但通常仍然会复用 系统日历 Provider,否则很多第三方 App(微信、钉钉等)就没办法添加日程了。
想要对CalendarProvider进行读写需要访问日历权限:
Manifest.permission.WRITE_CALENDAR
Manifest.permission.READ_CALENDAR
CalendarProvider 只有API >= 14 (Android 4.0) 才提供
CalendarProvider能提供查询和插入的数据种类都定义在CalendarContract中,比较重要的字段如下:
CalendarContract.Calendars._ID:日历账户唯一ID,想要写入日程最重要的一个参数就是账户ID,每一条日程记录都需要对应一个账户ID。一台手机上会有多个日历账户,也可能只有一个日历账户,例如Google日历支持登录多个账号,此时通过CalendarProvider查询账户能得到多个ID。而对于一些国产ROM例如OPPO即使你登录了OPPO账号,它也只有一个local account。
PS: 当权限不足时,也会出现ID为空的情况
CalendarContract.Calendars.VISIBLE:该日历是否在系统日历应用中对用户"可见"。仅是UI层面
CalendarContract.Events._ID:日程ID,可用于修改删除
权限获取
kotlin
val permissions = arrayOf(Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR)
if(!EasyPermissions.hasPermissions(this,*permissions)){
ActivityCompat.requestPermissions(this, permissions, REQ_LOCATION_PERMISSION)
}
在实际使用时发现有两种权限弹窗:
在 Android 权限模型里:
- READ_CALENDAR 和 WRITE_CALENDAR 是两个独立的危险权限。
- 但是它们都属于同一个 权限组(permission group) ------
android.permission-group.CALENDAR
。
这带来一个现象: 在 Android 6.0--9.0 (API 23--28) 系统只在 UI 上显示"日历"这个权限组,而不会细分到 READ 和 WRITE。
- 用户一旦授予"日历"权限组,应用同时获得 READ + WRITE。
- 用户无法单独选择"只授予 WRITE"。
在 Android 10 (API 29) 及以后 Google 修改了权限模型:同一权限组内的权限在 UI 上可能分开展示(例如 位置权限 会有"仅允许精确定位/仅允许大概定位")。
但日历组目前(截至 Android 14)依然是合并的,用户无法只勾 WRITE 而不给 READ。
既然Android权限模型中,不论是单独申请READ还是WRITE,实际都是一并给予权限,为什么还是会有机型出现图一的"仅允许创建"。并且当用户选择"仅允许创建"的情况下,确实读不到系统日程的内容
通过dumpsys package xxx | grep permission指令查询权限情况,能发现,即使是图一这种手机,无论用户选择"允许"还是"仅允许创建"得到的权限都是一样的。那只能说明厂商在CalendarProvider进行了二次开发
厂商为了提升"隐私权限可控性",在 系统 ContentProvider 框架层 增加了拦截逻辑:
- 即使底层
PackageManager
认为你有READ_CALENDAR
- 系统在访问
CalendarProvider
的时候,会根据用户选择的权限模式做"二次判断" - 如果用户选择了"仅允许创建",系统会在 Provider 层拦截
query
操作,只放行insert
。 - 所以 表面上有 READ,但访问时被屏蔽。
检查手机是否支持CalendarProvider
kotlin
fun hasCalendarProvider(context: Context): Boolean {
val pm = context.packageManager
// 2. 检查 Provider 是否存在
val providers = listOf(
ComponentName(
"com.android.providers.calendar",
"com.android.providers.calendar.CalendarProvider2"
),
ComponentName(
"com.android.providers.calendar",
"com.android.providers.calendar.CalendarProvider"
)
)
var providerExists = false
for (component in providers) {
try {
pm.getProviderInfo(component, 0)
providerExists = true
break
} catch (_: PackageManager.NameNotFoundException) {
// ignore
}
}
if (!providerExists) return false
// 3. 尝试访问 CalendarContract.Calendars.CONTENT_URI
return try {
val uri: Uri = CalendarContract.Calendars.CONTENT_URI
val cursor = context.contentResolver.query(
uri,
arrayOf(CalendarContract.Calendars._ID),
null,
null,
null
)
cursor?.use {
return it.count >= 0 // 能查询说明可用
}
false
} catch (e: Exception) {
false
}
}
查询所有账户
kotlin
// 查询所有可见账户
fun queryCalendars(context: Context): List<CalendarAccount> {
val calendars = mutableListOf<CalendarAccount>()
val projection = arrayOf(
CalendarContract.Calendars._ID, // 账户ID
CalendarContract.Calendars.ACCOUNT_NAME, // 该日历所属的 账号名。是 Android 账号管理(AccountManager)里的账号名,和 ACCOUNT_TYPE 一起标识一个账号。
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 日历名
CalendarContract.Calendars.OWNER_ACCOUNT, // 该日历的"所有者账号"。通常用来标识谁是这个日历的创建者/管理者。
CalendarContract.Calendars.VISIBLE, // 是否可见
CalendarContract.Calendars.ACCOUNT_TYPE //账号类型
)
val cursor: Cursor? = try {
context.contentResolver.query(
CalendarContract.Calendars.CONTENT_URI,
projection,
"${CalendarContract.Calendars.VISIBLE} = 1",
null,
null
)
} catch (e: SecurityException) {
e.printStackTrace()
null
}
cursor?.use {
val idIndex = it.getColumnIndex(CalendarContract.Calendars._ID)
val accountIndex = it.getColumnIndex(CalendarContract.Calendars.ACCOUNT_NAME)
val nameIndex = it.getColumnIndex(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME)
val ownerIndex = it.getColumnIndex(CalendarContract.Calendars.OWNER_ACCOUNT)
val visibleIndex = it.getColumnIndex(CalendarContract.Calendars.VISIBLE)
val accountTypeIndex = it.getColumnIndex(CalendarContract.Calendars.ACCOUNT_TYPE)
while (it.moveToNext()) {
val id = it.getLong(idIndex)
val accountName = it.getString(accountIndex) ?: ""
val displayName = it.getString(nameIndex) ?: ""
val ownerAccount = it.getString(ownerIndex)
val visible = it.getInt(visibleIndex) == 1
val accountType = it.getString(accountTypeIndex) ?: ""
calendars.add(
CalendarAccount(
id = id,
accountName = accountName,
displayName = displayName,
ownerAccount = ownerAccount,
visible = visible,
accountType = accountType
)
)
}
}
return calendars
}
向指定账户写入日程
kotlin
fun addEventToCalendar(context: Context, id: Long = 0) {
val startMillis: Long
val endMillis: Long
Calendar.getInstance().apply {
set(2025, Calendar.AUGUST, 20, 9, 0) // 2025-08-20 09:00
startMillis = timeInMillis
}
Calendar.getInstance().apply {
set(2025, Calendar.AUGUST, 20, 10, 0) // 2025-08-20 10:00
endMillis = timeInMillis
}
val values = ContentValues().apply {
put(CalendarContract.Events.DTSTART, startMillis)
put(CalendarContract.Events.DTEND, endMillis)
put(CalendarContract.Events.TITLE, "会议")
put(CalendarContract.Events.DESCRIPTION, "测试测试测试项目讨论会议")
put(CalendarContract.Events.CALENDAR_ID, id)
put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
}
val uri: Uri? = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, values)
val eventId = uri?.lastPathSegment
}
可支持写入参数

参数兼容性
以下参数是 Calendar API 最基础的字段,从 API 1(很早)就有:因此无需考虑兼容性,均可写入
- DTSTART / DTEND
- TITLE
- DESCRIPTION
- EVENT_LOCATION
- EVENT_TIMEZONE
- ALL_DAY
- HAS_ALARM
- CALENDAR_ID
注意: 厂商定制影响大:部分国产 ROM(比如国内的无 Google 框架设备)会裁剪 CalendarProvider,插入后可能不会显示或会丢字段。
Google 日历 vs 系统日历:Google 日历应用会支持大多数字段,但某些国产系统日历可能忽略颜色、参与人、访问级别等信息。
测试建议 : 插入数据前用context.contentResolver.query(CalendarContract.Events.CONTENT_URI, null, null, null, null)
看看当前设备支持哪些列,避免写入无效字段。
读取指定账号日程
kotlin
fun queryEventsByCalendarId(context: Context, calendarId: Long): List<CalendarEvent> {
val events = mutableListOf<CalendarEvent>()
val projection = arrayOf(
CalendarContract.Events._ID,
CalendarContract.Events.TITLE,
CalendarContract.Events.DESCRIPTION,
CalendarContract.Events.DTSTART,
CalendarContract.Events.DTEND,
CalendarContract.Events.ALL_DAY,
CalendarContract.Events.EVENT_TIMEZONE
)
val selection = "${CalendarContract.Events.CALENDAR_ID} = ?"
val selectionArgs = arrayOf(calendarId.toString())
val cursor: Cursor? = try {
context.contentResolver.query(
CalendarContract.Events.CONTENT_URI,
projection,
selection,
selectionArgs,
"${CalendarContract.Events.DTSTART} ASC" // 按开始时间升序
)
} catch (e: SecurityException) {
e.printStackTrace()
null
}
cursor?.use {
val idIndex = it.getColumnIndex(CalendarContract.Events._ID)
val titleIndex = it.getColumnIndex(CalendarContract.Events.TITLE)
val descIndex = it.getColumnIndex(CalendarContract.Events.DESCRIPTION)
val startIndex = it.getColumnIndex(CalendarContract.Events.DTSTART)
val endIndex = it.getColumnIndex(CalendarContract.Events.DTEND)
val allDayIndex = it.getColumnIndex(CalendarContract.Events.ALL_DAY)
val tzIndex = it.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE)
while (it.moveToNext()) {
events.add(
CalendarEvent(
id = it.getLong(idIndex),
title = it.getString(titleIndex),
description = it.getString(descIndex),
startTime = it.getLong(startIndex),
endTime = it.getLong(endIndex),
allDay = it.getInt(allDayIndex) != 0,
timezone = it.getString(tzIndex)
)
)
}
}
return events
}
删除指定日程
kotlin
fun deleteEventById(context: Context, eventId: Long): Boolean {
val uri: Uri = CalendarContract.Events.CONTENT_URI
val selection = "${CalendarContract.Events._ID} = ?"
val selectionArgs = arrayOf(eventId.toString())
return try {
val rowsDeleted = context.contentResolver.delete(uri, selection, selectionArgs)
rowsDeleted > 0
} catch (e: SecurityException) {
e.printStackTrace()
false
}
}