Skip to content

Commit bc876fc

Browse files
committed
fix rare app not opening from notification bug
In rare cases when tapping on a notification the app would not open, as well as other issues like the click not being counted either. The issue was due to a race condition between NotificationListener.start and processing the click event. To fix and to avoid this mistake in the future moved all the code from NotificationListener into NotificationLifecycleService. There wasn't a clear set of responsibility difference between these to classes so this is also a win to remove complexity. Was able to reproduce the issue 1/5 times on an Android 14 (no extension level) emulator. After testing 20 times I am no longer able to reproduce the issue now.
1 parent aaf6693 commit bc876fc

File tree

6 files changed

+130
-205
lines changed

6 files changed

+130
-205
lines changed

OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/NotificationsModule.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import com.onesignal.notifications.internal.lifecycle.impl.NotificationLifecycle
3939
import com.onesignal.notifications.internal.limiting.INotificationLimitManager
4040
import com.onesignal.notifications.internal.limiting.impl.NotificationLimitManager
4141
import com.onesignal.notifications.internal.listeners.DeviceRegistrationListener
42-
import com.onesignal.notifications.internal.listeners.NotificationListener
4342
import com.onesignal.notifications.internal.open.INotificationOpenedProcessor
4443
import com.onesignal.notifications.internal.open.INotificationOpenedProcessorHMS
4544
import com.onesignal.notifications.internal.open.impl.NotificationOpenedProcessor
@@ -94,6 +93,7 @@ internal class NotificationsModule : IModule {
9493

9594
builder.register<NotificationLifecycleService>()
9695
.provides<INotificationLifecycleService>()
96+
.provides<INotificationActivityOpener>()
9797

9898
builder.register {
9999
if (FirebaseAnalyticsTracker.canTrack()) {
@@ -141,10 +141,8 @@ internal class NotificationsModule : IModule {
141141

142142
// Startable services
143143
builder.register<DeviceRegistrationListener>().provides<IStartableService>()
144-
builder.register<NotificationListener>().provides<IStartableService>()
145144

146145
builder.register<NotificationsManager>()
147146
.provides<INotificationsManager>()
148-
.provides<INotificationActivityOpener>()
149147
}
150148
}

OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationsManager.kt

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import com.onesignal.notifications.INotificationClickListener
1010
import com.onesignal.notifications.INotificationLifecycleListener
1111
import com.onesignal.notifications.INotificationsManager
1212
import com.onesignal.notifications.IPermissionObserver
13-
import com.onesignal.notifications.internal.common.GenerateNotificationOpenIntentFromPushPayload
1413
import com.onesignal.notifications.internal.common.NotificationHelper
1514
import com.onesignal.notifications.internal.data.INotificationRepository
1615
import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleService
@@ -21,7 +20,6 @@ import com.onesignal.notifications.internal.summary.INotificationSummaryManager
2120
import kotlinx.coroutines.Dispatchers
2221
import kotlinx.coroutines.withContext
2322
import org.json.JSONArray
24-
import org.json.JSONException
2523

2624
interface INotificationActivityOpener {
2725
suspend fun openDestinationActivity(
@@ -42,7 +40,6 @@ internal class NotificationsManager(
4240
private val _notificationDataController: INotificationRepository,
4341
private val _summaryManager: INotificationSummaryManager,
4442
) : INotificationsManager,
45-
INotificationActivityOpener,
4643
INotificationPermissionChangedHandler,
4744
IApplicationLifecycleHandler {
4845
override var permission: Boolean = NotificationHelper.areNotificationsEnabled(_applicationService.appContext)
@@ -159,26 +156,4 @@ internal class NotificationsManager(
159156
Logging.debug("NotificationsManager.removeClickListener(listener: $listener)")
160157
_notificationLifecycleService.removeExternalClickListener(listener)
161158
}
162-
163-
override suspend fun openDestinationActivity(
164-
activity: Activity,
165-
pushPayloads: JSONArray,
166-
) {
167-
try {
168-
// Always use the top most notification if user tapped on the summary notification
169-
val firstPayloadItem = pushPayloads.getJSONObject(0)
170-
// val isHandled = _notificationLifecycleService.canOpenNotification(activity, firstPayloadItem)
171-
val intentGenerator = GenerateNotificationOpenIntentFromPushPayload.create(activity, firstPayloadItem)
172-
173-
val intent = intentGenerator.getIntentVisible()
174-
if (intent != null) {
175-
Logging.info("SDK running startActivity with Intent: $intent")
176-
activity.startActivity(intent)
177-
} else {
178-
Logging.info("SDK not showing an Activity automatically due to it's settings.")
179-
}
180-
} catch (e: JSONException) {
181-
e.printStackTrace()
182-
}
183-
}
184159
}

OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleEventHandler.kt

Lines changed: 0 additions & 17 deletions
This file was deleted.

OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleService.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import org.json.JSONObject
1818
* --------
1919
* [INotificationLifecycleCallback]: Can be used by other modules to use push
2020
* notifications to drive their own processing, and prevent it from being received and/or opened by the user.
21-
* [INotificationLifecycleEventHandler]: Used to hook into the received/opened events of a notification.
2221
*
2322
* External
2423
* --------
@@ -32,17 +31,11 @@ import org.json.JSONObject
3231
* 3. [INotificationServiceExtension.onNotificationReceived] To pre-process the notification, notification may be removed/changed. (Specified in AndroidManifest.xml).
3332
* 4. [INotificationLifecycleListener.onWillDisplay] To pre-process the notification while app in foreground, notification may be removed/changed.
3433
* 5. Process/Display the notification
35-
* 6. [INotificationLifecycleEventHandler.onNotificationReceived] To indicate the notification has been received and processed.
36-
* 7. User "opens" or "dismisses" the notification
37-
* 8. [INotificationLifecycleCallback.canOpenNotification] To determine if the notification can be opened by the notification module, or should be ignored.
38-
* 9. [INotificationLifecycleEventHandler.onNotificationOpened] To indicate the notification has been opened.
39-
* 10. [INotificationClickListener.onClick] To indicate the notification has been opened.
34+
* 6. User "opens" or "dismisses" the notification
35+
* 7. [INotificationLifecycleCallback.canOpenNotification] To determine if the notification can be opened by the notification module, or should be ignored.
36+
* 8. [INotificationClickListener.onClick] To indicate the notification has been opened.
4037
*/
4138
interface INotificationLifecycleService {
42-
fun addInternalNotificationLifecycleEventHandler(handler: INotificationLifecycleEventHandler)
43-
44-
fun removeInternalNotificationLifecycleEventHandler(handler: INotificationLifecycleEventHandler)
45-
4639
fun setInternalNotificationLifecycleCallback(callback: INotificationLifecycleCallback?)
4740

4841
fun addExternalForegroundLifecycleListener(listener: INotificationLifecycleListener)

OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,58 @@ package com.onesignal.notifications.internal.lifecycle.impl
33
import android.app.Activity
44
import android.content.Context
55
import com.onesignal.common.AndroidUtils
6+
import com.onesignal.common.JSONUtils
67
import com.onesignal.common.events.CallbackProducer
78
import com.onesignal.common.events.EventProducer
9+
import com.onesignal.common.exceptions.BackendException
10+
import com.onesignal.core.internal.application.AppEntryAction
811
import com.onesignal.core.internal.application.IApplicationService
12+
import com.onesignal.core.internal.config.ConfigModelStore
13+
import com.onesignal.core.internal.device.IDeviceService
914
import com.onesignal.core.internal.time.ITime
1015
import com.onesignal.debug.internal.logging.Logging
1116
import com.onesignal.notifications.INotificationClickListener
1217
import com.onesignal.notifications.INotificationLifecycleListener
1318
import com.onesignal.notifications.INotificationReceivedEvent
1419
import com.onesignal.notifications.INotificationServiceExtension
1520
import com.onesignal.notifications.INotificationWillDisplayEvent
21+
import com.onesignal.notifications.internal.INotificationActivityOpener
22+
import com.onesignal.notifications.internal.analytics.IAnalyticsTracker
23+
import com.onesignal.notifications.internal.backend.INotificationBackendService
24+
import com.onesignal.notifications.internal.common.GenerateNotificationOpenIntentFromPushPayload
1625
import com.onesignal.notifications.internal.common.NotificationConstants
26+
import com.onesignal.notifications.internal.common.NotificationFormatHelper
1727
import com.onesignal.notifications.internal.common.NotificationGenerationJob
1828
import com.onesignal.notifications.internal.common.NotificationHelper
29+
import com.onesignal.notifications.internal.common.OSNotificationOpenAppSettings
1930
import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleCallback
20-
import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleEventHandler
2131
import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleService
32+
import com.onesignal.notifications.internal.receivereceipt.IReceiveReceiptWorkManager
33+
import com.onesignal.session.internal.influence.IInfluenceManager
34+
import com.onesignal.user.internal.subscriptions.ISubscriptionManager
2235
import org.json.JSONArray
36+
import org.json.JSONException
2337
import org.json.JSONObject
2438

2539
internal class NotificationLifecycleService(
2640
applicationService: IApplicationService,
2741
private val _time: ITime,
28-
) : INotificationLifecycleService {
29-
private val intLifecycleHandler = EventProducer<INotificationLifecycleEventHandler>()
42+
private val _applicationService: IApplicationService,
43+
private val _configModelStore: ConfigModelStore,
44+
private val _influenceManager: IInfluenceManager,
45+
private val _subscriptionManager: ISubscriptionManager,
46+
private val _deviceService: IDeviceService,
47+
private val _backend: INotificationBackendService,
48+
private val _receiveReceiptWorkManager: IReceiveReceiptWorkManager,
49+
private val _analyticsTracker: IAnalyticsTracker,
50+
) : INotificationLifecycleService,
51+
INotificationActivityOpener {
3052
private val intLifecycleCallback = CallbackProducer<INotificationLifecycleCallback>()
3153
private val extRemoteReceivedCallback = CallbackProducer<INotificationServiceExtension>()
3254
private val extWillShowInForegroundCallback = EventProducer<INotificationLifecycleListener>()
3355
private val extOpenedCallback = EventProducer<INotificationClickListener>()
3456
private val unprocessedOpenedNotifs: ArrayDeque<JSONArray> = ArrayDeque()
35-
36-
override fun addInternalNotificationLifecycleEventHandler(handler: INotificationLifecycleEventHandler) =
37-
intLifecycleHandler.subscribe(
38-
handler,
39-
)
40-
41-
override fun removeInternalNotificationLifecycleEventHandler(handler: INotificationLifecycleEventHandler) =
42-
intLifecycleHandler.unsubscribe(
43-
handler,
44-
)
57+
private val postedOpenedNotifIds = mutableSetOf<String>()
4558

4659
override fun setInternalNotificationLifecycleCallback(callback: INotificationLifecycleCallback?) = intLifecycleCallback.set(callback)
4760

@@ -75,12 +88,28 @@ internal class NotificationLifecycleService(
7588

7689
override suspend fun canReceiveNotification(jsonPayload: JSONObject): Boolean {
7790
var canReceive = true
91+
// TODO: This can have late binding issues too
7892
intLifecycleCallback.suspendingFire { canReceive = it.canReceiveNotification(jsonPayload) }
7993
return canReceive
8094
}
8195

8296
override suspend fun notificationReceived(notificationJob: NotificationGenerationJob) {
83-
intLifecycleHandler.suspendingFire { it.onNotificationReceived(notificationJob) }
97+
_receiveReceiptWorkManager.enqueueReceiveReceipt(notificationJob.apiNotificationId)
98+
99+
_influenceManager.onNotificationReceived(notificationJob.apiNotificationId)
100+
101+
try {
102+
val jsonObject = JSONObject(notificationJob.jsonPayload.toString())
103+
jsonObject.put(NotificationConstants.BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationJob.androidId)
104+
val openResult = NotificationHelper.generateNotificationOpenedResult(JSONUtils.wrapInJsonArray(jsonObject), _time)
105+
106+
_analyticsTracker.trackReceivedEvent(
107+
openResult.notification.notificationId!!,
108+
NotificationHelper.getCampaignNameFromNotification(openResult.notification),
109+
)
110+
} catch (e: JSONException) {
111+
e.printStackTrace()
112+
}
84113
}
85114

86115
override suspend fun canOpenNotification(
@@ -96,7 +125,50 @@ internal class NotificationLifecycleService(
96125
activity: Activity,
97126
data: JSONArray,
98127
) {
99-
intLifecycleHandler.suspendingFire { it.onNotificationOpened(activity, data) }
128+
val config = _configModelStore.model
129+
val appId: String = config.appId ?: ""
130+
val subscriptionId: String = _subscriptionManager.subscriptions.push.id
131+
val deviceType = _deviceService.deviceType
132+
133+
for (i in 0 until data.length()) {
134+
val notificationId = NotificationFormatHelper.getOSNotificationIdFromJson(data[i] as JSONObject?) ?: continue
135+
136+
if (postedOpenedNotifIds.contains(notificationId)) {
137+
continue
138+
}
139+
140+
postedOpenedNotifIds.add(notificationId)
141+
142+
try {
143+
_backend.updateNotificationAsOpened(
144+
appId,
145+
notificationId,
146+
subscriptionId,
147+
deviceType,
148+
)
149+
} catch (ex: BackendException) {
150+
Logging.error("Notification opened confirmation failed with statusCode: ${ex.statusCode} response: ${ex.response}")
151+
}
152+
}
153+
154+
val openResult = NotificationHelper.generateNotificationOpenedResult(data, _time)
155+
_analyticsTracker.trackOpenedEvent(
156+
openResult.notification.notificationId!!,
157+
NotificationHelper.getCampaignNameFromNotification(openResult.notification),
158+
)
159+
160+
// Handle the first element in the data array as the latest notification
161+
val latestNotificationId = getLatestNotificationId(data)
162+
163+
if (shouldInitDirectSessionFromNotificationOpen(activity)) {
164+
// We want to set the app entry state to NOTIFICATION_CLICK when coming from background
165+
_applicationService.entryState = AppEntryAction.NOTIFICATION_CLICK
166+
if (latestNotificationId != null) {
167+
_influenceManager.onDirectInfluenceFromNotification(latestNotificationId)
168+
}
169+
}
170+
171+
openDestinationActivity(activity, data)
100172

101173
// queue up the opened notification in case the handler hasn't been set yet. Once set,
102174
// we will immediately fire the handler.
@@ -161,4 +233,43 @@ internal class NotificationLifecycleService(
161233
e.printStackTrace()
162234
}
163235
}
236+
237+
private fun shouldInitDirectSessionFromNotificationOpen(context: Activity): Boolean {
238+
if (_applicationService.isInForeground) {
239+
return false
240+
}
241+
242+
try {
243+
return OSNotificationOpenAppSettings.getShouldOpenActivity(context)
244+
} catch (e: JSONException) {
245+
e.printStackTrace()
246+
}
247+
return true
248+
}
249+
250+
private fun getLatestNotificationId(data: JSONArray): String? {
251+
val latestNotification = if (data.length() > 0) data[0] as JSONObject else null
252+
return NotificationFormatHelper.getOSNotificationIdFromJson(latestNotification)
253+
}
254+
255+
override suspend fun openDestinationActivity(
256+
activity: Activity,
257+
pushPayloads: JSONArray,
258+
) {
259+
try {
260+
// Always use the top most notification if user tapped on the summary notification
261+
val firstPayloadItem = pushPayloads.getJSONObject(0)
262+
val intentGenerator = GenerateNotificationOpenIntentFromPushPayload.create(activity, firstPayloadItem)
263+
264+
val intent = intentGenerator.getIntentVisible()
265+
if (intent != null) {
266+
Logging.info("SDK running startActivity with Intent: $intent")
267+
activity.startActivity(intent)
268+
} else {
269+
Logging.info("SDK not showing an Activity automatically due to it's settings.")
270+
}
271+
} catch (e: JSONException) {
272+
e.printStackTrace()
273+
}
274+
}
164275
}

0 commit comments

Comments
 (0)