From 796a2853577f44a671fbe1221fa450f5f532fc51 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 5 Nov 2026 11:23:32 -0800 Subject: [PATCH 1/2] fix: [SDK-5682] release non-OneSignal FCM broadcasts without waiting for the deadline A data message without a custom key made getOSNotificationIdFromJson pass null into JSONObject, which throws a NullPointerException on Android. The ingress handoff then never finished and the broadcast was held until the 8s deadline. Co-Authored-By: Claude Opus 5.5 --- .../common/NotificationFormatHelper.kt | 1 + .../common/NotificationFormatHelperTests.kt | 32 +++++++++++++++++++ .../ingress/NotificationIngressTests.kt | 11 +++++++ 3 files changed, 44 insertions(+) create mode 100644 OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/common/NotificationFormatHelperTests.kt diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationFormatHelper.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationFormatHelper.kt index 9e36f865a4..881068a43b 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationFormatHelper.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationFormatHelper.kt @@ -42,6 +42,7 @@ internal object NotificationFormatHelper { } private fun getOSNotificationIdFromJsonString(jsonStr: String?): String? { + if (jsonStr == null) return null try { val customJSON = JSONObject(jsonStr) if (customJSON.has(PAYLOAD_OS_NOTIFICATION_ID)) { diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/common/NotificationFormatHelperTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/common/NotificationFormatHelperTests.kt new file mode 100644 index 0000000000..6670e55afe --- /dev/null +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/common/NotificationFormatHelperTests.kt @@ -0,0 +1,32 @@ +package com.onesignal.notifications.internal.common + +import android.os.Bundle +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import org.json.JSONObject + +@RobolectricTest +class NotificationFormatHelperTests : FunSpec({ + test("getOSNotificationIdFromJson returns the id from custom") { + val json = JSONObject().put("custom", """{"i":"notification-id"}""") + + NotificationFormatHelper.getOSNotificationIdFromJson(json) shouldBe "notification-id" + } + + test("getOSNotificationIdFromJson returns null when custom is missing") { + val json = JSONObject().put("type", "call") + + NotificationFormatHelper.getOSNotificationIdFromJson(json) shouldBe null + } + + test("getOSNotificationIdFromJson returns null when custom is not JSON") { + val json = JSONObject().put("custom", "not-json") + + NotificationFormatHelper.getOSNotificationIdFromJson(json) shouldBe null + } + + test("isOneSignalBundle is false when custom is missing") { + NotificationFormatHelper.isOneSignalBundle(Bundle().apply { putString("type", "call") }) shouldBe false + } +}) diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/ingress/NotificationIngressTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/ingress/NotificationIngressTests.kt index bc7a82dd5c..4297b56cd1 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/ingress/NotificationIngressTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/ingress/NotificationIngressTests.kt @@ -84,6 +84,17 @@ class NotificationIngressTests : FunSpec({ NotificationIngress.pendingCountForTest(context) shouldBe 1 } + test("FCM input without a OneSignal payload is rejected without scheduling") { + var scheduled = false + NotificationIngress.drainSchedulerForTest = { scheduled = true } + val bundle = Bundle().apply { putString("type", "call") } + + NotificationIngress.persistFcm(context, Intent("com.google.android.c2dm.intent.RECEIVE"), bundle) shouldBe false + + NotificationIngress.pendingCountForTest(context) shouldBe 0 + scheduled shouldBe false + } + test("dismiss input is persisted before scheduling") { var countAtSchedule = 0 NotificationIngress.drainSchedulerForTest = { From 6bf7920bc1c41dd66512594e9981e3f8646337d6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 5 Nov 2026 12:07:59 -0800 Subject: [PATCH 2/2] chore: log when the JSON payload has no custom field Co-Authored-By: Claude Opus 5.5 --- .../internal/common/NotificationFormatHelper.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationFormatHelper.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationFormatHelper.kt index 881068a43b..80f02ce8ae 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationFormatHelper.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationFormatHelper.kt @@ -42,7 +42,10 @@ internal object NotificationFormatHelper { } private fun getOSNotificationIdFromJsonString(jsonStr: String?): String? { - if (jsonStr == null) return null + if (jsonStr == null) { + Logging.debug("Not a OneSignal formatted JSON. No 'custom' field in the payload.") + return null + } try { val customJSON = JSONObject(jsonStr) if (customJSON.has(PAYLOAD_OS_NOTIFICATION_ID)) {