Skip to content

Commit 808d1cc

Browse files
committed
refactor: move initialization process off main thread
1 parent 059cc4d commit 808d1cc

File tree

23 files changed

+956
-320
lines changed

23 files changed

+956
-320
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,16 @@ interface IOneSignal {
8585
*/
8686
fun initWithContext(
8787
context: Context,
88-
appId: String?,
88+
appId: String,
8989
): Boolean
9090

91+
/**
92+
* Initialize the OneSignal SDK, suspend until initialization is completed
93+
*
94+
* @param context The Android context the SDK should use.
95+
*/
96+
suspend fun initWithContext(context: Context): Boolean
97+
9198
/**
9299
* Login to OneSignal under the user identified by the [externalId] provided. The act of
93100
* logging a user into the OneSignal SDK will switch the [user] context to that specific user.

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ object OneSignal {
204204
* THIS IS AN INTERNAL INTERFACE AND SHOULD NOT BE USED DIRECTLY.
205205
*/
206206
@JvmStatic
207-
fun initWithContext(context: Context): Boolean {
208-
return oneSignal.initWithContext(context, null)
207+
suspend fun initWithContext(context: Context): Boolean {
208+
return oneSignal.initWithContext(context)
209209
}
210210

211211
/**
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.onesignal.common.threading
2+
3+
import com.onesignal.common.AndroidUtils
4+
import com.onesignal.debug.internal.logging.Logging
5+
import java.util.concurrent.CountDownLatch
6+
import java.util.concurrent.TimeUnit
7+
8+
/**
9+
* This class allows blocking execution until asynchronous initialization or completion is signaled, with support for configurable timeouts and detailed logging for troubleshooting.
10+
* It is designed for scenarios where certain tasks, such as SDK initialization, must finish before continuing.
11+
* When used on the main/UI thread, it applies a shorter timeout and logs a thread stack trace to warn developers, helping to prevent Application Not Responding (ANR) errors caused by blocking the UI thread.
12+
*
13+
* Usage:
14+
* val awaiter = LatchAwaiter("OneSignal SDK Init")
15+
* awaiter.release() // when done
16+
*/
17+
class LatchAwaiter(
18+
private val componentName: String = "Component",
19+
) {
20+
companion object {
21+
const val DEFAULT_TIMEOUT_MS = 30_000L // 30 seconds
22+
const val ANDROID_ANR_TIMEOUT_MS = 4_800L // Conservative ANR threshold
23+
}
24+
25+
private val latch = CountDownLatch(1)
26+
27+
/**
28+
* Releases the latch to unblock any waiting threads.
29+
*/
30+
fun release() {
31+
latch.countDown()
32+
}
33+
34+
/**
35+
* Wait for the latch to be released with an optional timeout.
36+
*
37+
* @return true if latch was released before timeout, false otherwise.
38+
*/
39+
fun await(timeoutMs: Long = getDefaultTimeout()): Boolean {
40+
val completed =
41+
try {
42+
latch.await(timeoutMs, TimeUnit.MILLISECONDS)
43+
} catch (e: InterruptedException) {
44+
Logging.warn("Interrupted while waiting for $componentName", e)
45+
logAllThreads()
46+
false
47+
}
48+
49+
if (!completed) {
50+
val message = createTimeoutMessage(timeoutMs)
51+
Logging.warn(message)
52+
}
53+
54+
return completed
55+
}
56+
57+
private fun getDefaultTimeout(): Long {
58+
return if (AndroidUtils.isRunningOnMainThread()) ANDROID_ANR_TIMEOUT_MS else DEFAULT_TIMEOUT_MS
59+
}
60+
61+
private fun createTimeoutMessage(timeoutMs: Long): String {
62+
return if (AndroidUtils.isRunningOnMainThread()) {
63+
"Timeout waiting for $componentName after ${timeoutMs}ms on the main thread. " +
64+
"This can cause ANRs. Consider calling from a background thread."
65+
} else {
66+
"Timeout waiting for $componentName after ${timeoutMs}ms."
67+
}
68+
}
69+
70+
private fun logAllThreads(): String {
71+
val allThreads = Thread.getAllStackTraces()
72+
val sb = StringBuilder()
73+
for ((thread, stack) in allThreads) {
74+
sb.append("ThreadDump Thread: ${thread.name} [${thread.state}]\n")
75+
for (element in stack) {
76+
sb.append("\tat $element\n")
77+
}
78+
}
79+
80+
return sb.toString()
81+
}
82+
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/activities/PermissionsActivity.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import android.os.Bundle
88
import android.os.Handler
99
import androidx.core.app.ActivityCompat
1010
import com.onesignal.OneSignal
11+
import com.onesignal.common.threading.suspendifyOnThread
1112
import com.onesignal.core.R
1213
import com.onesignal.core.internal.permissions.impl.RequestPermissionService
1314
import com.onesignal.core.internal.preferences.IPreferencesService
1415
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
1516
import com.onesignal.core.internal.preferences.PreferenceStores
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.withContext
1619

1720
class PermissionsActivity : Activity() {
1821
private var requestPermissionService: RequestPermissionService? = null
@@ -22,21 +25,29 @@ class PermissionsActivity : Activity() {
2225
override fun onCreate(savedInstanceState: Bundle?) {
2326
super.onCreate(savedInstanceState)
2427

25-
if (!OneSignal.initWithContext(this)) {
26-
finishActivity()
27-
return
28-
}
29-
3028
if (intent.extras == null) {
3129
// This should never happen, but extras is null in rare crash reports
3230
finishActivity()
3331
return
3432
}
3533

36-
requestPermissionService = OneSignal.getService()
37-
preferenceService = OneSignal.getService()
34+
// init in background
35+
suspendifyOnThread {
36+
val initialized = OneSignal.initWithContext(this)
3837

39-
handleBundleParams(intent.extras)
38+
// finishActivity() and handleBundleParams must be called from main
39+
withContext(Dispatchers.Main) {
40+
if (!initialized) {
41+
finishActivity()
42+
return@withContext
43+
}
44+
45+
requestPermissionService = OneSignal.getService()
46+
preferenceService = OneSignal.getService()
47+
48+
handleBundleParams(intent.extras)
49+
}
50+
}
4051
}
4152

4253
override fun onNewIntent(intent: Intent) {

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/services/SyncJobService.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,21 @@ import com.onesignal.debug.internal.logging.Logging
3535

3636
class SyncJobService : JobService() {
3737
override fun onStartJob(jobParameters: JobParameters): Boolean {
38-
if (!OneSignal.initWithContext(this)) {
39-
return false
40-
}
41-
42-
var backgroundService = OneSignal.getService<IBackgroundManager>()
43-
4438
suspendifyOnThread {
39+
// init OneSignal in background
40+
if (!OneSignal.initWithContext(this)) {
41+
jobFinished(jobParameters, false)
42+
return@suspendifyOnThread
43+
}
44+
45+
val backgroundService = OneSignal.getService<IBackgroundManager>()
4546
backgroundService.runBackgroundServices()
4647

4748
Logging.debug("LollipopSyncRunnable:JobFinished needsJobReschedule: " + backgroundService.needsJobReschedule)
4849

4950
// Reschedule if needed
5051
val reschedule = backgroundService.needsJobReschedule
5152
backgroundService.needsJobReschedule = false
52-
5353
jobFinished(jobParameters, reschedule)
5454
}
5555

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.onesignal.internal
2+
3+
/**
4+
* Represents the current initialization state of the OneSignal SDK.
5+
*
6+
* This enum is used to track the lifecycle of SDK initialization, ensuring that operations like `login`,
7+
* `logout`, or accessing services are only allowed when the SDK is fully initialized.
8+
*/
9+
internal enum class InitState {
10+
/**
11+
* SDK initialization has not yet started.
12+
* Calling SDK-dependent methods in this state will throw an exception.
13+
*/
14+
NOT_STARTED,
15+
16+
/**
17+
* SDK initialization is currently in progress.
18+
* Calls that require initialization will block (via a latch) until this completes.
19+
*/
20+
IN_PROGRESS,
21+
22+
/**
23+
* SDK initialization completed successfully.
24+
* All SDK-dependent operations can proceed safely.
25+
*/
26+
SUCCESS,
27+
28+
/**
29+
* SDK initialization has failed due to an unrecoverable error (e.g., missing app ID).
30+
* All dependent operations should fail fast or throw until re-initialized.
31+
*/
32+
FAILED,
33+
34+
;
35+
36+
fun isSDKAccessible(): Boolean {
37+
return this == IN_PROGRESS || this == SUCCESS
38+
}
39+
}

0 commit comments

Comments
 (0)