13、Calendar Provider:访问日历数据、读取事件、创建事件
日历功能,说白了就是手机上的「时间管家」。你想想看,现在哪个App不需要跟时间打交道?会议提醒、生日推送、行程同步……背后都是Calendar Provider在干活。
我最早接触这个API是在做一个企业协作App的时候。当时产品经理说:「我们要把钉钉的日程同步到系统日历里。」我心想,这不就是读写ContentProvider嘛,简单。结果一上手才发现——权限、账户、时区、重复事件……坑还真不少。
今天我就带你把这些坑一个个填平。
13.1 Calendar Provider 的数据模型
先搞清楚它长什么样。Calendar Provider 的核心表有三张:
| 表名 | 对应URI | 说明 |
|---|---|---|
| Calendars | content://com.android.calendar/calendars | 日历账户信息,比如「我的Google日历」、「公司Exchange日历」 |
| Events | content://com.android.calendar/events | 具体的事件,比如「周三下午3点开会」 |
| Instances | content://com.android.calendar/instances/when | 事件的实例,专门处理重复事件的时间展开 |
嗯,这里要注意:Instances 表是只读的。你不能直接往Instances里插数据,它是由系统根据Events里的重复规则自动生成的。
13.2 权限声明——第一步就卡住很多人
我记得第一次跑demo,直接崩了个SecurityException。为什么?因为日历权限分两级:
- READ_CALENDAR:读取日历和事件
- WRITE_CALENDAR:创建/修改/删除事件
而且从Android 6.0开始,这两个都是运行时权限。光在Manifest里声明不够,还得动态申请。
Manifest声明:
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
小技巧:我习惯把权限申请封装成一个工具方法,回调里统一处理。别在Activity里到处写requestPermissions,后期维护起来太痛苦。
13.3 读取日历列表——先找到能用的日历
你不能随便往任意日历里写事件。每个日历都属于一个账户(比如Google账号、手机本地账户)。写之前,得先查一下当前设备上有哪些日历可用。
// 查询所有可见的日历
val cursor = contentResolver.query(
Calendars.CONTENT_URI,
arrayOf(
Calendars._ID,
Calendars.NAME,
Calendars.ACCOUNT_NAME,
Calendars.ACCOUNT_TYPE
),
"${Calendars.VISIBLE} = 1", // 只查可见的
null,
null
)
cursor?.use {
while (it.moveToNext()) {
val id = it.getLong(0)
val name = it.getString(1)
val account = it.getString(2)
Log.d("Calendar", "日历ID: $id, 名称: $name, 账户: $account")
}
}
我个人习惯把查询结果缓存起来,因为日历列表不会频繁变化。每次写事件前都查一遍,性能上划不来。
13.4 读取事件——别一次性拉太多
读取事件时,最忌讳的就是全表扫描。你想想看,手机日历里可能存了几千条事件,全拉回来不卡才怪。
正确的做法是按时间范围过滤。用Instances表最合适:
val beginTime = // 今天0点的时间戳(毫秒)
val endTime = // 明天0点的时间戳
val uri = Uri.parse("content://com.android.calendar/instances/when/$beginTime/$endTime")
val cursor = contentResolver.query(
uri,
arrayOf(
Instances.TITLE,
Instances.BEGIN,
Instances.END,
Instances.EVENT_LOCATION
),
null,
null,
"${Instances.BEGIN} ASC"
)
cursor?.use {
while (it.moveToNext()) {
val title = it.getString(0)
val begin = it.getLong(1)
val end = it.getLong(2)
val location = it.getString(3)
// 处理事件...
}
}
注意:Instances表的URI格式是固定的,beginTime和endTime必须是毫秒级时间戳。我曾经传了秒级时间戳进去,结果查出来全是空的,排查了半天才发现是单位搞错了。
13.5 创建事件——核心操作
创建事件需要往Events表里插数据。这里有个关键点:必须指定日历ID,否则系统不知道这个事件属于哪个账户。
val calendarId = 1L // 从日历列表里选一个
val beginTime = System.currentTimeMillis() + 3600_000 // 一小时后
val endTime = beginTime + 1800_000 // 持续半小时
val values = ContentValues().apply {
put(Events.CALENDAR_ID, calendarId)
put(Events.TITLE, "Android架构课 - Calendar Provider")
put(Events.DESCRIPTION, "深度解析日历数据访问")
put(Events.EVENT_LOCATION, "线上会议室")
put(Events.DTSTART, beginTime)
put(Events.DTEND, endTime)
put(Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
put(Events.ALL_DAY, 0) // 非全天事件
}
val eventUri = contentResolver.insert(Events.CONTENT_URI, values)
val eventId = eventUri?.lastPathSegment?.toLong()
Log.d("Calendar", "创建成功,事件ID: $eventId")
嗯,这里有个坑:EVENT_TIMEZONE 必须填。不填的话,系统会拿默认时区,但不同厂商的ROM处理方式不一样。我遇到过小米手机上不填时区直接抛异常的情况。
13.6 重复事件——RRULE 的写法
如果你要创建「每周一上午10点开会」这种重复事件,就得用到RRULE(Recurrence Rule)。
val values = ContentValues().apply {
put(Events.CALENDAR_ID, calendarId)
put(Events.TITLE, "周会")
put(Events.DTSTART, monday10Am)
put(Events.DTEND, monday11Am)
put(Events.EVENT_TIMEZONE, "Asia/Shanghai")
put(Events.RRULE, "FREQ=WEEKLY;BYDAY=MO;COUNT=10")
// 每周一,共10次
}
RRULE的语法遵循iCalendar标准。常用的几个:
- FREQ:DAILY / WEEKLY / MONTHLY / YEARLY
- BYDAY:MO,TU,WE,TH,FR,SA,SU
- COUNT:重复次数
- UNTIL:结束日期(格式:20251231T235959Z)
避坑指南:我曾经写过一个每月最后一天重复的事件,用了BYMONTHDAY=-1。结果在2月份就出问题了——2月只有28天,系统直接跳到了3月。后来改用BYDAY=-1MO(最后一个周一)才稳定。所以,边界日期一定要测试。
13.7 知识体系总览
下面这张图把Calendar Provider的核心流程串起来了:
13.8 更新与删除事件
更新和删除跟其他ContentProvider一样,通过URI加WHERE条件:
// 更新事件标题
val updateValues = ContentValues().apply {
put(Events.TITLE, "更新后的标题")
}
contentResolver.update(
Events.CONTENT_URI,
updateValues,
"${Events._ID} = ?",
arrayOf(eventId.toString())
)
// 删除事件
contentResolver.delete(
Events.CONTENT_URI,
"${Events._ID} = ?",
arrayOf(eventId.toString())
)
注意:删除事件时,如果事件是重复事件,系统默认只删除当前实例。要删除整个系列,需要设置Events.ORIGINAL_ID和Events.ORIGINAL_SYNC_ID。这个逻辑有点绕,我建议你在测试环境先跑一遍,确认行为符合预期。
13.9 实战建议
最后,分享几个我在实战中沉淀下来的经验:
- 异步操作:所有日历操作都放在子线程。ContentProvider的CRUD虽然快,但数据量大时还是会卡UI。
- 账户选择:如果设备上有多个日历账户,最好弹个选择器让用户自己选。别自作主张往某个账户里写。
- 同步问题:写入事件后,系统不会立即同步到云端。如果需要强制同步,可以调用
ContentResolver.requestSync(),但别频繁调用,会耗电。 - 测试覆盖:不同厂商的ROM对日历Provider的实现有差异。华为、小米、三星我都踩过坑。建议在主流机型上都跑一遍。
好了,Calendar Provider的核心内容就这些。说白了就是三张表、两个权限、一个RRULE。你只要把数据模型理清楚,剩下的就是熟练工种了。