はじめに
こちらの通り、Androidアプリを作りました。
その際にCalendarContractを利用したのですが、かなり詰まることが多かったので備忘録がてら残しておきます。
CalendarContractとは
Androidが提供している機能で、デバイス内のカレンダー情報を取得したり登録・削除したり出来ます。
今回は取得しかしていないので、登録・削除に関しては他の資料をご参照ください・・。
GoogleCalendarAPIと違って、前準備がほぼいらないのが嬉しいポイント。
情報
前準備
たった一つだけ必要な準備があります。
それはカレンダーの読み取り権限の取得です。
下記の通り、AndroidManifest.xml
に権限がほしい旨を書いておきます。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_CALENDAR"/>
ちなみに、これだけだと良くある「許可しますか?」的なポップアップは出ません。
そこの処理は今回実装していないので、わからん!!!
CalendarContractのデータモデル
CalendarContractで取得出来るものは、ざっくりこちらの5つです。
参照:https://developer.android.com/identity/providers/calendar-provider?hl=ja
ちなみに、基本的に自分より上のテーブルが保持している項目をきっちり継承してくれています。
そのため、Attendees
のデータは自分より上のEvents
Calendars
のデータを取得出来ます。
Calendars
カレンダー自体の情報が入っています。
メールアドレスや表示名、色などです。
Events
カレンダーの予定の大元の情報です。
少しややこしいのが、予定1つ1つの情報ではないという点。
説明が難しいですが・・。
繰り返し予定だとしても単発の予定だとしても、Eventsは1つしかないといった感じです。
Instances
カレンダーの予定1つ1つの情報です。
予定を取得したい場合、こちらを取ってくるべき。
繰り返し予定であれば繰り返しの分だけデータがあります。
Attendees
予定の参加者情報です。
その予定に対して「参加するか/不参加とするか」を保持しています。
繰り返しの予定のうち、1つだけ不参加とした場合どういうデータ構造になるんだろう・・?
Reminders
使ってないのでわかりません。
実装
対象者の予定のうち、すべての予定を取得
Instances
にクエリをかけて取得する。
// 取得対象とするカレンダーのメールアドレス。
private val TARGET_ADDRESS_WORK = arrayOf("work1@example.com", "work2@example.com")
private val TARGET_ADDRESS_PERSONAL = arrayOf("personal@example.com")
// 予定取得時に取得する項目を定義
private val INSTANCES_PROJECTION = arrayOf(
Instances.TITLE,
Instances.BEGIN,
Instances.END,
Instances.EVENT_LOCATION,
Instances.EVENT_ID,
)
// あんまりよくわかってないけど、取得項目のindexを事前に定義しようって書いてありました
private val INSTANCES_IDX_TITLE = 0
private val INSTANCES_IDX_BEGIN = 1
private val INSTANCES_IDX_END = 2
private val INSTANCES_IDX_LOCATION = 3
private val INSTANCES_IDX_EVENT_ID = 4
fun searchCalendar(context: Context): MutableList<WidgetItem> {
// Uriの組み立て
// 対象がInstancesの場合、単純にCONTENT_URIを使うだけではなく開始日時と終了日時の定義が必要
val uriBuilder: Uri.Builder = Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(uriBuilder, START_DATE_MILLI)
ContentUris.appendId(uriBuilder, END_DATE_MILLI)
val uri: Uri = uriBuilder.build()
// 取得条件
val where = arrayOf("(${Instances.CALENDAR_DISPLAY_NAME} IN (?, ?, ?)"
,"AND ${Instances.TITLE} <> \"不在\"" // 不在用のスケジュールか否かを判定する項目がないので、タイトルで判定している
,")").joinToString(" ")
// 条件値
val args = mutableListOf<String>()
args.addAll(TARGET_ADDRESS_PERSONAL)
if (IS_ALL) {
args.addAll(TARGET_ADDRESS_WORK)
}
// カレンダーの予定を取得する
val cur = context.contentResolver.query(uri, INSTANCES_PROJECTION, where, args.toTypedArray(), Instances.BEGIN)
val instances: MutableList<Instance> = mutableListOf()
while (cur?.moveToNext() == true) {
val title = cur.getString(INSTANCES_IDX_TITLE)
val begin = cur.getLong(INSTANCES_IDX_BEGIN)
val end = cur.getLong(INSTANCES_IDX_END)
val location = cur.getString(INSTANCES_IDX_LOCATION)
val eventId = cur.getString(INSTANCES_IDX_EVENT_ID)
// 取得した内容をお好みで利用する
}
cur?.close()
return filteringCalendar(context, instances)
}
取得した予定のうち「参加しない」としている予定は省く
参加するか否かの情報はAttendees
が持っているので、別途取得する。
private fun filteringCalendar(context: Context, instances:MutableList<Instance>): MutableList<WidgetItem> {
// AttendeesはCONTENT_URIの加工不要
var uri = Attendees.CONTENT_URI
// 条件組み立て
val sb:StringBuilder = StringBuilder()
// 上で取得したInstancesの数分「?」を用意する
val eventIds:MutableList<String> = mutableListOf()
for (i in 0..instances.size-1) {
if (i > 0) {
sb.append(",")
}
sb.append("?")
eventIds.add(instances[i].eventId)
}
// 取得条件
val where = arrayOf("(${Attendees.ATTENDEE_EMAIL} IN (?, ?, ?)" // 複数の参加者が設定されている予定もあるので、こちらでもメールアドレスを絞り込む
,"AND ${Attendees.ATTENDEE_STATUS} = ${Attendees.ATTENDEE_STATUS_DECLINED}"
,"AND ${Attendees.EVENT_ID} IN (${sb})"
,")").joinToString(" ")
// 条件値
val args = mutableListOf<String>()
args.addAll(TARGET_ADDRESS_PERSONAL)
if (IS_ALL) {
args.addAll(TARGET_ADDRESS_WORK)
}
args.addAll(eventIds)
// 不参加になっている予定のみ取得する
val cur = context.contentResolver.query(uri, ATTENDEES_PROJECTION, where, args.toTypedArray(), null)
val declinedIdSet:HashSet<String> = hashSetOf()
while (cur?.moveToNext() == true) {
declinedIdSet.add(cur.getString(ATTENDEES_IDX_EVENT_ID))
}
// InstancesをWidgetItemに変換しつつ、不参加のものを除く
val widgetItems: MutableList<WidgetItem> = mutableListOf()
for (instance in instances) {
if (declinedIdSet.contains(instance.eventId)) {
continue
}
widgetItems.add(WidgetItem(
day
,instance.title
,instance.timeStr
,instance.location
))
}
return widgetItems
}
おわりに
なかなか日本語の資料が少なく、手間取ったけど楽しかったです。