Google Cloud MessagingでAndroidとiOSにプッシュ通知 後編

前回ご紹介した、Android・iOSの両方にプッシュ通知を送信できる新しいGCM。今回は、その国際化対応と、アプリ側の対応方法についてご紹介します。

GCMへのプッシュ通知要求は、必要な項目を詰め込んだJSONを、GCMコネクションサーバーへとPOSTすることで行えます
そのJSONのフォーマットは公式リファレンスで触れられていますが、本来、AndroidとiOSのそれぞれの端末に届くプッシュ通知は、それぞれフォーマットが異なっています。それをきちんと両方に対応させる、すなわち

  •  全く同じJSONを送信し、両OSそれぞれに適切な通知が受信される

ようにするためにはどうすればよいでしょうか。

また、新しいGCMの大きな特徴である、

  • 標準フォーマットの通知であれば、アプリ側が通知表示に関するコードを一切書く必要がない
  • 両OSで統一したローカライズができる

を満たすにはどうしたらよいでしょうか。

マイトでまとめたそのベストプラクティスはズバリ、以下のとおりです。

GCM コネクションサーバーに送信するJSONフォーマット

JSONの例

{
"to": "nrQ2o6FSr6c:AP...",
"priority": "normal",
"notification": {
"icon": "notification",
"color": "#fb9200",
"click_action": "N",
"title_loc_key": "title",
"title_loc_args": null,
"body_loc_key": "r_rcv",
"body_loc_args": ["雑談部屋", "2"],
"badge": "2",
"sound": "default",
"tag": "r300"
},
"data": {
"type": "r",
"rid": "300"
}
}

JSONフォーマット

キー名 OS 内容
to 文字列 両方 GCMが発行した、OSに依存しない、端末を表すデバイストークン
priority 文字列 iOS 通知の優先度、normal固定でよい
notification 辞書 両方 プッシュ通知の標準的なペイロード
data 辞書 両方 プッシュ通知のオプショナルなペイロード

notificationペイロードフォーマット

キー名 OS 内容
icon 文字列 Android プッシュ通知受信時に表示されるアイコンのリソース名
color 文字列 Android 5 プッシュ通知一覧画面でアイコンのバックに表示される円形の背景色
click_action 文字列 Android 通知タップ時にインテントに含められるカテゴリーの名前
iOS 8 通知に対するアクションを表すカテゴリー
title_loc_key 文字列 Android 通知のタイトルの文字列を表すリソースIDの名前
iOS 通知のタイトルの文字列に変換されるNSLocalizedStringのキー
title_loc_args 文字列配列
またはnull
両方 通知のタイトルをフォーマットする文字列の配列
body_loc_args 文字列 Android 通知の本文の文字列を表すリソースIDの名前
iOS 通知の本文の文字列に変換されるNSLocalizedStringのキー
body_loc_args 文字列配列
またはnull
両方 通知の本文をフォーマットする文字列の配列
sound 文字列 両方 通知時に鳴らすサウンド(現状、defaultのみ指定可能)
badge 文字列 iOS アプリアイコンバッジの表示数
tag 文字列 Android 同じ文字列が指定されていた場合、ユーザーがタップまたは削除していない古い通知を新しい通知が置換する

国際化対応

このJSONフォーマット、実によく考えられています。
とりわけ唸らされるのはclick_actionの意味付けの妙味なのですが、それ以上にストレートに、通知タイトルと本文の国際化については、GoogleがiOSに完全に歩み寄る形で整備されていることにもびっくりします。このあたりも、前回「Googleが本気を出してきた」と書かせていただいた理由です。

その国際化ですが、例示のJSONに対応する、リソースの定義は、それぞれ以下のとおりです。

values-ja/notifications.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="r_rcv">%1$sに%2$s件の新しい投稿が届きました</string>
</resources>
ja.lproj/Localizable.strings
"r_rcv" = "%@に新しい投稿が届きました";

いずれも日本語用のリソースファイルです。
例示のJSONでbody_loc_keyに指定した値”r_rcv”に対応するリソースを、各OSの各言語別でそれぞれこのように指定すれば、それぞれが言語に応じて自動的にユーザーへの通知に表示されるようになります。

なお、Androidでは、例示のJSONのbody_loc_argsで指定した2つの引数をフルに使っていますが、iOSでは最初の1つしか使っていません。これは、Androidの通知が、後述するtagの指定で常に同種の通知を上書き更新することで、通知が溢れることを抑止できる仕組みであるのに対し、iOSではプッシュ通知は(表示なし通知をプログラムで工夫しない限りは)すべてその履歴が残る仕組みになっていることに対応しています。
上書き可能なAndroidでは、「△△に1件のメッセージがあります」の次の通知で「△△に2件のメッセージがあります」を送ることがベストです。しかし全履歴が残るiOSでそのような表示をすることに意味はなく、「△△に新しいメッセージがあります」のような通知が並ぶ方がまだましです。
そのため、「現在の未読件数」たる2つ目の引数を、iOSでは無視させてみましたが、このような引数の冗長は、両OSで許容されるようです。

リソースIDの指定

Androidは、リソースの指定をIDで行います。そしてそのIDは、謎のRクラスの自動生成によって行われるという、Androidのダークサイド全開です。
この仕様のため、Androidのライブラリーは、Java開発であるにもかかわらず.jarのインポートだけで済まず、謎の.aarが必要とされた、という黒歴史もありますが、それがこの新しいGCMではなんともスッキリしています。
先のr_rcvですが、これはAndroidでは、R.strings.r_rcvで参照する整数値として使われます。
それを文字列”r_rcv”で参照できるというのもなんだか一周回って元に戻った感がありますが、これは他にもちゃんと応用されていて、それはiconです。通知アイコンを指すこの要素、やはりR.drawable.notificationという整数値で表すか、あるいはXML内に記述するなら@drawable/notificationですが、これも”notification”というシンプルな参照が可能となっています。

両OSの似て非なる機能に共通に割り当てられたclick_action

先に「唸らされた」としたclick_action。これは本来、Android専用のキーです。
Androidの標準的なIPCはすべてインテントで行われるので、OSからアプリへのプッシュ通知タップのアクションも、インテントを受信する形で行います。そしてAndroidManifest.xmlにその受信するインテントの制限を書くことで、受信するインテントをOS側にフィルタリングさせることができるわけです。
そのフィルタリングのための値をこのclick_actionでは指定するのですが、Googleは、この値を、iOSの別概念で重複利用させるようにしました。
iOS8以降では、通知に最大4種類のカテゴリーを設定できます。そしてこのカテゴリーに対応するコードを書くと、通知一覧表示のスワイプによって各カテゴリー別のボタンが表示され、ぞのボタンそれぞれのタップによって通知に対するアクションを切り替えられるようになっています。例えばiOSのメールアプリでもそれは活かされており、「開封済みにする」「ゴミ箱」を通知一覧から直接操作できるようになっています。
mailnotification
iOSでは、まさにこのclick_actionに与えた値は、このカテゴリーの名前として認識されます。この動作は、プッシュ通知のコンセプトじたいも異なる両OSで、比較的似た概念をうまく共通化させた妙味だと、私は感心しました。

…といいつつ、今回マイトで実装したプッシュ通知では、このカテゴリーは特に使っていません(^O^;)。そして逆にその場合、ここに何を指定すればいいのか? というか、そもそもカテゴリー別のアクションをiOS側で実装していないのに、click_actionすなわちcategoryを指定してもよいのか? という疑念もありましたが、実際にコードを動かした結果、この値が設定されていても、iOS側でそれに対応するコードを書いていない場合、ごく普通の通知と同じように扱われることがわかりました。そのため、これは、Android側で一応インテントフィルターとして設定し、iOS側では使わないとなり、ペイロードサイズを極力削るために、マイトでは一文字”N”を指定して使っています。

自動でAndroid通知の上書き更新を行えるtag

もう1つ、tagキーが、今回の新しいGCMの象徴の1つになっています。
これまでのAndroid専用のGCMでは、プッシュ通知に対する反応は、すべてアプリがコードとして実装しなければなりませんでした。具体的には、GCMBaseIntentService#onMessageに必ずすべての通知が寄せられ(ここでアプリプロセスがウェイクアップされるのはとてもありがたい話ですが)、そこでステータスバーに向けてローカル通知を自力で組み立てて表示する、という仕組みです。
そしてこの「全部コード側に丸投げ」を活用して、「この通知は新規通知」「この通知は以前の通知を上書きたい」という仕分けをコードで書き分けて、柔軟な通知一覧表示を実現することができていました。
しかしそれはもう過去のもの!
新しいGCMでは、先のJSONにおけるnotificationペイロードがAndroidでの必須キーをすべて定義している限り、新しいGCMListenerService#onMessageReceivedは呼びだされません(逆に、わざと不完全なペイロードを送信すると、上記メソッドは従来通り呼びだされます)。すべては、GCMのライブラリーによって処理されます。
もちろんこれを使うと、きめ細かな通知表示はできなくなります。が、個人的見解としては、これはむしろよいことです。少なからぬエンドユーザは、似た表現がアプリによって異なることに、苦痛や混乱を覚えるものです。OS側がプッシュ通知というパラダイムに紐付けるUXを限定することで、ユーザーはその通知をより活用できることになる、そう考えています。

ただ、そうすると、これまでAndroidでできていた、「同種の通知の上書きの制御」はどうるんだ? となります。
それを解決するのが、まさにtagキー。
これを指定すると、同じキーでの過去の通知がまだ通知一覧に残っている場合、新しい通知が自動的にそれを上書きしてくれます。
マイトでは今回、「同じトークルーム/ダイレクトメッセージの通知をまとめる」ために使っています。トークルームなら”r”、ダイレクトメッセージなら”d”をプレフィックスにし、その後にルーム/メッセージIDを付加した文字列を、tagに設定します。これにより、同じトークルーム/ダイレクトメッセージの通知は、Androidでは件数が増える度に上書かれることになります。まさに先述の、「△△に×件のメッセージがあります」というフォーマットは、この上書きのためのものなのです。

俺得ペイロード、data

ここまで、新しいGCMの標準ペイロードの話を書かせていただきました。
でももちろん、それとは別の、アプリ俺得データの送信もできるのがGCM。
まぁそれができないと、きめ細かなプッシュ通知受信後の制御はできないってものです。
そしてこの処理も、Android・iOSそれぞれで最適化されています。
Androidの場合、dataキーに渡されてくる辞書の各値は、Intent#getExtraのパラメーターにそのキーを渡すことで取得できます。
またiOSの場合は、AppDelegate#application:didReceiveRemoteNotification:fetchCompletionHandlerで受信できるuserInfoに、この辞書がそのまま設定されてきます。
これらの値で、アプリ内の細かな遷移が実現できるのです。

新しいGCMのまとめ

前・後編と2回に分けて紹介させていただいた、Googleの新しいGCM。
制限もいくつか増えましたが、そのほとんどは開発者にとって合点の行くもの。
一方で、Android・iOSでサーバーサイドのコード書き分けが不要になったメリットは絶大です。
そのメリットを享受するために、最低限必要な情報をまとめさせていただきました。

マイトは、新技術を常に取り込みつつ、それは皆様に最適なサービスを提供するための道具に過ぎない、その道具の選択をきっちり最適化する、という視点で、これからも精進してまいります。またそこで得られた技術的果実を、できるだけシェアーさせていただく所存です。
これからもよろしくお願い申し上げます。

Scalaエンジニアの求人