CalendarAPIを使用したアプリケーションの作成方法

GoogleカレンダーAPIを使用してAndroid用のカレンダーアプリケーションを作成します。
Calendar取得や書き込みはCalendarプロバイダ経由で行います。カレンダープロバイダAPIは4.0から公式APIとなりましたが、それ以前のバージョンでも非公式ながら使用することができます。ただしUriカラム名が一部異なるのでAndroidのバージョン毎に分岐させる必要があります。 非公式のAPIを使用するためにはソースコードを読み解く必要があります。今回はAPIレベル10以上をターゲットとしたいと思います。
まずはGitのインストール。 http://git-scm.com/download/win からWindows版Gitをダウンロードしてパソコンにインストールします。
次にソースコードの取得。Android API 14以降のソースコードは「Android SDKマネージャー」で簡単に取得できますが、それ以前のソースは別途取得する必要があります。今回、Gingerbreadでもカレンダーを使えるようにしたいのでAndroid2.3のカレンダープロバイダのソースを参照する必要があります。そのためGitでAndroid2.3のソースコードを取得してきましょう。 ソースはGoogle本家から取得するのはなかなか大変なのでhttps://github.com/OESF/を利用すると楽に取得できます。 たとえばAndroidのソース一式を取得するには次のようなGitコマンドで取得します。
[shell]
git init
git clone https://github.com/OESF/OHA-Android-2.3.4_r1.0.git
[/shell]
ソースが取得できたら、早速カレンダープロバイダのソースを参照してみましょう。ソースは \frameworks\base\core\java\android\provider\Calendar.java にあります。 このソースを見れば、プロバイダのURIやカラムが理解できると思います。
 
さて、それではプログラムを作成していきましょう。Googleカレンダーからスケジュールを取得しリスト表示するプログラムです。

まずはプロジェクトを作成して下さい。

設定項目

入力値/選択項目

New Android Application Application name

CalendarSample

Project Name CalendarSample
Package Name com.example.CalendarSample
Build SDK Android 4.1(API 16)
Minimum Required SDK API10:Android 2.3.3(Gingerbread)
Create Activity Create Activity BlankActivity
New Blank Activity Activity Name MainActivity
Layout Name activity_main
Navigation Type None
Hierarchical Parent (指定なし)
Title MainActivity

MainActivity.javaを次のように編集します。
[java]
package com.example.calendarsample;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
public class MainActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
[/java]
アクティビティはこれだけです。次にレイアウトです。
[xml]
<fragment xmlns:android="http://schemas.android.com/apk/res/android”
android:name="com.example.calendarsample.CalendarListFragment"
android:id="@+id/List"
android:layout_width="match_parent"
android:layout_height="match_parent" />
[/xml]
レイアウトには上のようにフラグメントを1つ配置しているだけです。ではそのフラグメントのソースを見ていきましょう。フラグメントはListFragmentを使い、スケジュールをリスト表示します。
CalendarListFragment.java
[java]
package com.example.calendarsample;
import android.annotation.TargetApi;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Instances;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SimpleCursorAdapter;
import android.support.v4.widget.SimpleCursorAdapter.ViewBinder;
import android.text.format.Time;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class CalendarListFragment extends ListFragment implements
    LoaderManager.LoaderCallbacks, ViewBinder {
    SimpleCursorAdapter mAdapter;
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setEmptyText("スケジュールはありません");
        String from;
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH){
            from = new String
{ Instances.TITLE, Instances.BEGIN };
        } else {
            from = new String { Instances.TITLE, Instances.BEGIN };
        }
        mAdapter = new SimpleCursorAdapter(getActivity(),
            android.R.layout.simple_list_item_2, null, from,
            new int
{ android.R.id.text1, android.R.id.text2 }, 0);
        mAdapter.setViewBinder(this);
        setListAdapter(mAdapter);
        getLoaderManager().initLoader(0, null, this);
    }
    @TargetApi(14)
    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        String tz = Time.TIMEZONE_UTC;
        Time time = new Time(tz);
        time.setToNow();
        time.allDay = true;
        time.year = time.year – 1;
        time.month = 0;
        time.monthDay = 1;
        time.hour = 0;
        time.minute = 0;
        time.second = 0;
        int begin = Time.getJulianDay(time.toMillis(true), 0);
        time.year += 4;
        time.month = 11;
        time.monthDay = 31;
        int end = Time.getJulianDay(time.toMillis(true), 0);
        Uri content_by_day_uri;
        String instance_projection;
        String sort_order;
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH){
            content_by_day_uri =
                CalendarContract.Instances.CONTENT_BY_DAY_URI;
            instance_projection = new String
{
                Instances.ID,
                Instances.EVENT_ID,
                Instances.BEGIN,
                Instances.END,
                Instances.TITLE
            };
            sort_order =
                Instances.BEGIN + " ASC, " + Instances.END + " DESC, "
                    + Instances.TITLE + " ASC";
        } else {
            final String authority = "com.android.calendar";
            content_by_day_uri = Uri.parse("content://" + authority
                + "/instances/whenbyday");
            instance_projection = new String[] {
                "
id",
                "event_id",
                "begin",
                "end",
                "title"
            };
            sort_order = "begin ASC, end DESC, title ASC";
        }
        Uri baseUri = buildQueryUri(begin, end, content_by_day_uri);
        return new CursorLoader(getActivity(), baseUri,
                instance_projection, null,
                null, sort_order);
    }
    @Override
    public void onLoadFinished(Loader loader, Cursor data) {
        mAdapter.swapCursor(data);
    }
    @Override
    public void onLoaderReset(Loader loader) {
        mAdapter.swapCursor(null);
    }
    @Override
    public boolean setViewValue(View view, Cursor cursor, int index) {
        Log.v("debug", "index:" + String.valueOf(index));
        switch (index) {
        case 4:
            *1;
            return true;
        case 2:
            String text = cursor.getString(index);
            Time time = new Time();
            time.set(Long.parseLong(text));
            *2;
            return true;
        default:
            break;
        }
        return false;
    }
    private Uri buildQueryUri(int start, int end, Uri content_by_day_uri) {
        StringBuilder path = new StringBuilder();
        path.append(start);
        path.append(‘/’);
        path.append(end);
        Uri uri =
            Uri.withAppendedPath(content_by_day_uri, path.toString());
        return uri;
    }
}
[/java]
ListFragmentを使用して、取得したスケジュールの情報を画面に一覧表示しています。取得したデータをListに渡すためにSimpleCursorAdapterを使っています。SimpleCursorAdapterはCursorAdapterを継承しておりCursorAdapterを継承したクラスには_id列がないと動作しません。
データの取得にはCursorLoaderを使います。CursorLoaderを使えば手軽に非同期でデータを取得することが出来ます。まずローダーの準備からです。

getLoaderManager().initLoader(0, null, this);

LoaderManagerがLoaderを管理します。LoaderManager.LoaderCallbacksを実装したクラスが必要になるので、Fragmentに実装しています。
onCreateLoader() コールバックメソッドでCursorLoaderを生成して戻り値にセットします。
リマインダーテーブルに個々のスケジュールが入っています。リマインダーテーブルを取得するコンテントプロバイダのURIは次のような形です。

content://com.android.calendar/instances/whenbyday/<開始日>/<終了日>

<開始日>および<終了日>は取得したいスケジュールの範囲をユリウス通日(Julian Day)で指定します。ユリウス通日とは、紀元前4713年1月1日から起算して何日目であるかという日数です。ユリウス通日を取得するにはTimeクラスを使います。

        String tz = Time.TIMEZONE_UTC;
        Time time = new Time(tz);
        time.setToNow();
        time.allDay = true;
        time.year = time.year - 1;
        time.month = 0;
        time.monthDay = 1;
        time.hour = 0;
        time.minute = 0;
        time.second = 0;
        int begin = Time.getJulianDay(time.toMillis(true), 0);

Timeクラスのインスタンスに現在日付を設定し、そこから1年前の1月1日の日付を設定しています。設定した日付からユリウス通日を取得するにはgetJulianDayメソッドを使います。このメソッドはUTCミリ秒をユリウス通日に変換してくれます。
CursorLoaderのコンストラクタにコンテントプロバイダのURIなどを渡して、CursorLoaderを生成して戻りとして戻しています。

        return new CursorLoader(getActivity(), baseUri,
                instance_projection, null,
                null, sort_order);

onLoadFinishedコールバックメソッドと、onLoaderResetコールバックメソッドはそれぞれローダーの読み込みが終わった時と、ローダーがリセットされてデータが読み込めなくなった時に呼ばれます。ここでAdapterに紐付くカーソルの更新を行うわけですが、SimpleCursorAdapterにカーソルの更新を行うためのメソッドであるswapCursorメソッドが用意されていますので、次のように実装します。

    @Override
    public void onLoadFinished(Loader loader, Cursor data) {
        mAdapter.swapCursor(data);
    }
    @Override
    public void onLoaderReset(Loader loader) {
        mAdapter.swapCursor(null);
    }

カレンダープロバイダから取得したタイトルと開始時間をリストに表示していますが、開始時間や終了時間はそのままだとUTCミリ秒なのでフォーマットして表示するようにしています。簡単にフォーマットを行うための仕組みとして、ViewBinderがあります。これを使ってフォーマットします。

        mAdapter.setViewBinder(this);

thisでFragment自身をViewBinderに指定しています。FragmentにはViewBinderインタフェースを実装し、フォーマット処理をsetViewValueコールバックメソッドに記述します。

    public boolean setViewValue(View view, Cursor cursor, int index) {
        Log.v("debug", "index:" + String.valueOf(index));
        switch (index) {
        case 4:
            ((TextView)view).setText(cursor.getString(index));
            return true;
        case 2:
            String text = cursor.getString(index);
            Time time = new Time();
            time.set(Long.parseLong(text));
            ((TextView)view).setText(time.format3339(false));
            return true;
        default:
            break;
        }
        return false;
    }

indexにカーソルのカラム番号を受け取るので、switchで分岐してカラム毎にフォーマット処理を行なっています。この例ではカラムとインデックスの対応は次のようになっています。

                Instances._ID,      // index 0
                Instances.EVENT_ID, // index 1
                Instances.BEGIN,    // index 2
                Instances.END,      // index 3
                Instances.TITLE     // index 4

indexが2の場合、開始時刻(BEGIN)を取得できるのでTimeクラスのformat3339メソッドでフォーマットしてリストに表示させています。
【追記】
最後に、カレンダーデータを読み取ることをAndroid Manifestに記載しなければなりません。READ_CALENDARパーミッションを追加します。
[xml]
<uses-permission android:name="android.permission.READ_CALENDAR" />
[/xml]
以上でカレンダープロバイダからデータを取得してリストに表示することが出来ました。

*1:TextView)view).setText(cursor.getString(index

*2:TextView)view).setText(time.format3339(false

「CalendarAPIを使用したアプリケーションの作成方法」への5件のフィードバック

  1. 初心者ですが、コードをコピーさせていただき、無事動きました。ありがとうございました。
    あと、下記のパーミッションをAndroidManifest.xmlに追加する必要があったのですね。


    私の環境だと、CalendarListFragment.javaの97,98行目は下記のようにしないとエラーとなってしまいました。

    public void onLoadFinished(Loader loader, Object data) {
    mAdapter.swapCursor((Cursor)data);

  2. ありがとうございます。
    Androidマニフェストに、READ_CALENDARパーミッションを追加する必要がありましたね。追記しておきます。

  3. 初心者ですが、こちらのアプリケーションを動作させることに成功しました。
    ありがとうございました。
    そこで、一つ質問があります。
    取得してきたカレンダーのデータを何らかの形式でAndroidの内部領域に保存することは可能でしょうか?
    もし方法があるのでしたら教えていただきたいです。
    よろしくお願いします。

  4. アンドロイドアプリ作成初心者です
    おそらくXmlファイルから
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.calendarsample/com.example.calendarsample.MainActivity}: android.view.InflateException: Binary XML file line #1: Error inflating class fragment
    このようなエラーがでてきました。どのように対策すればよいでしょうか

コメントは受け付けていません。

上部へスクロール