Android Calendar Provider API返回已删除重复事件实例识别方法
Android Calendar Provider 过滤重复事件已删除/改期实例方案
问题根源
CalendarContract.Instances接口展开重复事件时,仅会按照重复规则生成所有时间点的原始实例,不会自动过滤被删除、改期的实例。所有对单个重复实例的修改(删除、改期)都不会修改主重复事件,而是会在Events表中生成独立的异常事件条目,直接查询Instances表时会同时返回原始实例、异常实例,且原始实例的DELETED字段固定为0、STATUS字段也和主事件保持一致,无法通过单表字段直接判断有效性。
Google Calendar 等系统日历应用也是通过额外的匹配逻辑完成过滤,Calendar Provider 本身没有提供自动过滤的查询参数,REST API的单实例返回配置也无法在本地Provider接口中使用。
实现步骤
- 第一步:预加载查询时间范围内的所有异常事件,建立映射关系
所有异常事件都带有非空的ORIGINAL_EVENT_ID字段,指向其所属的主重复事件ID,同时带有ORIGINAL_INSTANCE_TIME字段标记其对应的原始重复实例的开始时间,查询后将这些信息存为映射表,key为(主事件ID, 原始实例开始时间),value为异常类型(删除/改期)。 - 第二步:遍历Instances查询结果时,逐条匹配映射表过滤无效实例
对每个返回的实例,用它的EVENT_ID和BEGIN时间戳去映射表中查找:- 无匹配项:为正常未修改的重复实例,可正常展示
- 匹配到的异常条目标记为已删除/已取消:直接跳过该实例
- 匹配到的异常条目为改期状态:跳过当前原始位置的实例,保留改期后的异常实例即可
参考实现代码
// 定义异常实例存储结构 data class EventInstanceException( val isCanceled: Boolean, val newStartTime: Long, val newEndTime: Long ) // 1. 预加载指定时间范围内的异常事件 val exceptionMap = mutableMapOf<Pair<Long, Long>, EventInstanceException>() context.contentResolver.query( CalendarContract.Events.CONTENT_URI, arrayOf( CalendarContract.Events.ORIGINAL_EVENT_ID, CalendarContract.Events.ORIGINAL_INSTANCE_TIME, CalendarContract.Events.STATUS, CalendarContract.Events.DELETED, CalendarContract.Events.DTSTART, CalendarContract.Events.DTEND ), "${CalendarContract.Events.ORIGINAL_EVENT_ID} IS NOT NULL AND ${CalendarContract.Events.ORIGINAL_INSTANCE_TIME} BETWEEN ? AND ?", arrayOf(from.toEpochMilli().toString(), to.toEpochMilli().toString()), null )?.use { cursor -> while (cursor.moveToNext()) { val originalEventId = cursor.getLong(cursor.getColumnIndexOrThrow(CalendarContract.Events.ORIGINAL_EVENT_ID)) val originalInstanceTime = cursor.getLong(cursor.getColumnIndexOrThrow(CalendarContract.Events.ORIGINAL_INSTANCE_TIME)) val isDeleted = cursor.getInt(cursor.getColumnIndexOrThrow(CalendarContract.Events.DELETED)) == 1 val isCanceled = cursor.getInt(cursor.getColumnIndexOrThrow(CalendarContract.Events.STATUS)) == CalendarContract.Events.STATUS_CANCELED val start = cursor.getLong(cursor.getColumnIndexOrThrow(CalendarContract.Events.DTSTART)) val end = cursor.getLong(cursor.getColumnIndexOrThrow(CalendarContract.Events.DTEND)) exceptionMap[Pair(originalEventId, originalInstanceTime)] = EventInstanceException( isCanceled = isDeleted || isCanceled, newStartTime = start, newEndTime = end ) } } // 2. 原有Instances查询逻辑不变 val builder = CalendarContract.Instances.CONTENT_URI.buildUpon() ContentUris.appendId(builder, from.toEpochMilli()) ContentUris.appendId(builder, to.toEpochMilli()) val validInstances = mutableListOf<EventInstance>() context.contentResolver.query(builder.build(), EVENTS_INSTANCES_PROJECTION, selection, selectionArgs, null)?.use { cursor -> while (cursor.moveToNext()) { val eventId = cursor.getLong(cursor.getColumnIndexOrThrow(CalendarContract.Instances.EVENT_ID)) val instanceStart = cursor.getLong(cursor.getColumnIndexOrThrow(CalendarContract.Instances.BEGIN)) // 匹配异常表 val exception = exceptionMap[Pair(eventId, instanceStart)] when { exception == null -> { // 正常实例,加入结果集 validInstances.add(parseInstanceFromCursor(cursor)) } exception.isCanceled -> { // 已删除/取消的实例,直接跳过 continue } else -> { // 该时间点的实例已被改期,原始实例跳过,改期后的实例会在后续游标遍历中作为独立条目返回 continue } } } }
注意事项
- 不要依赖Instances表返回的
DELETED、STATUS字段判断实例有效性,展开的原始实例是主事件的副本,字段值永远和主事件一致,不会体现单个实例的修改 - 预加载异常事件时增加时间范围过滤,不需要全量查询所有异常事件,可以大幅提升查询性能
- 该逻辑同时可以解决改期后原始实例和改期实例同时返回的问题,改期后的异常实例本身带有新的开始时间,不会被过滤规则命中,会正常加入结果集
内容的提问来源于stack exchange,提问作者shelll
相关产品推荐
相关产品推荐

