diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index 7b6ee4041..a007ea6b3 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -52,6 +52,9 @@ internal class OneSignalImp( @Volatile private var initState: InitState = InitState.NOT_STARTED + // Save the exception pointing to the caller that triggered init, not the async worker thread. + private var initFailureException: Exception? = null + override val sdkVersion: String = OneSignalUtils.sdkVersion override val isInitialized: Boolean @@ -279,7 +282,11 @@ internal class OneSignalImp( val startupService = bootstrapServices() val result = resolveAppId(appId, configModel, preferencesService) if (result.failed) { - Logging.warn("suspendInitInternal: no appId provided or found in legacy config.") + val message = "No OneSignal appId provided or found in local storage. Please pass a valid appId to initWithContext()." + val exception = IllegalStateException(message) + // attach the real crash cause to the init failure exception that will be throw shortly after + initFailureException?.addSuppressed(exception) + Logging.warn(message) initState = InitState.FAILED notifyInitComplete() return false @@ -395,12 +402,12 @@ internal class OneSignalImp( // Re-check state after waiting - init might have failed during the wait if (initState == InitState.FAILED) { - throw IllegalStateException("Initialization failed. Cannot proceed.") + throw initFailureException ?: IllegalStateException("Initialization failed. Cannot proceed.") } // initState is guaranteed to be SUCCESS here - consistent state } InitState.FAILED -> { - throw IllegalStateException("Initialization failed. Cannot proceed.") + throw initFailureException ?: IllegalStateException("Initialization failed. Cannot proceed.") } else -> { // SUCCESS - already initialized, no need to wait @@ -506,6 +513,9 @@ internal class OneSignalImp( ): Boolean { Logging.log(LogLevel.DEBUG, "initWithContext(context: $context, appId: $appId)") + // This ensures the stack trace points to the caller that triggered init, not the async worker thread. + initFailureException = IllegalStateException("OneSignal initWithContext failed.") + // Use IO dispatcher for initialization to prevent ANRs and optimize for I/O operations return withContext(ioDispatcher) { // do not do this again if already initialized or init is in progress diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt index 013899f8c..3350de859 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt @@ -37,20 +37,6 @@ class SDKInitTests : FunSpec({ beforeAny { Logging.logLevel = LogLevel.NONE - - // Aggressive pre-test cleanup to avoid state leakage across tests - val context = getApplicationContext() - val prefs = context.getSharedPreferences("OneSignal", Context.MODE_PRIVATE) - prefs.edit() - .clear() - .commit() - - val otherPrefs = context.getSharedPreferences("com.onesignal", Context.MODE_PRIVATE) - otherPrefs.edit() - .clear() - .commit() - - Thread.sleep(100) } afterAny { @@ -65,19 +51,27 @@ class SDKInitTests : FunSpec({ otherPrefs.edit() .clear() .commit() + } - // Wait longer to ensure cleanup is complete - Thread.sleep(100) + test("accessor instances after initWithContext without appID shows the failure reason") { + // Given + val context = getApplicationContext() + val os = OneSignalImp() - // Clear any in-memory state by initializing and logging out a fresh instance - try { - val os = OneSignalImp() - os.initWithContext(context, "appId") - os.logout() - Thread.sleep(100) - } catch (ignored: Exception) { - // ignore cleanup exceptions + // When + os.initWithContext(context) + + // Then + val ex = shouldThrow { + os.user.onesignalId // Should trigger waitUntilInitInternal → throw failure message } + ex.message shouldBe "suspendInitInternal: no appId provided or found in local storage. Please pass a valid appId to initWithContext()." + + // Calling initWithContext with an appID after the failure should not throw anymore + val result = os.initWithContext(context, "appID") + waitForInitialization(os) + result shouldBe true + os.isInitialized shouldBe true } test("OneSignal accessors throw before calling initWithContext") {