GCMからFCMへ移行する際の移行期間を考える

「もともとGCMを使っていて、FCMに移行しなければならない」
こういった話が最近出てきているプロダクトは多いのではないでしょうか。

単純にGCMをFCMへ置き換えた場合、
**アプリをアップデートしただけで起動していなかったら**まだFCMのデバイストークンをサーバへ送っておらず、
かつGCMを辞めてしまっているので、**Push通知を受け取る手段が無くなってしまいます。**
（Receiverでアプリアップデートを検知してデバイストークンを送るようにする手段もありますが）

・GCMを使っている旧バージョンのユーザが
・自動アップデートなどでアプリをFCM版へアップデートし
・アプリを起動していない場合は
・GCMで通知を受け続けなければならない
と、このように考え、その中で私がハマった事についてお話します。

ちなみにこの対処は「GCMからFCMへの移行期間のみの対応」ですので、
FCM版アプリへの移行が完全に終われば不要となります。

# 起こった問題
* 通知が２件来てしまう
    * 独自の通知とFCMの通知とダブルで出てしまう
    * FCMの通知をGCMが食ってしまう
    * GCMの通知をFCMが食ってしまう

これらの問題について、それぞれ解説していきます。

# 通知が２件出てきてしまう
## 問題点：独自の通知とFCMの通知が両方出てしまう
FCMでは、独自で通知の出力を実装せずともPush通知を出してくれます。
ただし、出してくれる条件は以下のように定義されています。
[Firebase公式リファレンス：メッセージの処理](https://firebase.google.com/docs/cloud-messaging/android/receive?hl=ja)
![スクリーンショット 2017-12-01 15.58.07.png](https://qiita-image-store.s3.amazonaws.com/0/84307/2caff328-d181-6ece-63b9-3bfb5e0360be.png)

この中で `システムトレイ` と書かれているところでは、FCMが自動でPush通知を出してくれます。
言い換えると、アプリを起動していない場合を除き、Push通知を出したい場合は独自で実装は必要になります。
（アプリを起動してバックグラウンドに回っている状態など）
ただ、独自で `onMessageReceived` に記載をして色々と試してみた所、Push通知がなぜか２件出てしまう事象となりました。

### 解決策：ダウンストリームメッセージにNotificationを含めてはならない
ユーザへ通知を出す際に独自のサーバを介する場合は、サーバからFCMへHTTPリクエストを出すことでユーザへPush通知を送信することが可能です。
その際にFCMへ出すリクエストに注意が必要です。
含められるリクエストの内容はこちらです。
[Firebase公式リファレンス：ダウンストリーム メッセージの構文](https://firebase.google.com/docs/cloud-messaging/http-server-ref?hl=ja#table1)

独自に定義できるパラメータも含めた場合、ドキュメント通りに記載すると下記のようになります。

```json:FcmDownStream.json
{
    "to": "[デバイストークン]",
    "notification": {
        "title": "ディズニー待ち時間",
        "body": "ディズニー待ち時間だといつでもどこでもパークの待ち時間が閲覧できます！",
        "icon": "icon_launcher",
        "color": "#f56df2"
    },
    "data": {
        "attractionId": "1",
        "type": "greeting",
        "waitTime": "180"
    }
}
```

ここで `Notification` を含めてしまうと、意図せずともFCMが勝手に通知を出してしまいます。
ということは、「独自のPush通知とFCMの通知が両方出てしまう」状態になります。
ですので、独自で出したい場合は `Notification` は含まないようにし、通知は全て独自で出すようにしましょう。


## FCMとGCMと両方の通知が出てしまう
### FCMの通知をGCMが食ってしまう
GCMの実装とFCMの実装が同時に存在すると
GCMのReceiverがFCMのイベントを食って、イベントを出してしまいます。

### 解決策：GCMから来たPush通知のイベントか否かを判断する
その為、GCMのイベントを捕まえるReceiverのクラスに、**GCMから来たPush通知のイベントか否か**の判断が必要です。

```java:GcmBroadcastReceiver.java
public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String from = intent.getStringExtra("from");
        String fcmSenderId = "[FCMのSenderId]";
        if (fcmSenderId.equals(from)) {
            return;
        }

        // Push通知を出す処理
        // ...
    }
}
```

「GCMから来たイベントか？」はIntentの `getStringExtra("from");` で取得できます。
ここでは「FCMから来たイベントの場合はその場でreturnする」としています。

こちらの記事で詳しく触れられています。
[【Android】GCMとFCMを両方入れるとPush通知が2回届く問題](https://qiita.com/kazuman519/items/181f876a138f53856e06)

### GCMの通知をFCMが食ってしまう
逆にFCMがGCMの通知を食ってしまうパターンも経験しました。

### 解決策：GCMから来たPush通知のイベントか否かを判断する
GCMが出してしまう時と同様の対策をします。

```java:PictlinkFirebaseMessagingService.java
public class MyFirebaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);

        if ("[GCMのSenderId]".equals(remoteMessage.getFrom())) {
            return;
        }
        // 通知を出す処理
        // ...
    }
```

FcmからのPush通知に必要な情報は `onMessageReceived` に `remoteMessage` として渡されます。
GCMでのチェック同様に `from` で取れるので、 `remoteMessage.getFrom()` で判断します。


## まとめ
* 通知を独自で出したい場合はFCMへのHTTPリクエストに `Notification` は含まないようにしましょう！
* GCMとFCMが混在する場合は、明示的に「自分自身の通知しか出さない」ようにしましょう！