Skip to content

Commit 485b1a0

Browse files
authored
Merge pull request #2289 from OneSignal/fix/rare-notification-click-not-opening-app
[Fix] very rare bug where app doesn't opening from notification tap when it cold starts the app
2 parents aaf6693 + 571f7ec commit 485b1a0

File tree

7 files changed

+228
-207
lines changed

7 files changed

+228
-207
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: 127 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,57 @@ 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(
26-
applicationService: IApplicationService,
40+
private val _applicationService: IApplicationService,
2741
private val _time: ITime,
28-
) : INotificationLifecycleService {
29-
private val intLifecycleHandler = EventProducer<INotificationLifecycleEventHandler>()
42+
private val _configModelStore: ConfigModelStore,
43+
private val _influenceManager: IInfluenceManager,
44+
private val _subscriptionManager: ISubscriptionManager,
45+
private val _deviceService: IDeviceService,
46+
private val _backend: INotificationBackendService,
47+
private val _receiveReceiptWorkManager: IReceiveReceiptWorkManager,
48+
private val _analyticsTracker: IAnalyticsTracker,
49+
) : INotificationLifecycleService,
50+
INotificationActivityOpener {
3051
private val intLifecycleCallback = CallbackProducer<INotificationLifecycleCallback>()
3152
private val extRemoteReceivedCallback = CallbackProducer<INotificationServiceExtension>()
3253
private val extWillShowInForegroundCallback = EventProducer<INotificationLifecycleListener>()
3354
private val extOpenedCallback = EventProducer<INotificationClickListener>()
3455
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-
)
56+
private val postedOpenedNotifIds = mutableSetOf<String>()
4557

4658
override fun setInternalNotificationLifecycleCallback(callback: INotificationLifecycleCallback?) = intLifecycleCallback.set(callback)
4759

@@ -70,17 +82,33 @@ internal class NotificationLifecycleService(
7082
override fun removeExternalClickListener(listener: INotificationClickListener) = extOpenedCallback.unsubscribe(listener)
7183

7284
init {
73-
setupNotificationServiceExtension(applicationService.appContext)
85+
setupNotificationServiceExtension(_applicationService.appContext)
7486
}
7587

7688
override suspend fun canReceiveNotification(jsonPayload: JSONObject): Boolean {
7789
var canReceive = true
90+
// TODO: This can have late binding issues too
7891
intLifecycleCallback.suspendingFire { canReceive = it.canReceiveNotification(jsonPayload) }
7992
return canReceive
8093
}
8194

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

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

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

0 commit comments

Comments
 (0)