From 3c784ce1f14c9746e0516ba5e6e5664a8e2910ca Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 11 Mar 2026 13:49:13 -0700 Subject: [PATCH] fix: skip ShortcutBadger on API 26+ to prevent SIGSEGV on Xiaomi devices On Android 8+ (API 26), notification channels handle app icon badges natively, making ShortcutBadger unnecessary. ShortcutBadger's DefaultBadger fallback causes native SIGSEGV crashes on certain Xiaomi devices (Redmi 10C, Redmi 9A) where MIUI's broadcast receiver has buggy native code that cannot be caught at the Java level. Fixes OneSignal/react-native-onesignal#1766 Made-with: Cursor Co-authored-by: Cursor --- .../internal/badges/impl/BadgeCountUpdater.kt | 25 +++++- .../internal/badges/BadgeCountUpdaterTests.kt | 89 +++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/badges/BadgeCountUpdaterTests.kt diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/badges/impl/BadgeCountUpdater.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/badges/impl/BadgeCountUpdater.kt index 6f2f8f0f0..5b0763386 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/badges/impl/BadgeCountUpdater.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/badges/impl/BadgeCountUpdater.kt @@ -3,6 +3,7 @@ package com.onesignal.notifications.internal.badges.impl import android.content.pm.PackageManager import android.os.Build import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.OneSignalDbContract @@ -14,11 +15,28 @@ import com.onesignal.notifications.internal.common.NotificationHelper import com.onesignal.notifications.internal.data.INotificationQueryHelper import com.onesignal.notifications.internal.limiting.INotificationLimitManager -internal class BadgeCountUpdater( +internal class BadgeCountUpdater private constructor( private val _applicationService: IApplicationService, private val _queryHelper: INotificationQueryHelper, private val _databaseProvider: IDatabaseProvider, + private val _sdkInt: Int, ) : IBadgeCountUpdater { + constructor( + applicationService: IApplicationService, + queryHelper: INotificationQueryHelper, + databaseProvider: IDatabaseProvider, + ) : this(applicationService, queryHelper, databaseProvider, Build.VERSION.SDK_INT) + + companion object { + @VisibleForTesting + internal fun createForTesting( + applicationService: IApplicationService, + queryHelper: INotificationQueryHelper, + databaseProvider: IDatabaseProvider, + sdkInt: Int, + ) = BadgeCountUpdater(applicationService, queryHelper, databaseProvider, sdkInt) + } + // Cache for manifest setting. private var badgesEnabled = -1 @@ -50,6 +68,10 @@ internal class BadgeCountUpdater( override fun update() { if (!areBadgesEnabled()) return + // On API 26+ the system handles badges via NotificationChannel, and + // ShortcutBadger can cause native SIGSEGV crashes on some OEM devices + // (e.g. Xiaomi Redmi) where the broadcast receiver has buggy native code. + if (_sdkInt >= Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { updateStandard() } else { @@ -83,6 +105,7 @@ internal class BadgeCountUpdater( override fun updateCount(count: Int) { if (!areBadgeSettingsEnabled()) return + if (_sdkInt >= Build.VERSION_CODES.O) return try { ShortcutBadger.applyCountOrThrow(_applicationService.appContext, count) } catch (e: ShortcutBadgeException) { diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/badges/BadgeCountUpdaterTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/badges/BadgeCountUpdaterTests.kt new file mode 100644 index 000000000..25aaa7d96 --- /dev/null +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/badges/BadgeCountUpdaterTests.kt @@ -0,0 +1,89 @@ +package com.onesignal.notifications.internal.badges + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Build +import com.onesignal.core.internal.application.IApplicationService +import com.onesignal.core.internal.database.IDatabaseProvider +import com.onesignal.notifications.internal.badges.impl.BadgeCountUpdater +import com.onesignal.notifications.internal.badges.impl.shortcutbadger.ShortcutBadger +import com.onesignal.notifications.internal.common.NotificationHelper +import com.onesignal.notifications.internal.data.INotificationQueryHelper +import io.kotest.core.spec.style.FunSpec +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkObject +import io.mockk.unmockkStatic +import io.mockk.verify + +private class Mocks { + val applicationService = mockk() + val queryHelper = mockk(relaxed = true) + val databaseProvider = mockk(relaxed = true) + + init { + val context = mockk() + val packageManager = mockk() + val applicationInfo = ApplicationInfo() + + every { applicationService.appContext } returns context + every { context.packageManager } returns packageManager + every { context.packageName } returns "com.onesignal.example" + every { + packageManager.getApplicationInfo("com.onesignal.example", PackageManager.GET_META_DATA) + } returns applicationInfo + } + + fun badgeCountUpdater(sdkInt: Int) = + BadgeCountUpdater.createForTesting( + applicationService, + queryHelper, + databaseProvider, + sdkInt, + ) +} + +class BadgeCountUpdaterTests : FunSpec({ + beforeEach { + mockkObject(NotificationHelper) + every { NotificationHelper.areNotificationsEnabled(any()) } returns true + mockkStatic(ShortcutBadger::class) + every { ShortcutBadger.applyCountOrThrow(any(), any()) } just Runs + } + + afterEach { + unmockkStatic(ShortcutBadger::class) + unmockkObject(NotificationHelper) + } + + test("update should not use ShortcutBadger on Android O") { + Mocks().badgeCountUpdater(Build.VERSION_CODES.O).update() + + verify(exactly = 0) { ShortcutBadger.applyCountOrThrow(any(), any()) } + } + + test("updateCount should not use ShortcutBadger on Android O") { + Mocks().badgeCountUpdater(Build.VERSION_CODES.O).updateCount(3) + + verify(exactly = 0) { ShortcutBadger.applyCountOrThrow(any(), any()) } + } + + test("updateCount should use ShortcutBadger before Android O") { + val mocks = Mocks() + val updater = + BadgeCountUpdater( + mocks.applicationService, + mocks.queryHelper, + mocks.databaseProvider, + ) + + updater.updateCount(3) + + verify(exactly = 1) { ShortcutBadger.applyCountOrThrow(any(), 3) } + } +})