前言

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

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

本篇属于 part2-体系运用部分。

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

在一些助手类的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结构同步适配器,拜见上文。