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

More than 1 year has passed since last update.

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

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