diff --git a/OneSignalSDK/coverage/jacoco.gradle b/OneSignalSDK/coverage/jacoco.gradle index 45ee9f09f2..c7b551de28 100644 --- a/OneSignalSDK/coverage/jacoco.gradle +++ b/OneSignalSDK/coverage/jacoco.gradle @@ -16,16 +16,6 @@ subprojects { testCoverageEnabled = true } } - - testOptions { - // Robolectric loads classes through its own classloader, which leaves them without - // a code source location. JaCoCo skips those by default, so Robolectric tests - // would contribute no coverage at all. - unitTests.all { test -> - test.jacoco.includeNoLocationClasses = true - test.jacoco.excludes = ['jdk.internal.*'] - } - } } def coverageExcludes = [ diff --git a/OneSignalSDK/onesignal/notifications/build.gradle b/OneSignalSDK/onesignal/notifications/build.gradle index 411010cce4..be636bb3bf 100644 --- a/OneSignalSDK/onesignal/notifications/build.gradle +++ b/OneSignalSDK/onesignal/notifications/build.gradle @@ -76,8 +76,6 @@ dependencies { // NOTE: firebase-messaging:24.0.0 requires customer's project to use // compileSdkVersion 34 or higher. - // `require` is intentionally non-strict: this module compiles against the preferred 24.0.0, - // while an app can select a newer version through Gradle conflict resolution. api('com.google.firebase:firebase-messaging') { version { require '[23.0.8, 24.0.99]' diff --git a/OneSignalSDK/onesignal/notifications/consumer-rules.pro b/OneSignalSDK/onesignal/notifications/consumer-rules.pro index f8942fd5c9..b7a21ca10b 100644 --- a/OneSignalSDK/onesignal/notifications/consumer-rules.pro +++ b/OneSignalSDK/onesignal/notifications/consumer-rules.pro @@ -25,16 +25,6 @@ -dontwarn com.google.firebase.** -dontwarn com.google.android.gms.** -# PushRegistratorFCM looks up FirebaseMessaging.register() by name, because this module compiles -# against firebase-messaging 24.x where the method does not exist yet. Nothing references it -# symbolically, it carries no @Keep, and firebase-messaging ships no consumer rules, so R8 full mode -# (AGP 8+) renames it along with the rest of the class, causing: -# java.lang.NoSuchMethodException: com.google.firebase.messaging.FirebaseMessaging.register [] -# keepclassmembers rather than keep, so Huawei apps that exclude firebase-messaging are unaffected. --keepclassmembers class com.google.firebase.messaging.FirebaseMessaging { - public *** register(); -} - # ADM handlers are instantiated by name from the app manifest AND their on* lifecycle callbacks # (onMessage/onRegistered/onRegistrationError/onUnregistered) are invoked by the ADM framework, not # the SDK, so keep both constructors and those methods. (Amazon-device-only path, untestable in CI.) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt index 41e4d9ee11..dd0af12514 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCM.kt @@ -1,17 +1,13 @@ package com.onesignal.notifications.internal.registration.impl import android.util.Base64 -import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions -import com.google.firebase.installations.FirebaseInstallations import com.google.firebase.messaging.FirebaseMessaging -import com.onesignal.common.AndroidUtils import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.config.ConfigModelStore import com.onesignal.core.internal.device.IDeviceService -import java.lang.reflect.InvocationTargetException import java.util.concurrent.ExecutionException internal class PushRegistratorFCM( @@ -23,8 +19,6 @@ internal class PushRegistratorFCM( companion object { private const val FCM_APP_NAME = "ONESIGNAL_SDK_FCM_APP_NAME" - private const val INSTALLATION_ID_ENABLED_METADATA = "firebase_messaging_installation_id_enabled" - // project_info.project_id private const val FCM_DEFAULT_PROJECT_ID = "onesignal-shared-public" @@ -55,45 +49,22 @@ internal class PushRegistratorFCM( @Throws(ExecutionException::class, InterruptedException::class) override suspend fun getToken(senderId: String): String { initFirebaseApp(senderId) - return getTokenWithClassFirebaseMessaging(senderId) + return getTokenWithClassFirebaseMessaging() } @Throws(ExecutionException::class, InterruptedException::class) - private fun getTokenWithClassFirebaseMessaging(senderId: String): String { + private fun getTokenWithClassFirebaseMessaging(): String { // We use firebaseApp.get(FirebaseMessaging.class) instead of FirebaseMessaging.getInstance() // as the latter uses the default Firebase app. We need to use a custom Firebase app as // the senderId is provided at runtime. val fcmInstance = firebaseApp!!.get(FirebaseMessaging::class.java) - return FCMTokenProvider.getToken( - senderId, - ::installationIdEnabled, - { fcmInstance.token }, - ::defaultAppRegistration, - ) - } - - // Manifest merging means the flag can arrive from a dependency instead of the app's own - // manifest, so report what the app actually resolved to. Read as a raw value because a - // string "true" reads as false when asked for a boolean. - private fun installationIdEnabled(): String { - val metaData = AndroidUtils.getManifestMetaBundle(_applicationService.appContext) - return metaData?.get(INSTALLATION_ID_ENABLED_METADATA)?.toString() ?: "not set" - } - - // Installation ID registration is rejected unless the sender id, app id, and api key all belong - // to the same Firebase project. Our own FirebaseApp pairs the app's sender id with OneSignal's - // shared project credentials, so only the host app's default FirebaseApp can be used for it. - private fun defaultAppRegistration(): FCMTokenProvider.InstallationIdRegistration? { - val defaultApp = - FirebaseApp - .getApps(_applicationService.appContext) - .firstOrNull { it.name == FirebaseApp.DEFAULT_APP_NAME } ?: return null - - return FCMTokenProvider.InstallationIdRegistration( - senderId = defaultApp.options.gcmSenderId, - register = { FCMTokenProvider.invokeRegister(defaultApp.get(FirebaseMessaging::class.java)) }, - installationId = { FirebaseInstallations.getInstance(defaultApp).id }, - ) + // FirebaseMessaging.getToken API was introduced in firebase-messaging:21.0.0 + val tokenTask = fcmInstance.token + try { + return Tasks.await(tokenTask) + } catch (e: ExecutionException) { + throw tokenTask.exception ?: e + } } private fun initFirebaseApp(senderId: String) { @@ -109,105 +80,3 @@ internal class PushRegistratorFCM( firebaseApp = FirebaseApp.initializeApp(_applicationService.appContext, firebaseOptions, FCM_APP_NAME) } } - -internal object FCMTokenProvider { - /** - * The Firebase Installation ID registration that replaces the legacy token API, along with the - * sender id of the Firebase project it would register against. - */ - class InstallationIdRegistration( - val senderId: String?, - val register: () -> Task<*>, - val installationId: () -> Task, - ) - - /** - * Retrieves an FCM token for [senderId], falling back to Firebase Installation ID registration - * when the host app has opted into it. Opting in disables the legacy token API for the whole - * app, not just the FirebaseApp that opted in. - */ - fun getToken( - senderId: String, - installationIdEnabled: () -> String, - legacyToken: () -> Task, - installationIdRegistration: () -> InstallationIdRegistration?, - ): String { - return try { - await(legacyToken()) - } catch (e: IllegalStateException) { - if (!isLegacyTokenApiDisabled(e)) throw e - - registerInstallationId(senderId, installationIdEnabled(), installationIdRegistration()) - } - } - - private fun registerInstallationId( - senderId: String, - installationIdEnabled: String, - registration: InstallationIdRegistration?, - ): String { - val optedIn = "firebase_messaging_installation_id_enabled=$installationIdEnabled" - - if (registration == null) { - throw IllegalStateException( - "Firebase Installation ID registration is enabled ($optedIn) but this app has no " + - "default FirebaseApp to register with. Add your Firebase configuration " + - "(google-services.json), or set firebase_messaging_installation_id_enabled to " + - "false in your manifest to keep using the legacy FCM token API.", - ) - } - - if (registration.senderId != senderId) { - throw IllegalStateException( - "Firebase Installation ID registration is enabled ($optedIn) but the default " + - "FirebaseApp uses sender id ${registration.senderId}, while OneSignal is " + - "configured with sender id $senderId. Point both at the same Firebase project, " + - "or set firebase_messaging_installation_id_enabled to false in your manifest " + - "to keep using the legacy FCM token API.", - ) - } - - await(registration.register()) - return await(registration.installationId()) - } - - /** - * Calls register() reflectively. FirebaseMessaging.register was added in firebase-messaging - * 25.1.0. This module compiles against the preferred 24.0.0, but the non-strict Gradle - * constraint lets apps select newer versions through conflict resolution. - */ - fun invokeRegister(target: Any): Task<*> { - val register = - try { - target.javaClass.getMethod("register") - } catch (e: NoSuchMethodException) { - throw IllegalStateException( - "Firebase Installation ID registration is enabled but " + - "FirebaseMessaging.register() was not found. It requires firebase-messaging " + - "25.1.0 or newer, and has to survive minification, so check that OneSignal's " + - "consumer ProGuard rules are applied.", - e, - ) - } - - // invoke() wraps anything register() throws synchronously, which would hide the cause. - return try { - register.invoke(target) as Task<*> - } catch (e: InvocationTargetException) { - throw e.targetException ?: e - } - } - - private fun isLegacyTokenApiDisabled(exception: IllegalStateException): Boolean { - val message = exception.message ?: return false - return message.contains("API disabled") && message.contains("register()") - } - - private fun await(task: Task): T { - try { - return Tasks.await(task) - } catch (e: ExecutionException) { - throw task.exception ?: e - } - } -} diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/FCMTokenProviderTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/FCMTokenProviderTests.kt deleted file mode 100644 index 70ede6a244..0000000000 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/FCMTokenProviderTests.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.onesignal.notifications.internal.registration.impl - -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.google.android.gms.tasks.Task -import com.google.android.gms.tasks.TaskCompletionSource -import com.google.android.gms.tasks.Tasks -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import io.kotest.matchers.types.shouldBeInstanceOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -private const val SENDER_ID = "388536902528" - -private fun completedTask(): Task = TaskCompletionSource().apply { setResult(null) }.task - -private fun failedTask(exception: Exception): Task = - TaskCompletionSource().apply { setException(exception) }.task - -private fun registration( - senderId: String? = SENDER_ID, - register: () -> Task<*> = { completedTask() }, - installationId: () -> Task = { Tasks.forResult("installation-id") }, -) = FCMTokenProvider.InstallationIdRegistration(senderId, register, installationId) - -private class Registrar(private val exception: Exception? = null) { - fun register(): Task { - exception?.let { throw it } - return completedTask() - } -} - -private class WithoutRegister - -@RobolectricTest -class FCMTokenProviderTests : FunSpec({ - val disabledLegacyApi = IllegalStateException("API disabled. Please use {@link #register()} instead.") - - test("returns the legacy FCM token when the API is enabled") { - val token = - withContext(Dispatchers.IO) { - FCMTokenProvider.getToken(SENDER_ID, { "true" }, { Tasks.forResult("fcm-token") }) { - throw AssertionError("should not fall back to installation id registration") - } - } - - token shouldBe "fcm-token" - } - - test("registers the installation id when the legacy API is disabled") { - var registered = false - val token = - withContext(Dispatchers.IO) { - FCMTokenProvider.getToken(SENDER_ID, { "true" }, { Tasks.forException(disabledLegacyApi) }) { - registration(register = { - registered = true - completedTask() - }) - } - } - - token shouldBe "installation-id" - registered shouldBe true - } - - test("does not register for unrelated IllegalStateExceptions") { - val unrelated = IllegalStateException("Firebase is not initialized") - - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { - FCMTokenProvider.getToken(SENDER_ID, { "true" }, { Tasks.forException(unrelated) }) { - throw AssertionError("should not fall back to installation id registration") - } - } - } - - thrown shouldBe unrelated - } - - test("explains the problem when there is no default FirebaseApp to register with") { - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { - FCMTokenProvider.getToken(SENDER_ID, { "true" }, { Tasks.forException(disabledLegacyApi) }) { null } - } - } - - thrown.message!! shouldContain "no default FirebaseApp" - } - - test("reports the manifest flag value it resolved, including when the app never set it") { - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { - FCMTokenProvider.getToken(SENDER_ID, { "not set" }, { Tasks.forException(disabledLegacyApi) }) { null } - } - } - - thrown.message!! shouldContain "firebase_messaging_installation_id_enabled=not set" - } - - test("explains the problem when the default FirebaseApp uses a different sender id") { - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { - FCMTokenProvider.getToken(SENDER_ID, { "true" }, { Tasks.forException(disabledLegacyApi) }) { - registration( - senderId = "999999999999", - register = { throw AssertionError("should not register on a sender id mismatch") }, - ) - } - } - } - - thrown.message!! shouldContain "sender id 999999999999" - } - - test("propagates registration failures") { - val registrationFailure = IllegalStateException("Registration failed") - - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { - FCMTokenProvider.getToken(SENDER_ID, { "true" }, { Tasks.forException(disabledLegacyApi) }) { - registration( - register = { failedTask(registrationFailure) }, - installationId = { throw AssertionError("should not run after registration fails") }, - ) - } - } - } - - thrown shouldBe registrationFailure - } - - test("invokes register reflectively") { - FCMTokenProvider.invokeRegister(Registrar()).isSuccessful shouldBe true - } - - test("unwraps what register throws instead of surfacing the reflection wrapper") { - val cause = IllegalStateException("register blew up") - - val thrown = shouldThrow { FCMTokenProvider.invokeRegister(Registrar(cause)) } - - thrown shouldBe cause - } - - test("explains the problem when register is missing") { - val thrown = shouldThrow { FCMTokenProvider.invokeRegister(WithoutRegister()) } - - thrown.message!! shouldContain "25.1.0 or newer" - thrown.cause.shouldBeInstanceOf() - } -}) diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt deleted file mode 100644 index 2bd21c9bfa..0000000000 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorFCMTests.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.onesignal.notifications.internal.registration.impl - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.google.android.gms.tasks.Task -import com.google.android.gms.tasks.Tasks -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.messaging.FirebaseMessaging -import com.onesignal.core.internal.application.IApplicationService -import com.onesignal.mocks.MockHelper -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -private const val SENDER_ID = "388536902528" - -private fun defaultApp(senderId: String): FirebaseApp { - val options = mockk() - every { options.gcmSenderId } returns senderId - - val app = mockk() - every { app.name } returns FirebaseApp.DEFAULT_APP_NAME - every { app.options } returns options - - return app -} - -private fun registrator( - legacyToken: Task, - installedApps: List = emptyList(), -): PushRegistratorFCM { - val messaging = mockk() - every { messaging.token } returns legacyToken - - val onesignalApp = mockk() - every { onesignalApp.get(FirebaseMessaging::class.java) } returns messaging - - mockkStatic(FirebaseApp::class) - every { FirebaseApp.initializeApp(any(), any(), any()) } returns onesignalApp - every { FirebaseApp.getApps(any()) } returns installedApps - - val applicationService = mockk() - every { applicationService.appContext } returns ApplicationProvider.getApplicationContext() - - return PushRegistratorFCM( - MockHelper.configModelStore(), - applicationService, - mockk(relaxed = true), - mockk(relaxed = true), - ) -} - -@RobolectricTest -class PushRegistratorFCMTests : FunSpec({ - val disabledLegacyApi = IllegalStateException("API disabled. Please use {@link #register()} instead.") - - afterEach { unmockkAll() } - - test("returns the FCM token from OneSignal's own FirebaseApp") { - val registrator = registrator(legacyToken = Tasks.forResult("fcm-token")) - - val token = withContext(Dispatchers.IO) { registrator.getToken(SENDER_ID) } - - token shouldBe "fcm-token" - } - - test("explains the problem when the app has no default FirebaseApp to register with") { - val registrator = registrator(legacyToken = Tasks.forException(disabledLegacyApi)) - - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { registrator.getToken(SENDER_ID) } - } - - thrown.message!! shouldContain "no default FirebaseApp" - thrown.message!! shouldContain "firebase_messaging_installation_id_enabled=not set" - } - - test("does not register against a default FirebaseApp with a different sender id") { - val registrator = - registrator( - legacyToken = Tasks.forException(disabledLegacyApi), - installedApps = listOf(defaultApp("999999999999")), - ) - - val thrown = - withContext(Dispatchers.IO) { - shouldThrow { registrator.getToken(SENDER_ID) } - } - - thrown.message!! shouldContain "sender id 999999999999" - } -})