Google I/O 2016で、AndroidへのPush通知の仕組みがGCMからFCM(Firebase Cloud Messaging)に移行されるという発表があったね!
そこでモケラボも、既存のアプリを1つ、GCMからFCMに移行してみたよ!
とっても簡単かなと思ってたけど、世の中そんなに甘くないね! ちゃんとPushが届くのを確認するまでに1人日(1モケラ日?)かかっちゃった。
AndroidアプリへのFCMの導入は簡単。GCMの時よりもっと簡単になってたよ!これからおはなしする問題に遭遇しなければね!
詳細はTech sheets でまとめるので、ここでは概要だけ紹介するね。
FCMを導入しよう
FirebaseのWebコンソールでプロジェクトを作る
検索するとたくさんでてくると思うので、ここは省略するね。
FirebaseのWebコンソールで、Androidアプリを登録する
アプリのパッケージ名をいれよう。SHA-1は空っぽでもだいじょうぶ
アプリを追加すると、勝手にgoogle-services.jsonのダウンロードが始まるよ。
google-services.jsonをapp配下に配置する
これは、ちょっと前のGCMと同じだね!ちなみにProductFlavorを使ってる場合は、app/src/Flavor1/google-services.json のように、app直下じゃなくてFlavorの配下に置こう。
ルートのbuild.gradleを編集する
dependenciesのところを次のようにするよ。
dependencies { classpath 'com.android.tools.build:gradle:2.1.0' classpath 'com.google.gms:google-services:3.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }
appのbuild.gradleを編集する
dependenciesに
compile 'com.google.firebase:firebase-messaging:9.0.1'
を追加しよう。そしてファイルの一番最後の行にapply plugin: 'com.google.gms.google-services'
を追加しよう。これだけ!
実はこれだけでOKなんだ。初期化のプログラムを書く必要すらなくなってる!サンプルを動かすとちゃんとPushとどいたよ!でも既存のアプリにいれたら動作しなかった!
ここからが本題。既存のアプリにいれたらなぜか動かなかったんだ。モケラボでいろいろ原因調査したので、その過程もセットでどうぞ!トークンを取ろうとすると例外を投げる
FCMのトークンは
String token = FirebaseInstanceId.getInstance().getToken();
java.lang.IllegalStateException: FirebaseApp with name [DEFAULT] doesn't exist.
このFirebaseAppというのがキーワードみたいだね。ということで次にFirebaseAppについて調査してみたんだ。すると。。
FirebaseAppが1つもみつからない
FirebaseAppというオブジェクトは、
でも動かなかった時はこのメソッドが長さ0のリストを返していたんだ。さっきの例外はこれが原因だったのかな?
小さいプロジェクトだと1つ返ってきて、ある程度の規模のプロジェクトでなぜか1つもとれないのはなぜだろう。と、その前にこのFirebaseAppはどこで生成されてるんだろう?
Applicationクラスをこっそり差し替えたりするのかな?と思って、あえて独自のApplicationクラスをいれてみたけど、結果は同じ。ここではなさそう。
「FirebaseAppの初期化に成功したよ!」というログだね。失敗してる時にはこれ無かったんだ。と、ここに新キャラの「FirebaseInitProvider」というのがいるね。Providerって名前がついてるから、きっとAndroidManifest.xmlにいるよね。
やっぱりいたね!ということは、なぜかマージされないAndroidManifestが今回の原因だったみたい。マージされないのなら自分で書けばいいや!ということで、アプリ本体のAndroidManifest.xmlに次の項目を追加したらちゃんと動くようになったよ!一部アプリのパッケージ名があるのでそのままコピペして動きません!という苦情コメントは受け付けないよ!
List<FirebaseApp> apps = FirebaseApp.getApps(this); // thisはContext
で取得できるよ。ちゃんと動作していれば1つ以上返ってくるよ。でも動かなかった時はこのメソッドが長さ0のリストを返していたんだ。さっきの例外はこれが原因だったのかな?
小さいプロジェクトだと1つ返ってきて、ある程度の規模のプロジェクトでなぜか1つもとれないのはなぜだろう。と、その前にこのFirebaseAppはどこで生成されてるんだろう?
Applicationクラスをこっそり差し替えたりするのかな?と思って、あえて独自のApplicationクラスをいれてみたけど、結果は同じ。ここではなさそう。
ログを眺めてみた
次に、うまくFirebaseAppが取得できた時と、失敗してる時のログをぼんやり眺めてみたよ。すると、うまくいっている時にはこんなのが見つかったよ!06-04 07:10:09.180 9933-9933/com.mokelab.fbdemo I/FirebaseInitProvider: FirebaseApp initialization successful
「FirebaseAppの初期化に成功したよ!」というログだね。失敗してる時にはこれ無かったんだ。と、ここに新キャラの「FirebaseInitProvider」というのがいるね。Providerって名前がついてるから、きっとAndroidManifest.xmlにいるよね。
AndroidManifest.xmlを見てみた
サンプルではAndroidManifest.xmlを全く編集していないので、きっとビルド時に追加されてると思ってbuild/intermidates/manifests/の下に生成されているAndroidManifest.xmlを見てみたよ。<provider android:name="com.google.firebase.provider.FirebaseInitProvider" android:authorities="com.mokelab.fbdemo.firebaseinitprovider" android:exported="false" android:initOrder="100" />
やっぱりいたね!ということは、なぜかマージされないAndroidManifestが今回の原因だったみたい。マージされないのなら自分で書けばいいや!ということで、アプリ本体のAndroidManifest.xmlに次の項目を追加したらちゃんと動くようになったよ!一部アプリのパッケージ名があるのでそのままコピペして動きません!という苦情コメントは受け付けないよ!
<receiver
android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="com.mokelab.fbdemo" />
</intent-filter>
</receiver>
<!--
Internal (not exported) receiver used by the app to start its own exported services
without risk of being spoofed.
-->
<receiver
android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver"
android:exported="false" />
<!--
FirebaseInstanceIdService performs security checks at runtime,
no need for explicit permissions despite exported="true"
-->
<service
android:name="com.google.firebase.iid.FirebaseInstanceIdService"
android:exported="true" >
<intent-filter android:priority="-500" >
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="com.mokelab.fbdemo.firebaseinitprovider"
android:exported="false"
android:initOrder="100" />