傳送訊息至多部裝置

如要將訊息指定至多部裝置,請使用主題訊息。這項功能可讓您傳送訊息給已訂閱特定主題的多部裝置。

本教學課程著重於使用 FCM 適用的 Admin SDKREST API 從應用程式伺服器傳送主題訊息,並在 Android 應用程式中接收及處理這些訊息。我們將介紹背景應用程式和前景應用程式的訊息處理方式。從設定到驗證,此過程的所有步驟都涵蓋在內。

設定 SDK

如果您已為 FCM 設定 Android 用戶端應用程式,或已按照傳送第一則訊息的步驟進行,本節內容可能會說明您已完成的步驟。

事前準備

  • 安裝或更新 Android Studio 至最新版本。

  • 請確認您的專案符合以下要求 (請注意,部分產品的要求可能更嚴格):

    • 以 API 級別 19 (KitKat) 以上版本為目標
    • 使用 Android 4.4 以上版本
    • 使用 Jetpack (AndroidX),其中包含下列版本需求:
      • com.android.tools.build:gradle 7.3.0 以上版本
      • compileSdkVersion 28 以上版本
  • 設定實體裝置或使用模擬器執行應用程式。
    請注意,含有依附元件的 Google Play 服務 Firebase SDK 需要裝置或模擬器安裝 Google Play 服務。

  • 使用 Google 帳戶登入 Firebase

如果您沒有 Android 專案,只是想試用 Firebase 產品,可以下載其中一個快速入門導覽課程範例

建立 Firebase 專案

您需要先建立用於連結 Android 應用程式的 Firebase 專案,才能將 Firebase 新增到 Android 應用程式。如要進一步瞭解 Firebase 專案,請參閱「瞭解 Firebase 專案」一文。

透過 Firebase 註冊應用程式

如要在 Android 應用程式中使用 Firebase,您需要透過 Firebase 專案註冊應用程式。註冊應用程式的做法通常稱為「新增」應用程式到專案中。

  1. 前往 Firebase 主控台

  2. 在專案總覽頁面的中央,按一下「Android」圖示 () 或「新增應用程式」,啟動設定工作流程。

  3. 在「Android package name」欄位中輸入應用程式的套件名稱。

  4. (選用) 輸入其他應用程式資訊:應用程式暱稱偵錯簽署憑證 SHA-1

  5. 按一下 [Register app] (註冊應用程式)

新增 Firebase 設定檔

  1. 請下載 Firebase Android 設定檔 (google-services.json),然後將其加入應用程式:

    1. 按一下「Download google-services.json」取得 Firebase Android 設定檔。

    2. 將設定檔移至應用程式的 module (應用程式層級) 根目錄。

  2. 如要讓 Firebase SDK 存取 google-services.json 設定檔中的值,須有 Google 服務 Gradle 外掛程式 (google-services)。

    1. 根層級 (專案層級) Gradle 檔案 (<project>/build.gradle.kts<project>/build.gradle) 中,將 Google 服務外掛程式新增為依附元件:

      Kotlin

      plugins {
        id("com.android.application") version "7.3.0" apply false
        // ...
      
        // Add the dependency for the Google services Gradle plugin
        id("com.google.gms.google-services") version "4.4.1" apply false
      }
      

      Groovy

      plugins {
        id 'com.android.application' version '7.3.0' apply false
        // ...
      
        // Add the dependency for the Google services Gradle plugin
        id 'com.google.gms.google-services' version '4.4.1' apply false
      }
      
    2. 模組 (應用程式層級) Gradle 檔案 (通常是 <project>/<app-module>/build.gradle.kts<project>/<app-module>/build.gradle) 中,新增 Google 服務外掛程式:

      Kotlin

      plugins {
        id("com.android.application")
      
        // Add the Google services Gradle plugin
        id("com.google.gms.google-services")
        // ...
      }
      

      Groovy

      plugins {
        id 'com.android.application'
      
        // Add the Google services Gradle plugin
        id 'com.google.gms.google-services'
        // ...
      }
      

在應用程式中加入 Firebase SDK

  1. 模組 (應用程式層級) Gradle 檔案 (通常是 <project>/<app-module>/build.gradle.kts<project>/<app-module>/build.gradle) 中,新增 Android 版 Firebase 雲端通訊程式庫的依附元件。建議您使用 Firebase Android BoM 來控管程式庫版本管理。

    為了提供最佳的 Firebase 雲端通訊體驗,建議您在 Firebase 專案中啟用 Google Analytics (分析),並將 Google Analytics (分析) 專用 Firebase SDK 新增至應用程式。

    dependencies {
        // Import the BoM for the Firebase platform
        implementation(platform("com.google.firebase:firebase-bom:33.0.0"))
    
        // Add the dependencies for the Firebase Cloud Messaging and Analytics libraries
        // When using the BoM, you don't specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-messaging")
        implementation("com.google.firebase:firebase-analytics")
    }
    

    如果使用 Firebase Android BoM,應用程式就會一律使用相容的 Firebase Android 程式庫版本。

    (替代方法) 新增 Firebase 程式庫依附元件,「不必」使用 BoM

    如果選擇不使用 Firebase BoM,請務必在依附元件行中指定各個 Firebase 程式庫版本。

    請注意,如果在應用程式中使用多個 Firebase 程式庫,強烈建議您使用 BoM 管理程式庫版本,確保所有版本都相容。

    dependencies {
        // Add the dependencies for the Firebase Cloud Messaging and Analytics libraries
        // When NOT using the BoM, you must specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-messaging:24.0.0")
        implementation("com.google.firebase:firebase-analytics:22.0.0")
    }
    
    在尋找 Kotlin 專用的程式庫模組嗎?2023 年 10 月 (Firebase BoM 32.5.0) 起,Kotlin 和 Java 開發人員都可以依賴主程式庫模組 (詳情請參閱這項計畫的常見問題)。

  2. 將 Android 專案與 Gradle 檔案同步處理。

為用戶端應用程式訂閱主題

用戶端應用程式可以訂閱任何現有主題,或是建立新主題。當用戶端應用程式訂閱新的主題名稱 (您的 Firebase 專案還不存在的名稱) 時,系統會在 FCM 中建立一個該名稱的新主題,所有用戶端之後都能訂閱該名稱。

如要訂閱主題,用戶端應用程式會使用 FCM 主題名稱呼叫 Firebase 雲端通訊 subscribeToTopic()。這個方法會傳回 Task,可供完成事件監聽器用於判斷訂閱項目是否成功:

Kotlin+KTX

Firebase.messaging.subscribeToTopic("weather")
    .addOnCompleteListener { task ->
        var msg = "Subscribed"
        if (!task.isSuccessful) {
            msg = "Subscribe failed"
        }
        Log.d(TAG, msg)
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
    }

Java

FirebaseMessaging.getInstance().subscribeToTopic("weather")
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                String msg = "Subscribed";
                if (!task.isSuccessful()) {
                    msg = "Subscribe failed";
                }
                Log.d(TAG, msg);
                Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
            }
        });

如要取消訂閱,用戶端應用程式會以主題名稱呼叫 Firebase 雲端通訊 unsubscribeFromTopic()

接收及處理主題訊息

FCM 傳送主題訊息的方式與其他下游訊息相同。

如要接收訊息,請使用擴充 FirebaseMessagingService 的服務。您的服務應覆寫 onMessageReceivedonDeletedMessages 回呼。

視呼叫 onMessageReceived 前發生的延遲而定,訊息處理時間範圍可能會少於 20 秒,包括作業系統延遲、應用程式啟動時間、其他作業遭到封鎖的主要執行緒,或是先前的 onMessageReceived 呼叫處理時間過長。在那之後,如 Android 的程序終止或 Android O 的 背景執行限制等各種 OS 行為,可能會幹擾您完成工作的能力。

為大多數訊息類型提供 onMessageReceived,但以下除外:

  • 當應用程式在背景執行時,傳送通知的訊息。在此情況下,通知會傳送到裝置的系統匣。根據預設,使用者輕觸通知後即可開啟應用程式啟動器。

  • 在背景接收時,同時含有通知和資料酬載的訊息。在此情況下,通知會傳送至裝置的系統匣,且資料酬載會透過啟動器活動意圖的額外項目傳送。

簡單來說:

應用程式狀態 通知 資料 兩者並用
前景 onMessageReceived onMessageReceived onMessageReceived
背景 系統匣 onMessageReceived 通知:系統匣
資料:意圖額外項目。
如要進一步瞭解訊息類型,請參閱「通知與資料訊息」。

編輯應用程式資訊清單

如要使用 FirebaseMessagingService,您必須在應用程式資訊清單中新增以下內容:

<service
    android:name=".java.MyFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

此外,建議您設定預設值,以自訂通知外觀。您可以指定自訂預設圖示和自訂預設顏色,如果未在通知酬載中設定相等的值,系統就會套用該顏色。

application 標記中加入以下幾行,即可設定自訂預設圖示和自訂顏色:

<!-- Set custom default icon. This is used when no icon is set for incoming notification messages.
     See README(https://goo.gl/l4GJaQ) for more. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_stat_ic_notification" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming
     notification message. See README(https://goo.gl/6BKBk7) for more. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorAccent" />

Android 會顯示

  • Notifications Composer 傳送的所有通知訊息。
  • 所有未在通知酬載中明確設定圖示的通知訊息。

Android 會使用

  • Notifications Composer 傳送的所有通知訊息。
  • 未明確設定通知酬載顏色的任何通知訊息。

如果未設定自訂預設圖示,且通知酬載中未設定圖示,Android 就會顯示以白色顯示的應用程式圖示。

覆寫 onMessageReceived

覆寫 FirebaseMessagingService.onMessageReceived 方法後,即可根據收到的 RemoteMessage 物件執行操作,並取得訊息資料:

Kotlin+KTX

override fun onMessageReceived(remoteMessage: RemoteMessage) {
    // TODO(developer): Handle FCM messages here.
    // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
    Log.d(TAG, "From: ${remoteMessage.from}")

    // Check if message contains a data payload.
    if (remoteMessage.data.isNotEmpty()) {
        Log.d(TAG, "Message data payload: ${remoteMessage.data}")

        // Check if data needs to be processed by long running job
        if (needsToBeScheduled()) {
            // For long-running tasks (10 seconds or more) use WorkManager.
            scheduleJob()
        } else {
            // Handle message within 10 seconds
            handleNow()
        }
    }

    // Check if message contains a notification payload.
    remoteMessage.notification?.let {
        Log.d(TAG, "Message Notification Body: ${it.body}")
    }

    // Also if you intend on generating your own notifications as a result of a received FCM
    // message, here is where that should be initiated. See sendNotification method below.
}

Java

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
    // TODO(developer): Handle FCM messages here.
    // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
    Log.d(TAG, "From: " + remoteMessage.getFrom());

    // Check if message contains a data payload.
    if (remoteMessage.getData().size() > 0) {
        Log.d(TAG, "Message data payload: " + remoteMessage.getData());

        if (/* Check if data needs to be processed by long running job */ true) {
            // For long-running tasks (10 seconds or more) use WorkManager.
            scheduleJob();
        } else {
            // Handle message within 10 seconds
            handleNow();
        }

    }

    // Check if message contains a notification payload.
    if (remoteMessage.getNotification() != null) {
        Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
    }

    // Also if you intend on generating your own notifications as a result of a received FCM
    // message, here is where that should be initiated. See sendNotification method below.
}

覆寫 onDeletedMessages

在某些情況下,FCM 可能無法傳送訊息。當應用程式在特定裝置上連線時,應用程式有過多待處理的訊息 (超過 100),或裝置超過一個月未連線至 FCM 時,就會發生這種情況。在這類情況下,您可能會收到對 FirebaseMessagingService.onDeletedMessages() 的回呼。應用程式執行個體收到這個回呼時,應與應用程式伺服器進行完整的同步作業。如果在過去 4 週內並未在該裝置上傳送訊息至應用程式,FCM 就不會呼叫 onDeletedMessages()

在背景應用程式中處理通知訊息

當應用程式在背景執行時,Android 會將通知訊息導向系統匣。根據預設,使用者輕觸通知即可開啟應用程式啟動器。

包括含有通知和資料酬載的訊息,以及從通知主控台傳送的所有訊息。在這類情況下,通知會傳送到裝置的系統匣,且資料酬載會透過啟動器活動意圖的額外項目傳送。

如要進一步瞭解向應用程式傳送的訊息,請參閱 FCM 報表資訊主頁,其中記錄了在 Apple 和 Android 裝置上傳送及開啟的訊息數量,以及 Android 應用程式的「曝光」(使用者看到的通知) 資料。

版本傳送要求

建立主題後,您可以在用戶端或透過伺服器 API 將用戶端應用程式執行個體訂閱主題,藉此將訊息傳送至主題。如果這是您第一次建構 FCM 要求,請參閱伺服器環境和 FCM 指南,瞭解重要的背景和設定資訊。

在後端的傳送邏輯中,指定所需的主題名稱,如下所示:

Node.js

// The topic name can be optionally prefixed with "/topics/".
const topic = 'highScores';

const message = {
  data: {
    score: '850',
    time: '2:45'
  },
  topic: topic
};

// Send a message to devices subscribed to the provided topic.
getMessaging().send(message)
  .then((response) => {
    // Response is a message ID string.
    console.log('Successfully sent message:', response);
  })
  .catch((error) => {
    console.log('Error sending message:', error);
  });

Java

// The topic name can be optionally prefixed with "/topics/".
String topic = "highScores";

// See documentation on defining a message payload.
Message message = Message.builder()
    .putData("score", "850")
    .putData("time", "2:45")
    .setTopic(topic)
    .build();

// Send a message to the devices subscribed to the provided topic.
String response = FirebaseMessaging.getInstance().send(message);
// Response is a message ID string.
System.out.println("Successfully sent message: " + response);

Python

# The topic name can be optionally prefixed with "/topics/".
topic = 'highScores'

# See documentation on defining a message payload.
message = messaging.Message(
    data={
        'score': '850',
        'time': '2:45',
    },
    topic=topic,
)

# Send a message to the devices subscribed to the provided topic.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)

Go

// The topic name can be optionally prefixed with "/topics/".
topic := "highScores"

// See documentation on defining a message payload.
message := &messaging.Message{
	Data: map[string]string{
		"score": "850",
		"time":  "2:45",
	},
	Topic: topic,
}

// Send a message to the devices subscribed to the provided topic.
response, err := client.Send(ctx, message)
if err != nil {
	log.Fatalln(err)
}
// Response is a message ID string.
fmt.Println("Successfully sent message:", response)

C#

// The topic name can be optionally prefixed with "/topics/".
var topic = "highScores";

// See documentation on defining a message payload.
var message = new Message()
{
    Data = new Dictionary<string, string>()
    {
        { "score", "850" },
        { "time", "2:45" },
    },
    Topic = topic,
};

// Send a message to the devices subscribed to the provided topic.
string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
// Response is a message ID string.
Console.WriteLine("Successfully sent message: " + response);

REST

POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA
{
  "message":{
    "topic" : "foo-bar",
    "notification" : {
      "body" : "This is a Firebase Cloud Messaging Topic Message!",
      "title" : "FCM Message"
      }
   }
}

cURL 指令:

curl -X POST -H "Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA" -H "Content-Type: application/json" -d '{
  "message": {
    "topic" : "foo-bar",
    "notification": {
      "body": "This is a Firebase Cloud Messaging Topic Message!",
      "title": "FCM Message"
    }
  }
}' https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

如要向主題組合傳送訊息,請指定「條件」,這是指定目標主題的布林值運算式。舉例來說,以下條件會將訊息傳送到訂閱 TopicA,以及 TopicBTopicC 的裝置:

"'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"

FCM 會先評估括號中的任何條件,再從左到右評估運算式。在上述運算式中,訂閱任何單一主題的使用者不會收到訊息。同樣地,未訂閱 TopicA 的使用者也不會收到訊息。這些組合會接收到:

  • TopicATopicB
  • TopicATopicC

條件運算式中最多可加入五個主題。

如何傳送到條件:

Node.js

// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
const condition = '\'stock-GOOG\' in topics || \'industry-tech\' in topics';

// See documentation on defining a message payload.
const message = {
  notification: {
    title: '$FooCorp up 1.43% on the day',
    body: '$FooCorp gained 11.80 points to close at 835.67, up 1.43% on the day.'
  },
  condition: condition
};

// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
getMessaging().send(message)
  .then((response) => {
    // Response is a message ID string.
    console.log('Successfully sent message:', response);
  })
  .catch((error) => {
    console.log('Error sending message:', error);
  });

Java

// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
String condition = "'stock-GOOG' in topics || 'industry-tech' in topics";

// See documentation on defining a message payload.
Message message = Message.builder()
    .setNotification(Notification.builder()
        .setTitle("$GOOG up 1.43% on the day")
        .setBody("$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.")
        .build())
    .setCondition(condition)
    .build();

// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
String response = FirebaseMessaging.getInstance().send(message);
// Response is a message ID string.
System.out.println("Successfully sent message: " + response);

Python

# Define a condition which will send to devices which are subscribed
# to either the Google stock or the tech industry topics.
condition = "'stock-GOOG' in topics || 'industry-tech' in topics"

# See documentation on defining a message payload.
message = messaging.Message(
    notification=messaging.Notification(
        title='$GOOG up 1.43% on the day',
        body='$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.',
    ),
    condition=condition,
)

# Send a message to devices subscribed to the combination of topics
# specified by the provided condition.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)

Go

// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
condition := "'stock-GOOG' in topics || 'industry-tech' in topics"

// See documentation on defining a message payload.
message := &messaging.Message{
	Data: map[string]string{
		"score": "850",
		"time":  "2:45",
	},
	Condition: condition,
}

// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
response, err := client.Send(ctx, message)
if err != nil {
	log.Fatalln(err)
}
// Response is a message ID string.
fmt.Println("Successfully sent message:", response)

C#

// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
var condition = "'stock-GOOG' in topics || 'industry-tech' in topics";

// See documentation on defining a message payload.
var message = new Message()
{
    Notification = new Notification()
    {
        Title = "$GOOG up 1.43% on the day",
        Body = "$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.",
    },
    Condition = condition,
};

// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
// Response is a message ID string.
Console.WriteLine("Successfully sent message: " + response);

REST

POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA
{
   "message":{
    "condition": "'dogs' in topics || 'cats' in topics",
    "notification" : {
      "body" : "This is a Firebase Cloud Messaging Topic Message!",
      "title" : "FCM Message",
    }
  }
}

cURL 指令:

curl -X POST -H "Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA" -H "Content-Type: application/json" -d '{
  "notification": {
    "title": "FCM Message",
    "body": "This is a Firebase Cloud Messaging Topic Message!",
  },
  "condition": "'dogs' in topics || 'cats' in topics"
}' https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

後續步驟