From edf04b40f5dc03d52c275b17b944744480480027 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Mon, 9 Mar 2026 18:21:56 -0400 Subject: [PATCH] notification priority set --- .../impl/NotificationBundleProcessor.kt | 3 +- .../impl/NotificationChannelManager.kt | 10 +- .../common/NotificationPriorityMapper.kt | 37 +++++++ .../impl/NotificationDisplayBuilder.kt | 8 +- .../NotificationBundleProcessorTests.kt | 72 +++++++++++++ .../NotificationChannelManagerTests.kt | 67 ++++++++++++ .../NotificationDisplayBuilderTests.kt | 102 ++++++++++++++++++ 7 files changed, 284 insertions(+), 15 deletions(-) create mode 100644 OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationPriorityMapper.kt create mode 100644 OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/bundle/NotificationBundleProcessorTests.kt create mode 100644 OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/display/NotificationDisplayBuilderTests.kt diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/bundle/impl/NotificationBundleProcessor.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/bundle/impl/NotificationBundleProcessor.kt index 9e95d4115e..138f2bc0c7 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/bundle/impl/NotificationBundleProcessor.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/bundle/impl/NotificationBundleProcessor.kt @@ -7,6 +7,7 @@ import com.onesignal.core.internal.time.ITime import com.onesignal.notifications.internal.bundle.INotificationBundleProcessor import com.onesignal.notifications.internal.common.NotificationConstants import com.onesignal.notifications.internal.common.NotificationFormatHelper +import com.onesignal.notifications.internal.common.NotificationPriorityMapper import com.onesignal.notifications.internal.generation.INotificationGenerationWorkManager import org.json.JSONArray import org.json.JSONException @@ -84,7 +85,7 @@ internal class NotificationBundleProcessor( val jsonPayload = JSONUtils.bundleAsJSONObject(bundle) val timestamp = _time.currentTimeMillis / 1000L val isRestoring = bundle.getBoolean("is_restoring", false) - val isHighPriority = bundle.getString("pri", "0").toInt() > 9 + val isHighPriority = NotificationPriorityMapper.isHighPriority(bundle.getString("pri", "0").toInt()) val osNotificationId = NotificationFormatHelper.getOSNotificationIdFromJson(jsonPayload) var androidNotificationId = 0 diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/channels/impl/NotificationChannelManager.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/channels/impl/NotificationChannelManager.kt index 45577fc7c9..f6ec933cd1 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/channels/impl/NotificationChannelManager.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/channels/impl/NotificationChannelManager.kt @@ -7,13 +7,13 @@ import android.app.NotificationManager import android.content.Context import android.os.Build import androidx.annotation.RequiresApi -import androidx.core.app.NotificationManagerCompat import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.language.ILanguageContext import com.onesignal.debug.internal.logging.Logging import com.onesignal.notifications.internal.channels.INotificationChannelManager import com.onesignal.notifications.internal.common.NotificationGenerationJob import com.onesignal.notifications.internal.common.NotificationHelper +import com.onesignal.notifications.internal.common.NotificationPriorityMapper import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -226,11 +226,5 @@ internal class NotificationChannelManager( } } - private fun priorityToImportance(priority: Int): Int { - if (priority > 9) return NotificationManagerCompat.IMPORTANCE_MAX - if (priority > 7) return NotificationManagerCompat.IMPORTANCE_HIGH - if (priority > 5) return NotificationManagerCompat.IMPORTANCE_DEFAULT - if (priority > 3) return NotificationManagerCompat.IMPORTANCE_LOW - return if (priority > 1) NotificationManagerCompat.IMPORTANCE_MIN else NotificationManagerCompat.IMPORTANCE_NONE - } + private fun priorityToImportance(priority: Int): Int = NotificationPriorityMapper.toAndroidImportance(priority) } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationPriorityMapper.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationPriorityMapper.kt new file mode 100644 index 0000000000..7c73871150 --- /dev/null +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/NotificationPriorityMapper.kt @@ -0,0 +1,37 @@ +package com.onesignal.notifications.internal.common + +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat + +/** + * Single source of truth for mapping OneSignal payload priority (1–10) to + * Android notification priority and channel importance levels. + * + * Threshold table (OneSignal pri → Android level): + * 9–10 → MAX + * 7–8 → HIGH + * 5–6 → DEFAULT + * 3–4 → LOW + * 1–2 → MIN + * 0 → NONE (importance only) + */ +internal object NotificationPriorityMapper { + private const val HIGH_PRIORITY_THRESHOLD = 9 + + fun isHighPriority(osPriority: Int): Boolean = osPriority >= HIGH_PRIORITY_THRESHOLD + + fun toAndroidPriority(osPriority: Int): Int { + if (osPriority >= HIGH_PRIORITY_THRESHOLD) return NotificationCompat.PRIORITY_MAX + if (osPriority >= 7) return NotificationCompat.PRIORITY_HIGH + if (osPriority >= 5) return NotificationCompat.PRIORITY_DEFAULT + return if (osPriority >= 3) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN + } + + fun toAndroidImportance(osPriority: Int): Int { + if (osPriority >= HIGH_PRIORITY_THRESHOLD) return NotificationManagerCompat.IMPORTANCE_MAX + if (osPriority >= 7) return NotificationManagerCompat.IMPORTANCE_HIGH + if (osPriority >= 5) return NotificationManagerCompat.IMPORTANCE_DEFAULT + if (osPriority >= 3) return NotificationManagerCompat.IMPORTANCE_LOW + return if (osPriority >= 1) NotificationManagerCompat.IMPORTANCE_MIN else NotificationManagerCompat.IMPORTANCE_NONE + } +} diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/display/impl/NotificationDisplayBuilder.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/display/impl/NotificationDisplayBuilder.kt index 6bcb1e8af4..f72f4e98f3 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/display/impl/NotificationDisplayBuilder.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/display/impl/NotificationDisplayBuilder.kt @@ -19,6 +19,7 @@ import com.onesignal.notifications.internal.channels.INotificationChannelManager import com.onesignal.notifications.internal.common.NotificationConstants import com.onesignal.notifications.internal.common.NotificationGenerationJob import com.onesignal.notifications.internal.common.NotificationHelper +import com.onesignal.notifications.internal.common.NotificationPriorityMapper import com.onesignal.notifications.internal.display.INotificationDisplayBuilder import com.onesignal.notifications.receivers.NotificationDismissReceiver import org.json.JSONException @@ -455,12 +456,7 @@ internal class NotificationDisplayBuilder( } } - private fun convertOSToAndroidPriority(priority: Int): Int { - if (priority > 9) return NotificationCompat.PRIORITY_MAX - if (priority > 7) return NotificationCompat.PRIORITY_HIGH - if (priority > 4) return NotificationCompat.PRIORITY_DEFAULT - return if (priority > 2) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN - } + private fun convertOSToAndroidPriority(priority: Int): Int = NotificationPriorityMapper.toAndroidPriority(priority) internal class OneSignalNotificationBuilder { var compatBuilder: NotificationCompat.Builder? = null diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/bundle/NotificationBundleProcessorTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/bundle/NotificationBundleProcessorTests.kt new file mode 100644 index 0000000000..bd696ae686 --- /dev/null +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/bundle/NotificationBundleProcessorTests.kt @@ -0,0 +1,72 @@ +package com.onesignal.notifications.internal.bundle + +import android.content.Context +import android.os.Bundle +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.mocks.MockHelper +import com.onesignal.notifications.internal.bundle.impl.NotificationBundleProcessor +import com.onesignal.notifications.internal.generation.INotificationGenerationWorkManager +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.json.JSONObject +import org.robolectric.annotation.Config + +@Config( + packageName = "com.onesignal.example", + sdk = [26], +) +@RobolectricTest +class NotificationBundleProcessorTests : FunSpec({ + beforeAny { + Logging.logLevel = LogLevel.NONE + } + + fun buildOneSignalBundle(pri: String): Bundle { + val bundle = Bundle() + bundle.putString("custom", JSONObject().put("i", "test-notif-id").toString()) + bundle.putString("alert", "test message") + bundle.putString("pri", pri) + return bundle + } + + fun captureIsHighPriority(pri: String): Boolean { + val isHighPrioritySlot = slot() + val workManager = mockk() + every { + workManager.beginEnqueueingWork( + any(), any(), any(), any(), any(), any(), + capture(isHighPrioritySlot), + ) + } returns true + + val processor = NotificationBundleProcessor(workManager, MockHelper.time(1111)) + val context = mockk(relaxed = true) + processor.processBundleFromReceiver(context, buildOneSignalBundle(pri)) + + return isHighPrioritySlot.captured + } + + test("pri 10 should be treated as high priority") { + captureIsHighPriority("10") shouldBe true + } + + test("pri 9 should be treated as high priority") { + captureIsHighPriority("9") shouldBe true + } + + test("pri 8 should not be treated as high priority") { + captureIsHighPriority("8") shouldBe false + } + + // Regression: pri=9 was previously not treated as high priority due to strict > 9 check. + // The backend sends pri=9 for the highest dashboard priority setting, so it must be + // classified as high priority for correct work manager scheduling. + test("regression - pri 9 must be classified as high priority") { + captureIsHighPriority("9") shouldBe true + } +}) diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/channels/NotificationChannelManagerTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/channels/NotificationChannelManagerTests.kt index 6636094989..bf810444d8 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/channels/NotificationChannelManagerTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/channels/NotificationChannelManagerTests.kt @@ -243,6 +243,73 @@ class NotificationChannelManagerTests : FunSpec({ getChannel("OS_id1", ApplicationProvider.getApplicationContext()) shouldNotBe null } + fun createChannelWithPri(pri: Int): Int { + val mockTime = MockHelper.time(1111) + val notificationChannelManager = NotificationChannelManager(AndroidMockHelper.applicationService(), MockHelper.languageContext()) + val channelId = "test_pri_$pri" + val payload = + JSONObject() + .put("pri", pri) + .put( + "chnl", + JSONObject() + .put("id", channelId), + ) + notificationChannelManager.createNotificationChannel(NotificationGenerationJob(payload, mockTime)) + val notificationManager = + ApplicationProvider.getApplicationContext() + .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + return notificationManager.getNotificationChannel(channelId)!!.importance + } + + test("createNotificationChannel with pri 10 should have IMPORTANCE_MAX") { + createChannelWithPri(10) shouldBe NotificationManager.IMPORTANCE_MAX + } + + test("createNotificationChannel with pri 9 should have IMPORTANCE_MAX") { + createChannelWithPri(9) shouldBe NotificationManager.IMPORTANCE_MAX + } + + test("createNotificationChannel with pri 8 should have IMPORTANCE_HIGH") { + createChannelWithPri(8) shouldBe NotificationManager.IMPORTANCE_HIGH + } + + test("createNotificationChannel with pri 7 should have IMPORTANCE_HIGH") { + createChannelWithPri(7) shouldBe NotificationManager.IMPORTANCE_HIGH + } + + test("createNotificationChannel with pri 6 should have IMPORTANCE_DEFAULT") { + createChannelWithPri(6) shouldBe NotificationManager.IMPORTANCE_DEFAULT + } + + test("createNotificationChannel with pri 5 should have IMPORTANCE_DEFAULT") { + createChannelWithPri(5) shouldBe NotificationManager.IMPORTANCE_DEFAULT + } + + test("createNotificationChannel with pri 4 should have IMPORTANCE_LOW") { + createChannelWithPri(4) shouldBe NotificationManager.IMPORTANCE_LOW + } + + test("createNotificationChannel with pri 3 should have IMPORTANCE_LOW") { + createChannelWithPri(3) shouldBe NotificationManager.IMPORTANCE_LOW + } + + test("createNotificationChannel with pri 2 should have IMPORTANCE_MIN") { + createChannelWithPri(2) shouldBe NotificationManager.IMPORTANCE_MIN + } + + test("createNotificationChannel with pri 1 should have IMPORTANCE_MIN") { + createChannelWithPri(1) shouldBe NotificationManager.IMPORTANCE_MIN + } + + // Regression: pri=9 previously mapped to IMPORTANCE_HIGH due to strict > 9 check. + // The backend sends pri=9 for the highest dashboard priority, so the channel must + // be created with IMPORTANCE_MAX. + test("regression - createNotificationChannel with pri 9 must not have IMPORTANCE_HIGH") { + createChannelWithPri(9) shouldBe NotificationManager.IMPORTANCE_MAX + createChannelWithPri(9) shouldNotBe NotificationManager.IMPORTANCE_HIGH + } + test("processChannelList multilanguage") { // Given val notificationChannelManager = NotificationChannelManager(AndroidMockHelper.applicationService(), MockHelper.languageContext()) diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/display/NotificationDisplayBuilderTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/display/NotificationDisplayBuilderTests.kt new file mode 100644 index 0000000000..bc129538e8 --- /dev/null +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/display/NotificationDisplayBuilderTests.kt @@ -0,0 +1,102 @@ +package com.onesignal.notifications.internal.display + +import androidx.core.app.NotificationCompat +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.mocks.AndroidMockHelper +import com.onesignal.mocks.MockHelper +import com.onesignal.notifications.internal.channels.INotificationChannelManager +import com.onesignal.notifications.internal.common.NotificationGenerationJob +import com.onesignal.notifications.internal.display.impl.NotificationDisplayBuilder +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockk +import org.json.JSONObject +import org.robolectric.annotation.Config + +@Config( + packageName = "com.onesignal.example", + sdk = [26], +) +@RobolectricTest +class NotificationDisplayBuilderTests : FunSpec({ + beforeAny { + Logging.logLevel = LogLevel.NONE + } + + fun buildNotificationPriority(pri: Int?): Int { + val channelManager = mockk() + every { channelManager.createNotificationChannel(any()) } returns "test_channel" + + val builder = NotificationDisplayBuilder( + AndroidMockHelper.applicationService(), + channelManager, + ) + + val payload = JSONObject() + .put("alert", "test") + .put("custom", JSONObject().put("i", "test-id")) + if (pri != null) { + payload.put("pri", pri) + } + + val job = NotificationGenerationJob(payload, MockHelper.time(1111)) + val result = builder.getBaseOneSignalNotificationBuilder(job) + return result.compatBuilder!!.build().priority + } + + test("pri 10 should map to PRIORITY_MAX") { + buildNotificationPriority(10) shouldBe NotificationCompat.PRIORITY_MAX + } + + test("pri 9 should map to PRIORITY_MAX") { + buildNotificationPriority(9) shouldBe NotificationCompat.PRIORITY_MAX + } + + test("pri 8 should map to PRIORITY_HIGH") { + buildNotificationPriority(8) shouldBe NotificationCompat.PRIORITY_HIGH + } + + test("pri 7 should map to PRIORITY_HIGH") { + buildNotificationPriority(7) shouldBe NotificationCompat.PRIORITY_HIGH + } + + test("pri 6 should map to PRIORITY_DEFAULT") { + buildNotificationPriority(6) shouldBe NotificationCompat.PRIORITY_DEFAULT + } + + test("pri 5 should map to PRIORITY_DEFAULT") { + buildNotificationPriority(5) shouldBe NotificationCompat.PRIORITY_DEFAULT + } + + test("pri 4 should map to PRIORITY_LOW") { + buildNotificationPriority(4) shouldBe NotificationCompat.PRIORITY_LOW + } + + test("pri 3 should map to PRIORITY_LOW") { + buildNotificationPriority(3) shouldBe NotificationCompat.PRIORITY_LOW + } + + test("pri 2 should map to PRIORITY_MIN") { + buildNotificationPriority(2) shouldBe NotificationCompat.PRIORITY_MIN + } + + test("pri 1 should map to PRIORITY_MIN") { + buildNotificationPriority(1) shouldBe NotificationCompat.PRIORITY_MIN + } + + test("missing pri should default to PRIORITY_DEFAULT") { + buildNotificationPriority(null) shouldBe NotificationCompat.PRIORITY_DEFAULT + } + + // Regression: pri=9 previously mapped to PRIORITY_HIGH due to strict > 9 check. + // The backend sends pri=9 for the highest dashboard priority, so this must yield + // PRIORITY_MAX to match competitor notification ranking behavior. + test("regression - pri 9 must not map to PRIORITY_HIGH") { + buildNotificationPriority(9) shouldBe NotificationCompat.PRIORITY_MAX + buildNotificationPriority(9) shouldNotBe NotificationCompat.PRIORITY_HIGH + } +})