使用日历丰富产品的用户体验

前言

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

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

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

desc

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

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

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

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

重点先行

值得关注的重点:

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

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

权限

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

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


<manifest><uses-permission android:name="android.permission.READ_CALENDAR"/><uses-permission android:name="android.permission.WRITE_CALENDAR"/>...
</manifest>

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

查询、创建本地日历

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

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

查询

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

 // dynamic lookups improves performance.
private val EVENT_PROJECTION: Array<String> = 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<String> = 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配合实现该功能:

<!--注意:exported需设置为true,进行适配-->
<activity android:name="XXXActivity"><intent-filter><action android:name="android.provider.calendar.action.HANDLE_CUSTOM_EVENT"/><category android:name="android.intent.category.DEFAULT"/><data android:mimeType="vnd.android.cursor.item/event"/></intent-filter>
</activity>

并从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<String> = 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构造同步适配器,参见上文。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/26241.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023 热点营销日历:179 个重要节日 + 46 个关键事件 + 12 个经典案例

又是一年初始时&#xff0c;为大家准备了一份全年营销日历&#xff0c;速速领取收藏哟&#xff5e; 一月营销热点 营销话题&#xff1a;元旦、春节、春运、年味、团圆、旅游、FLAG…… 2023 开年注定是特殊的一年。三年消费欲望在这个春节集中释放&#xff0c;在外漂泊的游子踏…

likewen的救赎之路

likewen的救赎之路 标准化救赎之路,只针对本人 文章目录 likewen的救赎之路java的八股文算法操作系统&计算机网络企业开发基础mysqlspring系列spring5springbootspringcloud linux前端**设计模式**缓存redis 消息队列nginx?Netty**微服务**docker认识一下常用的类库测试其…

大会议题重磅出炉,豪华阵容等你面基!RustChinaConf 2023!【附第一天议程】

本次大会议题品质一流&#xff0c;嘉宾多来自行业一线&#xff0c;干货多多&#xff0c;且在各领域遍地开花&#xff0c;可看出Rust星星之火在中国已成燎原之势&#xff01; 大会时间地址 6.17 - 6.18 浦东新区张杨路777号 上海锦江汤臣洲际酒店 官网地址 https://rustcc.cn/20…

张俊林:大语言模型带来的交互方式变革

来自&#xff1a;机器之心 演讲&#xff1a;张俊林 进NLP群—>加入大模型与NLP交流群 7 月 8 日&#xff0c;在机器之心举办的 2023 WAIC AI 开发者论坛上&#xff0c;新浪微博新技术研发负责人张俊林先生发表了主题演讲《自然语言交互&#xff1a;大语言模型带来的交互方式…

Hyper-v的客户端连接工具VMConnect

我们在玩Hyper-v的时候&#xff0c;一般也是走走套路&#xff0c;创建个虚拟机&#xff0c;中间可能会因为到底要给这台虚拟机分配多少内存多大硬盘作一番思想半争&#xff0c;因为内存不多了&#xff0c;硬盘不够了&#xff0c;然后&#xff0c;就是连接虚拟机&#xff0c;启动…

vmware 虚拟机nat连接,局域网访问

vmware nat设置可以让虚拟机联网&#xff0c;这是比较常接触的用法。网上很多教程。 联网成功后想让局域网内别的机器访问&#xff0c;就需要绕个弯。nat的方式仅仅只是让你的虚拟机在你的机器上&#xff0c;通过你的网卡转发&#xff0c;才能访问网络。也就是说&#xff0c;你…

两台虚拟机实现tcp连接(使用telnet)

两台虚拟机实现tcp连接&#xff08;使用telnet&#xff09; 今天做实验&#xff0c;为了实现两台winxp虚拟机之间建立tcp连接&#xff0c;使用了系统的telnet服务。 首先&#xff0c;两台虚拟机都需开启telnet服务。 在控制面板 —管理工具 —服务 —telnet 将telnet的启动类型…

VMware 虚拟机与主机网络互通

VMware连接网络 一、虚拟机服务开启二、本地网络虚拟机的网卡启动三、设置虚拟机四、IPv4设置 一、虚拟机服务开启 1.右击“此电脑”图标&#xff0c;单击“管理”&#xff0c;出现以下界面。 查看VMware是否开启&#xff0c;没有开启的话&#xff0c;就右击→“启动”。 二…

【Linux】VMware连接虚拟网络的三种方式

目录 一、Bridged&#xff08;桥接模式&#xff09;二、NAT&#xff08;地址转换模式&#xff09;三、Host-Only&#xff08;仅主机模式&#xff09; 由于Linux目前很热门&#xff0c;越来越多的人在学习linux&#xff0c;但是买一台服务放家里来学习&#xff0c;实在是很浪费。…

VM虚拟机设置网关,连接网络

VM虚拟机设置网关服务 vi /etc/sysconfig/network-scripts/ifcfg-ens33 编辑虚拟机信息 TYPEEthernet PROXY_METHODnone BROWSER_ONLYno BOOTPROTOstatic #修改成静态 DEFROUTEyes IPV4_FAILURE_FATALno IPV6INITyes IPV6_AUTOCONFyes IPV6_DEFROUTEyes IPV6_FAILURE_FATALno…

Hyper-V虚拟机连接外部网络

Hyper-V连接外部网络 Hyper-V连接外网共需要三个步骤1.使用hyper-v创建一个虚拟交换机。第一步&#xff1a;点击虚拟交换机管理第二步&#xff1a;新建虚拟交换机第三步&#xff1a;修改名称和备注&#xff0c;注意选择外部网络第四步&#xff1a;点击是&#xff0c;并等待一会…

自己的电脑netassist软件(其他上位机软件同理)建立的虚拟TCP服务器其他客户端连不上??但客户端能连接WiFi。

1.检查下位机程序的ip地址和端口号是否正确&#xff0c;尽量使用手机热点测试&#xff0c;最好不要连接路由器&#xff01; 2.第一条没问题后检查第二条&#xff0c;电脑的公共网络的防火墙中该软件没有在白名单里(我的是这个原因&#xff0c;亲测可用)。 下面我们主要说一下第…

千牛群发消息怎么发?推荐UiBot千牛群发消息机器人

千牛是专供淘宝卖家和天猫商家使用的工作软件&#xff0c;可以说是卖家旺旺的升级版&#xff0c;很多客户对于千牛群发消息有很大的需求&#xff0c;但又不知如何在千牛群发消息。其实在千牛群发消息很简单&#xff0c;来试试UiBot千牛群发消息机器人就知道了。 千牛群发消息机…

千牛工作台使用教程

千牛工作台几乎是各个淘宝天猫卖家必备的后台工作软件&#xff0c;管理着我们网上的店铺&#xff0c;我们在后台可以看到自己店铺的交易信息&#xff0c;每日流量和交易的统计 工具/原料 电脑 千牛工作台 方法/步骤 1、下载千牛工作台 通过百度软件或者360软件管家直接下载…

移动千牛开放体验治理实践与防治方案

作者&#xff1a;王文华(连墨) 千牛是阿里巴巴商家的多端开放式工作平台&#xff0c;每天服务数百万的活跃商家在移动和桌面端操作业务&#xff0c;包含店铺管理、客服接待、资讯消息等多项功能。 同时&#xff0c;千牛本身是一个开放的端体系架构&#xff0c;二三方能通过开…

python clicknium 库自动化千牛桌面端

python clicknium 库自动化千牛桌面端 千牛是阿里巴巴集团卖家工作台&#xff0c;商家经营的必备工具&#xff0c;今天我们使用python来自动化千牛桌面端。 clicknium 是基于 python 实现的一套 免费的UI 自动化的库&#xff0c;功能强大、简单易用&#xff0c;可以用来操作桌…

云空间 千牛 net 对接

{"error":"incorrect region, please use up-z2.qiniup.com, bucket is: 76demo"} 接入七牛云对象存储 注册七牛云账号&#xff0c;点右上角的密钥管理&#xff0c;key和secret需要复制到application.properties中。 因为用到对象云存储&#xff0c;放一些…

千牛工作台linux版,深度商店应用千牛工作台、Discord、QQ(Linux)、新浪微博安卓版...

深度商店收录千牛工作台、Discord、QQ(Linux)、新浪微博安卓版应用&#xff0c;如果使用Deepin或是统信UOS操作系统&#xff0c;可在深度商店程序上安装它们。 1、千牛工作台 千牛工作台是阿里巴巴官方出品的卖家一站式店铺管理工具&#xff0c;卖家可以通过它发布经营资讯消息…

千牛中文件已存在于服务器上,千牛登陆在云服务器上

千牛登陆在云服务器上 内容精选 换一换 如果Windows操作系统云服务器未安装密码重置插件&#xff0c;可以参见本节内容重新设置密码。本节操作介绍的方法仅适用于修改Windows本地账户密码&#xff0c;不能修改域账户密码。Linux操作系统请参见重置Linux云服务器密码(未安装重置…

千牛如何撤销发往服务器的文件,千牛服务器没有返回数据

千牛服务器没有返回数据 内容精选 换一换 当服务器中的磁盘发生故障、或者由于人为误操作导致服务器数据丢失时&#xff0c;可以使用已经创建成功的备份恢复服务器。云服务器备份仅支持将服务器中的所有云硬盘作为整体进行备份和恢复&#xff0c;不支持对服务器中的部分云硬盘进…