fix: [SDK-4946] prefer google-services.json over the shared FCM project - #2725
fix: [SDK-4946] prefer google-services.json over the shared FCM project#2725abdulraqeeb33 wants to merge 1 commit into
Conversation
Read the host app's google-services.json (via the default FirebaseApp) so FCM registration uses one consistent customer Firebase project instead of mixing the dashboard sender id with onesignal-shared-public (754795614042). When the legacy token API is disabled, fall back to Firebase Installation ID registration against that same host project. Co-authored-by: abdulraqeeb33 <abdulraqeeb33@users.noreply.github.com>
📊 Diff Coverage ReportDiff Coverage Report (Changed Lines Only)Gate: aggregate coverage on changed executable lines must be ≥ 80% (JaCoCo line data for lines touched in the diff). Changed Files Coverage
Overall (aggregate gate)137/138 touched executable lines covered (99.3% — requires ≥ 80%) |
There was a problem hiding this comment.
Multi-model review of the FCM project-selection change (Claude Opus 5, GPT 5.6 Sol, Cursor Grok 4.6).
The host-app preference order is sound: matching google-services.json → complete backend fcm params → shared project. The mixed sender + onesignal-shared-public config is the right thing to stop doing for Installation ID registration.
Act on
- FID vs what
register()actually minted. AfterFirebaseMessaging.register(), the SDK always uploadsFirebaseInstallations.id. On Play Services below the V1 threshold,register()silently does a legacy registration and the FID is not a sendable push token — aSUBSCRIBEDsubscription that never gets messages. All three reviewers flagged this. BACKENDis not a consistency check. Completeness (non-blank fields) is treated as “real customer project.” Dashboard sender is then paired with those fields, and FID is allowed on any non-SHARED_DEFAULTsource. Ifandroid_params.jsstill ships the shared public project — or anyapplicationIdwhose project number ≠ dashboard sender — this recreates the mixed config SDK-4946 is about, now on the FID path.
Consider
3. initFirebaseApp is unsynchronized check-then-act; a second initializeApp(..., ONESIGNAL_SDK_FCM_APP_NAME) throws and never recovers. First success also freezes config for the process.
4. FirebaseApp.initializeApp(context) creates the host default app (Analytics/Crashlytics/data-collection start) when FirebaseInitProvider was removed. Prefer a read-only lookup.
5. No PushRegistratorFCM test drives a successful register() + installation id; the only FID-path test asserts the 25.1.0 missing-method failure.
Noted / dismissed
- Fallback keyed on English
IllegalStateExceptiontext ("API disabled"+"register()") — brittle; the manifest flag is the stable signal. - Reusing the default FirebaseApp changes the stored token and couples it to the host app’s
deleteToken()/unregister()lifecycle — changelog-worthy. - Gradle
require '[23.0.8, 24.0.99]'blocking 25.x — dismissed:requirecan still upgrade via conflict resolution;strictlywould not. Tasks.awaitwithout timeout,gcmSenderIdvs Firebase’sapplicationIdfallback, JaCoCoexcludes =clobber — nits / pre-existing, not blocking.
Sent by Cursor Automation: PR Reviews
| await(registration.register()) | ||
| return await(registration.installationId()) |
There was a problem hiding this comment.
Critical (3/3 models): register() is Task<Void>; the value uploaded is a separate FirebaseInstallations.id hop, not the identifier FCM just registered.
On Play Services below the V1 threshold (GMS_VERSION_Y2026W12 / 261200000 in firebase-messaging 25.1), FirebaseMessaging.register() silently takes the legacy getToken path. getToken() still throws “API disabled” from the manifest flag alone, so this branch runs, register() succeeds with a normal FCM token, and the SDK reports an FID that was never registered as a send target. Result: SUBSCRIBED with an identifier that cannot receive pushes, with no log distinguishing it.
Do not synthesize the token independently of what FCM produced. Gate the FID path on Play Services support, or use the identifier FCM actually registered (the value blockingRegister / onRegistered delivers).
| val matchingDefaultApp = | ||
| defaultApp?.takeIf { it.isComplete && it.senderId == dashboardSenderId } | ||
| val completeBackend = backend?.takeIf { it.isComplete } | ||
| return when { | ||
| matchingDefaultApp != null -> | ||
| FcmFirebaseConfig( | ||
| credentials = matchingDefaultApp, | ||
| source = FcmFirebaseConfig.Source.GOOGLE_SERVICES, | ||
| reuseDefaultApp = true, | ||
| ) | ||
| completeBackend != null -> | ||
| FcmFirebaseConfig( | ||
| credentials = completeBackend, | ||
| source = FcmFirebaseConfig.Source.BACKEND, | ||
| reuseDefaultApp = false, |
There was a problem hiding this comment.
Warning (3/3 models): isComplete only checks non-blank strings. The dashboard sender is injected in backendCredentials() and is never compared to the project number embedded in applicationId (1:<sender>:android:…).
firebaseAppForInstallationId() then allows FID registration for any non-SHARED_DEFAULT source. If android_params.js still returns the shared public project (onesignal-shared-public / 1:754795614042:android:…) as a complete fcm object, this is classified BACKEND, not SHARED_DEFAULT, and FID runs against the mixed config this PR exists to stop.
Treat backend params that match the shared defaults — or whose applicationId project number ≠ dashboard sender — as SHARED_DEFAULT. Only allow FID on GOOGLE_SERVICES, or on BACKEND after proving the four fields belong to one non-shared project.
| } | ||
|
|
||
| private fun initFirebaseApp(senderId: String) { | ||
| if (firebaseApp != null) return |
There was a problem hiding this comment.
Warning (3/3 models): Unsynchronized check-then-act on a plain var. DeviceRegistrationListener.start() and onModelReplaced(HYDRATE) can both call getToken on IO. Two threads can both pass this null check and call FirebaseApp.initializeApp(..., ONESIGNAL_SDK_FCM_APP_NAME); the second hits “already exists” and unlike hostDefaultFirebaseApp there is no getInstance(FCM_APP_NAME) recovery.
The first success also freezes firebaseApp / resolvedSource for the process, so a later HYDRATE that brings complete backend params cannot re-resolve.
Serialize init, recover from an existing named app, and/or cache a credentials fingerprint so sender / fcmParams changes are picked up.
| */ | ||
| internal fun hostDefaultFirebaseApp(context: Context): FirebaseApp? { | ||
| return try { | ||
| FirebaseApp.initializeApp(context) |
There was a problem hiding this comment.
Warning (2/3 models): This is not a read-only lookup. FirebaseApp.initializeApp(context) creates the default app from plugin string resources and starts every Firebase component (initializeAllApis()), including when the sender later mismatches and OneSignal builds a named app anyway.
Apps that remove FirebaseInitProvider (tools:node="remove") to gate Analytics/Crashlytics/data collection behind consent now get a default FirebaseApp from OneSignal on a background thread at push-registration time, with no opt-out.
Prefer FirebaseApp.getApps(context).firstOrNull { it.name == DEFAULT_APP_NAME } / getInstance(), and only call initializeApp(context) if creating the host default app is an intentional, documented side effect.
|
Host
Restrict FID to |
|
Id make this a separate linear ticket to not mix approaches for SDK-4946. |


Description
One Line Summary
Prefer the host app's
google-services.jsonfor FCM registration so we stop mixing the dashboard sender id with OneSignal's shared Firebase project (onesignal-shared-public/754795614042).Details
Motivation
Google now requires the FCM sender id, project id, application id, and API key to belong to the same Firebase project, and Firebase Installation IDs are issued per project. The Android SDK has historically initialized its own
FirebaseAppwith OneSignal's shared public credentials and the customer's sender id at runtime. That mixed configuration is rejected for Installation ID registration (ApiException: 8) and is the dead end behind SDK-4946.The SDK can get off the shared project by reading the file Google already asks every app to ship:
google-services.json(compiled into string resources by the google-services Gradle plugin, then loaded viaFirebaseApp.initializeApp(context)).Scope
FCM registration now picks one consistent project, in this order:
google-services.json— reuse the defaultFirebaseAppwhen its sender id matches the OneSignal dashboard.fcmparams fromandroid_params.js, when complete. These are now read at registration time, not snapshotted in thePushRegistratorFCMconstructor (so they were previously ignored if the registrator was created before params hydration).onesignal-shared-public/754795614042) as a last-resort fallback for apps that never addedgoogle-services.json. This still will not work with Installation ID registration.When the legacy
getToken()API is disabled (firebase_messaging_installation_id_enabled), registration falls back toFirebaseMessaging.register()against that same host project and returns the Firebase Installation ID. Apps that opt in still need their owngoogle-services.jsonwhose sender id matches the dashboard.If
google-services.jsonis present but its sender id does not match the dashboard, the SDK logs a warning and does not use it (so an Analytics project can still coexist with a different OneSignal FCM project via backend/shared credentials).No public API changes. Apps without
google-services.jsonkeep the legacy shared-project path.Testing
Unit testing
FcmFirebaseConfigResolverTestscovers host match, host mismatch, incomplete host/backend credentials, backend params, and the shared fallback.FCMTokenProviderTestscovers legacy tokens, Installation ID registration, missing default app / sender mismatch diagnostics, reflection, and error propagation.PushRegistratorFCMTestscovers reusing the host app, backend params (including reading them after construction), shared fallback, sender mismatch, Installation ID wiring, andFirebaseOptionsmapping.:OneSignal:notifications:testDebugUnitTest, Spotless, and Detekt pass locally.Manual testing
Not run on a device in this environment. The registration branches are covered with unit tests. To verify on a device:
google-services.jsonwhose sender id matches the OneSignal dashboard into the example app, apply the google-services plugin, and confirm logs say registration is using that project and that a push arrives.firebase_messaging_installation_id_enabledand confirm the subscription token is a Firebase Installation ID and delivery still works.google-services.jsonand confirm the shared-project fallback still returns a legacy FCM token when the Installation ID flag is off.Affected code checklist
Checklist
Overview
Testing
Final pass