使用日历丰富产品的用户体验
创始人
2024-05-29 22:48:10
0

前言

经过一段时间的梳理和遴选,我挑选出了Android知识图谱中重要的部分,制作了一张脑图。读者朋友们可按照脑图查漏补缺了, 图片尺寸较大,仅附链接 。

当然,这是我按照自己的判断、结合参考其他博主的观点进行的挑选,不同的细分领域要求的重点有所不同,不可一以概之,且未曾遴选内容并非没必要掌握。图中的4-5层没有展示,以后文章见。

本篇属于 part2-系统应用部分。

desc

在一些助手类的APP中,在使用应用的过程中会产生 “日程” 数据。作为用户理所当然的希望在事情发生前收到提醒。

而我们知道,通过 推送进行提醒 存在一定的不可靠性。那么 在用户手机日历中自动插入事件 则是一个重要的补充手段。

本篇中,我们将用 5-10分钟的时间,回顾操作日历的知识点。

_
注:开发者官网具有更详尽地说明,只是有点啰嗦,英文版 中文版_

重点先行

值得关注的重点:

  • 权限
  • ContentResolver 进行 查询、插入、修改、删除 操作
  • CalendarContract 中 各"表"含义和字段作用 (文中不会详细列举,看API doc即可,足够详细)
  • 同步适配器以及何时需要使用同步适配器
  • EntityIterator简化模板代码
  • RFC 5545 简要规则
  • 使用逻辑删除与物理删除
  • 从日历日程跳转回APP

如果您已经掌握了这些内容,可忽略下文,下文面向初学者。

权限

小于23时,不需要获取动态权限,Manifest声明日历读写权限即可。

API 14 即 Android 4.0以下不支持,庆幸没那么古老的手机了


...

大于等于23时,用你喜欢的方式处理动态权限获取即可。

查询、创建本地日历

实际上,从这里开始的所有内容均和 ContentResolver 有关,相应的,日历应用通过 ContentProvider提供了这些服务,以及通过 Intent 做功能补充。

日历账户信息属于 CalendarContract.Calendars 范畴,可将其看做一张数据库表理解查询与插入

查询

定义关心的列,可理解为 SQL 语句中 Select片段的Column,当然使用null传参可获取所有列,这将增加I/O成本和内存开销。

 // dynamic lookups improves performance.
private val EVENT_PROJECTION: Array = arrayOf(CalendarContract.Calendars._ID,                     // 0CalendarContract.Calendars.ACCOUNT_NAME,            // 1CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,   // 2CalendarContract.Calendars.OWNER_ACCOUNT            // 3
)// The indices for the projection array above.
private const val PROJECTION_ID_INDEX: Int = 0
private const val PROJECTION_ACCOUNT_NAME_INDEX: Int = 1
private const val PROJECTION_DISPLAY_NAME_INDEX: Int = 2
private const val PROJECTION_OWNER_ACCOUNT_INDEX: Int = 3

uri则类似SQL中的 FROM片段,代表了表名

val uri: Uri = CalendarContract.Calendars.CONTENT_URI

拼接条件模板,条件模板+参数 则类似SQL中的 Where片段。代码中演示了查询条件为 账户名为"张三" 且 账户类型为本地账户 且 账户拥有者为"张三"

val selection: String = "((${CalendarContract.Calendars.ACCOUNT_NAME} = ?) AND (" +"${CalendarContract.Calendars.ACCOUNT_TYPE} = ?) AND (" +"${CalendarContract.Calendars.OWNER_ACCOUNT} = ?))"
val selectionArgs: Array = arrayOf("张三", CalendarContract.ACCOUNT_TYPE_LOCAL, "张三")
val cur: Cursor? = contentResolver.query(uri, EVENT_PROJECTION, selection, selectionArgs, null)

执行查询,注意此类操作均回避主线程,养成好习惯

遍历cursor,略

_
注:可能您在使用Sqlite数据库时,因为表结构是自行定义的,已经习惯了编码操作cursor、或者依赖ORM框架。而在ContentResolver相关的模块中,您可以尝试使用 android.content.EntityIterator
进而遍历 Entity,可直接获得 ContentValue,减少很多模板代码_

插入

可以选择插入 本地账户在线同步账户,区别就是是否通过服务器同步数据。

一般Exchange协议的邮件服务器适用性更强,但本地账户已经足够满足需求。

我们需要注意:这张表的数据列来自4处定义:

public static final class Calendarsimplements BaseColumns, SyncColumns, CalendarColumns {}

牵涉到 SyncColumns 中定义的字段时,其写操作必须以 同步适配器 方式进行。

作者按:不需要死记,有十几个列,记住规则即可,开发时注意

需对uri做一定处理,包括:

  • CALLER_IS_SYNCADAPTER 设置为 true
  • 提供 ACCOUNT_NAMEACCOUNT_TYPE,作为 URI 中的查询参数,插入时据实填写即可,修改时注意数据有效性。

代码固定如下:

private fun Uri.asSyncAdapter(accountName: String, accountType: String): Uri {return this.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, accountName).appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, accountType).build()
}//例如:
CalendarContract.Calendars.CONTENT_URI.asSyncAdapter("张三", CalendarContract.ACCOUNT_TYPE_LOCAL)

以下代码演示插入的关键代码,您可以按照需求增加列参数,例如是否显示、时区、地区等

//构造行数据
val values = ContentValues().apply {// The new display name for the calendarput(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, "${username}的日历")put(CalendarContract.Calendars.ACCOUNT_NAME, username)put(CalendarContract.Calendars.OWNER_ACCOUNT, username)put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
}//插入
val resultUri = contentResolver.insert(CalendarContract.Calendars.CONTENT_URI.asSyncAdapter(username, CalendarContract.ACCOUNT_TYPE_LOCAL),values
)//解析id
resultUri?.let {calendarId = ContentUris.parseId(it)
}

插入日程和提醒

注:源码中体现为Event、文档中直译为事件,文中采用日程,更符合用语习惯,并非新事物

日程数据隶属于日历,因此我们需要事先获取操作的日历的日历id,参见上一节

插入日程

日程对应 CalendarContract.Events “表” :

public static final class Events implements BaseColumns,SyncColumns, EventsColumns, CalendarColumns {
}

同理,写 SyncColumns 中的字段时,需要使用同步适配器,不再赘述。

业务相关字段主要定义于:EventsColumns,包含以下类别:

  • 所属日历id
  • 日程的名称、描述
  • 颜色等样式相关
  • 时间和规则描述,如起止时间、是否全天时间、如何重复
  • 访客控制权限

如果是非重复日程,则必须提供起止时间,如下代码构建ContentValue:

val event = ContentValues().let {//UTC 毫秒级时间戳it.put(CalendarContract.Events.DTSTART, startMillis)it.put(CalendarContract.Events.DTEND, endMillis)//非全天it.put(CalendarContract.Events.ALL_DAY, 0)//标题和描述it.put(CalendarContract.Events.TITLE, title)it.put(CalendarContract.Events.DESCRIPTION, desc)//所属日历的idit.put(CalendarContract.Events.CALENDAR_ID, calendarId)//时区it.put(CalendarContract.Events.EVENT_TIMEZONE, SimpleTimeZone.getDefault().displayName)//API >=16// 来源APP的应用包名it.put(CalendarContract.Events.CUSTOM_APP_PACKAGE, pkg)// 为日程自定义uri,在支持的设备上,打开来源APP时可获取该uri值it.put(CalendarContract.Events.CUSTOM_APP_URI, uri)it
}

其他字段参考API文档选择使用。

如果是重复事件,则无需传递结束时间戳,而需要提供规则信息

//单次持续时间,而非从第一次起到最后一次截至的时间
it.put(CalendarContract.Events.DURATION, duration)
//日程的重复发生规则
it.put(CalendarContract.Events.RRULE, rRule)
//日程的日期重复规则
it.put(CalendarContract.Events.RDATE, rDate)

这三个参数的值,均遵循 RFC 5545

  • DURATION: ”P600S” 标识持续600s即10分钟, “PT1H” 表示持续1小时, “P2W” 表示持续 2周。
  • RRULE: ”FREQ=DAILY;WKST=SU;UNTIL=20230225T070000Z” 表示每日重复直至2023年2月25号7点;
  • RDATE: 配合RRULE生成更加复杂的规则,如有必要,请研究 RFC 5545

插入日程并获取日程id

val uri: Uri? = contentResolver.insert(CalendarContract.Events.CONTENT_URI, event)// get the event ID that is the last element in the Uri
val eventID: Long = uri?.lastPathSegment?.toLong() ?: -1

从日程详情回到来源APP

插入日程时,我们使用了如下字段,标识了日程的来源APP和日程的自定义Uri。

//API >=16
// 来源APP的应用包名
it.put(CalendarContract.Events.CUSTOM_APP_PACKAGE, pkg)
// 为日程自定义uri,在支持的设备上,打开来源APP时可获取该uri值
it.put(CalendarContract.Events.CUSTOM_APP_URI, uri)

在大多数ROM的内置日历中,均支持在日程详情中跳转到来源应用。注意,存在一些例外。鸿蒙系统内置日历也并未完全支持该特性

您可以通过注册IntentFilter配合实现该功能:




并从Intent中获取日程的自定义Uri:

getIntent().getStringExtra(CalendarContract.EXTRA_CUSTOM_APP_URI)

为日程插入提醒

首先,要获取日程的id,可以在插入日程时从返回uri中解析得出,也可以通过查询日程解析得出

此时,操作的是提醒表,CalendarContract.Reminders:

public static final class Reminders implements BaseColumns,RemindersColumns, EventsColumns {}

一般设置提前时间、 提醒方式、日程id即可

val values = ContentValues().apply {put(CalendarContract.Reminders.MINUTES, 1)put(CalendarContract.Reminders.EVENT_ID, eventID)put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
}contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, values)

读取日程

掌握了插入之后,您已经掌握了表和字段含义,读取日程则更加简单

按实际需求拼接查询条件后,执行查询。

val selection = "((${CalendarContract.Events.CALENDAR_ID} = ?))"
val selectionArgs: Array = arrayOf(calendarId.toString())
val cur: Cursor? = contentResolver.query(CalendarContract.Events.CONTENT_URI, null, selection, selectionArgs, null)

解析:

cur?.let {val events = CalendarContract.EventsEntity.newEntityIterator(cur, contentResolver).asSequence().map { entity -> entity.entityValues }.map {//解析转换实体对象}.toCollection(arrayListOf())
}

更改日程

通过向URI追加ID的方式,可以限定至修改的条目(类似数据库ORM框架中按主键更新),而不必使用限定条件。

val values = ContentValues().apply {// The new title for the eventput(CalendarContract.Events.TITLE, "Kickboxing")
}
val updateUri: Uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventID)
//影响的行数
val rows: Int = contentResolver.update(updateUri, values, null, null)

而使用限定条件可以更加灵活

删除日程

同样的,删除也可以使用追加ID方式,或者使用限定条件方式。

删除可分为两种:应用删除(逻辑删除)、同步适配器删除(物理删除)

应用删除将 deleted 列的值设置为 1,即逻辑删除。此标记告知同步适配器该行已删除,并且应将此删除传播至服务器。

同步适配器删除将会从数据库中移除事件及其所有关联数据。

以下为逻辑删:

val deleteUri: Uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, 日程id
)
return contentResolver.delete(deleteUri, null, null)

物理删除则需通过URI构造同步适配器,参见上文。

相关内容

热门资讯

安永许旭明:上市银行需形成多元... 在利率持续下行、存量按揭利率调整、让利实体经济政策叠加的背景下,中国银行业正经历净息差收窄的严峻考验...
超三百家上市公司披露逾千亿元回... 股票回购增持贷款业务持续落地。今年4月以来,已经有超300家上市公司公开披露回购增持计划,金额上限超...
专访|研究中国带回的月球样品是...   新华社伦敦5月12日电 专访|研究中国带回的月球样品是“至高荣誉”——访英国行星科学家马赫什·阿...
Perpl 完成由 Drago... 吴说获悉,去中心化永续期货交易平台 Perpl 完成由 Dragonfly 领投的 925 万美元融...
国际投行上调中国股票评级 外资... 国际投行上调中国股票评级外资机构分析师认为,风险偏好抬升有望推动更多资金流入A股◎记者 汪友若 中美...
一面迟来的锦旗 一份诚挚的谢... 本报讯(西海新闻记者 祁宗珠)“警官,谢谢你们,我是来送锦旗的。”5月10日下午,一名中年男子走进西...
讲述品牌故事 “电”亮中国品牌... 在第九个“中国品牌日”前夕,国网台州供电公司“永宁橘光”品牌日活动启幕,活动涵盖配网不停电作业观摩、...
布局AI生态 字节系大模型“实... ◎记者 罗茂林 5月13日,字节跳动旗下火山引擎开启上海站的大模型巡展活动,一批新的大模型产品亮相。...
强化防灾减灾行动 筑牢安全“防... 据媒体报道,湖北省兴山县5·12防灾减灾宣传周近日在兴山县实验中学举行启动仪式,全县中小学同步开展地...
中日专家对话肺癌治疗前沿进展 日前,在中日肺癌MDT(Multi-Disciplinary Treatment,多学科治疗)治疗前...
“挖掘机指数”显示 4月基建项... 记者日前获悉,三一重工基于树根互联工业互联网平台打造的“挖掘机指数”显示,今年4月份,全国工程机械各...
农发行推动“银期保”模式落地吉... 近日,中国农业发展银行(简称“农发行”)支持吉林省四平市梨树县玉米“种产销”产业链的“银期保”模式正...
民间家书纸短情长 述说普通家庭... 转自:千龙网原标题:中国人民大学家书博物馆再迎2700余封家书;抢救民间家书项目启动20年来共收藏8...
国家发展改革委:抓好以工代赈项... 记者5月13日获悉,近日,国家发展改革委地区振兴司联合有关部门组织召开会议,部署推进加力扩围实施以工...
聚力推动泛共和盆地绿色崛起 本报讯(记者 石成砚)5月12日至13日,省委副书记、省长罗东川在海南藏族自治州贵德县、贵南县、同德...
大通一女子 手提包失而复得 本报讯(西海新闻记者 郭红霞)5月12日,大通回族土族自治县居民马女士将一面印有“为民办实事 人民好...
壮大技术工人队伍 增强先进制造... 高质量发展需要高素质的劳动者,需要吸引更多高素质人才加入技术工人队伍。中共中央、国务院印发的《关于深...
马斯克称沙特阿拉伯已批准“星链... 2025 年 5 月 13 日,在沙特阿拉伯利雅得举行的沙特 - 美国投资论坛上,特斯拉首席执行官埃...
通胀报告正中特朗普下怀 再次敦...   特朗普利用低于预期的通胀报告再次向美联储主席施压,要求鲍威尔尽快下调利率。  “没有通胀!汽油、...
青海“双冷”产业发展热力十足 长势良好的生菜叶。西海新闻记者 祁晓军 摄冷水鱼现场加工。受访者供图龙羊峡三文鱼实拍图。受访者供图冷...