Android&iOSアプリでFCMメッセージを受信する

今までは年に1本記事を書くかどうかという感じでしたが、驚くことに今回は前回の記事作成から1カ月も経っていません。
つまり本業が珍しく暇ということです。素晴らしいですね。

さて、今回は FCM (Firebase Cloud Messaging) メッセージの受信処理についての記事となります。
具体的には公式サイトの以下の辺りの記述について、自分が案件で使用した際に知りたかった(分かりづらかった)点のまとめが主な内容となります。

この記事では実装の具体例ではなく、〇〇を実現したいのだけど可能なのだろうか?というような仕様検討段階の疑問点について答えるための内容が主となります。

FCMやPUSH通知の概要説明や、実装のチュートリアルなどは世の中に先達の記事がたくさんあると思いますので割愛させていただきますが、記事の後半にはメッセージの送信方法や送受信のサンプルデータなども記載します。

FCM メッセージについて

FCMのメッセージはJSON形式での送信となり、notificationキーを含む通知メッセージとdataキーを含むデータメッセージが存在します。
公式サイトの記述(2019/08時点)だとこの辺りの用語の表記ゆれが分かりづらさの要因になっていると感じたため、本記事では通知メッセージはnotificationキーを含む、データメッセージはdataキーを含むと送信内容を元にした表記で統一します。

片方のキーだけを含む/両方のキーを含む、アプリがフォアグラウンド/バックグラウンド、等の状態で受信時の処理内容が違ったり、Android/iOS でも仕様に差分があります。
そのため送信内容についてはOS間の差分を考慮した上で決める必要があります。

詳細は後述しますが、仕様に関わる重要な点としては「受信直後に目的の処理を行えるか」「どのような時にシステムトレイに通知が表示されるか」辺りかと思います。

Android アプリでメッセージを受信する

アプリの状態 notificationキーのみ含む dataキーのみ含む 両方のキーを含む
フォアグラウンド onMessageReceived onMessageReceived onMessageReceived
バックグラウンド システムトレイ onMessageReceived システムトレイ

Android 端末がメッセージを受信した場合は上記の表の通り、FirebaseMessagingService(を継承したクラス)の onMessageReceived メソッドが動くか、システムトレイへの通知が行われます。
この表だけは分かりやすかったので、公式サイトから引用させていただきました。

フォアグラウンドの場合は以下のような特徴があります。

  • notificationキーのみ、dataキーのみ、両方のキーを含む、全てのメッセージの場合で onMessageReceived が呼ばれる。
  • onMessageReceived 内の処理は 20秒(Android Marshmallow では 10秒)以内に終わらせる必要がある。
  • 自動でシステムトレイに通知が表示されたりはしない。

バックグラウンドの場合は以下のような特徴があります。

  • dataキーのみ含むメッセージは onMessageReceived が呼ばれる。
  • notificationキーを含むメッセージはシステムトレイに通知が表示される。
    • 通知が表示されるのは防げない。
    • 通知をタップするとデフォルトではアプリのランチャーActivityが開く。(click_actionで起動するActivityを変更できる)
    • dataキーを含む場合、データペイロードは起動した Activity に渡される Intent の追加データから取得可能。

システムトレイに通知が表示される場合、通常は通知をタップするまで独自の処理を動かすことはできません。
ただし、以下のように BroadcastReceiver を自作して AndroidManifest に登録すれば受信時に処理を行うことも可能です。

<receiver
    android:name=".MyReceiver"
    android:exported="false"
    android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
    </intent-filter>
</receiver>

実装した BroadcastReceiver の onReceive メソッドでは intent.getStringExtra("title") などで送信した値を取得できます。

フォアグラウンド/バックグラウンド共通で行いたい処理はここにまとめてしまうのも手ではあります。
ただし、公式に記載されたやり方ではないので、裏技的なやり方であるという点は留意すべきかと思います。

iOS アプリでメッセージを受信する

アプリの状態 notificationキーのみ含む dataキーのみ含む 両方のキーを含む
フォアグラウンド APNs経由 didReceiveMessage APNs経由
バックグラウンド APNs経由 受信不可 APNs経由

iOS 端末がメッセージを受信した場合の動作は公式サイトでは表になっていなかったのですが、Android に倣って記載してみました。

iOS 端末の場合、一部例外を除いてAPNs経由で受信することになります。

フォアグラウンドの場合は以下のような特徴があります。

  • notificationキーを含むメッセージはAPNs経由で処理される。
    • dataキーを含む場合、データペイロードは受信メソッドに渡される userInfo のカスタムキー(aps dictionary 以外)から取得可能。
  • dataキーのみを含むメッセージはAPNs経由ではなく、FCMサービスに接続して直接データメッセージを受信することが可能。受信処理は FIRMessagingDelegate の didReceiveMessage で行う。
    • この方法はレガシー HTTP API によるメッセージ送信でのみ有効。
  • 既定で FCM SDK による Method swizzling が行われ、ユーザ通知(UNUserNotificationCenterなど)への投げ直しなどが行われる。
    • iOS 10 以降の場合、APNs経由のメッセージを受信するには UNUserNotificationCenter delegate、FCMサービスから直接データメッセージを受信するには FIRMessaging delegate を設定する必要がある。
  • 自動でシステムトレイに通知が表示されたりはしない。
    • APNs経由の場合、通知の表示は UNUserNotificationCenterDelegate の実装に委ねられる。

バックグラウンドの場合は以下のような特徴があります。

  • notificationキーを含まないメッセージはバックグラウンドだと処理できない。
  • notificationキーを含むメッセージはAPNs経由でシステムトレイに送られ、通知をタップすると通知内容が AppDelegate の didReceiveRemoteNotification に渡される。
    • 通知が勝手に表示されるのを防ぎたい場合はサイレント通知を使用する必要がある。
    • dataキーを含む場合、Notification Service Extension を使えば通知の表示前にデータペイロードを使用した処理を挟むことが可能。

送信メッセージについて

本記事の主題ではありませんが、送信メッセージ視点でも気になった点について記載します。

iOS でバックグラウンド受信を行う場合、notificationキーを含めることが必須となります。
しかし、Android だとnotificationキーを含んだメッセージをバックグラウンドで受信すると自動でシステムトレイに通知が出る、という制限が発生します。

このような制限を回避してOSごとに可能な限り理想の挙動をさせるためには、OSごとに送信メッセージの内容を変えてやる必要があります。
これは Firebase コンソールNotifications Composerレガシー HTTP API では対応できず、
HTTP v1 API を使用する必要があります。

一応、現時点ではレガシー HTTP API の廃止時期などの情報は見当たりませんでしたが、名称にレガシーと付けるなどして HTTP v1 API への移行を促しているようなので、今後レガシーが廃止になる可能性も視野に入れて新規案件では HTTP v1 API を初めから使っておくのが良さそうです。(Notifications Composer で十分という場合は除く)

公式サイトの iOS の記述には「非常に高い信頼性が要求される特別なユースケースでは、ダイレクト FCM チャネル メッセージを処理できます。」と記載されており、APNs経由ではないメッセージの受信をどうしても行いたい場合などはレガシーの使用も仕方ないかも知れません。

ただ、そういうケースでも、Android/iOS 間の機能差分問題や FCM 以外への乗り換えが難しくなるリスクなども考慮すると、代替となる他の技術が使えないか(可能なら要件を調整できないか)なども先に検討する余地はあるかなと思います。

HTTP v1 API によるメッセージの送信について

Notifications Composer だとクライアントアプリ開発者でも簡単に使えて便利なのですが、実際に HTTP v1 API などを使うのは初見だと調べるところから始まるので結構面倒です。

しかし、Android はまだしも iOS は公式サイトの記述が控えめに言ってもひどいので説明を読んだだけだと受信時の各データの取得方法などがイマイチ分かりません。
そこで実際の動きを確認するため、アプリ開発者自身がメッセージ送信を試してみたくなります。

ここではそういう人達のために、HTTP v1 API の送信実験ができるようになるまでの手順を記載しておきます。

以下の手順は Windows10 のコマンドプロンプト上で行っています。
curl と Node.js が必要ですが、最近買ってもらったPCには curl は最初から入っていたので新規でインストールしたのは Node.js のみでした。

他の環境で実行する場合は適宜読み替えるようお願いします。

  1. 適当な作業用ディレクトリを作ってターミナルで開く。
  2. npm init -y && npm install googleapis コマンドを実行する。
  3. サービス アカウント用の秘密鍵ファイルを生成するにはに記載の以下の手順を実行してJSONファイルを取得する。
    1. Firebase コンソールで、[設定] > [サービス アカウント] を開きます。
    2. [新しい秘密鍵の生成] をクリックし、[キーを生成] をクリックして確定します。
    3. キーを含む JSON ファイルを安全に保管します。
  4. 取得したJSONファイルを service-account.json にリネームして作業用ディレクトリに配置する。
  5. ここを参照してメッセージとして送信したいJSONを作成し、message.json という名称で作業用ディレクトリに配置する。
  6. 後述する内容を記述した index.js を作業用ディレクトリに保存する。
  7. 作業用ディレクトリを開いたターミナル上で node index.js <プロジェクトID> コマンドを実行する。
    ※<プロジェクトID> は Firebase コンソールの[設定]に記載のプロジェクトIDです。
index.js の中身
const { google } = require('googleapis');
const { exec } = require('child_process');

const SCOPES = [
  'https://www.googleapis.com/auth/firebase.messaging',
];

function getAccessToken() {
  return new Promise(function (resolve, reject) {
    const key = require('./service-account.json');
    const jwtClient = new google.auth.JWT(
      key.client_email,
      null,
      key.private_key,
      SCOPES,
      null
    );
    jwtClient.authorize(function (err, tokens) {
      if (err) {
        reject(err);
        return;
      }
      resolve(tokens.access_token);
    });
  });
}

if (!process.argv[2]) {
  console.error("[ERROR] Set Firebase project ID as argument.")
  return;
}

getAccessToken().then(function (value) {
  const command = `curl -X POST -H "Authorization: Bearer ${value}" -H "Content-Type: application/json" -d @message.json "https://fcm.googleapis.com/v1/projects/${process.argv[2]}/messages:send"`;
  exec(command, (error, stdout, stderr) => {
    if (error) {
      console.error(`[ERROR] ${error}`);
      return;
    }
    console.log(stdout);
  });
}).catch(function (error) {
  console.error(`[ERROR] ${error}`);
});

HTTP v1 API のメッセージ内容について

HTTP v1 API によるメッセージの送信について」内で記載した message.json の記載内容についても少し記載しておきます。
詳細は以下を確認するようお願いします。

iOS についての補足

上記の公式の説明文が分かりづらいため、先に簡易な送信内容で iOS についての補足説明をしておきます。
※唐突に出てくる APS が未だに何の略称なのか分からない(Apple Push Service?)

まずnotificationキーとdataキーのみを使用する場合の送信データは以下のような形式になります。

{
  "message": {
    "notification": {
      "title": "Title",
      "body": "Body"
    },
    "data": {
      "type": "Type",
      "url": "Url"
    }
  }
}

次に ApnsConfig の payload を使用する場合の送信データは以下のような形式になります。

{
  "message": {
    "apns": {
      "payload": {
        "aps": {
          "alert": {
            "title": "Title",
            "body": "Body"
          }
        },
        "type": "Type",
        "url": "Url"
      }
    }
  }
}

上記メッセージを送信してAPNs経由で受信した場合、どちらのパターンであっても UNNotificationContent から取得できる値は以下のようになります。
※必要な部分以外の記述は省略しています
※userInfo の値は説明を分かりやすくするため、JSON形式で表記しています

  • title -> Title
  • body -> Body
  • userInfo ->
{
  "aps": {
    "alert": {
      "title": "Title",
      "body": "Body"
    }
  },
  "type": "Type",
  "url": "Url"
}

ApnsConfig にはFCM用語である notificationdata というキー名称は出てきませんが、例の送信データは「両方のキーを含む」場合と同等という認識で問題ありません。
※詳細に関してはFCMではなく、APNs側の仕様を参照

そして notification/data/apns の各キーを同時に指定した場合、payload 配下の値が notification や data 配下の値を上書きします。

複雑なメッセージの送受信例

ここでは Android/iOS に向けて以下のようなメッセージを送信した場合にどうなるか見てみます。

{
  "message": {
    "notification": {
      "title": "Default Title",
      "body": "Default Body"
    },
    "data": {
      "type": "Default Type",
      "url": "Default Url"
    },
    "android": {
      "notification": {
        "title": "Android Title"
      },
      "data": {
        "type": "Android Type"
      }
    },
    "apns": {
      "payload": {
        "aps": {
          "alert": {
            "body": "iOS Body"
          }
        },
        "url": "iOS Url"
      }
    }
  }
}

上記送信データを端末で受信した際に各キーなどから取得できる値は以下のような結果となります。

OS title body type url
Android Android Title Default Body Android Type Default Url
iOS Default Title iOS Body Default Type iOS Url

最後に

以上で本記事におけるFCMメッセージの受信動作に関するまとめは終わりとなります。

実は自分が案件でFCMを使ったのは1年近く前で、その時にこの記事の元となる資料はまとめてありました。
しかし、この記事を書くに辺り公式サイトの最新記述を改めて読み直したところ、当時まとめた資料が若干中途半端だったこともあり細かい挙動がどうなるのか全く理解できませんでした。(特に iOS)

この記事は次回自分が再度FCMを使う際に「この記事さえ読み返せばOK」となるよう、当時の資料に足りなかった情報を書き足しまくったものとなります。

公式サイトに説明はあるんだけど、分かりづらい!」という人達の声に応えることができていたら嬉しいです。

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ