📌

【過去Blogからの移行記事】Androidでical4jを使う

2022/09/18に公開

表題の件について、拙作のアプリ「Gcal Importer」での実装を、自分の頭を整理するついでにかいつまんで書き出してみたいと思います。

対象source: Gcal_Importer -

【Topアクティビティで行っていること】
Google Calendarからエクスポートしたカレンダーデータは、ダウンロード直後はiCalendar形式のデータ(以後ICSデータと呼ぶ)を内包したzip圧縮ファイルになっています。このzipファイルを展開し、展開されたICSデータファイルをリストにして表示することで、ユーザが端末ローカルカレンダーにImportするICSデータをカレンダー単位で選択できるようにします。

【ShowEventListアクティビティで行っていること】
Topアクティビティで選択されたICSデータをical4jというライブラリを使ってあれこれ処理します。なお、ICSデータを読み込んでModelオブジェクト化する際には、Eventを開始日付時刻順にsortしておきます。

AsyncTaskでICSデータを読み込む

Googleカレンダーから取得するICSデータは、通常の手段だとWebブラウザを使ってカレンダーのUIから「設定>カレンダー>カレンダーをエクスポート」という手順で取得できます。
このデータは例えば私の場合、ToDoを除いた3つのカレンダーがあって総イベント件数は2000件余りとなります。(メインのカレンダーにほぼすべてのイベントが詰まっている)
これをAndroidのActivityで読み込む時にUIスレッドでやると読み込み終了までに時間がかかるので、AsyncTaskという仕組みを使ってUIスレッドとは別のスレッドで実行させます。

ICSデータを読み込む時の実装の方針は、コードを書いてるうちに下記のようになりました。(ぇ
なお、MVCアーキテクチャについての勉強はちゃんとやってないので、Modelクラスとか設計は超テキトーです。

  1. ICSデータの読み込みメソッドは、Modelクラスに持たせた。ical4jを使った読み込み手順は 本家のIntroduction に書いてある通りにやるだけ。
  2. TopActivityでは、ICSデータからはカレンダーのProperty中の「カレンダー名」しか使わない。あと使うのはファイルのタイムスタンプぐらい。
  3. ShowEventListActivityでは単一のICSデータの内容を全部保持する。その上で、Eventデータを日付時刻順にソートできるようにメソッドを作っておく。

TopActivity では上記1. 2.の通りなので

// [TopActivity]
    new TopIcsListAsync(activityObj).execute(pathArray);

でICSデータのpath配列をAsyncTaskを継承したクラスに渡して実行し、そのTopIcsListAsyncクラスでは

// [TopIcsListAsync]
    for(String pathStr : pathArray) { model.addCalName(pathStr); }

としてModelオブジェクトのICSデータ読み込みメソッドにpathを渡し、モデル側の当該メソッドでは

// [Model4Top]
    public ArrayList calNames;
    // ~~省略~~
        FileInputStream fin = new FileInputStream(pathStr);
        CalendarBuilder builder = new CalendarBuilder();
        Calendar calendar = builder.build(fin);
        calNames.add(calendar.getProperties().getProperty("X-WR-CALNAME"));

としてカレンダー名を抜き出してArrayListに保持するだけです。

ShowEventListActivityでは上記1. 3.の通りですが、基本的にはTopActivityと同じように

// [ShowEventListActivity]
    new ShowEventListAsync(activityObj).execute(selectedFilePath);

でICSデータのpathをAsyncTaskを継承したクラスに渡して実行し、そのShowEventListAsyncクラスでは

// [ShowEventListAsync]
    for(String pathStr : pathList) { model.readIcs(pathStr); }

としてModelオブジェクトのICSデータ読み込みメソッドにpathを渡し、モデル側の当該メソッドで

// [Model4EventList]
    public Calendar calendar;
    // ~~省略~~
    public void readIcs(String pathStr) {
        FileInputStream fin;
        try {
            fin = new FileInputStream(pathStr);
            CalendarBuilder builder = new CalendarBuilder();
            calendar = builder.build(fin);
        } catch (Exception e){
            // ~~省略~~
        }

としてICSデータにあるイベントを全てModelオブジェクトの中に持たせます。

こうして保持したCalendarオブジェクトに対して、続けてShowEventListAsync側からModelオブジェクトの持つsortメソッド(イベントを日付時刻で新しい順にソートする)を呼び出します。

// [ShowEventListAsync]
    model.mapSort();

ここで呼んでいるmapSort()というメソッドは、大変に素人くさいコードかもしれませんがそこはご容赦下さい。コードを全部引用するには長いので、GitHub上のソースのURLを示しつつ処理の流れを箇条書きにしたいと思います。

対象source: Gcal_Importer - src/net/crappo/android/androics/Model4EventList.java

mapSort()メソッド内での処理の流れ

  1. 「key/value」のペアとして 「イベントのUID(uid)/開始時刻(dtstart)」 を持つHashMapのオブジェクトを用意する。(開始時刻は「19700101T000000」のような書式で格納されている)
  2. オリジナルCalendarオブジェクトのComponentListからVEventだけを取り出して、uidとdtstartとをkey/valueのセットにして、1.で用意したHashMapに逐次putしていく。
  3. 2.で作ったHashMapを、Collectionクラスのsortメソッドを使ってvalue比較で降順(日付の新しい順)sortしてArrayList<Map.Entry<String,String>に入れていく。
  4. テンポラリCalendarオブジェクトをnewして、オリジナルCalendarオブジェクトから取り出したVEvent以外の要素を格納しておく。
  5. 3.でsort済みのArrayListから順次key(uid)を取り出して、オリジナルCalendarオブジェクトのVEvent1つずつのuidと照合し、一致したVEventをテンポラリCalendarオブジェクトに追加していく。 (これでsort済みのCalendarオブジェクトが出来たことになる)
  6. オリジナルCalendarオブジェクトをテンポラリCalendarオブジェクトで置き換える。

なお、この後ShowEventListAsync側でTimeZoneの設定読み込みを行っていますが、iCalendarのデータ構造におけるTimeZoneの扱いについて、下記URLの図を参考にしたらとても理解しやすかったです。
http://www.asahi-net.or.jp/~CI5M-NMR/iCal/ref.html

以上が、本アプリにおいてICSデータをAsyncTaskで読み込む時に行っている処理です。
sort処理の部分は、ICSデータを元にアプリ側で生成したCalendarオブジェクトの内部要素にどのようにアクセスするかの参考になるといいなと思います。

取得したイベントの日付時刻表示を意図した通りにする

ShowEventListActivityでは、上記までの処理によってModel化してアプリ内に取り込んだsort済みCalendarオブジェクトから、VEventを1つずつ取り出してListViewに表示させています。

この時、ical4jのVEventオブジェクトが持つProperty.DTSTARTやProperty.DTENDは、「19700101T000000」のような書式で表現されているため、SimpleDateFormat等を使って書式整形をしたいと考えました。実際のところこのプロパティの書式はTimeZoneの情報が付加されたりすることがあるのでもう少し複雑です。そこで、これらをエポック秒に変換して扱います。

ical4jではVEventオブジェクトから下記のようにしてProperty.DTSTARTやProperty.DTENDの値を得ることが出来ます。

// 変数「calendar」はICSデータから読み込んだCalendarオブジェクト*
ComponentList components = calendar.getComponents("VEVENT");
for(Object obj : components) {
    if( obj instanceof VEvent) {
        VEvent event = (VEvent)obj;
        String dtstart = event.getStartDate().getValue();
        Log.v("Test LogCat", "Event Start: " + dtstart);
        String dtend = event.getEndDate().getValue();
        Log.v("Test LogCat", "Event End: " + dtend);
    }
}

本アプリでは、上記のようにしてVEventオブジェクトから得たdtstartやdtendの文字列を、ical4jが持っているDateTimeクラスのオブジェクトにして返すメソッドを作りました。

    private DateTime getDateTime(String strDateTime) {
        DateTime dt = null;
        try { //日付のみだった場合は時刻情報も付け足す
            if(strDateTime.length() == 8)    dt = new DateTime(strDateTime + "T000000");
            else                             dt = new DateTime(strDateTime);
        } catch (ParseException e) { e.printStackTrace(); }
        return dt;
    }

こうして得たDateTimeオブジェクトの持つgetTime()メソッドを使ってエポック秒を得ることが出来ます。本アプリにおける実際の書式整形は上記メソッドを使って次のようにして行っています。

// 内部クラスViewHolderAdapterでの処理
    str = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", locale).format(getDateTime(event.getStartDate().getValue()).getTime());
    str = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", locale).format(getDateTime(event.getEndDate().getValue()).getTime());

以上で、ical4jのVEventオブジェクトの日付時刻情報を任意の書式に整形することができました。

Discussion