@@ -9,7 +9,6 @@ import com.onesignal.common.modules.IModule
99import com.onesignal.common.services.IServiceProvider
1010import com.onesignal.common.services.ServiceBuilder
1111import com.onesignal.common.services.ServiceProvider
12- import com.onesignal.common.threading.CompletionAwaiter
1312import com.onesignal.common.threading.OneSignalDispatchers
1413import com.onesignal.common.threading.suspendifyOnIO
1514import com.onesignal.core.CoreModule
@@ -39,19 +38,16 @@ import com.onesignal.user.internal.identity.IdentityModelStore
3938import com.onesignal.user.internal.properties.PropertiesModelStore
4039import com.onesignal.user.internal.resolveAppId
4140import com.onesignal.user.internal.subscriptions.SubscriptionModelStore
41+ import kotlinx.coroutines.CompletableDeferred
4242import kotlinx.coroutines.CoroutineDispatcher
43- import kotlinx.coroutines.TimeoutCancellationException
4443import kotlinx.coroutines.runBlocking
4544import kotlinx.coroutines.withContext
46- import kotlinx.coroutines.withTimeout
47-
48- private const val MAX_TIMEOUT_TO_INIT = 30_000L // 30 seconds
4945
5046internal class OneSignalImp (
5147 private val ioDispatcher : CoroutineDispatcher = OneSignalDispatchers .IO ,
5248) : IOneSignal, IServiceProvider {
53- @Volatile
54- private var initAwaiter = CompletionAwaiter ( " OneSignalImp " )
49+
50+ private val suspendCompletion = CompletableDeferred < Unit >( )
5551
5652 @Volatile
5753 private var initState: InitState = InitState .NOT_STARTED
@@ -263,7 +259,6 @@ internal class OneSignalImp(
263259 suspendifyOnIO {
264260 internalInit(context, appId)
265261 }
266- initState = InitState .SUCCESS
267262 return true
268263 }
269264
@@ -306,22 +301,16 @@ internal class OneSignalImp(
306301 ) {
307302 Logging .log(LogLevel .DEBUG , " Calling deprecated login(externalId: $externalId , jwtBearerToken: $jwtBearerToken )" )
308303
309- if (! initState.isSDKAccessible()) {
310- throw IllegalStateException (" Must call 'initWithContext' before 'login'" )
311- }
304+ waitForInit(operationName = " login" )
312305
313- waitForInit()
314306 suspendifyOnIO { loginHelper.login(externalId, jwtBearerToken) }
315307 }
316308
317309 override fun logout () {
318310 Logging .log(LogLevel .DEBUG , " Calling deprecated logout()" )
319311
320- if (! initState.isSDKAccessible()) {
321- throw IllegalStateException (" Must call 'initWithContext' before 'logout'" )
322- }
312+ waitForInit(operationName = " logout" )
323313
324- waitForInit()
325314 suspendifyOnIO { logoutHelper.logout() }
326315 }
327316
@@ -333,34 +322,82 @@ internal class OneSignalImp(
333322
334323 override fun <T > getAllServices (c : Class <T >): List <T > = services.getAllServices(c)
335324
336- private fun waitForInit () {
337- val completed = initAwaiter.await()
338- if (! completed) {
339- throw IllegalStateException (" initWithContext was not called or timed out" )
325+ /* *
326+ * Blocking version that waits for initialization to complete.
327+ * Uses runBlocking to bridge to the suspend implementation.
328+ * Waits indefinitely until init completes and logs how long it took.
329+ *
330+ * @param operationName Optional operation name to include in error messages (e.g., "login", "logout")
331+ */
332+ private fun waitForInit (operationName : String? = null) {
333+ runBlocking(ioDispatcher) {
334+ waitUntilInitInternal(operationName)
340335 }
341336 }
342337
343338 /* *
344339 * Notifies both blocking and suspend callers that initialization is complete
345340 */
346341 private fun notifyInitComplete () {
347- initAwaiter .complete()
342+ suspendCompletion .complete(Unit )
348343 }
349344
350- private suspend fun suspendUntilInit () {
345+ /* *
346+ * Suspend version that waits for initialization to complete.
347+ * Waits indefinitely until init completes and logs how long it took.
348+ *
349+ * @param operationName Optional operation name to include in error messages (e.g., "login", "logout")
350+ */
351+ private suspend fun suspendUntilInit (operationName : String? = null) {
352+ waitUntilInitInternal(operationName)
353+ }
354+
355+ /* *
356+ * Common implementation for waiting until initialization completes.
357+ * Waits indefinitely until init completes (SUCCESS or FAILED) to ensure consistent state.
358+ * Logs how long initialization took when it completes.
359+ *
360+ * @param operationName Optional operation name to include in error messages (e.g., "login", "logout")
361+ */
362+ private suspend fun waitUntilInitInternal (operationName : String? = null) {
351363 when (initState) {
352364 InitState .NOT_STARTED -> {
353- throw IllegalStateException (" Must call 'initWithContext' before use" )
365+ val message = if (operationName != null ) {
366+ " Must call 'initWithContext' before '$operationName '"
367+ } else {
368+ " Must call 'initWithContext' before use"
369+ }
370+ throw IllegalStateException (message)
354371 }
355372 InitState .IN_PROGRESS -> {
356- Logging .debug(" Suspend waiting for init to complete..." )
357- try {
358- withTimeout(MAX_TIMEOUT_TO_INIT ) {
359- initAwaiter.awaitSuspend()
360- }
361- } catch (e: TimeoutCancellationException ) {
362- throw IllegalStateException (" initWithContext was timed out after $MAX_TIMEOUT_TO_INIT ms" )
373+ Logging .debug(" Waiting for init to complete..." )
374+
375+ val startTime = System .currentTimeMillis()
376+
377+ // Wait indefinitely until init actually completes - ensures consistent state
378+ // Function only returns when initState is SUCCESS or FAILED
379+ // NOTE: This is a suspend function, so it's non-blocking when called from coroutines.
380+ // However, if waitForInit() (which uses runBlocking) is called from the main thread,
381+ // it will block the main thread indefinitely until init completes, which can cause ANRs.
382+ // This is intentional per PR #2412: "ANR is the lesser of two evils and the app can recover,
383+ // where an uncaught throw it can not." To avoid ANRs, call SDK methods from background threads
384+ // or use the suspend API from coroutines.
385+ suspendCompletion.await()
386+
387+ // Log how long initialization took
388+ val elapsed = System .currentTimeMillis() - startTime
389+ val message = if (operationName != null ) {
390+ " OneSignalImp initialization completed before '$operationName ' (took ${elapsed} ms)"
391+ } else {
392+ " OneSignalImp initialization completed (took ${elapsed} ms)"
393+ }
394+ Logging .debug(message)
395+
396+ // Re-check state after waiting - init might have failed during the wait
397+ if (initState == InitState .FAILED ) {
398+ throw IllegalStateException (" Initialization failed. Cannot proceed." )
363399 }
400+ // initState is guaranteed to be SUCCESS here - consistent state
364401 }
365402 InitState .FAILED -> {
366403 throw IllegalStateException (" Initialization failed. Cannot proceed." )
@@ -377,23 +414,7 @@ internal class OneSignalImp(
377414 }
378415
379416 private fun <T > waitAndReturn (getter : () -> T ): T {
380- when (initState) {
381- InitState .NOT_STARTED -> {
382- throw IllegalStateException (" Must call 'initWithContext' before use" )
383- }
384- InitState .IN_PROGRESS -> {
385- Logging .debug(" Waiting for init to complete..." )
386- waitForInit()
387- }
388- InitState .FAILED -> {
389- throw IllegalStateException (" Initialization failed. Cannot proceed." )
390- }
391- else -> {
392- // SUCCESS
393- waitForInit()
394- }
395- }
396-
417+ waitForInit()
397418 return getter()
398419 }
399420
@@ -407,8 +428,9 @@ internal class OneSignalImp(
407428 // because Looper.getMainLooper() is not mocked. This is safe to ignore.
408429 Logging .debug(" Could not check main thread status (likely in test environment): ${e.message} " )
409430 }
431+ // Call suspendAndReturn directly to avoid nested runBlocking (waitAndReturn -> waitForInit -> runBlocking)
410432 return runBlocking(ioDispatcher) {
411- waitAndReturn (getter)
433+ suspendAndReturn (getter)
412434 }
413435 }
414436
@@ -508,7 +530,8 @@ internal class OneSignalImp(
508530 ) = withContext(ioDispatcher) {
509531 Logging .log(LogLevel .DEBUG , " login(externalId: $externalId , jwtBearerToken: $jwtBearerToken )" )
510532
511- suspendUntilInit()
533+ suspendUntilInit(operationName = " login" )
534+
512535 if (! isInitialized) {
513536 throw IllegalStateException (" 'initWithContext failed' before 'login'" )
514537 }
@@ -520,7 +543,7 @@ internal class OneSignalImp(
520543 withContext(ioDispatcher) {
521544 Logging .log(LogLevel .DEBUG , " logoutSuspend()" )
522545
523- suspendUntilInit()
546+ suspendUntilInit(operationName = " logout " )
524547
525548 if (! isInitialized) {
526549 throw IllegalStateException (" 'initWithContext failed' before 'logout'" )
0 commit comments