From bc876fc0b50096f3a84daa77cd1aff77fb2fe02f Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Wed, 30 Apr 2025 14:34:20 -0400 Subject: [PATCH 1/2] 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. --- .../notifications/NotificationsModule.kt | 4 +- .../internal/NotificationsManager.kt | 25 ---- .../INotificationLifecycleEventHandler.kt | 17 --- .../INotificationLifecycleService.kt | 13 +- .../impl/NotificationLifecycleService.kt | 141 ++++++++++++++++-- .../listeners/NotificationListener.kt | 135 ----------------- 6 files changed, 130 insertions(+), 205 deletions(-) delete mode 100644 OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleEventHandler.kt delete mode 100644 OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/listeners/NotificationListener.kt diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/NotificationsModule.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/NotificationsModule.kt index a5a4679007..c26dea5a45 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/NotificationsModule.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/NotificationsModule.kt @@ -39,7 +39,6 @@ import com.onesignal.notifications.internal.lifecycle.impl.NotificationLifecycle import com.onesignal.notifications.internal.limiting.INotificationLimitManager import com.onesignal.notifications.internal.limiting.impl.NotificationLimitManager import com.onesignal.notifications.internal.listeners.DeviceRegistrationListener -import com.onesignal.notifications.internal.listeners.NotificationListener import com.onesignal.notifications.internal.open.INotificationOpenedProcessor import com.onesignal.notifications.internal.open.INotificationOpenedProcessorHMS import com.onesignal.notifications.internal.open.impl.NotificationOpenedProcessor @@ -94,6 +93,7 @@ internal class NotificationsModule : IModule { builder.register() .provides() + .provides() builder.register { if (FirebaseAnalyticsTracker.canTrack()) { @@ -141,10 +141,8 @@ internal class NotificationsModule : IModule { // Startable services builder.register().provides() - builder.register().provides() builder.register() .provides() - .provides() } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationsManager.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationsManager.kt index 96a6bad747..f835a4a502 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationsManager.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationsManager.kt @@ -10,7 +10,6 @@ import com.onesignal.notifications.INotificationClickListener import com.onesignal.notifications.INotificationLifecycleListener import com.onesignal.notifications.INotificationsManager import com.onesignal.notifications.IPermissionObserver -import com.onesignal.notifications.internal.common.GenerateNotificationOpenIntentFromPushPayload import com.onesignal.notifications.internal.common.NotificationHelper import com.onesignal.notifications.internal.data.INotificationRepository import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleService @@ -21,7 +20,6 @@ import com.onesignal.notifications.internal.summary.INotificationSummaryManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONArray -import org.json.JSONException interface INotificationActivityOpener { suspend fun openDestinationActivity( @@ -42,7 +40,6 @@ internal class NotificationsManager( private val _notificationDataController: INotificationRepository, private val _summaryManager: INotificationSummaryManager, ) : INotificationsManager, - INotificationActivityOpener, INotificationPermissionChangedHandler, IApplicationLifecycleHandler { override var permission: Boolean = NotificationHelper.areNotificationsEnabled(_applicationService.appContext) @@ -159,26 +156,4 @@ internal class NotificationsManager( Logging.debug("NotificationsManager.removeClickListener(listener: $listener)") _notificationLifecycleService.removeExternalClickListener(listener) } - - override suspend fun openDestinationActivity( - activity: Activity, - pushPayloads: JSONArray, - ) { - try { - // Always use the top most notification if user tapped on the summary notification - val firstPayloadItem = pushPayloads.getJSONObject(0) - // val isHandled = _notificationLifecycleService.canOpenNotification(activity, firstPayloadItem) - val intentGenerator = GenerateNotificationOpenIntentFromPushPayload.create(activity, firstPayloadItem) - - val intent = intentGenerator.getIntentVisible() - if (intent != null) { - Logging.info("SDK running startActivity with Intent: $intent") - activity.startActivity(intent) - } else { - Logging.info("SDK not showing an Activity automatically due to it's settings.") - } - } catch (e: JSONException) { - e.printStackTrace() - } - } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleEventHandler.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleEventHandler.kt deleted file mode 100644 index d19327be7b..0000000000 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleEventHandler.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.onesignal.notifications.internal.lifecycle - -import android.app.Activity -import com.onesignal.notifications.internal.common.NotificationGenerationJob -import org.json.JSONArray - -interface INotificationLifecycleEventHandler { - /** - * Called *after* the notification has been generated and processed by the SDK. - */ - suspend fun onNotificationReceived(notificationJob: NotificationGenerationJob) - - suspend fun onNotificationOpened( - activity: Activity, - data: JSONArray, - ) -} diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleService.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleService.kt index 3dc7f3da5d..377f55883e 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleService.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/INotificationLifecycleService.kt @@ -18,7 +18,6 @@ import org.json.JSONObject * -------- * [INotificationLifecycleCallback]: Can be used by other modules to use push * notifications to drive their own processing, and prevent it from being received and/or opened by the user. - * [INotificationLifecycleEventHandler]: Used to hook into the received/opened events of a notification. * * External * -------- @@ -32,17 +31,11 @@ import org.json.JSONObject * 3. [INotificationServiceExtension.onNotificationReceived] To pre-process the notification, notification may be removed/changed. (Specified in AndroidManifest.xml). * 4. [INotificationLifecycleListener.onWillDisplay] To pre-process the notification while app in foreground, notification may be removed/changed. * 5. Process/Display the notification - * 6. [INotificationLifecycleEventHandler.onNotificationReceived] To indicate the notification has been received and processed. - * 7. User "opens" or "dismisses" the notification - * 8. [INotificationLifecycleCallback.canOpenNotification] To determine if the notification can be opened by the notification module, or should be ignored. - * 9. [INotificationLifecycleEventHandler.onNotificationOpened] To indicate the notification has been opened. - * 10. [INotificationClickListener.onClick] To indicate the notification has been opened. + * 6. User "opens" or "dismisses" the notification + * 7. [INotificationLifecycleCallback.canOpenNotification] To determine if the notification can be opened by the notification module, or should be ignored. + * 8. [INotificationClickListener.onClick] To indicate the notification has been opened. */ interface INotificationLifecycleService { - fun addInternalNotificationLifecycleEventHandler(handler: INotificationLifecycleEventHandler) - - fun removeInternalNotificationLifecycleEventHandler(handler: INotificationLifecycleEventHandler) - fun setInternalNotificationLifecycleCallback(callback: INotificationLifecycleCallback?) fun addExternalForegroundLifecycleListener(listener: INotificationLifecycleListener) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt index cd10dd9b93..036a8cec28 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt @@ -3,9 +3,14 @@ package com.onesignal.notifications.internal.lifecycle.impl import android.app.Activity import android.content.Context import com.onesignal.common.AndroidUtils +import com.onesignal.common.JSONUtils import com.onesignal.common.events.CallbackProducer import com.onesignal.common.events.EventProducer +import com.onesignal.common.exceptions.BackendException +import com.onesignal.core.internal.application.AppEntryAction import com.onesignal.core.internal.application.IApplicationService +import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.device.IDeviceService import com.onesignal.core.internal.time.ITime import com.onesignal.debug.internal.logging.Logging import com.onesignal.notifications.INotificationClickListener @@ -13,35 +18,43 @@ import com.onesignal.notifications.INotificationLifecycleListener import com.onesignal.notifications.INotificationReceivedEvent import com.onesignal.notifications.INotificationServiceExtension import com.onesignal.notifications.INotificationWillDisplayEvent +import com.onesignal.notifications.internal.INotificationActivityOpener +import com.onesignal.notifications.internal.analytics.IAnalyticsTracker +import com.onesignal.notifications.internal.backend.INotificationBackendService +import com.onesignal.notifications.internal.common.GenerateNotificationOpenIntentFromPushPayload import com.onesignal.notifications.internal.common.NotificationConstants +import com.onesignal.notifications.internal.common.NotificationFormatHelper import com.onesignal.notifications.internal.common.NotificationGenerationJob import com.onesignal.notifications.internal.common.NotificationHelper +import com.onesignal.notifications.internal.common.OSNotificationOpenAppSettings import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleCallback -import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleEventHandler import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleService +import com.onesignal.notifications.internal.receivereceipt.IReceiveReceiptWorkManager +import com.onesignal.session.internal.influence.IInfluenceManager +import com.onesignal.user.internal.subscriptions.ISubscriptionManager import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject internal class NotificationLifecycleService( applicationService: IApplicationService, private val _time: ITime, -) : INotificationLifecycleService { - private val intLifecycleHandler = EventProducer() + private val _applicationService: IApplicationService, + private val _configModelStore: ConfigModelStore, + private val _influenceManager: IInfluenceManager, + private val _subscriptionManager: ISubscriptionManager, + private val _deviceService: IDeviceService, + private val _backend: INotificationBackendService, + private val _receiveReceiptWorkManager: IReceiveReceiptWorkManager, + private val _analyticsTracker: IAnalyticsTracker, +) : INotificationLifecycleService, + INotificationActivityOpener { private val intLifecycleCallback = CallbackProducer() private val extRemoteReceivedCallback = CallbackProducer() private val extWillShowInForegroundCallback = EventProducer() private val extOpenedCallback = EventProducer() private val unprocessedOpenedNotifs: ArrayDeque = ArrayDeque() - - override fun addInternalNotificationLifecycleEventHandler(handler: INotificationLifecycleEventHandler) = - intLifecycleHandler.subscribe( - handler, - ) - - override fun removeInternalNotificationLifecycleEventHandler(handler: INotificationLifecycleEventHandler) = - intLifecycleHandler.unsubscribe( - handler, - ) + private val postedOpenedNotifIds = mutableSetOf() override fun setInternalNotificationLifecycleCallback(callback: INotificationLifecycleCallback?) = intLifecycleCallback.set(callback) @@ -75,12 +88,28 @@ internal class NotificationLifecycleService( override suspend fun canReceiveNotification(jsonPayload: JSONObject): Boolean { var canReceive = true + // TODO: This can have late binding issues too intLifecycleCallback.suspendingFire { canReceive = it.canReceiveNotification(jsonPayload) } return canReceive } override suspend fun notificationReceived(notificationJob: NotificationGenerationJob) { - intLifecycleHandler.suspendingFire { it.onNotificationReceived(notificationJob) } + _receiveReceiptWorkManager.enqueueReceiveReceipt(notificationJob.apiNotificationId) + + _influenceManager.onNotificationReceived(notificationJob.apiNotificationId) + + try { + val jsonObject = JSONObject(notificationJob.jsonPayload.toString()) + jsonObject.put(NotificationConstants.BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationJob.androidId) + val openResult = NotificationHelper.generateNotificationOpenedResult(JSONUtils.wrapInJsonArray(jsonObject), _time) + + _analyticsTracker.trackReceivedEvent( + openResult.notification.notificationId!!, + NotificationHelper.getCampaignNameFromNotification(openResult.notification), + ) + } catch (e: JSONException) { + e.printStackTrace() + } } override suspend fun canOpenNotification( @@ -96,7 +125,50 @@ internal class NotificationLifecycleService( activity: Activity, data: JSONArray, ) { - intLifecycleHandler.suspendingFire { it.onNotificationOpened(activity, data) } + val config = _configModelStore.model + val appId: String = config.appId ?: "" + val subscriptionId: String = _subscriptionManager.subscriptions.push.id + val deviceType = _deviceService.deviceType + + for (i in 0 until data.length()) { + val notificationId = NotificationFormatHelper.getOSNotificationIdFromJson(data[i] as JSONObject?) ?: continue + + if (postedOpenedNotifIds.contains(notificationId)) { + continue + } + + postedOpenedNotifIds.add(notificationId) + + try { + _backend.updateNotificationAsOpened( + appId, + notificationId, + subscriptionId, + deviceType, + ) + } catch (ex: BackendException) { + Logging.error("Notification opened confirmation failed with statusCode: ${ex.statusCode} response: ${ex.response}") + } + } + + val openResult = NotificationHelper.generateNotificationOpenedResult(data, _time) + _analyticsTracker.trackOpenedEvent( + openResult.notification.notificationId!!, + NotificationHelper.getCampaignNameFromNotification(openResult.notification), + ) + + // Handle the first element in the data array as the latest notification + val latestNotificationId = getLatestNotificationId(data) + + if (shouldInitDirectSessionFromNotificationOpen(activity)) { + // We want to set the app entry state to NOTIFICATION_CLICK when coming from background + _applicationService.entryState = AppEntryAction.NOTIFICATION_CLICK + if (latestNotificationId != null) { + _influenceManager.onDirectInfluenceFromNotification(latestNotificationId) + } + } + + openDestinationActivity(activity, data) // queue up the opened notification in case the handler hasn't been set yet. Once set, // we will immediately fire the handler. @@ -161,4 +233,43 @@ internal class NotificationLifecycleService( e.printStackTrace() } } + + private fun shouldInitDirectSessionFromNotificationOpen(context: Activity): Boolean { + if (_applicationService.isInForeground) { + return false + } + + try { + return OSNotificationOpenAppSettings.getShouldOpenActivity(context) + } catch (e: JSONException) { + e.printStackTrace() + } + return true + } + + private fun getLatestNotificationId(data: JSONArray): String? { + val latestNotification = if (data.length() > 0) data[0] as JSONObject else null + return NotificationFormatHelper.getOSNotificationIdFromJson(latestNotification) + } + + override suspend fun openDestinationActivity( + activity: Activity, + pushPayloads: JSONArray, + ) { + try { + // Always use the top most notification if user tapped on the summary notification + val firstPayloadItem = pushPayloads.getJSONObject(0) + val intentGenerator = GenerateNotificationOpenIntentFromPushPayload.create(activity, firstPayloadItem) + + val intent = intentGenerator.getIntentVisible() + if (intent != null) { + Logging.info("SDK running startActivity with Intent: $intent") + activity.startActivity(intent) + } else { + Logging.info("SDK not showing an Activity automatically due to it's settings.") + } + } catch (e: JSONException) { + e.printStackTrace() + } + } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/listeners/NotificationListener.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/listeners/NotificationListener.kt deleted file mode 100644 index 022048632c..0000000000 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/listeners/NotificationListener.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.onesignal.notifications.internal.listeners - -import android.app.Activity -import com.onesignal.common.JSONUtils -import com.onesignal.common.exceptions.BackendException -import com.onesignal.core.internal.application.AppEntryAction -import com.onesignal.core.internal.application.IApplicationService -import com.onesignal.core.internal.config.ConfigModelStore -import com.onesignal.core.internal.device.IDeviceService -import com.onesignal.core.internal.startup.IStartableService -import com.onesignal.core.internal.time.ITime -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.notifications.internal.INotificationActivityOpener -import com.onesignal.notifications.internal.analytics.IAnalyticsTracker -import com.onesignal.notifications.internal.backend.INotificationBackendService -import com.onesignal.notifications.internal.common.NotificationConstants -import com.onesignal.notifications.internal.common.NotificationFormatHelper -import com.onesignal.notifications.internal.common.NotificationGenerationJob -import com.onesignal.notifications.internal.common.NotificationHelper -import com.onesignal.notifications.internal.common.OSNotificationOpenAppSettings -import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleEventHandler -import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleService -import com.onesignal.notifications.internal.receivereceipt.IReceiveReceiptWorkManager -import com.onesignal.session.internal.influence.IInfluenceManager -import com.onesignal.user.internal.subscriptions.ISubscriptionManager -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject - -internal class NotificationListener( - private val _applicationService: IApplicationService, - private val _notificationLifecycleService: INotificationLifecycleService, - private val _configModelStore: ConfigModelStore, - private val _influenceManager: IInfluenceManager, - private val _subscriptionManager: ISubscriptionManager, - private val _deviceService: IDeviceService, - private val _backend: INotificationBackendService, - private val _receiveReceiptWorkManager: IReceiveReceiptWorkManager, - private val _activityOpener: INotificationActivityOpener, - private val _analyticsTracker: IAnalyticsTracker, - private val _time: ITime, -) : IStartableService, INotificationLifecycleEventHandler { - private val postedOpenedNotifIds = mutableSetOf() - - override fun start() { - _notificationLifecycleService.addInternalNotificationLifecycleEventHandler(this) - } - - override suspend fun onNotificationReceived(notificationJob: NotificationGenerationJob) { - _receiveReceiptWorkManager.enqueueReceiveReceipt(notificationJob.apiNotificationId) - - _influenceManager.onNotificationReceived(notificationJob.apiNotificationId) - - try { - val jsonObject = JSONObject(notificationJob.jsonPayload.toString()) - jsonObject.put(NotificationConstants.BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationJob.androidId) - val openResult = NotificationHelper.generateNotificationOpenedResult(JSONUtils.wrapInJsonArray(jsonObject), _time) - - _analyticsTracker.trackReceivedEvent( - openResult.notification.notificationId!!, - NotificationHelper.getCampaignNameFromNotification(openResult.notification), - ) - } catch (e: JSONException) { - e.printStackTrace() - } - } - - override suspend fun onNotificationOpened( - activity: Activity, - data: JSONArray, - ) { - val config = _configModelStore.model - val appId: String = config.appId ?: "" - val subscriptionId: String = _subscriptionManager.subscriptions.push.id - val deviceType = _deviceService.deviceType - - for (i in 0 until data.length()) { - val notificationId = NotificationFormatHelper.getOSNotificationIdFromJson(data[i] as JSONObject?) ?: continue - - if (postedOpenedNotifIds.contains(notificationId)) { - continue - } - - postedOpenedNotifIds.add(notificationId) - - try { - _backend.updateNotificationAsOpened( - appId, - notificationId, - subscriptionId, - deviceType, - ) - } catch (ex: BackendException) { - Logging.error("Notification opened confirmation failed with statusCode: ${ex.statusCode} response: ${ex.response}") - } - } - - val openResult = NotificationHelper.generateNotificationOpenedResult(data, _time) - _analyticsTracker.trackOpenedEvent( - openResult.notification.notificationId!!, - NotificationHelper.getCampaignNameFromNotification(openResult.notification), - ) - - // Handle the first element in the data array as the latest notification - val latestNotificationId = getLatestNotificationId(data) - - if (shouldInitDirectSessionFromNotificationOpen(activity)) { - // We want to set the app entry state to NOTIFICATION_CLICK when coming from background - _applicationService.entryState = AppEntryAction.NOTIFICATION_CLICK - if (latestNotificationId != null) { - _influenceManager.onDirectInfluenceFromNotification(latestNotificationId) - } - } - - _activityOpener.openDestinationActivity(activity, data) - } - - private fun shouldInitDirectSessionFromNotificationOpen(context: Activity): Boolean { - if (_applicationService.isInForeground) { - return false - } - - try { - return OSNotificationOpenAppSettings.getShouldOpenActivity(context) - } catch (e: JSONException) { - e.printStackTrace() - } - return true - } - - private fun getLatestNotificationId(data: JSONArray): String? { - val latestNotification = if (data.length() > 0) data[0] as JSONObject else null - return NotificationFormatHelper.getOSNotificationIdFromJson(latestNotification) - } -} From 571f7ec58a2ff80b46d6fc29098483ae40afebe2 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Wed, 30 Apr 2025 19:19:05 -0400 Subject: [PATCH 2/2] added NotificationLifecycleServiceTests --- .../impl/NotificationLifecycleService.kt | 5 +- .../NotificationLifecycleServiceTests.kt | 97 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/lifecycle/NotificationLifecycleServiceTests.kt diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt index 036a8cec28..b4ae79a5ec 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt @@ -37,9 +37,8 @@ import org.json.JSONException import org.json.JSONObject internal class NotificationLifecycleService( - applicationService: IApplicationService, - private val _time: ITime, private val _applicationService: IApplicationService, + private val _time: ITime, private val _configModelStore: ConfigModelStore, private val _influenceManager: IInfluenceManager, private val _subscriptionManager: ISubscriptionManager, @@ -83,7 +82,7 @@ internal class NotificationLifecycleService( override fun removeExternalClickListener(listener: INotificationClickListener) = extOpenedCallback.unsubscribe(listener) init { - setupNotificationServiceExtension(applicationService.appContext) + setupNotificationServiceExtension(_applicationService.appContext) } override suspend fun canReceiveNotification(jsonPayload: JSONObject): Boolean { diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/lifecycle/NotificationLifecycleServiceTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/lifecycle/NotificationLifecycleServiceTests.kt new file mode 100644 index 0000000000..d98d58948a --- /dev/null +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/lifecycle/NotificationLifecycleServiceTests.kt @@ -0,0 +1,97 @@ +package com.onesignal.notifications.internal.lifecycle + +import android.app.Activity +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import com.onesignal.core.internal.application.impl.ApplicationService +import com.onesignal.core.internal.device.IDeviceService +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.mocks.MockHelper +import com.onesignal.notifications.internal.analytics.IAnalyticsTracker +import com.onesignal.notifications.internal.backend.INotificationBackendService +import com.onesignal.notifications.internal.lifecycle.impl.NotificationLifecycleService +import com.onesignal.notifications.internal.receivereceipt.IReceiveReceiptWorkManager +import com.onesignal.notifications.shadows.ShadowRoboNotificationManager +import com.onesignal.session.internal.influence.IInfluenceManager +import com.onesignal.user.internal.subscriptions.ISubscriptionManager +import com.onesignal.user.subscriptions.IPushSubscription +import io.kotest.core.spec.style.FunSpec +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import org.json.JSONArray +import org.json.JSONObject +import org.robolectric.Robolectric + +@RobolectricTest +class NotificationLifecycleServiceTests : FunSpec({ + beforeAny { + Logging.logLevel = LogLevel.NONE + ShadowRoboNotificationManager.reset() + } + + test("Fires openDestinationActivity") { + // Given + val context = ApplicationProvider.getApplicationContext() + val applicationService = ApplicationService() + applicationService.start(context) + + val mockSubscriptionManager = mockk() + every { mockSubscriptionManager.subscriptions.push } returns + mockk().apply { every { id } returns "UUID1" } + + val notificationLifecycleService = + spyk( + NotificationLifecycleService( + applicationService, + MockHelper.time(0), + MockHelper.configModelStore(), + mockk().apply { + every { onDirectInfluenceFromNotification(any()) } returns Unit + }, + mockSubscriptionManager, + mockk().apply { + every { deviceType } returns IDeviceService.DeviceType.Android + }, + mockk().apply { + coEvery { updateNotificationAsOpened(any(), any(), any(), any()) } returns Unit + }, + mockk(), + mockk().apply { + every { trackOpenedEvent(any(), any()) } returns Unit + }, + ), + ) + val activity: Activity + Robolectric.buildActivity(Activity::class.java).use { controller -> + controller.setup() // Moves Activity to RESUMED state + activity = controller.get() + } + + // When + val payload = + JSONArray() + .put( + JSONObject() + .put("alert", "test message") + .put( + "custom", + JSONObject() + .put("i", "UUID1"), + ), + ) + notificationLifecycleService.notificationOpened(activity, payload) + + // Then + coVerify(exactly = 1) { + notificationLifecycleService.openDestinationActivity( + withArg { Any() }, + withArg { Any() }, + ) + } + } +})