From bca0573915a61d8cec9735fcecc2486fa95941c2 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Fri, 22 Aug 2025 11:49:30 -0500 Subject: [PATCH 1/3] Activity was not being closed on main thread instead background thread --- .../java/com/onesignal/common/AndroidUtils.kt | 10 ++++ .../onesignal/common/threading/ThreadUtils.kt | 19 ++++++- .../NotificationOpenedActivityBase.kt | 28 +++++---- .../NotificationOpenedActivityBaseTest.kt | 57 +++++++++++++++++++ 4 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/activities/NotificationOpenedActivityBaseTest.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/AndroidUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/AndroidUtils.kt index 69ad3c7aaf..b8a49e9551 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/AndroidUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/AndroidUtils.kt @@ -285,4 +285,14 @@ object AndroidUtils { } } } + + /** + * Safely finishes the activity only if it's still valid. + * Prevents redundant or unsafe calls to finish(), reducing lifecycle issues or potential leaks. + */ + fun finishSafely(activity: Activity) { + if (!activity.isDestroyed && !activity.isFinishing) { + activity.finish() + } + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt index 1a6b4ff48c..ad272887d8 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt @@ -63,12 +63,25 @@ fun suspendifyOnMain(block: suspend () -> Unit) { fun suspendifyOnThread( priority: Int = -1, block: suspend () -> Unit, +) { + suspendifyOnThread(priority, block, null) +} + +/** + * Allows a non suspending function to create a scope that can + * call suspending functions. This is a nonblocking call, which + * means the scope will run on a background thread. This will + * return immediately!!! Also provides an optional onComplete. + */ +fun suspendifyOnThread( + priority: Int = -1, + block: suspend () -> Unit, + onComplete: (() -> Unit)? = null ) { thread(priority = priority) { try { - runBlocking { - block() - } + runBlocking { block() } + onComplete?.invoke() } catch (e: Exception) { Logging.error("Exception on thread", e) } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt index ab89cec04a..e031c6c347 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt @@ -30,6 +30,7 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import com.onesignal.OneSignal +import com.onesignal.common.AndroidUtils import com.onesignal.common.threading.suspendifyOnThread import com.onesignal.notifications.internal.open.INotificationOpenedProcessor @@ -44,18 +45,25 @@ abstract class NotificationOpenedActivityBase : Activity() { processIntent() } - private fun processIntent() { + internal open fun processIntent() { if (!OneSignal.initWithContext(applicationContext)) { return } - suspendifyOnThread { - val openedProcessor = OneSignal.getService() - openedProcessor.processFromContext(this, intent) - // KEEP: Xiaomi Compatibility: - // Must keep this Activity alive while trampolining, that is - // startActivity() must be called BEFORE finish(), otherwise - // the app is never foregrounded. - finish() - } + suspendifyOnThread ( + block = { + val openedProcessor = OneSignal.getService() + openedProcessor.processFromContext(this, intent) + // KEEP: Xiaomi Compatibility: + // Must keep this Activity alive while trampolining, that is + // startActivity() must be called BEFORE finish(), otherwise + // the app is never foregrounded. + }, onComplete = { + // Safely finish the activity on the main thread after processing is complete. + // This gives the system enough time to complete rendering before closing the Trampoline activity. + runOnUiThread { + AndroidUtils.finishSafely(this) + } + } + ) } } diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/activities/NotificationOpenedActivityBaseTest.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/activities/NotificationOpenedActivityBaseTest.kt new file mode 100644 index 0000000000..3913fd4399 --- /dev/null +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/activities/NotificationOpenedActivityBaseTest.kt @@ -0,0 +1,57 @@ +package com.onesignal.notifications.activities + +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import org.robolectric.Robolectric +import org.robolectric.shadows.ShadowLooper + +class TestNotificationOpenedActivity : NotificationOpenedActivityBase() { + var wasFinishCalledOnMainThread = false + var wasProcessIntentCalled = false + + override fun finish() { + wasFinishCalledOnMainThread = Looper.myLooper() == Looper.getMainLooper() + super.finish() + } + + override fun getIntent(): Intent { + return Intent().apply { + putExtra("some_key", "some_value") // simulate a valid OneSignal intent + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // This triggers processIntent inside base + } + + override fun processIntent() { + wasProcessIntentCalled = true + super.processIntent() + } +} + +@RobolectricTest +class NotificationOpenedActivityTest : FunSpec({ + test("finishSafely should be called on main thread") { + val controller = Robolectric.buildActivity(TestNotificationOpenedActivity::class.java) + val activity = controller.setup().get() + Handler(Looper.getMainLooper()).post { + activity.finish() + } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + activity.wasFinishCalledOnMainThread shouldBe true + } + + test("processIntent should be called during activity setup") { + val controller = Robolectric.buildActivity(TestNotificationOpenedActivity::class.java) + val activity = controller.setup().get() + + activity.wasProcessIntentCalled shouldBe true + } +}) \ No newline at end of file From 37f92e7e6908504c90dcd29c323f8ab126c37ba3 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Fri, 22 Aug 2025 12:13:20 -0500 Subject: [PATCH 2/3] fix: Finishing the activity on main thread when a notification is opened --- .../java/com/onesignal/common/threading/ThreadUtils.kt | 2 +- .../activities/NotificationOpenedActivityBase.kt | 7 ++++--- .../activities/NotificationOpenedActivityBaseTest.kt | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt index ad272887d8..6a0506d4c5 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt @@ -76,7 +76,7 @@ fun suspendifyOnThread( fun suspendifyOnThread( priority: Int = -1, block: suspend () -> Unit, - onComplete: (() -> Unit)? = null + onComplete: (() -> Unit)? = null, ) { thread(priority = priority) { try { diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt index e031c6c347..b083e6107e 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt @@ -49,7 +49,7 @@ abstract class NotificationOpenedActivityBase : Activity() { if (!OneSignal.initWithContext(applicationContext)) { return } - suspendifyOnThread ( + suspendifyOnThread( block = { val openedProcessor = OneSignal.getService() openedProcessor.processFromContext(this, intent) @@ -57,13 +57,14 @@ abstract class NotificationOpenedActivityBase : Activity() { // Must keep this Activity alive while trampolining, that is // startActivity() must be called BEFORE finish(), otherwise // the app is never foregrounded. - }, onComplete = { + }, + onComplete = { // Safely finish the activity on the main thread after processing is complete. // This gives the system enough time to complete rendering before closing the Trampoline activity. runOnUiThread { AndroidUtils.finishSafely(this) } - } + }, ) } } diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/activities/NotificationOpenedActivityBaseTest.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/activities/NotificationOpenedActivityBaseTest.kt index 3913fd4399..6e162131e5 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/activities/NotificationOpenedActivityBaseTest.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/activities/NotificationOpenedActivityBaseTest.kt @@ -54,4 +54,4 @@ class NotificationOpenedActivityTest : FunSpec({ activity.wasProcessIntentCalled shouldBe true } -}) \ No newline at end of file +}) From b11bf625cf526825c3df48ef5aec63339a5d3c75 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Tue, 26 Aug 2025 13:55:55 -0500 Subject: [PATCH 3/3] fix: Finishing the activity on main thread when a notification is opened --- .../com/onesignal/common/threading/ThreadUtils.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt index 6a0506d4c5..504a0e4339 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt @@ -72,7 +72,17 @@ fun suspendifyOnThread( * call suspending functions. This is a nonblocking call, which * means the scope will run on a background thread. This will * return immediately!!! Also provides an optional onComplete. - */ + * + * @param priority The priority of the background thread. Default is -1. + * Higher values indicate higher thread priority. + * + * @param block A suspending lambda to be executed on the background thread. + * This is where you put your suspending code. + * + * @param onComplete An optional lambda that will be invoked on the same + * background thread after [block] has finished executing. + * Useful for cleanup or follow-up logic. + **/ fun suspendifyOnThread( priority: Int = -1, block: suspend () -> Unit,