方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【ミーア】FlutterアプリからGoogleカレンダーにアクセスして当日の予定をコンソールに表示する方法

flutter-google-calendar-integration
この記事は約6分で読めます。

はじめに

様々な方言を話す。おしゃべりに小型ロボット「ミーア」を開発中。

現在、ミーアが提供する便利な機能としては、アプリで居住地を入力して、天気お知らせ時刻を設定していると、その時刻にその日の天気と一言アドバイス(雨の場合は、「折り畳み傘忘れないでね」など)を音声で伝える機能がある。

自分だったら、他にどんな機能があったら使うかなと思い、「カレンダー連携で予定を通知する機能」 を追加することにした。

例えば、次のようなシナリオを想定

例:「次のミーティング『開発打ち合わせ』があと5分で始まります。プレゼン資料の準備できていますか?」

こんな風に、カレンダー情報を基に予定を音声で知らせてくれることで、ユーザーの時間管理をサポートする。

カレンダー連携で予定を音声通知する実装概要

この機能の実現には以下のステップが必要

1)FlutterアプリとGoogleカレンダーの連携
ユーザーのGoogleカレンダーから予定情報を取得。
必要な予定は「当日(今日の0:00〜23:59)」の範囲内。
予定は数時間おきに最新情報を取得して、ユーザーの変更に対応。

2)5分前の通知トリガー
予定開始の5分前に、イベント情報をアプリ内で処理します。

3)音声通知の生成と送信
サーバーで予定タイトルのテキストから音声合成し、ミーア本体に送信します。

4)音声再生
ミーアが音声データを再生してユーザーに通知する

今回は一番手前の、FlutterアプリからGoogleカレンダーにアクセスして、一旦は当日の予定情報を(開始時刻とタイトル)をコンソールに表示するのを実装したいと思う。

テキストから音声合成とミーア本体に送信して音声再生する部分は、すでに天気お知らせ機能でできているので、上記ができれば後は繋げるだけ。

GCPでGoogleカレンダーAPIを有効化

Googleカレンダーにアクセスするには、「Google Calendar API」を利用する必要がある。

google cloudを開き、検索フィールド で「google calendar api」と入力して、表示されたcalendar apiを「有効にする」をクリック。

OAuth 同意画面を設定

Google Calendar APIを使用する際には、OAuth 2.0認証を利用するために、OAuth同意画面の設定も必要。

今回は、一般のGoogleアカウントを持つすべてのユーザー向けにもサービス提供するので、「外部」を選択して、作成をクリック。

アプリ登録の編集

すると、アプリ登録編集画面に移動するので、アプリ情報(アプリ名、サポートメールアドレス)を入力。他にもロゴやアプリのドメインなど任意の設定項目があるが、今回はスキップ。

スコープの設定

Google Calendar APIにアクセスするには、次のスコープを追加する必要があるので、追加

  • https://www.googleapis.com/auth/calendar
  • https://www.googleapis.com/auth/calendar.readonly

追加すると、機密性の高いスコープの欄に下記のように表示される。

テストユーザーの設定

開発段階では、アプリを使用できるテストユーザー(Googleアカウント)を設定する必要があるので、自分や開発者のgmailをリストに追加する。

公開ステータスが「テスト中」に設定されている間は、テストユーザーのみがアプリにアクセスできる。アプリの確認前の許可済みユーザー数の上限は 100 。

OAuth 2.0クライアントIDを生成

OAuth 同意画面の設定が完了したら、APIとサービス→「認証情報」タブでOAuth 2.0クライアントIDを生成する。

FirebaseやGoogle Cloudを使用している場合、Googleが特定のサービス(例: Firebase Authentication)を有効化した際に必要なOAuthクライアントIDを自動で作成することがあり、「(auto created by Google Service)」と記載されて、素手のOAuth2.0クライアントIDが表示されていることがある。

その場合には、中身を開いて「承認済みのJavaScript生成元」や「承認済みのリダイレクトURI」が正しく設定されているか確認して、問題なければ自動生成されたクライアントIDを使用する。

Flutterアプリの設定

必要なパッケージをインストール:googleapis・googleapis_auth・google_sign_in

必要な下記3つのパッケージをインストールする

  • googleapis:Googleの各種APIにアクセスするための公式パッケージ。
  • extension_google_sign_in_as_googleapis_auth :OAuth 2.0 認証をブラウザに遷移せずにアプリ内で完了するためのパッケージ。
  • http:APIリクエストを送信するためのHTTPクライアント。
  • google_sign_in :Google アカウントでサインインするためのパッケージ
ShellScript
flutter pub add googleapis
flutter pub add googleapis_auth
flutter pub add http
flutter pub add google_sign_in

Google Sign-Inを使用する場合のカレンダー連携の流れ

サインイン要求

  • google_sign_in パッケージを使って、ユーザーにサインインを要求する。

OAuth同意画面

  • 初回サインイン時、アプリが要求するスコープに対する同意画面がGoogleによって自動的に表示される。ユーザーは権限を許可または拒否できます。

トークンの取得

  • ユーザーが同意した場合、認証トークン(アクセストークン)が返され、Google Calendar APIなどにアクセスできるようになる。

flutter公式で、Google Calendar APIを含むGoogle APIsの認証とアクセス方法について記載されている。

https://docs.flutter.dev/data-and-backend/google-apis

Info.plist にURLスキームを追加

Google Sign-In をiOSアプリで動作させるために必要な設定として、GIDClientID(Google Sign-Inで必要)と、URLスキーム(リバースクライアントID)をInfo.plistに追加する。

Info.plist に外部連携や特定のURLスキームを追加することで、iOSがこれを許可リストとして扱う。iOSのセキュリティ要件を満たすために必要な設定

GIDClientID

  • Google Cloud Platform (GCP) の「APIとサービス > 認証情報」で作成した「iOSクライアントID」 のこと。

CFBundleURLSchemes

  • この設定は、Google Sign-Inが認証後にアプリに戻るための「リダイレクトURLスキーム」を登録するもの。
  • クライアントIDをリバース形式に変換して登録する(例: com.googleusercontent.apps.YOUR_REVERSED_CLIENT_ID)。
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<!-- 既存のキーはそのまま維持 -->
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
	...
	
	<!-- Google Sign-In の設定を追加 -->
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleTypeRole</key>
			<string>Editor</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<!-- firebaseIosIosClientId のリバースIDを設定 -->
				<string>com.googleusercontent.apps.YOUR_REVERSED_CLIENT_ID</string>
			</array>
		</dict>
	</array>
	<key>GIDClientID</key>
	<string>YOUR_IOS_CLIENT_ID</string>
</dict>
</plist>

実装例: 当日の予定を取得してコンソールに表示

Google Sign-Inの初期化

  • firebase_options.dart に定義された iosClientIdGIDClientIDと同じなので、直接コード内に記載して呼び出しても動作する)をGoogle Sign-Inで使用。

日付範囲の指定:当日の予定だけを取得するため、timeMintimeMax を当日の日付に設定

イベントのデータ取得:

取得データの表示:

Dart
import 'package:clocky_app/firebase_options.dart';
import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/calendar/v3.dart' as calendar;

class CalendarIntegrationScreen extends StatelessWidget {
  const CalendarIntegrationScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('当日の予定を表示'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            await _getTodayEvents(context);
          },
          child: const Text('予定を取得する'),
        ),
      ),
    );
  }

  Future<void> _getTodayEvents(BuildContext context) async {
    try {
      // Google Sign-Inの初期化
      final GoogleSignIn googleSignIn = GoogleSignIn(
        scopes: [
          'https://www.googleapis.com/auth/calendar.readonly', // Google Calendarの読み取りスコープ
        ],
        clientId:
            DefaultFirebaseOptions.ios.iosClientId, // FirebaseのiOSクライアントID
      );

      // サインイン実行
      final GoogleSignInAccount? account = await googleSignIn.signIn();
      if (account == null) {
        // サインインがキャンセルされた場合
        print('サインインがキャンセルされました');
        return;
      }

      // 認証クライアントの取得
      final authClient = await googleSignIn.authenticatedClient();
      if (authClient == null) {
        throw Exception('認証クライアントの取得に失敗しました');
      }

      // Google Calendar APIの初期化
      final calendarApi = calendar.CalendarApi(authClient);

      // 当日の開始時刻と終了時刻を設定
      final now = DateTime.now();
      final startOfDay = DateTime(now.year, now.month, now.day).toUtc();
      final endOfDay =
          DateTime(now.year, now.month, now.day, 23, 59, 59).toUtc();

      // 当日の予定を取得
      final events = await calendarApi.events.list(
        'primary', // デフォルトのカレンダー
        timeMin: startOfDay,
        timeMax: endOfDay,
        singleEvents: true,
        orderBy: 'startTime',
      );

      // 取得した予定をコンソールに表示
      if (events.items != null && events.items!.isNotEmpty) {
        debugPrint('当日の予定: ${events.items!.length}件');
        for (var event in events.items!) {
          final startTime = event.start?.dateTime ?? event.start?.date; // 開始時間
          final title = event.summary ?? 'タイトルなし'; // イベントタイトル
          debugPrint('予定: $title, 開始時間: $startTime');
        }
      } else {
        debugPrint('本日の予定はありません');
      }
    } catch (e) {
      // エラーハンドリング
      debugPrint('エラーが発生しました: $e');
    }
  }
}

動作確認

アプリの設定画面で、カレンダー連携をクリックし、カレンダー連携画面に遷移して連携するをタップ(デザインは後ほど修正)

すると、アプリ内webビューでgoogleアカウントでのサインイン画面が開く。

サインインする時に、カレンダー情報の取得同意が表示されるので、チェックして「続行」をクリック。

すると、カレンダーが連携が完了し、アプリコンソール画面に下記のように当日の予定が表示された。

ShellScript
flutter: 当日の予定: 6
flutter: 予定: 子供保育園送り・移動, 開始時間: 2024-11-20 22:00:00.000Z
flutter: 予定: 移動, 開始時間: 2024-11-21 04:30:00.000Z
flutter: 予定: XX先生, 開始時間: 2024-11-21 05:45:00.000Z
flutter: 予定: 移動, 開始時間: 2024-11-21 08:30:00.000Z
flutter: 予定: 子供迎え・風呂, 開始時間: 2024-11-21 09:00:00.000Z

無事、当日の予定をアプリコンソールに出力するところまでできた。

実際には、ユーザーがGoogleカレンダーを当日にも変更する可能性があるため、数時間おきに当日の予定だけをアプリからバックグラウンド(flutter_background_fetchパッケージなど)でチェックし、取得した予定を一旦アプリローカルで保持し、各イベントの「5分前」になったらサーバーにイベントのテキスト情報を送る

という風にする必要がある。こちらの実装に関しては次回。

タイトルとURLをコピーしました