傳送訊息至多部裝置

如要將訊息指定至多部裝置,請使用主題訊息功能。您可以透過這項功能,將訊息傳送給已選擇加入特定主題的多部裝置。

本教學課程著重於使用 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 專案

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

透過 Firebase 註冊應用程式

如要在 Android 應用程式中使用 Firebase,您必須使用 Firebase 專案註冊應用程式。註冊應用程式通常稱為「新增」應用程式至專案中。

  1. 前往 Firebase 主控台

  2. 在專案總覽頁面中間,按一下「Android」圖示 () 或「Add app」,啟動設定工作流程。

  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 (app-level)」根目錄。

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

    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.2" 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.2' 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.1.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.1")
    }
    
    在尋找 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 回呼。

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

大多數訊息類型都會提供 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 會顯示自訂預設圖示

  • 通知編輯器傳送的所有通知訊息。
  • 未明確設定通知酬載中圖示的任何通知訊息。

Android 會使用自訂預設顏色

  • 通知編輯器傳送的所有通知訊息。
  • 未明確設定通知酬載中顏色的任何通知訊息。

如果沒有設定自訂預設圖示,也未在通知酬載中設定圖示,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

後續步驟