Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,16 @@ interface IOneSignal {
*/
fun initWithContext(
context: Context,
appId: String?,
appId: String,
): Boolean

/**
* Initialize the OneSignal SDK, suspend until initialization is completed
*
* @param context The Android context the SDK should use.
*/
suspend fun initWithContext(context: Context): Boolean

/**
* Login to OneSignal under the user identified by the [externalId] provided. The act of
* logging a user into the OneSignal SDK will switch the [user] context to that specific user.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ object OneSignal {
* THIS IS AN INTERNAL INTERFACE AND SHOULD NOT BE USED DIRECTLY.
*/
@JvmStatic
fun initWithContext(context: Context): Boolean {
return oneSignal.initWithContext(context, null)
suspend fun initWithContext(context: Context): Boolean {
return oneSignal.initWithContext(context)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.onesignal.common.threading

import com.onesignal.common.AndroidUtils
import com.onesignal.debug.internal.logging.Logging
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

/**
* This class allows blocking execution until asynchronous initialization or completion is signaled, with support for configurable timeouts and detailed logging for troubleshooting.
* It is designed for scenarios where certain tasks, such as SDK initialization, must finish before continuing.
* 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.
*
* Usage:
* val awaiter = LatchAwaiter("OneSignal SDK Init")
* awaiter.release() // when done
*/
class LatchAwaiter(
private val componentName: String = "Component",
) {
companion object {
const val DEFAULT_TIMEOUT_MS = 30_000L // 30 seconds
const val ANDROID_ANR_TIMEOUT_MS = 4_800L // Conservative ANR threshold
}

private val latch = CountDownLatch(1)

/**
* Releases the latch to unblock any waiting threads.
*/
fun release() {
latch.countDown()
}

/**
* Wait for the latch to be released with an optional timeout.
*
* @return true if latch was released before timeout, false otherwise.
*/
fun await(timeoutMs: Long = getDefaultTimeout()): Boolean {
val completed =
try {
latch.await(timeoutMs, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
Logging.warn("Interrupted while waiting for $componentName", e)
logAllThreads()
false
}

if (!completed) {
val message = createTimeoutMessage(timeoutMs)
Logging.warn(message)
}

return completed
}

private fun getDefaultTimeout(): Long {
return if (AndroidUtils.isRunningOnMainThread()) ANDROID_ANR_TIMEOUT_MS else DEFAULT_TIMEOUT_MS
}

private fun createTimeoutMessage(timeoutMs: Long): String {
return if (AndroidUtils.isRunningOnMainThread()) {
"Timeout waiting for $componentName after ${timeoutMs}ms on the main thread. " +
"This can cause ANRs. Consider calling from a background thread."
} else {
"Timeout waiting for $componentName after ${timeoutMs}ms."
}
}

private fun logAllThreads(): String {
val allThreads = Thread.getAllStackTraces()
val sb = StringBuilder()
for ((thread, stack) in allThreads) {
sb.append("ThreadDump Thread: ${thread.name} [${thread.state}]\n")
for (element in stack) {
sb.append("\tat $element\n")
}
}

return sb.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import android.os.Bundle
import android.os.Handler
import androidx.core.app.ActivityCompat
import com.onesignal.OneSignal
import com.onesignal.common.threading.suspendifyOnThread
import com.onesignal.core.R
import com.onesignal.core.internal.permissions.impl.RequestPermissionService
import com.onesignal.core.internal.preferences.IPreferencesService
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
import com.onesignal.core.internal.preferences.PreferenceStores
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

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

if (!OneSignal.initWithContext(this)) {
finishActivity()
return
}

if (intent.extras == null) {
// This should never happen, but extras is null in rare crash reports
finishActivity()
return
}

requestPermissionService = OneSignal.getService()
preferenceService = OneSignal.getService()
// init in background
suspendifyOnThread {
val initialized = OneSignal.initWithContext(this)

handleBundleParams(intent.extras)
// finishActivity() and handleBundleParams must be called from main
withContext(Dispatchers.Main) {
if (!initialized) {
finishActivity()
return@withContext
}

requestPermissionService = OneSignal.getService()
preferenceService = OneSignal.getService()
Comment on lines +45 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these two lines run from suspendifyOnThread instead of withContext(Dispatchers.Main)? Otherwise we are not lower the main thread load here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initWithContext will be in background, but calls like finishActivity and handleBundleParams will need to be called from main.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initWithContext will be done in background, but for calls like finishActivity and handleBundleParams they must be called from main


handleBundleParams(intent.extras)
}
}
}

override fun onNewIntent(intent: Intent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@ import com.onesignal.debug.internal.logging.Logging

class SyncJobService : JobService() {
override fun onStartJob(jobParameters: JobParameters): Boolean {
if (!OneSignal.initWithContext(this)) {
return false
}

var backgroundService = OneSignal.getService<IBackgroundManager>()

suspendifyOnThread {
// init OneSignal in background
if (!OneSignal.initWithContext(this)) {
jobFinished(jobParameters, false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we need this here @jinliu9508 ? looks like jobFinished was not called earlier when returning false

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, since now we always return true, jobFinished needs to be called to cancel the job if initialization is unsuccessful.

return@suspendifyOnThread
}

val backgroundService = OneSignal.getService<IBackgroundManager>()
backgroundService.runBackgroundServices()

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

// Reschedule if needed
val reschedule = backgroundService.needsJobReschedule
backgroundService.needsJobReschedule = false

jobFinished(jobParameters, reschedule)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.onesignal.internal

/**
* Represents the current initialization state of the OneSignal SDK.
*
* This enum is used to track the lifecycle of SDK initialization, ensuring that operations like `login`,
* `logout`, or accessing services are only allowed when the SDK is fully initialized.
*/
internal enum class InitState {
/**
* SDK initialization has not yet started.
* Calling SDK-dependent methods in this state will throw an exception.
*/
NOT_STARTED,

/**
* SDK initialization is currently in progress.
* Calls that require initialization will block (via a latch) until this completes.
*/
IN_PROGRESS,

/**
* SDK initialization completed successfully.
* All SDK-dependent operations can proceed safely.
*/
SUCCESS,

/**
* SDK initialization has failed due to an unrecoverable error (e.g., missing app ID).
* All dependent operations should fail fast or throw until re-initialized.
*/
FAILED,

;

fun isSDKAccessible(): Boolean {
return this == IN_PROGRESS || this == SUCCESS
}
}
Loading
Loading