はじめに
様々な方言を話す。おしゃべりに小型ロボット「ミーア」を開発中。
現在、ミーアが提供する便利な機能としては、アプリで居住地を入力して、天気お知らせ時刻を設定していると、その時刻にその日の天気と一言アドバイス(雨の場合は、「折り畳み傘忘れないでね」など)を音声で伝える機能がある。
自分だったら、他にどんな機能があったら使うかなと思い、「カレンダー連携で予定を通知する機能」 を追加することにした。
例えば、次のようなシナリオを想定
例:「次のミーティング『開発打ち合わせ』があと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 アカウントでサインインするためのパッケージ
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 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
に定義されたiosClientId
(GIDClientID
と同じなので、直接コード内に記載して呼び出しても動作する)をGoogle Sign-Inで使用。
日付範囲の指定:当日の予定だけを取得するため、timeMin
と timeMax
を当日の日付に設定
イベントのデータ取得:
取得データの表示:
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アカウントでのサインイン画面が開く。
サインインする時に、カレンダー情報の取得同意が表示されるので、チェックして「続行」をクリック。
すると、カレンダーが連携が完了し、アプリコンソール画面に下記のように当日の予定が表示された。
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分前」になったらサーバーにイベントのテキスト情報を送る
という風にする必要がある。こちらの実装に関しては次回。