Skip to content

Commit 90ca1f0

Browse files
fix: [SDK-4901] skip ShortcutBadger on API 26+ to prevent SIGSEGV on Xiaomi devices (#2570)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a5df046 commit 90ca1f0

2 files changed

Lines changed: 140 additions & 2 deletions

File tree

OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/badges/impl/BadgeCountUpdater.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.onesignal.notifications.internal.badges.impl
33
import android.content.pm.PackageManager
44
import android.os.Build
55
import androidx.annotation.RequiresApi
6+
import androidx.annotation.VisibleForTesting
67
import com.onesignal.core.internal.application.IApplicationService
78
import com.onesignal.core.internal.database.IDatabaseProvider
89
import com.onesignal.core.internal.database.impl.OneSignalDbContract
@@ -14,11 +15,28 @@ import com.onesignal.notifications.internal.common.NotificationHelper
1415
import com.onesignal.notifications.internal.data.INotificationQueryHelper
1516
import com.onesignal.notifications.internal.limiting.INotificationLimitManager
1617

17-
internal class BadgeCountUpdater(
18+
internal class BadgeCountUpdater private constructor(
1819
private val _applicationService: IApplicationService,
1920
private val _queryHelper: INotificationQueryHelper,
2021
private val _databaseProvider: IDatabaseProvider,
22+
private val _sdkInt: Int,
2123
) : IBadgeCountUpdater {
24+
constructor(
25+
applicationService: IApplicationService,
26+
queryHelper: INotificationQueryHelper,
27+
databaseProvider: IDatabaseProvider,
28+
) : this(applicationService, queryHelper, databaseProvider, Build.VERSION.SDK_INT)
29+
30+
companion object {
31+
@VisibleForTesting
32+
internal fun createForTesting(
33+
applicationService: IApplicationService,
34+
queryHelper: INotificationQueryHelper,
35+
databaseProvider: IDatabaseProvider,
36+
sdkInt: Int,
37+
) = BadgeCountUpdater(applicationService, queryHelper, databaseProvider, sdkInt)
38+
}
39+
2240
// Cache for manifest setting.
2341
private var badgesEnabled = -1
2442

@@ -50,7 +68,11 @@ internal class BadgeCountUpdater(
5068

5169
override fun update() {
5270
if (!areBadgesEnabled()) return
53-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
71+
// On API 26+ the system handles badges via NotificationChannel, and
72+
// ShortcutBadger can cause native SIGSEGV crashes on some OEM devices
73+
// (e.g. Xiaomi Redmi) where the broadcast receiver has buggy native code.
74+
if (_sdkInt >= Build.VERSION_CODES.O) return
75+
if (_sdkInt >= Build.VERSION_CODES.M) {
5476
updateStandard()
5577
} else {
5678
updateFallback()
@@ -83,6 +105,7 @@ internal class BadgeCountUpdater(
83105

84106
override fun updateCount(count: Int) {
85107
if (!areBadgeSettingsEnabled()) return
108+
if (_sdkInt >= Build.VERSION_CODES.O) return
86109
try {
87110
ShortcutBadger.applyCountOrThrow(_applicationService.appContext, count)
88111
} catch (e: ShortcutBadgeException) {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.onesignal.notifications.internal.badges
2+
3+
import android.content.Context
4+
import android.content.pm.ApplicationInfo
5+
import android.content.pm.PackageManager
6+
import android.os.Build
7+
import com.onesignal.core.internal.application.IApplicationService
8+
import com.onesignal.core.internal.database.ICursor
9+
import com.onesignal.core.internal.database.IDatabase
10+
import com.onesignal.core.internal.database.IDatabaseProvider
11+
import com.onesignal.notifications.internal.badges.impl.BadgeCountUpdater
12+
import com.onesignal.notifications.internal.badges.impl.shortcutbadger.ShortcutBadger
13+
import com.onesignal.notifications.internal.common.NotificationHelper
14+
import com.onesignal.notifications.internal.data.INotificationQueryHelper
15+
import io.kotest.core.spec.style.FunSpec
16+
import io.mockk.Runs
17+
import io.mockk.every
18+
import io.mockk.just
19+
import io.mockk.mockk
20+
import io.mockk.mockkObject
21+
import io.mockk.mockkStatic
22+
import io.mockk.unmockkObject
23+
import io.mockk.unmockkStatic
24+
import io.mockk.verify
25+
26+
private class Mocks {
27+
val applicationService = mockk<IApplicationService>()
28+
val queryHelper = mockk<INotificationQueryHelper>(relaxed = true)
29+
val database = mockk<IDatabase>(relaxed = true)
30+
val databaseProvider =
31+
mockk<IDatabaseProvider> {
32+
every { os } returns database
33+
}
34+
35+
init {
36+
val context = mockk<Context>()
37+
val packageManager = mockk<PackageManager>()
38+
val applicationInfo = ApplicationInfo()
39+
40+
every { applicationService.appContext } returns context
41+
every { context.packageManager } returns packageManager
42+
every { context.packageName } returns "com.onesignal.example"
43+
every { queryHelper.recentUninteractedWithNotificationsWhere() } returns StringBuilder("1=1")
44+
every {
45+
packageManager.getApplicationInfo("com.onesignal.example", PackageManager.GET_META_DATA)
46+
} returns applicationInfo
47+
}
48+
49+
fun queryReturnsCount(count: Int) {
50+
val cursor = mockk<ICursor>()
51+
every { cursor.count } returns count
52+
every {
53+
database.query(any(), any(), any(), any(), any(), any(), any(), any(), any())
54+
} answers {
55+
arg<(ICursor) -> Unit>(8).invoke(cursor)
56+
}
57+
}
58+
59+
fun badgeCountUpdater(sdkInt: Int) =
60+
BadgeCountUpdater.createForTesting(
61+
applicationService,
62+
queryHelper,
63+
databaseProvider,
64+
sdkInt,
65+
)
66+
}
67+
68+
class BadgeCountUpdaterTests : FunSpec({
69+
beforeEach {
70+
mockkObject(NotificationHelper)
71+
every { NotificationHelper.areNotificationsEnabled(any()) } returns true
72+
mockkStatic(ShortcutBadger::class)
73+
every { ShortcutBadger.applyCountOrThrow(any(), any()) } just Runs
74+
}
75+
76+
afterEach {
77+
unmockkStatic(ShortcutBadger::class)
78+
unmockkObject(NotificationHelper)
79+
}
80+
81+
test("update should not use ShortcutBadger on Android O") {
82+
Mocks().badgeCountUpdater(Build.VERSION_CODES.O).update()
83+
84+
verify(exactly = 0) { ShortcutBadger.applyCountOrThrow(any(), any()) }
85+
}
86+
87+
test("updateCount should not use ShortcutBadger on Android O") {
88+
Mocks().badgeCountUpdater(Build.VERSION_CODES.O).updateCount(3)
89+
90+
verify(exactly = 0) { ShortcutBadger.applyCountOrThrow(any(), any()) }
91+
}
92+
93+
test("update should use ShortcutBadger on Android N MR1") {
94+
every { NotificationHelper.getActiveNotifications(any()) } returns emptyArray()
95+
96+
Mocks().badgeCountUpdater(Build.VERSION_CODES.N_MR1).update()
97+
98+
verify(exactly = 1) { ShortcutBadger.applyCountOrThrow(any(), 0) }
99+
}
100+
101+
test("update should use ShortcutBadger before Android M") {
102+
val mocks = Mocks()
103+
mocks.queryReturnsCount(3)
104+
105+
mocks.badgeCountUpdater(Build.VERSION_CODES.LOLLIPOP_MR1).update()
106+
107+
verify(exactly = 1) { ShortcutBadger.applyCountOrThrow(any(), 3) }
108+
}
109+
110+
test("updateCount should use ShortcutBadger before Android O") {
111+
Mocks().badgeCountUpdater(Build.VERSION_CODES.O - 1).updateCount(3)
112+
113+
verify(exactly = 1) { ShortcutBadger.applyCountOrThrow(any(), 3) }
114+
}
115+
})

0 commit comments

Comments
 (0)