Skip to content

Conversation

abdulraqeeb33
Copy link
Contributor

@abdulraqeeb33 abdulraqeeb33 commented Oct 1, 2025

  • Create MainApplicationKT.kt as Kotlin version of MainApplication.java
  • Add initWithContextSuspend() method for async initialization
  • Refactor OneSignalImp to use IO dispatcher internally for initialization
  • Add comprehensive unit tests for suspend initialization
  • Rename LatchAwaiter to CompletionAwaiter for better semantics and unify suspend and blocking thread notification
  • Add helper classes for user management (AppIdHelper, LoginHelper, LogoutHelper, UserSwitcher)
  • Ensure ANR prevention by using background threads for initialization

Description

One Line Summary

Modernizing the SDK to use Kotlin Coroutines

Details

Motivation

Convert MainApplication from Java to Kotlin with modern async handling using coroutines, add suspend methods to OneSignal SDK for better ANR prevention, and deprecate the Java implementation to encourage migration to the improved Kotlin version.

Scope

SDK Initialization
Added new public APIs

Testing

Unit testing

Added a bunch of tests

Manual testing

Tested happy path

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
    • If it is hard to explain how any codes changes are related to each other then it most likely needs to be more than one PR
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
    • Simplify with less code, followed by splitting up code into well named functions and variables, followed by adding comments to the code.
  • I have reviewed this PR myself, ensuring it meets each checklist item
    • WIP (Work In Progress) is ok, but explain what is still in progress and what you would like feedback on. Start the PR title with "WIP" to indicate this.

This change is Reviewable

AR Abdul Azeez added 6 commits October 1, 2025 16:19
- Add MainApplicationKT.kt as Kotlin version of MainApplication.java
- Add initWithContextSuspend() method for async initialization
- Refactor OneSignalImp to use IO dispatcher internally for initialization
- Add comprehensive unit tests for suspend initialization
- Rename LatchAwaiter to CompletionAwaiter for better semantics
- Add helper classes for user management (AppIdHelper, LoginHelper, LogoutHelper, UserSwitcher)
- Update build.gradle to include Kotlin coroutines dependency
- Ensure ANR prevention by using background threads for initialization
@abdulraqeeb33 abdulraqeeb33 marked this pull request as ready for review October 3, 2025 00:08
@OptIn(DelicateCoroutinesApi::class)
fun scheduleStart() {
Thread {
GlobalScope.launch(Dispatchers.Default) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Creating a thread is very expensive. Google promotes usages of Dispatcher

https://kotlinlang.org/docs/coroutines-basics.html#coroutines-are-light-weight

}.build()

// get the current config model, if there is one
private val configModel: ConfigModel by lazy { services.getService<ConfigModelStore>().model }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

earlier it was a var and we could probably run into race conditions.
lazy is thread safe and handles concurrent access automatically.
and most importantly its null safe and we dont need to use !! anymore.

private val logoutLock: Any = Any()
private val userSwitcher by lazy {
val appContext = services.getService<IApplicationService>().appContext
UserSwitcher(
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 class is broken down significantly into all these subclasses to address seperation of concern and improve testability

jwtBearerToken: String?,
) {
Logging.log(LogLevel.DEBUG, "login(externalId: $externalId, jwtBearerToken: $jwtBearerToken)")
Logging.log(LogLevel.DEBUG, "Calling deprecated login(externalId: $externalId, jwtBearerToken: $jwtBearerToken)")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Logging deprecate calls so that someday we can get an estimate its usage and plan on its removal.

if (AndroidUtils.isRunningOnMainThread()) {
Logging.warn("This is called on main thread. This is not recommended.")
}
} catch (e: RuntimeException) {
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 exception catch here is for mostly testing

*/
suspend fun login(
context: Context,
appId: String,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see a bunch of crashes where login is called before initializing the SDK.

Is it ok to just accept the appId and context here and initialize the SDK (if its not done already) and continuing with the login process? It shouldn't be very hard for the consumer to pass the AppId IMHO.

Does it impact anything else?

Copy link
Member

Choose a reason for hiding this comment

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

Thinking about this as a consumer of this SDK, having both initialize and login with appId raises questions;

  1. Does it matter if which one I pass appId to?
  2. Is one better than the other?, which one is preferred?
  3. Does this means login is designed to move the User to a different OneSignal app?

As far as internals of the SDK goes, it doesn't matter, but you see the confusion this would add to the public API.

*/
suspend fun logout(
context: Context,
appId: String,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

same with logout.

Comment on lines +143 to 144
@Deprecated(message = "Use suspend version", ReplaceWith("suspend fun logout()"))
fun logout()
Copy link
Member

Choose a reason for hiding this comment

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

There isn't a return value, so it is valid to call it and not wait. What do you think about keeping both for these cases?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants