diff --git a/messaging/integration_test/AndroidManifest.xml b/messaging/integration_test/AndroidManifest.xml index e40aad746f..6d027bf7ec 100644 --- a/messaging/integration_test/AndroidManifest.xml +++ b/messaging/integration_test/AndroidManifest.xml @@ -67,6 +67,8 @@ + + diff --git a/messaging/integration_test/Info.plist b/messaging/integration_test/Info.plist index 478f919bb9..00badb5115 100644 --- a/messaging/integration_test/Info.plist +++ b/messaging/integration_test/Info.plist @@ -40,5 +40,7 @@ UILaunchStoryboardName LaunchScreen + FirebaseMessagingInstallationIdEnabled + diff --git a/messaging/integration_test/src/integration_test.cc b/messaging/integration_test/src/integration_test.cc index 453d5b5c7c..fc33313998 100644 --- a/messaging/integration_test/src/integration_test.cc +++ b/messaging/integration_test/src/integration_test.cc @@ -127,6 +127,8 @@ class FirebaseMessagingTest : public FirebaseTest { static firebase::messaging::PollableListener* shared_listener_; static std::string* shared_token_; static bool is_desktop_stub_; + // Used to track if the newer registration method is being used. + static bool is_new_registration_id_; static firebase::functions::Functions* functions_; }; @@ -138,6 +140,7 @@ firebase::App* FirebaseMessagingTest::shared_app_ = nullptr; firebase::messaging::PollableListener* FirebaseMessagingTest::shared_listener_ = nullptr; bool FirebaseMessagingTest::is_desktop_stub_; +bool FirebaseMessagingTest::is_new_registration_id_ = false; firebase::functions::Functions* FirebaseMessagingTest::functions_ = nullptr; void FirebaseMessagingTest::SetUpTestSuite() { @@ -287,7 +290,16 @@ bool FirebaseMessagingTest::WaitForToken(int timeout) { if (shared_listener_->PollRegistrationToken(&new_token)) { if (!new_token.empty()) { *shared_token_ = new_token; - LogInfo("Got token: %s", shared_token_->c_str()); + is_new_registration_id_ = false; + LogInfo("Got FCM Token: %s", shared_token_->c_str()); + return true; + } + } + if (shared_listener_->PollRegistration(&new_token)) { + if (!new_token.empty()) { + *shared_token_ = new_token; + is_new_registration_id_ = true; + LogInfo("Got FIS: %s", shared_token_->c_str()); return true; } } @@ -535,4 +547,40 @@ TEST_F(FirebaseMessagingTest, DeliverMetricsToBigQuery) { #endif } +TEST_F(FirebaseMessagingTest, TestRegistrationOnInitEnabled) { + bool initial_val = firebase::messaging::IsRegistrationOnInitEnabled(); + EXPECT_TRUE(initial_val); + + firebase::messaging::SetRegistrationOnInitEnabled(false); + EXPECT_FALSE(firebase::messaging::IsRegistrationOnInitEnabled()); + + firebase::messaging::SetRegistrationOnInitEnabled(true); + EXPECT_TRUE(firebase::messaging::IsRegistrationOnInitEnabled()); +} + +TEST_F(FirebaseMessagingTest, TestRegisterAndUnregister) { + TEST_REQUIRES_USER_INTERACTION_ON_IOS; + EXPECT_TRUE(RequestPermission()); + + // Note that these methods should only work if the newer registration + // method is enabled. Otherwise, it returns an error. + if (is_new_registration_id_) { + firebase::Future reg_future = firebase::messaging::Register(); + EXPECT_TRUE(WaitForCompletion(reg_future, "Register")); + EXPECT_EQ(reg_future.error(), 0); + + firebase::Future unreg_future = firebase::messaging::Unregister(); + EXPECT_TRUE(WaitForCompletion(unreg_future, "Unregister")); + EXPECT_EQ(unreg_future.error(), 0); + } else { + firebase::Future reg_future = firebase::messaging::Register(); + EXPECT_TRUE(WaitForCompletion(reg_future, "Register", + {firebase::messaging::kErrorUnknown})); + + firebase::Future unreg_future = firebase::messaging::Unregister(); + EXPECT_TRUE(WaitForCompletion(unreg_future, "Unregister", + {firebase::messaging::kErrorUnknown})); + } +} + } // namespace firebase_testapp_automated diff --git a/messaging/src/android/cpp/message_reader.cc b/messaging/src/android/cpp/message_reader.cc index 9224a983ce..45d0f88803 100644 --- a/messaging/src/android/cpp/message_reader.cc +++ b/messaging/src/android/cpp/message_reader.cc @@ -31,11 +31,17 @@ using com::google::firebase::messaging::cpp::SerializedEvent; using com::google::firebase::messaging::cpp::SerializedEventUnion; using com::google::firebase::messaging::cpp:: SerializedEventUnion_SerializedMessage; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedRegistrationReceived; using com::google::firebase::messaging::cpp:: SerializedEventUnion_SerializedTokenReceived; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedUnregistrationReceived; using com::google::firebase::messaging::cpp::SerializedMessage; using com::google::firebase::messaging::cpp::SerializedNotification; +using com::google::firebase::messaging::cpp::SerializedRegistrationReceived; using com::google::firebase::messaging::cpp::SerializedTokenReceived; +using com::google::firebase::messaging::cpp::SerializedUnregistrationReceived; using com::google::firebase::messaging::cpp::VerifySerializedEventBuffer; static const char kMessageReadError[] = @@ -96,6 +102,21 @@ void MessageReader::ReadFromBuffer(const std::string& buffer) const { ConsumeTokenReceived(serialized_token_received); break; } + case SerializedEventUnion_SerializedRegistrationReceived: { + const SerializedRegistrationReceived* serialized_registration_received = + static_cast( + serialized_event->event()); + ConsumeRegistrationReceived(serialized_registration_received); + break; + } + case SerializedEventUnion_SerializedUnregistrationReceived: { + const SerializedUnregistrationReceived* + serialized_unregistration_received = + static_cast( + serialized_event->event()); + ConsumeUnregistrationReceived(serialized_unregistration_received); + break; + } default: { // This should never happen. LogError(kMessageReadError, "Detected invalid FCM event type."); @@ -205,6 +226,32 @@ void MessageReader::ConsumeTokenReceived( token_callback_(token, token_callback_data_); } +// Convert the SerializedRegistrationReceived to an installation ID and calls +// the registered callback. +void MessageReader::ConsumeRegistrationReceived( + const SerializedRegistrationReceived* serialized_registration_received) + const { + if (!serialized_registration_received) return; + const char* installation_id = + SafeFlatbufferString(serialized_registration_received->installation_id()); + if (registration_callback_) { + registration_callback_(installation_id, registration_callback_data_); + } +} + +// Convert the SerializedUnregistrationReceived to an installation ID and calls +// the registered callback. +void MessageReader::ConsumeUnregistrationReceived( + const SerializedUnregistrationReceived* serialized_unregistration_received) + const { + if (!serialized_unregistration_received) return; + const char* installation_id = SafeFlatbufferString( + serialized_unregistration_received->installation_id()); + if (unregistration_callback_) { + unregistration_callback_(installation_id, unregistration_callback_data_); + } +} + } // namespace internal } // namespace messaging } // namespace firebase diff --git a/messaging/src/android/cpp/message_reader.h b/messaging/src/android/cpp/message_reader.h index 2eff3896cd..8d315971ad 100644 --- a/messaging/src/android/cpp/message_reader.h +++ b/messaging/src/android/cpp/message_reader.h @@ -35,13 +35,29 @@ class MessageReader { // Notify the currently set listener of a new token. typedef void (*TokenCallback)(const char* token, void* callback_data); + // Notify the currently set listener of a new registration installation ID. + typedef void (*RegistrationCallback)(const char* installationId, + void* callback_data); + + // Notify the currently set listener of an unregistration installation ID. + typedef void (*UnregistrationCallback)(const char* installationId, + void* callback_data); + // Construct a reader with message and token callbacks. MessageReader(MessageCallback message_callback, void* message_callback_data, - TokenCallback token_callback, void* token_callback_data) + TokenCallback token_callback, void* token_callback_data, + RegistrationCallback registration_callback = nullptr, + void* registration_callback_data = nullptr, + UnregistrationCallback unregistration_callback = nullptr, + void* unregistration_callback_data = nullptr) : message_callback_(message_callback), message_callback_data_(message_callback_data), token_callback_(token_callback), - token_callback_data_(token_callback_data) {} + token_callback_data_(token_callback_data), + registration_callback_(registration_callback), + registration_callback_data_(registration_callback_data), + unregistration_callback_(unregistration_callback), + unregistration_callback_data_(unregistration_callback_data) {} // Read messages or tokens from a buffer, calling message_callback and // token_callback (set on construction) on each message or token respectively. @@ -59,6 +75,20 @@ class MessageReader { const com::google::firebase::messaging::cpp::SerializedTokenReceived* serialized_token_received) const; + // Convert the SerializedRegistrationReceived to an installation ID and calls + // the registered registration_callback. + void ConsumeRegistrationReceived( + const com::google::firebase::messaging::cpp:: + SerializedRegistrationReceived* serialized_registration_received) + const; + + // Convert the SerializedUnregistrationReceived to an installation ID and + // calls the registered unregistration_callback. + void ConsumeUnregistrationReceived( + const com::google::firebase::messaging::cpp:: + SerializedUnregistrationReceived* serialized_unregistration_received) + const; + // Get the message callback function. MessageCallback message_callback() const { return message_callback_; } @@ -81,6 +111,10 @@ class MessageReader { void* message_callback_data_; TokenCallback token_callback_; void* token_callback_data_; + RegistrationCallback registration_callback_; + void* registration_callback_data_; + UnregistrationCallback unregistration_callback_; + void* unregistration_callback_data_; }; } // namespace internal diff --git a/messaging/src/android/cpp/messaging.cc b/messaging/src/android/cpp/messaging.cc index 9bed02f97e..74a8f2ea46 100644 --- a/messaging/src/android/cpp/messaging.cc +++ b/messaging/src/android/cpp/messaging.cc @@ -163,7 +163,9 @@ static bool g_intent_message_fired = false; X(SetDeliveryMetricsExportToBigQuery, \ "setDeliveryMetricsExportToBigQuery", "(Z)V"), \ X(GetToken, "getToken", "()Lcom/google/android/gms/tasks/Task;"), \ - X(DeleteToken, "deleteToken", "()Lcom/google/android/gms/tasks/Task;") + X(DeleteToken, "deleteToken", "()Lcom/google/android/gms/tasks/Task;"), \ + X(Register, "register", "()Lcom/google/android/gms/tasks/Task;"), \ + X(Unregister, "unregister", "()Lcom/google/android/gms/tasks/Task;") // clang-format on METHOD_LOOKUP_DECLARATION(firebase_messaging, FIREBASE_MESSAGING_METHODS); METHOD_LOOKUP_DEFINITION(firebase_messaging, @@ -289,6 +291,19 @@ static void ConsumeEvents() { } NotifyListenerOnTokenReceived(token); }, + nullptr, + [](const char* installationId, void* callback_data) { + if (g_registration_token_mutex) { + MutexLock lock(*g_registration_token_mutex); + g_registration_token_received = true; + HandlePendingSubscriptions(); + } + NotifyListenerOnRegistrationReceived(installationId); + }, + nullptr, + [](const char* installationId, void* callback_data) { + NotifyListenerOnUnregistrationReceived(installationId); + }, nullptr); reader.ReadFromBuffer(buffer); } @@ -975,6 +990,14 @@ void SetDeliveryMetricsExportToBigQuery(bool enabled) { } } +void SetRegistrationOnInitEnabled(bool enabled) { + SetTokenRegistrationOnInitEnabled(enabled); +} + +bool IsRegistrationOnInitEnabled() { + return IsTokenRegistrationOnInitEnabled(); +} + void SetTokenRegistrationOnInitEnabled(bool enabled) { // If this is called before JNI is initialized, we'll just cache the intent // and handle it on actual init. @@ -1018,6 +1041,72 @@ bool IsTokenRegistrationOnInitEnabled() { return static_cast(result); } +Future Register() { + FIREBASE_ASSERT_MESSAGE_RETURN(Future(), internal::IsInitialized(), + kMessagingNotInitializedError); + MutexLock lock(*g_registration_token_mutex); + ReferenceCountedFutureImpl* api = FutureData::Get()->api(); + SafeFutureHandle handle = api->SafeAlloc(kMessagingFnRegister); + + JNIEnv* env = g_app->GetJNIEnv(); + jobject task = env->CallObjectMethod( + g_firebase_messaging, + firebase_messaging::GetMethodId(firebase_messaging::kRegister)); + + std::string error = util::GetAndClearExceptionMessage(env); + if (error.empty()) { + util::RegisterCallbackOnTask(env, task, CompleteVoidCallback, + reinterpret_cast(handle.get().id()), + kApiIdentifier); + } else { + api->Complete(handle, -1, error.c_str()); + } + env->DeleteLocalRef(task); + util::CheckAndClearJniExceptions(env); + + return MakeFuture(api, handle); +} + +Future RegisterLastResult() { + FIREBASE_ASSERT_RETURN(Future(), internal::IsInitialized()); + ReferenceCountedFutureImpl* api = FutureData::Get()->api(); + return static_cast&>( + api->LastResult(kMessagingFnRegister)); +} + +Future Unregister() { + FIREBASE_ASSERT_MESSAGE_RETURN(Future(), internal::IsInitialized(), + kMessagingNotInitializedError); + MutexLock lock(*g_registration_token_mutex); + ReferenceCountedFutureImpl* api = FutureData::Get()->api(); + SafeFutureHandle handle = api->SafeAlloc(kMessagingFnUnregister); + + JNIEnv* env = g_app->GetJNIEnv(); + jobject task = env->CallObjectMethod( + g_firebase_messaging, + firebase_messaging::GetMethodId(firebase_messaging::kUnregister)); + + std::string error = util::GetAndClearExceptionMessage(env); + if (error.empty()) { + util::RegisterCallbackOnTask(env, task, CompleteVoidCallback, + reinterpret_cast(handle.get().id()), + kApiIdentifier); + } else { + api->Complete(handle, -1, error.c_str()); + } + env->DeleteLocalRef(task); + util::CheckAndClearJniExceptions(env); + + return MakeFuture(api, handle); +} + +Future UnregisterLastResult() { + FIREBASE_ASSERT_RETURN(Future(), internal::IsInitialized()); + ReferenceCountedFutureImpl* api = FutureData::Get()->api(); + return static_cast&>( + api->LastResult(kMessagingFnUnregister)); +} + Future GetToken() { FIREBASE_ASSERT_MESSAGE_RETURN(Future(), internal::IsInitialized(), diff --git a/messaging/src/android/java/com/google/firebase/messaging/cpp/ListenerService.java b/messaging/src/android/java/com/google/firebase/messaging/cpp/ListenerService.java index 243af4a3b0..ca5cdc0b7f 100644 --- a/messaging/src/android/java/com/google/firebase/messaging/cpp/ListenerService.java +++ b/messaging/src/android/java/com/google/firebase/messaging/cpp/ListenerService.java @@ -71,4 +71,16 @@ public void onNewToken(String token) { DebugLogging.log(TAG, String.format("onNewToken token=%s", token)); RegistrationIntentService.writeTokenToInternalStorage(this, token); } + + @Override + public void onRegistered(String installationId) { + DebugLogging.log(TAG, String.format("onRegistered installationId=%s", installationId)); + RegistrationIntentService.writeRegistrationToInternalStorage(this, installationId); + } + + @Override + public void onUnregistered(String installationId) { + DebugLogging.log(TAG, String.format("onUnregistered installationId=%s", installationId)); + RegistrationIntentService.writeUnregistrationToInternalStorage(this, installationId); + } } diff --git a/messaging/src/android/java/com/google/firebase/messaging/cpp/RegistrationIntentService.java b/messaging/src/android/java/com/google/firebase/messaging/cpp/RegistrationIntentService.java index d365a778e7..0a262fe7b5 100644 --- a/messaging/src/android/java/com/google/firebase/messaging/cpp/RegistrationIntentService.java +++ b/messaging/src/android/java/com/google/firebase/messaging/cpp/RegistrationIntentService.java @@ -61,7 +61,25 @@ public void onComplete(@NonNull Task task) { /** Write token to internal storage so it can be accessed by the C++ layer. */ public static void writeTokenToInternalStorage(Context context, String token) { - byte[] buffer = generateTokenByteBuffer(token); + writeBufferToInternalStorage(context, generateTokenByteBuffer(token)); + } + + /** + * Write registration installation ID to internal storage so it can be accessed by the C++ layer. + */ + public static void writeRegistrationToInternalStorage(Context context, String installationId) { + writeBufferToInternalStorage(context, generateRegistrationByteBuffer(installationId)); + } + + /** + * Write unregistration installation ID to internal storage so it can be accessed by the C++ + * layer. + */ + public static void writeUnregistrationToInternalStorage(Context context, String installationId) { + writeBufferToInternalStorage(context, generateUnregistrationByteBuffer(installationId)); + } + + private static void writeBufferToInternalStorage(Context context, byte[] buffer) { ByteBuffer sizeBuffer = ByteBuffer.allocate(4); // Write out the buffer length into the first four bytes. sizeBuffer.order(ByteOrder.LITTLE_ENDIAN); @@ -98,4 +116,40 @@ private static byte[] generateTokenByteBuffer(String token) { return builder.sizedByteArray(); } + + private static byte[] generateRegistrationByteBuffer(String installationId) { + FlatBufferBuilder builder = new FlatBufferBuilder(0); + + int idOffset = builder.createString(installationId != null ? installationId : ""); + + SerializedRegistrationReceived.startSerializedRegistrationReceived(builder); + SerializedRegistrationReceived.addInstallationId(builder, idOffset); + int registrationReceivedOffset = + SerializedRegistrationReceived.endSerializedRegistrationReceived(builder); + + SerializedEvent.startSerializedEvent(builder); + SerializedEvent.addEventType(builder, SerializedEventUnion.SerializedRegistrationReceived); + SerializedEvent.addEvent(builder, registrationReceivedOffset); + builder.finish(SerializedEvent.endSerializedEvent(builder)); + + return builder.sizedByteArray(); + } + + private static byte[] generateUnregistrationByteBuffer(String installationId) { + FlatBufferBuilder builder = new FlatBufferBuilder(0); + + int idOffset = builder.createString(installationId != null ? installationId : ""); + + SerializedUnregistrationReceived.startSerializedUnregistrationReceived(builder); + SerializedUnregistrationReceived.addInstallationId(builder, idOffset); + int unregistrationReceivedOffset = + SerializedUnregistrationReceived.endSerializedUnregistrationReceived(builder); + + SerializedEvent.startSerializedEvent(builder); + SerializedEvent.addEventType(builder, SerializedEventUnion.SerializedUnregistrationReceived); + SerializedEvent.addEvent(builder, unregistrationReceivedOffset); + builder.finish(SerializedEvent.endSerializedEvent(builder)); + + return builder.sizedByteArray(); + } } diff --git a/messaging/src/android/schemas/messaging.fbs b/messaging/src/android/schemas/messaging.fbs index a52e2c23d5..41357b4c8f 100644 --- a/messaging/src/android/schemas/messaging.fbs +++ b/messaging/src/android/schemas/messaging.fbs @@ -228,12 +228,26 @@ table SerializedTokenReceived { token:string; } +table SerializedRegistrationReceived { + installation_id:string; +} + +table SerializedUnregistrationReceived { + installation_id:string; +} + union SerializedEventUnion { // This event represents receiving a new message. SerializedMessage, // This event represents receiving a new registration token. SerializedTokenReceived, + + // This event represents receiving a new registration installation ID. + SerializedRegistrationReceived, + + // This event represents receiving an unregistration installation ID. + SerializedUnregistrationReceived } table SerializedEvent { diff --git a/messaging/src/common.cc b/messaging/src/common.cc index 8d805c1098..135af14df6 100644 --- a/messaging/src/common.cc +++ b/messaging/src/common.cc @@ -17,6 +17,7 @@ #include #include +#include #include "app/src/cleanup_notifier.h" #include "app/src/include/firebase/internal/common.h" @@ -51,6 +52,11 @@ static std::string* g_prev_token_received = nullptr; // Keep track if that token was receieved without a listener set. static bool g_has_pending_token = false; +// Keep track of the most recent registration installation ID received. +static std::string* g_prev_registration_received = nullptr; +// Keep track if that registration was received without a listener set. +static bool g_has_pending_registration = false; + namespace internal { const char kMessagingModuleName[] = "messaging"; @@ -92,6 +98,9 @@ Listener* SetListener(Listener* listener) { if (listener && !g_prev_token_received) { g_prev_token_received = new std::string; } + if (listener && !g_prev_registration_received) { + g_prev_registration_received = new std::string; + } g_listener = listener; // If we have a pending token, send it before notifying other systems about // the listener. @@ -99,12 +108,26 @@ Listener* SetListener(Listener* listener) { g_listener->OnTokenReceived(g_prev_token_received->c_str()); g_has_pending_token = false; } + // If we have a pending registration installation ID, send it as well. + if (g_listener && g_has_pending_registration && + g_prev_registration_received) { + g_listener->OnRegistrationReceived(g_prev_registration_received->c_str()); + g_has_pending_registration = false; + } NotifyListenerSet(listener); - if (!listener && g_prev_token_received) { - std::string* ptr = g_prev_token_received; - g_prev_token_received = nullptr; - delete ptr; - g_has_pending_token = false; + if (!listener) { + if (g_prev_token_received) { + std::string* ptr = g_prev_token_received; + g_prev_token_received = nullptr; + delete ptr; + g_has_pending_token = false; + } + if (g_prev_registration_received) { + std::string* ptr = g_prev_registration_received; + g_prev_registration_received = nullptr; + delete ptr; + g_has_pending_registration = false; + } } return previous_listener; } @@ -144,9 +167,49 @@ void NotifyListenerOnTokenReceived(const char* token) { } } +void NotifyListenerOnRegistrationReceived(const char* installationId) { + if (!installationId || installationId[0] == '\0') { + return; + } + MutexLock lock(g_listener_lock); + if (g_prev_registration_received && + *g_prev_registration_received == installationId) { + return; + } + + if (!g_prev_registration_received) + g_prev_registration_received = new std::string; + *g_prev_registration_received = installationId; + + if (g_listener) { + g_listener->OnRegistrationReceived(installationId); + } else { + g_has_pending_registration = true; + } +} + +void NotifyListenerOnUnregistrationReceived(const char* installationId) { + if (!installationId || installationId[0] == '\0') { + return; + } + MutexLock lock(g_listener_lock); + // If the unregistration is for a pending registration that we haven't sent to + // a listener yet, clear the pending state. + if (g_prev_registration_received && + *g_prev_registration_received == installationId) { + delete g_prev_registration_received; + g_prev_registration_received = nullptr; + g_has_pending_registration = false; + } + if (g_listener) { + g_listener->OnUnregistrationReceived(installationId); + } +} + class PollableListenerImpl { public: - PollableListenerImpl() : token_(), messages_() {} + PollableListenerImpl() + : token_(), registration_id_(), unregistration_id_(), messages_() {} void OnMessage(const Message& message) { // Since locking can potentially take a while, especially given that we @@ -173,6 +236,16 @@ class PollableListenerImpl { token_ = token; } + void OnRegistrationReceived(const char* installationId) { + MutexLock lock(mutex_); + registration_id_ = installationId; + } + + void OnUnregistrationReceived(const char* installationId) { + MutexLock lock(mutex_); + unregistration_id_ = installationId; + } + bool PollMessage(Message* message) { MutexLock lock(mutex_); if (messages_.empty()) { @@ -193,19 +266,40 @@ class PollableListenerImpl { return true; } + bool PollRegistration(std::string* installation_id) { + MutexLock lock(mutex_); + if (registration_id_.empty()) { + return false; + } + *installation_id = registration_id_; + registration_id_.clear(); + return true; + } + + bool PollUnregistration(std::string* installation_id) { + MutexLock lock(mutex_); + if (unregistration_id_.empty()) { + return false; + } + *installation_id = unregistration_id_; + unregistration_id_.clear(); + return true; + } + private: - // Mutex to prevent race conditions when + // Mutex to prevent race conditions Mutex mutex_; - // The newest registration token to be received. Once this valud has been - // polled, it is cleared until a new registration token is received. + // The newest registration token to be received. std::string token_; - // A queue of all enqueued messages. This is not expected to be large as in - // an app that has many incoming messages it would be unusual to receive lots - // of them in the same frame before they are consumed by PollMessage. - // TODO(amablue): Make this an actual contiguous ring buffer instead of an - // std::queue. + // The newest registration installation ID to be received. + std::string registration_id_; + + // The newest unregistration installation ID to be received. + std::string unregistration_id_; + + // A queue of all enqueued messages. std::queue messages_; }; @@ -221,6 +315,14 @@ void PollableListener::OnTokenReceived(const char* token) { impl_->OnTokenReceived(token); } +void PollableListener::OnRegistrationReceived(const char* installationId) { + impl_->OnRegistrationReceived(installationId); +} + +void PollableListener::OnUnregistrationReceived(const char* installationId) { + impl_->OnUnregistrationReceived(installationId); +} + bool PollableListener::PollMessage(Message* out_message) { return impl_->PollMessage(out_message); } @@ -231,6 +333,18 @@ std::string PollableListener::PollRegistrationToken(bool* got_token) { return out_token; } +std::string PollableListener::PollRegistration(bool* got_registration) { + std::string out_registration; + *got_registration = impl_->PollRegistration(&out_registration); + return out_registration; +} + +std::string PollableListener::PollUnregistration(bool* got_unregistration) { + std::string out_unregistration; + *got_unregistration = impl_->PollUnregistration(&out_unregistration); + return out_unregistration; +} + FutureData* FutureData::s_future_data_; // Create FutureData singleton. diff --git a/messaging/src/common.h b/messaging/src/common.h index 09453a433f..c3e6b5febe 100644 --- a/messaging/src/common.h +++ b/messaging/src/common.h @@ -40,6 +40,8 @@ enum MessagingFn { kMessagingFnUnsubscribe, kMessagingFnGetToken, kMessagingFnDeleteToken, + kMessagingFnRegister, + kMessagingFnUnregister, kMessagingFnCount }; @@ -81,6 +83,12 @@ void NotifyListenerOnMessage(const Message& message); // Notify the currently set listener of a new token. void NotifyListenerOnTokenReceived(const char* token); +// Notify the currently set listener of a new registration installation ID. +void NotifyListenerOnRegistrationReceived(const char* installationId); + +// Notify the currently set listener of an unregistration installation ID. +void NotifyListenerOnUnregistrationReceived(const char* installationId); + } // namespace messaging } // namespace firebase diff --git a/messaging/src/include/firebase/messaging.h b/messaging/src/include/firebase/messaging.h index 3d1535279a..aebeb09cee 100644 --- a/messaging/src/include/firebase/messaging.h +++ b/messaging/src/include/firebase/messaging.h @@ -376,7 +376,25 @@ class Listener { /// firebase::messaging::Initialize(...). /// /// @param[in] token The registration token. - virtual void OnTokenReceived(const char* token) = 0; + /// @deprecated Use OnRegistrationReceived(const char*) instead. + FIREBASE_DEPRECATED virtual void OnTokenReceived(const char* token); + + /// @brief Called on the client when the current app instance has been + /// successfully registered with FCM. + /// + /// This callback provides the unique Firebase Installation ID (FID), which + /// should be used to target this app instance for direct-send messaging. + /// + /// @param[in] installationId The Firebase Installation ID used for sending + /// messages to the current app instance. + virtual void OnRegistrationReceived(const char* installationId); + + /// @brief Called on the client when the current app instance has been + /// successfully unregistered from FCM via a call to Unregister(). + /// + /// @param[in] installationId The Firebase Installation ID of the current app + /// instance that was unregistered with FCM. + virtual void OnUnregistrationReceived(const char* installationId); }; /// @brief Initialize Firebase Cloud Messaging. @@ -417,10 +435,62 @@ InitResult Initialize(const App& app, Listener* listener, /// @note On Android, the services will not be shut down by this method. void Terminate(); +/// @brief Determines if automatic registration during initialization is +/// enabled. +/// +/// @return true if auto registration is enabled and false if disabled. +bool IsRegistrationOnInitEnabled(); + +/// @brief Enable or disable registration during initialization of Firebase +/// Cloud Messaging. +/// +/// The installation ID returned is what identifies the user to Firebase, so +/// disabling this avoids creating any new identity and automatically sending +/// it to Firebase, unless consent has been granted. +/// +/// If this setting is enabled, it triggers the registration refresh +/// immediately. This setting is persisted across app restarts and overrides the +/// setting "firebase_messaging_auto_init_enabled" specified in your Android +/// manifest (on Android) or Info.plist (on iOS and tvOS). +/// +///

By default, registration during initialization is enabled. +/// +/// The registration happens before you can programmatically disable it, so +/// if you need to change the default, (for example, because you want to prompt +/// the user before FCM generates/refreshes a registration on app +/// startup), add to your application’s manifest: +/// +/// @if NOT_DOXYGEN +/// +/// @else +/// @code +/// <meta-data android:name="firebase_messaging_auto_init_enabled" +/// android:value="false" /> +/// @endcode +/// @endif +/// +/// or on iOS or tvOS to your Info.plist: +/// +/// @if NOT_DOXYGEN +/// FirebaseMessagingAutoInitEnabled +/// +/// @else +/// @code +/// <key>FirebaseMessagingAutoInitEnabled</key> +/// <false/> +/// @endcode +/// @endif +/// +/// @param enable sets if a registration should be requested on +/// initialization. +void SetRegistrationOnInitEnabled(bool enable); + /// Determines if automatic token registration during initalization is enabled. /// /// @return true if auto token registration is enabled and false if disabled. -bool IsTokenRegistrationOnInitEnabled(); +/// @deprecated Use IsRegistrationOnInitEnabled() instead. +FIREBASE_DEPRECATED bool IsTokenRegistrationOnInitEnabled(); /// Enable or disable token registration during initialization of Firebase Cloud /// Messaging. @@ -466,7 +536,8 @@ bool IsTokenRegistrationOnInitEnabled(); /// /// @param enable sets if a registration token should be requested on /// initialization. -void SetTokenRegistrationOnInitEnabled(bool enable); +/// @deprecated Use SetRegistrationOnInitEnabled(bool) instead. +FIREBASE_DEPRECATED void SetTokenRegistrationOnInitEnabled(bool enable); #ifndef SWIG /// @brief Set the listener for events from the Firebase Cloud Messaging @@ -573,17 +644,52 @@ bool DeliveryMetricsExportToBigQueryEnabled(); /// delivery metrics to BigQuery. void SetDeliveryMetricsExportToBigQuery(bool enable); +/// @brief Registers the current Firebase app instance with the Firebase Cloud +/// Messaging (FCM) backend to receive messages. +/// +/// This creates a Firebase Installations ID (FID), if one does not exist, and +/// sends information about the application and the device where it's running to +/// the Firebase backend. +/// +/// Upon completion, `OnRegistrationReceived` will be triggered with the current +/// FID. Calling this function when already registered will still invoke the +/// `OnRegistrationReceived` callback with the existing FID. +/// +/// @return A future that completes when the registration is completed. +Future Register(); + +/// @brief Gets the result of the most recent call to Register(). +/// +/// @return Result of the most recent call to Register(). +Future RegisterLastResult(); + +/// @brief Unregisters the current app instance with FCM. +/// +/// Note that this does not delete the Firebase Installations ID that may have +/// been created during registration. See Installations.Delete() for +/// deleting that. +/// +/// @return A future that completes when the unregistration is completed. +Future Unregister(); + +/// @brief Gets the result of the most recent call to Unregister(). +/// +/// @return Result of the most recent call to Unregister(). +Future UnregisterLastResult(); + /// @brief This creates a Firebase Installations ID, if one does not exist, and /// sends information about the application and the device where it's running to /// the Firebase backend. /// /// @return A future with the token. -Future GetToken(); +/// @deprecated Use Register() instead. +FIREBASE_DEPRECATED Future GetToken(); /// @brief Gets the result of the most recent call to GetToken(); /// /// @return Result of the most recent call to GetToken(). -Future GetTokenLastResult(); +/// @deprecated Use RegisterLastResult() instead. +FIREBASE_DEPRECATED Future GetTokenLastResult(); /// @brief Deletes the default token for this Firebase project. /// @@ -592,40 +698,28 @@ Future GetTokenLastResult(); /// deleting that. /// /// @return A future that completes when the token is deleted. -Future DeleteToken(); +/// @deprecated Use Unregister() instead. +FIREBASE_DEPRECATED Future DeleteToken(); /// @brief Gets the result of the most recent call to DeleteToken(); /// /// @return Result of the most recent call to DeleteToken(). -Future DeleteTokenLastResult(); +/// @deprecated Use UnregisterLastResult() instead. +FIREBASE_DEPRECATED Future DeleteTokenLastResult(); class PollableListenerImpl; -/// @brief A listener that can be polled to consume pending `Message`s. +/// @brief A listener that can be polled to consume pending `Message`s and +/// registration events. /// /// This class is intended to be used with applications that have a main loop /// that frequently updates, such as in the case of a game that has a main /// loop that updates 30 to 60 times a second. Rather than respond to incoming -/// messages and tokens via the `OnMessage` virtual function, this class will -/// queue up the message internally in a thread-safe manner so that it can be -/// consumed with `PollMessage`. For example: -/// -/// ::firebase::messaging::PollableListener listener; -/// ::firebase::messaging::Initialize(app, &listener); -/// -/// while (true) { -/// std::string token; -/// if (listener.PollRegistrationToken(&token)) { -/// LogMessage("Received a registration token"); -/// } -/// -/// ::firebase::messaging::Message message; -/// while (listener.PollMessage(&message)) { -/// LogMessage("Received a new message"); -/// } -/// -/// // Remainder of application logic... -/// } +/// messages and registration events via the `OnMessage`, +/// `OnRegistrationReceived`, or `OnUnregistrationReceived` virtual functions, +/// this class will queue up events internally in a thread-safe manner so that +/// they can be consumed with `PollMessage`, `PollRegistration`, or +/// `PollUnregistration`. class PollableListener : public Listener { public: /// @brief The default constructor. @@ -634,13 +728,24 @@ class PollableListener : public Listener { /// @brief The required virtual destructor. virtual ~PollableListener(); - /// @brief An implementation of `OnMessage` which adds the incoming messages + /// @brief An implementation of `OnMessage` which adds incoming messages /// to a queue, which can be consumed by calling `PollMessage`. - virtual void OnMessage(const Message& message); + void OnMessage(const Message& message) override; /// @brief An implementation of `OnTokenReceived` which stores the incoming /// token so that it can be consumed by calling `PollRegistrationToken`. - virtual void OnTokenReceived(const char* token); + /// @deprecated Use OnRegistrationReceived(const char*) instead. + FIREBASE_DEPRECATED void OnTokenReceived(const char* token) override; + + /// @brief An implementation of `OnRegistrationReceived` which stores the + /// incoming installation ID so that it can be consumed by calling + /// `PollRegistration`. + void OnRegistrationReceived(const char* installationId) override; + + /// @brief An implementation of `OnUnregistrationReceived` which stores the + /// incoming installation ID so that it can be consumed by calling + /// `PollUnregistration`. + void OnUnregistrationReceived(const char* installationId) override; /// @brief Returns the first message queued up, if any. /// @@ -648,12 +753,7 @@ class PollableListener : public Listener { /// queue will be popped and used to populate the `message` argument and the /// function will return `true`. If there are no pending messages, `false` is /// returned. This function should be called in a loop until all messages have - /// been consumed, like so: - /// - /// ::firebase::messaging::Message message; - /// while (listener.PollMessage(&message)) { - /// LogMessage("Received a new message"); - /// } + /// been consumed. /// /// @param[out] message The `Message` struct to be populated. If there were no /// pending messages, `message` is not modified. @@ -662,23 +762,8 @@ class PollableListener : public Listener { bool PollMessage(Message* message); /// @brief Returns the registration key, if a new one has been received. - /// - /// When a new registration token is received, it is cached internally and can - /// be retrieved by calling `PollRegistrationToken`. The cached registration - /// token will be used to populate the `token` argument, then the cache will - /// be cleared and the function will return `true`. If there is no cached - /// registration token this function retuns `false`. - /// - /// std::string token; - /// if (listener.PollRegistrationToken(&token)) { - /// LogMessage("Received a registration token"); - /// } - /// - /// @param[out] token A string to be populated with the new token if one has - /// been received. If there were no new token, the string is left unmodified. - /// - /// @return Returns `true` if there was a new token, `false` otherwise. - bool PollRegistrationToken(std::string* token) { + /// @deprecated Use PollRegistration(std::string*) instead. + FIREBASE_DEPRECATED bool PollRegistrationToken(std::string* token) { bool got_token; std::string token_received = PollRegistrationToken(&got_token); if (got_token) { @@ -687,8 +772,56 @@ class PollableListener : public Listener { return got_token; } + /// @brief Returns the registration installation ID, if a new one has been + /// received. + /// + /// When a new registration installation ID is received, it is cached + /// internally and can be retrieved by calling `PollRegistration`. The cached + /// installation ID will be used to populate the `installation_id` argument, + /// then the cache will be cleared and the function will return `true`. If + /// there is no cached registration installation ID, this function returns + /// `false`. + /// + /// @param[out] installation_id A string to be populated with the new + /// installation ID. + /// @return Returns `true` if there was a new installation ID, `false` + /// otherwise. + bool PollRegistration(std::string* installation_id) { + bool got_registration; + std::string id_received = PollRegistration(&got_registration); + if (got_registration) { + *installation_id = id_received; + } + return got_registration; + } + + /// @brief Returns the unregistration installation ID, if an unregistration + /// has occurred. + /// + /// When an unregistration event is received, it is cached internally and can + /// be retrieved by calling `PollUnregistration`. The cached unregistration + /// installation ID will be used to populate the `installation_id` argument, + /// then the cache will be cleared and the function will return `true`. If + /// there is no cached unregistration installation ID, this function returns + /// `false`. + /// + /// @param[out] installation_id A string to be populated with the unregistered + /// installation ID. + /// @return Returns `true` if there was an unregistration event, `false` + /// otherwise. + bool PollUnregistration(std::string* installation_id) { + bool got_unregistration; + std::string id_received = PollUnregistration(&got_unregistration); + if (got_unregistration) { + *installation_id = id_received; + } + return got_unregistration; + } + private: std::string PollRegistrationToken(bool* got_token); + std::string PollRegistration(bool* got_registration); + std::string PollUnregistration(bool* got_unregistration); // The implementation of the `PollableListener`. PollableListenerImpl* impl_; diff --git a/messaging/src/ios/fake/FIRMessaging.h b/messaging/src/ios/fake/FIRMessaging.h index 255e2ef320..f3513490f1 100644 --- a/messaging/src/ios/fake/FIRMessaging.h +++ b/messaging/src/ios/fake/FIRMessaging.h @@ -252,6 +252,14 @@ NS_SWIFT_NAME(MessagingDelegate) didReceiveRegistrationToken:(nonnull NSString *)fcmToken NS_SWIFT_NAME(messaging(_:didReceiveRegistrationToken:)); +- (void)messaging:(nonnull FIRMessaging *)messaging + didReceiveRegistration:(nullable NSString *)installationId + NS_SWIFT_NAME(messaging(_:didReceiveRegistration:)); + +- (void)messaging:(nonnull FIRMessaging *)messaging + didUnregister:(nullable NSString *)installationId + NS_SWIFT_NAME(messaging(_:didUnregister:)); + @optional /// This method is called on iOS 10 devices to handle data messages received via FCM through its /// direct channel (not via APNS). For iOS 9 and below, the FCM data message is delivered via the @@ -531,4 +539,10 @@ NS_SWIFT_NAME(Messaging) - (void)deleteTokenWithCompletion:(void (^)(NSError *__nullable error))completion; +- (void)registerWithCompletion:(nonnull void (^)(NSError *__nullable error))completion + NS_SWIFT_NAME(register(completion:)); + +- (void)unregisterWithCompletion:(nonnull void (^)(NSError *__nullable error))completion + NS_SWIFT_NAME(unregister(completion:)); + @end diff --git a/messaging/src/ios/fake/FIRMessaging.mm b/messaging/src/ios/fake/FIRMessaging.mm index c4b4ba16f1..eca4684abc 100644 --- a/messaging/src/ios/fake/FIRMessaging.mm +++ b/messaging/src/ios/fake/FIRMessaging.mm @@ -127,6 +127,10 @@ - (void)tokenWithCompletion:(FIRMessagingFCMTokenFetchCompletion)completion - (void)deleteTokenWithCompletion:(FIRMessagingDeleteFCMTokenCompletion)completion NS_SWIFT_NAME(deleteFCMToken(completion:)) {} +- (void)registerWithCompletion:(void (^)(NSError *__nullable error))completion {} + +- (void)unregisterWithCompletion:(void (^)(NSError *__nullable error))completion {} + @end NS_ASSUME_NONNULL_END diff --git a/messaging/src/ios/messaging.mm b/messaging/src/ios/messaging.mm index c2fd55ebd4..83d02305f8 100644 --- a/messaging/src/ios/messaging.mm +++ b/messaging/src/ios/messaging.mm @@ -47,11 +47,14 @@ @interface FIRCppDelegate : NSObject // messaging:didReceiveRegistrationToken: is called. @property(nonatomic, readwrite, nullable) FIRMessaging *cachedMessaging; @property(nonatomic, readwrite, nullable) NSString *cachedFCMToken; +@property(nonatomic, readwrite, nullable) NSString *cachedRegistrationId; // If a listener is set, this will notify Messaging of the token as normal. // If no listener is set yet, the data will be cached until one is. // NOLINTNEXTLINE - (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(nullable NSString *)FCMToken; +- (void)messaging:(FIRMessaging *)messaging didReceiveRegistration:(nullable NSString *)installationId; +- (void)messaging:(FIRMessaging *)messaging didUnregister:(nullable NSString *)installationId; // Once the listener is registered, process cached token (if there is one). // This will call messaging:didReceiveRegistrationToken:, passing in the cached values: @@ -281,6 +284,7 @@ void Terminate() { g_delegate.isListenerSet = NO; g_delegate.cachedMessaging = nil; g_delegate.cachedFCMToken = nil; + g_delegate.cachedRegistrationId = nil; } SetListener(nullptr); g_app = nullptr; @@ -793,6 +797,12 @@ void SetDeliveryMetricsExportToBigQuery(bool /*enable*/) { LogWarning("SetDeliveryMetricsExportToBigQuery is not currently implemented on iOS"); } +bool IsRegistrationOnInitEnabled() { return IsTokenRegistrationOnInitEnabled(); } + +void SetRegistrationOnInitEnabled(bool enable) { + SetTokenRegistrationOnInitEnabled(enable); +} + bool IsTokenRegistrationOnInitEnabled() { return [FIRMessaging messaging].autoInitEnabled; } void SetTokenRegistrationOnInitEnabled(bool enable) { @@ -807,6 +817,50 @@ void SetTokenRegistrationOnInitEnabled(bool enable) { } } +Future Register() { + FIREBASE_ASSERT_RETURN(RegisterLastResult(), internal::IsInitialized()); + + ReferenceCountedFutureImpl* api = FutureData::Get()->api(); + SafeFutureHandle handle = api->SafeAlloc(kMessagingFnRegister); + + [[FIRMessaging messaging] registerWithCompletion:^(NSError *_Nullable error) { + api->Complete(handle, + error == nullptr ? kErrorNone : kErrorUnknown, + util::NSStringToString(error.localizedDescription).c_str()); + }]; + + return MakeFuture(api, handle); +} + +Future RegisterLastResult() { + FIREBASE_ASSERT_RETURN(Future(), internal::IsInitialized()); + ReferenceCountedFutureImpl* api = FutureData::Get()->api(); + return static_cast&>( + api->LastResult(kMessagingFnRegister)); +} + +Future Unregister() { + FIREBASE_ASSERT_RETURN(UnregisterLastResult(), internal::IsInitialized()); + + ReferenceCountedFutureImpl* api = FutureData::Get()->api(); + SafeFutureHandle handle = api->SafeAlloc(kMessagingFnUnregister); + + [[FIRMessaging messaging] unregisterWithCompletion:^(NSError *_Nullable error) { + api->Complete(handle, + error == nullptr ? kErrorNone : kErrorUnknown, + util::NSStringToString(error.localizedDescription).c_str()); + }]; + + return MakeFuture(api, handle); +} + +Future UnregisterLastResult() { + FIREBASE_ASSERT_RETURN(Future(), internal::IsInitialized()); + ReferenceCountedFutureImpl* api = FutureData::Get()->api(); + return static_cast&>( + api->LastResult(kMessagingFnUnregister)); +} + Future GetToken() { FIREBASE_ASSERT_RETURN(GetTokenLastResult(), internal::IsInitialized()); @@ -934,21 +988,49 @@ - (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSStrin } } +- (void)messaging:(FIRMessaging *)messaging didReceiveRegistration:(NSString *)installationId { + ::firebase::messaging::g_delegate_mutex.Acquire(); + if (_isListenerSet) { + ::firebase::messaging::g_delegate_mutex.Release(); + ::firebase::LogInfo("FCM: registration installation ID received."); + ::firebase::messaging::NotifyListenerOnRegistrationReceived(installationId.UTF8String); + } else { + _cachedMessaging = messaging; + _cachedRegistrationId = installationId; + ::firebase::messaging::g_delegate_mutex.Release(); + ::firebase::LogInfo( + "FCM: registration installation ID received, but no listener set yet - cached the ID."); + } +} + +- (void)messaging:(FIRMessaging *)messaging didUnregister:(NSString *)installationId { + ::firebase::LogInfo("FCM: unregistration received."); + ::firebase::messaging::NotifyListenerOnUnregistrationReceived(installationId.UTF8String); +} + - (void)processCachedRegistrationToken { FIRMessaging *msg; NSString *token; + NSString *regId; { firebase::MutexLock lock(::firebase::messaging::g_delegate_mutex); // TODO(butterfield): What if there's no cached message but there is a cached token? // The user may never get notified of the old token before a new one is registered. - if (!_isListenerSet || !_cachedMessaging || !_cachedFCMToken) { + if (!_isListenerSet) { return; } msg = _cachedMessaging; token = _cachedFCMToken; + regId = _cachedRegistrationId; _cachedFCMToken = nil; + _cachedRegistrationId = nil; _cachedMessaging = nil; } - [self messaging:msg didReceiveRegistrationToken:token]; + if (token) { + [self messaging:msg didReceiveRegistrationToken:token]; + } + if (regId) { + [self messaging:msg didReceiveRegistration:regId]; + } } @end diff --git a/messaging/src/listener.cc b/messaging/src/listener.cc index f0587b84eb..7eba18dda9 100644 --- a/messaging/src/listener.cc +++ b/messaging/src/listener.cc @@ -21,5 +21,11 @@ namespace messaging { // to prevent its vtable being emitted in each translation unit. Listener::~Listener() {} +void Listener::OnTokenReceived(const char* /*token*/) {} + +void Listener::OnRegistrationReceived(const char* /*installationId*/) {} + +void Listener::OnUnregistrationReceived(const char* /*installationId*/) {} + } // namespace messaging } // namespace firebase diff --git a/messaging/src/stub/messaging.cc b/messaging/src/stub/messaging.cc index 20b5a3a9a2..c07b69a4f8 100644 --- a/messaging/src/stub/messaging.cc +++ b/messaging/src/stub/messaging.cc @@ -190,6 +190,10 @@ Future RequestPermissionLastResult() { return GetLastResultFuture(kMessagingFnRequestPermission); } +bool IsRegistrationOnInitEnabled() { return true; } + +void SetRegistrationOnInitEnabled(bool /*enable*/) {} + bool IsTokenRegistrationOnInitEnabled() { return true; } void SetTokenRegistrationOnInitEnabled(bool /*enable*/) {} @@ -198,6 +202,24 @@ bool DeliveryMetricsExportToBigQueryEnabled() { return false; } void SetDeliveryMetricsExportToBigQuery(bool /*enable*/) {} +Future Register() { + NotifyListenerOnRegistrationReceived("StubRegistrationId"); + return CreateAndCompleteStubFuture(kMessagingFnRegister); +} + +Future RegisterLastResult() { + return GetLastResultFuture(kMessagingFnRegister); +} + +Future Unregister() { + NotifyListenerOnUnregistrationReceived("StubRegistrationId"); + return CreateAndCompleteStubFuture(kMessagingFnUnregister); +} + +Future UnregisterLastResult() { + return GetLastResultFuture(kMessagingFnUnregister); +} + Future GetToken() { ReferenceCountedFutureImpl* api = FutureData::Get()->api(); SafeFutureHandle handle = diff --git a/messaging/tests/CMakeLists.txt b/messaging/tests/CMakeLists.txt index afad30765d..dd3ed3a3b6 100644 --- a/messaging/tests/CMakeLists.txt +++ b/messaging/tests/CMakeLists.txt @@ -16,7 +16,7 @@ if(ANDROID OR IOS) set(messaging_test_util_common_SRCS messaging_test_util.h) set(messaging_test_util_android_SRCS - android/messaging_test_util.cc) + android/cpp/messaging_test_util.cc) set(messaging_test_util_ios_SRCS ios/messaging_test_util.mm) diff --git a/messaging/tests/android/cpp/message_reader_test.cc b/messaging/tests/android/cpp/message_reader_test.cc index aa3d1a2f17..57bbb9bf71 100644 --- a/messaging/tests/android/cpp/message_reader_test.cc +++ b/messaging/tests/android/cpp/message_reader_test.cc @@ -39,15 +39,23 @@ using com::google::firebase::messaging::cpp::CreateDataPairDirect; using com::google::firebase::messaging::cpp::CreateSerializedEvent; using com::google::firebase::messaging::cpp::CreateSerializedMessageDirect; using com::google::firebase::messaging::cpp::CreateSerializedNotificationDirect; +using com::google::firebase::messaging::cpp:: + CreateSerializedRegistrationReceived; using com::google::firebase::messaging::cpp::CreateSerializedTokenReceived; +using com::google::firebase::messaging::cpp:: + CreateSerializedUnregistrationReceived; using com::google::firebase::messaging::cpp::DataPair; using com::google::firebase::messaging::cpp::FinishSerializedEventBuffer; using com::google::firebase::messaging::cpp::SerializedEventUnion; using com::google::firebase::messaging::cpp::SerializedEventUnion_MAX; using com::google::firebase::messaging::cpp:: SerializedEventUnion_SerializedMessage; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedRegistrationReceived; using com::google::firebase::messaging::cpp:: SerializedEventUnion_SerializedTokenReceived; +using com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedUnregistrationReceived; using flatbuffers::FlatBufferBuilder; class MessageReaderTest : public ::testing::Test { @@ -57,6 +65,8 @@ class MessageReaderTest : public ::testing::Test { void TearDown() override { messages_received_.clear(); tokens_received_.clear(); + registrations_received_.clear(); + unregistrations_received_.clear(); } // Stores the message in this class. @@ -73,11 +83,31 @@ class MessageReaderTest : public ::testing::Test { test->tokens_received_.push_back(std::string(token)); } + // Stores the registration installation ID in this class. + static void RegistrationReceived(const char* installationId, + void* callback_data) { + MessageReaderTest* test = + reinterpret_cast(callback_data); + test->registrations_received_.push_back(std::string(installationId)); + } + + // Stores the unregistration installation ID in this class. + static void UnregistrationReceived(const char* installationId, + void* callback_data) { + MessageReaderTest* test = + reinterpret_cast(callback_data); + test->unregistrations_received_.push_back(std::string(installationId)); + } + protected: // Messages received by MessageReceived(). std::vector messages_received_; // Tokens received by TokenReceived(). std::vector tokens_received_; + // Registrations received by RegistrationReceived(). + std::vector registrations_received_; + // Unregistrations received by UnregistrationReceived(). + std::vector unregistrations_received_; }; TEST_F(MessageReaderTest, Construct) { @@ -279,10 +309,70 @@ TEST_F(MessageReaderTest, ReadFromBufferInvalidEventType) { AppendFlatBufferToString(&buffer, fbb); MessageReader reader(MessageReaderTest::MessageReceived, this, - MessageReaderTest::TokenReceived, this); + MessageReaderTest::TokenReceived, this, + MessageReaderTest::RegistrationReceived, this, + MessageReaderTest::UnregistrationReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); + EXPECT_EQ(0, registrations_received_.size()); + EXPECT_EQ(0, unregistrations_received_.size()); +} + +// Read registration events from a buffer. +TEST_F(MessageReaderTest, ReadFromBufferRegistrationReceived) { + std::string buffer; + std::string reg_ids[2] = {"install_id_1", "install_id_2"}; + for (size_t i = 0; i < 2; ++i) { + FlatBufferBuilder fbb; + FinishSerializedEventBuffer( + fbb, CreateSerializedEvent( + fbb, SerializedEventUnion_SerializedRegistrationReceived, + CreateSerializedRegistrationReceived( + fbb, fbb.CreateString(reg_ids[i])) + .Union())); + AppendFlatBufferToString(&buffer, fbb); + } + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this, + MessageReaderTest::RegistrationReceived, this, + MessageReaderTest::UnregistrationReceived, this); + reader.ReadFromBuffer(buffer); + EXPECT_EQ(0, messages_received_.size()); + EXPECT_EQ(0, tokens_received_.size()); + EXPECT_EQ(2, registrations_received_.size()); + EXPECT_EQ(reg_ids[0], registrations_received_[0]); + EXPECT_EQ(reg_ids[1], registrations_received_[1]); + EXPECT_EQ(0, unregistrations_received_.size()); +} + +// Read unregistration events from a buffer. +TEST_F(MessageReaderTest, ReadFromBufferUnregistrationReceived) { + std::string buffer; + std::string unreg_ids[2] = {"unreg_id_1", "unreg_id_2"}; + for (size_t i = 0; i < 2; ++i) { + FlatBufferBuilder fbb; + FinishSerializedEventBuffer( + fbb, CreateSerializedEvent( + fbb, SerializedEventUnion_SerializedUnregistrationReceived, + CreateSerializedUnregistrationReceived( + fbb, fbb.CreateString(unreg_ids[i])) + .Union())); + AppendFlatBufferToString(&buffer, fbb); + } + + MessageReader reader(MessageReaderTest::MessageReceived, this, + MessageReaderTest::TokenReceived, this, + MessageReaderTest::RegistrationReceived, this, + MessageReaderTest::UnregistrationReceived, this); reader.ReadFromBuffer(buffer); EXPECT_EQ(0, messages_received_.size()); EXPECT_EQ(0, tokens_received_.size()); + EXPECT_EQ(0, registrations_received_.size()); + EXPECT_EQ(2, unregistrations_received_.size()); + EXPECT_EQ(unreg_ids[0], unregistrations_received_[0]); + EXPECT_EQ(unreg_ids[1], unregistrations_received_[1]); } } // namespace internal diff --git a/messaging/tests/android/cpp/messaging_test_util.cc b/messaging/tests/android/cpp/messaging_test_util.cc index 1e71f1ca89..e6e8fd3e4b 100644 --- a/messaging/tests/android/cpp/messaging_test_util.cc +++ b/messaging/tests/android/cpp/messaging_test_util.cc @@ -29,13 +29,21 @@ using ::com::google::firebase::messaging::cpp::CreateDataPair; using ::com::google::firebase::messaging::cpp::CreateSerializedEvent; +using ::com::google::firebase::messaging::cpp:: + CreateSerializedRegistrationReceived; using ::com::google::firebase::messaging::cpp::CreateSerializedTokenReceived; +using ::com::google::firebase::messaging::cpp:: + CreateSerializedUnregistrationReceived; using ::com::google::firebase::messaging::cpp::DataPair; using ::com::google::firebase::messaging::cpp::SerializedEventUnion; using ::com::google::firebase::messaging::cpp:: SerializedEventUnion_SerializedMessage; +using ::com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedRegistrationReceived; using ::com::google::firebase::messaging::cpp:: SerializedEventUnion_SerializedTokenReceived; +using ::com::google::firebase::messaging::cpp:: + SerializedEventUnion_SerializedUnregistrationReceived; using ::com::google::firebase::messaging::cpp::SerializedMessageBuilder; using ::com::google::firebase::messaging::cpp::SerializedNotification; using ::com::google::firebase::messaging::cpp::SerializedNotificationBuilder; @@ -96,6 +104,28 @@ void OnTokenReceived(const char* tokenstr) { WriteBuffer(builder); } +void OnRegistrationReceived(const char* registration_id) { + flatbuffers::FlatBufferBuilder builder; + auto reg = builder.CreateString(registration_id ? registration_id : ""); + auto regreceived = CreateSerializedRegistrationReceived(builder, reg); + auto event = CreateSerializedEvent( + builder, SerializedEventUnion_SerializedRegistrationReceived, + regreceived.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + +void OnUnregistrationReceived(const char* registration_id) { + flatbuffers::FlatBufferBuilder builder; + auto unreg = builder.CreateString(registration_id ? registration_id : ""); + auto unregreceived = CreateSerializedUnregistrationReceived(builder, unreg); + auto event = CreateSerializedEvent( + builder, SerializedEventUnion_SerializedUnregistrationReceived, + unregreceived.Union()); + builder.Finish(event); + WriteBuffer(builder); +} + void OnDeletedMessages() { ::flatbuffers::FlatBufferBuilder builder; auto from = builder.CreateString(""); diff --git a/messaging/tests/ios/messaging_test_util.mm b/messaging/tests/ios/messaging_test_util.mm index 9e4004b5d8..1e67a4cc98 100644 --- a/messaging/tests/ios/messaging_test_util.mm +++ b/messaging/tests/ios/messaging_test_util.mm @@ -54,6 +54,16 @@ void OnTokenReceived(const char *tokenstr) { didReceiveRegistrationToken:@(tokenstr)]; } +void OnRegistrationReceived(const char *registration_id) { + [[FIRMessaging messaging].delegate messaging:[FIRMessaging messaging] + didReceiveRegistration:registration_id ? @(registration_id) : nil]; +} + +void OnUnregistrationReceived(const char *registration_id) { + [[FIRMessaging messaging].delegate messaging:[FIRMessaging messaging] + didUnregister:registration_id ? @(registration_id) : nil]; +} + void SleepMessagingTest(double seconds) { // We want the main loop to process messages while we wait. [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:seconds]]; diff --git a/messaging/tests/messaging_test.cc b/messaging/tests/messaging_test.cc index 7544750511..0e2117a2ee 100644 --- a/messaging/tests/messaging_test.cc +++ b/messaging/tests/messaging_test.cc @@ -48,20 +48,38 @@ class MessagingTestListener : public Listener { public: void OnMessage(const Message& message) override; void OnTokenReceived(const char* token) override; + void OnRegistrationReceived(const char* registration_id) override; + void OnUnregistrationReceived(const char* registration_id) override; const Message& GetMessage() const { return message_; } const std::string& GetToken() const { return token_; } + const std::string& GetRegistrationId() const { return registration_id_; } + + const std::string& GetUnregistrationId() const { return unregistration_id_; } + int GetOnTokenReceivedCount() const { return on_token_received_count_; } int GetOnMessageReceivedCount() const { return on_message_received_count_; } + int GetOnRegistrationReceivedCount() const { + return on_registration_received_count_; + } + + int GetOnUnregistrationReceivedCount() const { + return on_unregistration_received_count_; + } + private: Message message_; std::string token_; + std::string registration_id_; + std::string unregistration_id_; int on_token_received_count_ = 0; int on_message_received_count_ = 0; + int on_registration_received_count_ = 0; + int on_unregistration_received_count_ = 0; }; class MessagingTest : public ::testing::Test { @@ -113,6 +131,18 @@ void MessagingTestListener::OnTokenReceived(const char* token) { on_token_received_count_++; } +void MessagingTestListener::OnRegistrationReceived( + const char* registration_id) { + registration_id_ = registration_id; + on_registration_received_count_++; +} + +void MessagingTestListener::OnUnregistrationReceived( + const char* registration_id) { + unregistration_id_ = registration_id; + on_unregistration_received_count_++; +} + // Tests only run on Android for now. TEST_F(MessagingTest, TestInitializeTwice) { MessagingTestListener listener; @@ -369,6 +399,83 @@ TEST_F(MessagingTest, TestDeleteToken) { AddExpectationAndroid("FirebaseMessaging.deleteToken", {}); } +TEST_F(MessagingTest, TestRegister) { + Future result = Register(); + SleepMessagingTest(1); + EXPECT_EQ(result.status(), kFutureStatusComplete); + EXPECT_EQ(result.error(), 0); + AddExpectationAndroid("FirebaseMessaging.register", {}); +} + +TEST_F(MessagingTest, TestUnregister) { + Future result = Unregister(); + SleepMessagingTest(1); + EXPECT_EQ(result.status(), kFutureStatusComplete); + EXPECT_EQ(result.error(), 0); + AddExpectationAndroid("FirebaseMessaging.unregister", {}); +} + +TEST_F(MessagingTest, TestRegistrationOnInitEnabled) { + EXPECT_TRUE(IsRegistrationOnInitEnabled()); + SetRegistrationOnInitEnabled(false); + EXPECT_FALSE(IsRegistrationOnInitEnabled()); + SetRegistrationOnInitEnabled(true); + EXPECT_TRUE(IsRegistrationOnInitEnabled()); +} + +TEST_F(MessagingTest, TestRegistrationReceived) { + OnRegistrationReceived("my_installation_id"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetRegistrationId(), StrEq("my_installation_id")); + EXPECT_EQ(listener_.GetOnRegistrationReceivedCount(), 1); +} + +TEST_F(MessagingTest, TestNullRegistrationReceived) { + OnRegistrationReceived(nullptr); + OnRegistrationReceived(""); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnRegistrationReceivedCount(), 0); +} + +TEST_F(MessagingTest, TestUnregistrationReceived) { + OnUnregistrationReceived("my_installation_id"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetUnregistrationId(), StrEq("my_installation_id")); + EXPECT_EQ(listener_.GetOnUnregistrationReceivedCount(), 1); +} + +TEST_F(MessagingTest, TestNullUnregistrationReceived) { + OnUnregistrationReceived(nullptr); + OnUnregistrationReceived(""); + SleepMessagingTest(1); + EXPECT_EQ(listener_.GetOnUnregistrationReceivedCount(), 0); +} + +TEST_F(MessagingTest, TestRegistrationReceivedBeforeInitialize) { + Terminate(); + OnRegistrationReceived("my_installation_id"); + EXPECT_EQ(Initialize(*firebase_app_, &listener_), kInitResultSuccess); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetRegistrationId(), StrEq("my_installation_id")); +} + +TEST_F(MessagingTest, TestTwoRegistrationsReceivedBeforeInitialize) { + Terminate(); + OnRegistrationReceived("my_installation_id1"); + OnRegistrationReceived("my_installation_id2"); + EXPECT_EQ(Initialize(*firebase_app_, &listener_), kInitResultSuccess); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetRegistrationId(), StrEq("my_installation_id2")); +} + +TEST_F(MessagingTest, TestTwoRegistrationsReceivedAfterInitialize) { + OnRegistrationReceived("my_installation_id1"); + OnRegistrationReceived("my_installation_id2"); + SleepMessagingTest(1); + EXPECT_THAT(listener_.GetRegistrationId(), StrEq("my_installation_id2")); + EXPECT_EQ(listener_.GetOnRegistrationReceivedCount(), 2); +} + #endif // defined(FIREBASE_ANDROID_FOR_DESKTOP) } // namespace messaging diff --git a/messaging/tests/messaging_test_util.h b/messaging/tests/messaging_test_util.h index 78ee6f6fee..39f0074861 100644 --- a/messaging/tests/messaging_test_util.h +++ b/messaging/tests/messaging_test_util.h @@ -37,6 +37,12 @@ void TerminateMessagingTest(); // Simulate a token received/refresh event from the OS-level implementation. void OnTokenReceived(const char* tokenstr); +// Simulate a registration event from the OS-level implementation. +void OnRegistrationReceived(const char* registration_id); + +// Simulate an unregistration event from the OS-level implementation. +void OnUnregistrationReceived(const char* registration_id); + void OnDeletedMessages(); void OnMessageReceived(const Message& message); diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 22bbebc02e..b41d9c6efc 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -616,6 +616,8 @@ code. ### Upcoming - Changes - Realtime Database (Desktop): Fixed an intermittent use-after-free crash (`ACCESS_VIOLATION`) when detaching a listener while a WebSocket listen response is pending. + - Messaging: Added new Registration methods using Installation Ids. + Deprecated old Token based methods. ### 13.10.0 - Changes