- 
                Notifications
    
You must be signed in to change notification settings  - Fork 376
 
Refactor: move part of initWithContext off main thread #2368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
                
      
                  jkasten2 marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| 
     | 
||
| /** | ||
| * 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 | 
|---|---|---|
| 
          
            
          
           | 
    @@ -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 | ||
| 
        
          
        
         | 
    @@ -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
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't these two lines run from  There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
| 
          
            
          
           | 
    ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -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) | ||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
| 
     | 
||
| 
          
            
          
           | 
    ||
| 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 | ||
| } | ||
| } | 
Uh oh!
There was an error while loading. Please reload this page.