【Android】GCMからFCM(Firebase Notification)に移行したときの情報まとめ

<img width="501" alt="スクリーンショット 2016-06-16 0.11.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/1901/e8307a8c-44ba-cebe-dfa3-4cb7e00a56f6.png">

Google I/Oで発表されてから盛り上がっているFirebase
GCMからFirebaseに乗り換える方向けに移行方法、テスト方法をまとめたものです。
https://firebase.google.com/

# FCMはすごく簡単
**gcm.jarの頃**

* Registration IDが空になったら再発行処理が必要だった

**play-services-gcmに移行したての頃**

* gcm.jarでは内部でやってくれていた、アプリバージョンをチェックして再発行などの再登録処理を自前で用意

**FCM**

* SDK内ですべてやってくれる

# 移行メリット

* 移行コストが低い。シンプル
* 基本的にサーバーサイド変更不要
* デバイス登録処理やトークン発行処理は自動でやってくれる
* 今後のプッシュ関連の機能追加はFCMで行われる
* サーバーレスも可能
* Firebase Analyticsと連携した配信も可能
* GCMはいつか止まる


# 移行方法

## gcm.jarを使ってる場合

とりあえず、gcm.jarを消して↓を見てください
[Github firebase/quickstart-android](https://github.com/firebase/quickstart-android/tree/master/messaging/app/src/main)

そのあとに、ここを見ると良いです。
[Set Up a Firebase Cloud Messaging Client App on Android](https://firebase.google.com/docs/cloud-messaging/android/client)

## play-services-gcmを使ってる場合
基本的にはこの通りやるだけ
[Migrate a GCM Client App for Android to Firebase Cloud Messaging](https://developers.google.com/cloud-messaging/android/android-migrate-fcm)

* AndroidManifest.xmlはたったの２つのServiceを追加するだけ
* Permissionはなんと不要！(※ライブラリ側のAndroidManifest.xmlに記述されているため)
* 移行するとコード減ります。

#確認方法など

## トークン発行タイミング

1. 初回起動時に自動でFCM SDKがregistration tokenを発行してくれる。
2. tokenが変わるたびに```FirebaseInstanceIdService```の```onTokenRefresh()```で最新のtokenを取得できるのでサーバーに登録したりする

```java:MyFirebaseInstanceIDService.java
@Override
public void onTokenRefresh() {
    // Get updated InstanceID token.
    String refreshedToken = FirebaseInstanceId.getInstance().getToken();
    Log.d(TAG, "Refreshed token: " + refreshedToken);

    // TODO: Implement this method to send any registration to your app's servers.
    sendRegistrationToServer(refreshedToken);
}
```

## InstanceIDが変わった時のテスト

InstanceIDが変わる条件はいくつかあるようです。

* アプリ内でInstanceIDを削除した時
* 新しいデバイスで復元された時
* アンインストール/再インストール
* アプリデータを削除

```bash:adb
adb shell am startservice -a com.google.android.gms.iid.InstanceID --es "CMD" "RST" -n your.package.name/your.own.MyInstanceIDListenerService
```

↑の方法でもonTokenRefresh()が呼ばれなかったので、[DeleteTokenService](http://qiita.com/srea/items/f0836a0857cab743a94a#deletetokenservice)を仕込んで呼ぶことにしてみます。

```bash:startservice
$ adb shell am startservice your.packeage.name/your.own.service.DeleteTokenService
```

すると、onTokenRefresh()が呼ばれることが確認できました。

DeleteTokenServiceは内部で```FirebaseInstanceId.getInstance().deleteInstanceId();```を呼んでるため、過去に発行された```registrationId```も無効にります。

[Documen deleteInstanceID()](https://developers.google.com/android/reference/com/google/android/gms/iid/InstanceID.html#public-methods)


## ユーザーへの影響の確認

GCMからFCMに移行できたけど、アップデートの検証もしたい所。

* 現在ストアに上がってるGCM版のAPKを用意
* FCMに移行したバージョンのAPKを用意

```sh:手順
// 事前にonTokenRefresh()の中でログを仕込んでおく(今回はtokenというタグ）
$ adb logcat | grep token

// GCM版をインストールしたら一度起動する
$ adb install gcm.apk 

// 新しいバージョンを入れて、onTokenRefresh()内に仕込んだtokenログが出力されていることを確認する
$ adb install -r fcm.apk
```

サーバーに新しいデバイストークンを送ってる場合、トークンが書き換わってるかなども確認しておきましょう。

# 注意

## Firebaseプロジェクト作成時
<img width="285" alt="スクリーンショット 2016-06-16 0.11.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/1901/f3b5281e-1e35-43f9-520e-1c4ea69cf92d.png">

すでにGoogleプロジェクトがある場合、Firebaseで新規プロジェクトを作らないこと。
apiキーが違うのでいろいろトラブルが起きます。
Googleプロジェクトをインポートから追加してください。

(AmazonSNSを利用しているプロダクトで、すでにGoogleプロジェクトと連携させている状態で、Firebase Analyticsを新規で導入したところ、APIキーが合わずプッシュが来なくなる問題が発生しました。)

## FirebaseMessagingService内でHandlerを使う時

gcm.jarの頃、```GCMBaseIntentService```は```IntentService```を継承していたため、```new Handler()```が使えました。
今回は```FirebaseMessagingService```は```Service```を継承しているため、そのまま```new Handler()```をしようとすると```RuntimeException```が発生します。

```
java.lang.RuntimeException
Can't create handler inside thread that has not called Looper.prepare()
```

```java:looperを作りましょう。
if (Looper.myLooper() == null) {
    Looper.prepare();
}
```

# エラーがでたら

## java.lang.IllegalStateException: FirebaseApp with name [DEFAULT] doesn't exist.

```groovy:build.gradle
// これが追加されてるか確認
apply plugin: 'com.google.gms.google-services' 
```


# 期待したいこと

実装方法がだいぶ簡単になったので、しばらくは大きく変わることは無いでしょう。(希望)
これからはプッシュ配信運用で便利な機能追加を期待しています。
定期配信ができるとか。
(あと複数Token発行してしまっても、自動で古い方をオフに出来るとか...)

# 参考
[When will InstanceIDListenerService be called and how to test it?](http://stackoverflow.com/questions/30637347/when-will-instanceidlistenerservice-be-called-and-how-to-test-it)

[FCM Vs GCM? Why we need to migrate from GCM to FCM](http://stackoverflow.com/questions/37452463/fcm-vs-gcm-why-we-need-to-migrate-from-gcm-to-fcm)

[Firebase FCM force onTokenRefresh() to be called](http://gohowall.com/firebase-fcm-force-ontokenrefresh-to-be-called/)

[FirebaseInstanceIdService](https://developers.google.com/android/reference/com/google/firebase/iid/FirebaseInstanceIdService.html#public-methods)

[FirebaseApp with name [DEFAULT] doesn't exist](http://stackoverflow.com/questions/37342403/firebaseapp-with-name-default-doesnt-exist)

[Firebase FCM force onTokenRefresh() to be called](http://gohowall.com/firebase-fcm-force-ontokenrefresh-to-be-called/)

# DeleteTokenService

> http://gohowall.com/firebase-fcm-force-ontokenrefresh-to-be-called/

```java:DeleteTokenService.java
public class DeleteTokenService extends IntentService
{
    public static final String TAG = DeleteTokenService.class.getSimpleName();

    public DeleteTokenService()
    {
        super(TAG);
    }

    @Override
    protected void onHandleIntent(Intent intent)
    {
        try
        {
            // Check for current token
            String originalToken = getTokenFromPrefs();
            Log.d(TAG, "Token before deletion: " + originalToken);

            // Resets Instance ID and revokes all tokens.
            FirebaseInstanceId.getInstance().deleteInstanceId();

            // Clear current saved token
            saveTokenToPrefs("");

            // Check for success of empty token
            String tokenCheck = getTokenFromPrefs();
            Log.d(TAG, "Token deleted. Proof: " + tokenCheck);

            // Now manually call onTokenRefresh()
            Log.d(TAG, "Getting new token");
            FirebaseInstanceId.getInstance().getToken();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    private void saveTokenToPrefs(String _token)
    {
        // Access Shared Preferences
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = preferences.edit();

        // Save to SharedPreferences
        editor.putString("registration_id", _token);
        editor.apply();
    }

    private String getTokenFromPrefs()
    {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        return preferences.getString("registration_id", null);
    }
}
```

# メモ

これを書くのに2時間も掛かった。。。
段落の使い方が理解できてない
もっと質の良いアウトプットをしていこう。