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

news2024/11/26 11:36:57

前言

经过一段时间的梳理和遴选,我挑选出了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,                     // 0
    CalendarContract.Calendars.ACCOUNT_NAME,            // 1
    CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,   // 2
    CalendarContract.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 Calendars
        implements 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 calendar
    put(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)

    //所属日历的id
    it.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 event
    put(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.coloradmin.cn/o/393471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sort函数对结构体|pair对组|vector容器|map排序|二维数组的第x列 的排序

目录 sort对 vector容器 sort对 vector<pair<int,int>>对组 sort对 结构体 结构体外部规定排序 结构体内部运算符重载 map容器的排序 map的键排序 map的值排序 sort对二维数组的排序 sort对 vector容器 sort()函数可以用于对vector容器进行排序。具体来…

java基础学习 day49(JDK8的接口新特性,JDK9的新特性,接口的应用场景)

JDK8以后&#xff0c;接口中新增的默认方法 允许在接口中定义默认方法&#xff0c;需要使用关键字default修饰&#xff0c;作用为&#xff0c;解决接口升级时&#xff0c;需要强制修改所有实现类的问题接口中默认方法的定义格式&#xff1a; a. 格式&#xff1a; public defau…

【黄河流域公安院校网络空间安全技能挑战赛】部分wp

文章目录webbabyPHPfunnyPHPEzphp**遍历文件目录的类**1、DirectoryIterator&#xff1a;2、FilesystemIterator:3、**Globlterator**读取文件内容的类&#xff1a;SplFileObjectMisc套娃web babyPHP <?php highlight_file(__FILE__); error_reporting(0);$num $_GET[nu…

排序模型:DIN、DINE、DSIN

目录 DIN 输入 输出&#xff1a; 与transformer注意力机制的区别与联系&#xff1a; DINE 改善DIN 输入&#xff1a; DSIN 动机&#xff1a; DIN 适用与精排&#xff0c;论文&#xff1a; Deep Interest Network for Click-Through Rate Prediction DIN模型提出的动…

蓝桥web基础知识学习

HTMLCSS 知识点重要指数HTML 基础标签&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;HTML5 新特性&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;HTML5 本地存储&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;CSS 基础语法…

Java中方法引用(引用静态方法、引用成员方法(引用其他类的成员方法、引用本类的成员方法、引用父类的成员方法)、引用构造方法、其他调用方式、小练习)

方法引用&#xff1a;把已经存在的方法拿过来用&#xff0c;当作函数式接口中抽象方法的方法体 我们前面学到Arrays工具类中的sort方法&#xff0c;当我们需要指定排序规则时&#xff0c;需要传递Comparator接口的实现类对象&#xff0c;我们之前使用匿名内部类类的形式作为参…

下一个元宇宙热点?探讨元宇宙婚礼的未来趋势

欢迎来到Hubbleverse &#x1f30d; 关注我们 关注宇宙新鲜事 &#x1f4cc; 预计阅读时长&#xff1a;7分钟 本文仅代表作者个人观点&#xff0c;不代表平台意见&#xff0c;不构成投资建议。 专家认为&#xff0c;在不久的将来&#xff0c;传统婚礼和元宇宙婚礼有可能共存…

taobao.item.barcode.update( 更新商品条形码信息 )

&#xffe5;免费必须用户授权 通过该接口&#xff0c;将商品以及SKU上得条形码信息补全 公共参数 请求地址: HTTP地址 http://gw.api.taobao.com/router/rest 公共请求参数: 公共响应参数: 请求参数 响应参数 点击获取key和secret 请求示例 TaobaoClient client new Def…

【Spring】资源操作管理:Resource、ResourceLoader、ResourceLoaderAware;

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 资源操作&#xff1a;Spring Resources一、Res…

智能家居创意产品一Homkit智能通断器

智能通断器&#xff0c;也叫开关模块&#xff0c;可以非常方便地接入家中原有开关、插座、灯具、电器的线路中&#xff0c;通过手机App或者语音即可控制电路通断&#xff0c;轻松实现原有家居设备的智能化改造。 随着智能家居概念的普及&#xff0c;越来越多的人想将自己的家改…

Pytest自动化测试框架-权威教程06-使用Marks标记测试用例

使用Marks标记测试用例通过使用pytest.mark你可以轻松地在测试用例上设置元数据。例如, 一些常用的内置标记&#xff1a;skip - 始终跳过该测试用例skipif - 遇到特定情况跳过该测试用例xfail - 遇到特定情况,产生一个“期望失败”输出parametrize - 在同一个测试用例上运行多次…

自旋锁,读写锁以及他们的异同

自旋锁 自旋锁是一种用于多线程编程的同步机制。它是一种基于忙等待的锁&#xff0c;当线程尝试获取锁时&#xff0c;如果锁已被其他线程占用&#xff0c;则该线程会一直循环检查锁是否被释放&#xff0c;直到获取到锁为止。 在自旋锁的实现中&#xff0c;使用了CPU的硬件特性…

ArcGIS制图之阴影效果的表达与运用

一、运用制图表达进行投影表达 在专题图的制作过程中&#xff0c;经常需要将目标区域从底图中进行突显&#xff0c;运用制图表达制作图层投影可以较好地实现这一目的。具体步骤如下&#xff1a; 1.将目标图层存储于数据库中并加载至窗口&#xff08;shapefile格式数据无法支持…

Android Looper简介

本文基于安卓11。 Looper是一个用具&#xff0c;在安卓程序中&#xff0c;UI线程是由事件驱动的&#xff08;onCreate, onResume, onDestory&#xff09;&#xff0c;Looper就是处理这些事件的工具&#xff0c;事件被描述为消息&#xff08;Message&#xff09;&#xff0c;Lo…

【PHP代码注入】PHP代码注入漏洞

漏洞原理RCE为两种漏洞的缩写&#xff0c;分别为Remote Command/Code Execute&#xff0c;远程命令/代码执行PHP代码注入也叫PHP代码执行(Code Execute)(Web方面)&#xff0c;是指应用程序过滤不严&#xff0c;用户可以通过HTTP请求将代码注入到应用中执行。代码注入(代码执行)…

python甜橙歌曲音乐网站平台源码

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;python音乐 获取完整源码源文件说明文档配置教程等 在虚拟环境下输入命令“python manage.py runserver”启动项目&#xff0c;启动成功后&#xff0c;访问“http://127.0.0.1:5000”进入甜橙音乐网首页&#xff0c;如图1所…

YOLOS调试记录

YOLOS是由华中科大提出的将Transformer迁移到计算机视觉领域的目标检测方法&#xff0c;其直接魔改ViT&#xff01;本文首次证明&#xff0c;通过将一系列固定大小的非重叠图像块作为输入&#xff0c;可以以纯序列到序列的方式实现2D目标检测。 模型结构 下面来调试一下该项目…

【微信小程序】-- 页面事件 - 上拉触底 - 案例(二十七)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…

javaScript基础面试题 ---对象考点

1、对象是通过new操作符构建出来的&#xff0c;所以对象之间不相等 2、对象注意引用类型&#xff0c;如果是引用类型&#xff0c;就可能会相等 3、现在对象本身查找 -> 构造函数中找 -> 对象原型中找 -> 构造函数原型中找 -> 对象上一层原型… 1、对象是通过new操作…

被骗进一个很隐蔽的外包公司,入职一个月才发现,已经有了社保记录,简历污了,以后面试有影响吗?...

职场的套路防不胜防&#xff0c;一不留神就会掉坑&#xff0c;一位网友就被“骗”进了外包公司&#xff0c;他说公司非常隐蔽&#xff0c;入职一个月才发现是外包&#xff0c;但已经有了社保记录&#xff0c;简历污了&#xff0c;不知道对以后面试有影响吗&#xff1f;楼主说&a…