0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CalendarContractを使って、参加している予定だけ取得した

Posted at

はじめに

こちらの通り、Androidアプリを作りました。
その際にCalendarContractを利用したのですが、かなり詰まることが多かったので備忘録がてら残しておきます。

CalendarContractとは

Androidが提供している機能で、デバイス内のカレンダー情報を取得したり登録・削除したり出来ます。
今回は取得しかしていないので、登録・削除に関しては他の資料をご参照ください・・。

GoogleCalendarAPIと違って、前準備がほぼいらないのが嬉しいポイント。

情報

前準備

たった一つだけ必要な準備があります。
それはカレンダーの読み取り権限の取得です。

下記の通り、AndroidManifest.xmlに権限がほしい旨を書いておきます。

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つです。

image.png
参照: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
}

おわりに

なかなか日本語の資料が少なく、手間取ったけど楽しかったです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?