Firebase Cloud Messagingでのプッシュ受信Android実装まとめ(background時の挙動とか)

  • 50
    いいね
  • 1
    コメント

Firebase Cloud Messagingによるプッシュ通知を実装したらちょっとややこしかったのでメモです。
公式ドキュメントをちゃんと読めば書いてある内容ですが。

Androidでの実装

プッシュ通知受信時にNotificationを表示し、Notoficationタップでプッシュから受け取ったデータを元に詳細画面を表示させるような動作の実装を想定します。

やること

やることは主に

  • build.gradleにライブラリを定義
  • Firebaseのコンソールでアプリを登録し、google-services.jsonをapp直下に配置
  • FirebaseInstanceIdService継承クラスを作成し、onTokenRefreshに自前サーバーなどへのRegistrationId送信処理を実装
  • FirebaseMessagingService継承クラスを作成し、onMessageReceivedにプッシュ受信時の処理(データを取得しNotificationを表示)を実装
  • Notificationタップ後の処理(スタックを積みながらデータを保持しつつ詳細画面へ遷移)

という感じになると思います。

参考
https://firebase.google.com/docs/cloud-messaging/android/client
https://firebase.google.com/docs/cloud-messaging/downstream#receiving-messages-on-an-android-client-app

RegistrationId送信

基本的にはonTokenRefresh時に送信するようにしておけば大丈夫なようです。
RegistrationIdは以下のケースで変更されるとの事です。

  • アプリがInstance IDを削除した時
  • アプリが別のデバイスでリストアされた時
  • アプリをアンインストール・再インストールした時
  • アプリのデータを初期化した時

ただし、アプリがログイン必須でログインしたユーザー情報をRegistrationIdと紐付けて送りたいというような場合は、送信処理を別Serviceに切り出し、onTokenRefreshの他にログイン後のTop画面などでも送信するような制御をする必要がありそうです。

public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {

    @Override
    public void onTokenRefresh() {
        String refreshedToken = FirebaseInstanceId.getInstance().getToken();
        // 自前サーバーへのRegistrationId送信処理を実装
        sendRegistrationToServer(refreshedToken);
    }
}
<service
  android:name=".MyFirebaseInstanceIDService">
  <intent-filter>
      <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
  </intent-filter>
</service>

プッシュ通知受信

ここがちょっとややこしいのですが、プッシュ受信時は常にonMessageReceivedが呼ばれるというわけではなく、送信するプッシュメッセージの構造と、アプリがForegroundの場合とBackgroundの場合で挙動が変わります。

詳細はこのページにかかれています。
https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages

それによると、以下の図のようになるようです。

状態 Notification Data 両方を含む
Foreground onMessageReceived onMessageReceived onMessageReceived
Background 通知自動生成 onMessageReceived 通知自動生成、DataはIntentのextrasに

iOSとの挙動の兼ね合いもあるので、基本的にはNotificationとDataの両方を含めた一番右側のデータ構造を利用すると思います。
その場合、Foreground時はonMessageReceivedが呼ばれるが、Background時はonMessageReceivedは呼ばれず、プッシュメッセージのデータを元にNotificationが自動生成され、タップ時にはデフォルトの(ランチャーから起動される)Acitivtyが呼ばれる、という挙動となります。

それぞれのケースを見ていきましょう。

onMessageReceivedが呼ばれるケース

この場合はonMessageReceived内でPendingIntentを含むNotificationを自前で作成し表示する事になります。
以下のような感じでしょうか。

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // プッシュメッセージのdataに含めた値を取得
        Map<String, String> data = remoteMessage.getData();
        String contentId = data.get("id");
        String contentType = data.get("type");

        // Notificationを生成
        NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext());
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentTitle(getString(R.string.app_name));
        builder.setContentText(remoteMessage.getNotification().getBody());
        builder.setDefaults(Notification.DEFAULT_SOUND
                | Notification.DEFAULT_VIBRATE
                | Notification.DEFAULT_LIGHTS);
        builder.setAutoCancel(true);

        // タップ時に呼ばれるIntentを生成
        Intent intent = new Intent(this, PushReceiveActivity.class);
        intent.putExtra(PushReceiveActivity.ARG_ID, contentId);
        intent.putExtra(PushReceiveActivity.ARG_TYPE, contentType);
        PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        builder.setContentIntent(contentIntent);

        // Notification表示
        NotificationManagerCompat manager = NotificationManagerCompat.from(getApplicationContext());
        manager.notify(Integer.parseInt(contentId), builder.build());
    }
}
<service
    android:name=".MyFirebaseMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>

Notificationが自動生成されるケース

この場合はプッシュメッセージに含めた値を元にシステムがNotificationを自動生成します。
タップ時にはデフォルトのActivityが表示され、intentのextraからdataに含めた値を取得する事が出来ます。

この時に呼ばれるActivityは、プッシュメッセージにclick_actionの値を含め、その値をintent-filterのactionとして指定する事で変更する事も出来ます。

ところで、アプリの同じタスクがすでに存在している場合にはActivityが再生成されずランチャーからの起動時と同じようにアプリがforegroundに戻ってくるだけの挙動になってしまう場合があります。
なのでActivityにsingleTaskを設定するなどの対応が必要になりますが、ランチャーと同じActivityをsingleTaskにしてしまうと普通にランチャーからアプリを選択した時も毎回タスクが初期化されてしまうため、上記のclick_actionを使用しプッシュからの起動専用のActivityをクッションとして用意しそれをsingleTaskにしてあげると上手く行く気がします。
(この辺りはあまり自信がないです)

自動生成されたNotificationから起動した場合のdataの取得。

public class PushReceiveActivity extends AppCompatActivity {

    public static final String ARG_TYPE = "type";
    public static final String ARG_ID = "id";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String id = getIntent().getStringExtra(ARG_ID);
        String type = getIntent().getStringExtra(ARG_TYPE);

        // 表示したい画面へデータを渡して画面遷移
    }
}
<activity android:name=".activity.PushReceiveActivity"
  android:launchMode="singleTask">
  <intent-filter>
    <action android:name="PUSH" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

おわり

これらの情報はFirebaseのバージョンによって変わる可能性があるので、正確な情報は最新の公式ドキュメントを参照してください。

公式ドキュメント内の参考にしたページ
https://firebase.google.com/docs/cloud-messaging/android/client
https://firebase.google.com/docs/cloud-messaging/downstream#receiving-messages-on-an-android-client-app
https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages

GCMからの移行について
https://developers.google.com/cloud-messaging/android/android-migrate-fcm

また、あまり深く検証できておらず間違いがあるかもしれないので、その場合はご指摘お願いします。