Skip to content

Commit 66507e8

Browse files
abdulraqeeb33AR Abdul Azeez
andauthored
feat: Add Kotlin MainApplication and suspend initialization support (#2374)
* feat: Add Kotlin MainApplication and suspend initialization support - 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 * Added more tests * mandating passing app id in the login/logout methods * linting * Made app id mandatory for login and logout. * cleanup * reduce the forks * Time out, deprecate annotation and appid,context * ktlin * include MainApplication.java, locks, early returns * chore: Dispatcher Threads (#2375) * Using dispatcher * Update threads to 2 * Updated methods * linting * readme * using the same thread pool * lint * making sure initstate has the right value * lint * Clear state and skip performance tests * lint * clear preferences * fixing tests * fixing tests * fixing tests * fixing tests * fixing tests * addressed PR comments * Addressed comments and fixed tests * lint * lint * fix test * lint * rewrote the test * fix test * made the test more robust * clear all preferences and simplified mocks * added more robustness --------- Co-authored-by: AR Abdul Azeez <[email protected]> * Fix OperationRepoTests CI/CD flakiness by using individual coVerify calls - Replace coVerifyOrder with individual coVerify(exactly = 1) calls - Makes tests more resilient to timing variations in CI/CD environments - Maintains verification of all critical operations while allowing flexibility in exact timing * remove try catch and lint * Addressed comments, removed global scope launches, broke down userswitcher * fixed flag * fix tests * making sure using the name instead of value * test isolation * logout test * comments * cleanup --------- Co-authored-by: AR Abdul Azeez <[email protected]>
1 parent 6293840 commit 66507e8

File tree

68 files changed

+5035
-858
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+5035
-858
lines changed

Examples/OneSignalDemo/app/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
id 'com.android.application'
3+
id 'kotlin-android'
34
}
45

56
android {
@@ -48,6 +49,18 @@ android {
4849
// signingConfig null
4950
// productFlavors.huawei.signingConfig signingConfigs.huawei
5051
debuggable true
52+
// Note: profileable is automatically enabled when debuggable=true
53+
// Enable method tracing for detailed performance analysis
54+
testCoverageEnabled false
55+
}
56+
// Profileable release build for performance testing
57+
profileable {
58+
initWith release
59+
debuggable false
60+
profileable true
61+
minifyEnabled false
62+
signingConfig signingConfigs.debug
63+
matchingFallbacks = ['release']
5164
}
5265
}
5366

@@ -74,6 +87,7 @@ android {
7487

7588
dependencies {
7689
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
90+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
7791
implementation 'androidx.multidex:multidex:2.0.1'
7892
implementation 'androidx.cardview:cardview:1.0.0'
7993
implementation 'androidx.appcompat:appcompat:1.5.1'

Examples/OneSignalDemo/app/src/huawei/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
package="com.onesignal.sdktest">
66

77
<application
8-
android:name=".application.MainApplication">
8+
android:name=".application.MainApplicationKT">
99

1010
<service
1111
android:name="com.onesignal.sdktest.notification.HmsMessageServiceAppLevel"

Examples/OneSignalDemo/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<uses-permission android:name="android.permission.WAKE_LOCK" />
2121

2222
<application
23-
android:name=".application.MainApplication"
23+
android:name=".application.MainApplicationKT"
2424
android:allowBackup="true"
2525
android:icon="@mipmap/ic_onesignal_launcher"
2626
android:label="@string/app_name"

Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,26 @@
3333
import java.util.concurrent.ExecutorService;
3434
import java.util.concurrent.Executors;
3535

36+
/**
37+
* This Java implementation is not used any more. Use {@link MainApplicationKT} instead.
38+
* The Kotlin version provides better async handling and modern coroutines support.
39+
*
40+
*/
3641
public class MainApplication extends MultiDexApplication {
3742
private static final int SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION = 2000;
3843

3944
public MainApplication() {
4045
// run strict mode to surface any potential issues easier
4146
StrictMode.enableDefaults();
47+
Log.w(Tag.LOG_TAG, "MainApplication (Java) is deprecated. Please use MainApplicationKT (Kotlin) instead.");
4248
}
4349

4450
@SuppressLint("NewApi")
4551
@Override
4652
public void onCreate() {
4753
super.onCreate();
54+
Log.w(Tag.LOG_TAG, "DEPRECATED: Using MainApplication (Java). Please migrate to MainApplicationKT (Kotlin) for better async support.");
55+
4856
OneSignal.getDebug().setLogLevel(LogLevel.DEBUG);
4957

5058
// OneSignal Initialization
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.onesignal.sdktest.application
2+
3+
/**
4+
* Modern Kotlin implementation of MainApplication.
5+
*
6+
* This replaces the deprecated MainApplication.java with:
7+
* - Better async handling using Kotlin Coroutines
8+
* - Modern OneSignal API usage
9+
* - Cleaner code structure
10+
* - Proper ANR prevention
11+
*
12+
* @see MainApplication.java (deprecated Java version)
13+
*/
14+
import android.annotation.SuppressLint
15+
import android.os.StrictMode
16+
import android.util.Log
17+
import androidx.annotation.NonNull
18+
import androidx.multidex.MultiDexApplication
19+
import com.onesignal.OneSignal
20+
import com.onesignal.debug.LogLevel
21+
import com.onesignal.inAppMessages.IInAppMessageClickEvent
22+
import com.onesignal.inAppMessages.IInAppMessageClickListener
23+
import com.onesignal.inAppMessages.IInAppMessageDidDismissEvent
24+
import com.onesignal.inAppMessages.IInAppMessageDidDisplayEvent
25+
import com.onesignal.inAppMessages.IInAppMessageLifecycleListener
26+
import com.onesignal.inAppMessages.IInAppMessageWillDismissEvent
27+
import com.onesignal.inAppMessages.IInAppMessageWillDisplayEvent
28+
import com.onesignal.notifications.IDisplayableNotification
29+
import com.onesignal.notifications.INotificationClickEvent
30+
import com.onesignal.notifications.INotificationClickListener
31+
import com.onesignal.notifications.INotificationLifecycleListener
32+
import com.onesignal.notifications.INotificationWillDisplayEvent
33+
import com.onesignal.sdktest.R
34+
import com.onesignal.sdktest.constant.Tag
35+
import com.onesignal.sdktest.constant.Text
36+
import com.onesignal.sdktest.notification.OneSignalNotificationSender
37+
import com.onesignal.sdktest.util.SharedPreferenceUtil
38+
import com.onesignal.user.state.IUserStateObserver
39+
import com.onesignal.user.state.UserChangedState
40+
import com.onesignal.user.state.UserState
41+
import kotlinx.coroutines.CoroutineScope
42+
import kotlinx.coroutines.DelicateCoroutinesApi
43+
import kotlinx.coroutines.Dispatchers
44+
import kotlinx.coroutines.SupervisorJob
45+
import kotlinx.coroutines.launch
46+
47+
class MainApplicationKT : MultiDexApplication() {
48+
49+
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
50+
51+
init {
52+
// run strict mode to surface any potential issues easier
53+
StrictMode.enableDefaults()
54+
}
55+
56+
@SuppressLint("NewApi")
57+
override fun onCreate() {
58+
super.onCreate()
59+
OneSignal.Debug.logLevel = LogLevel.DEBUG
60+
61+
// OneSignal Initialization
62+
var appId = SharedPreferenceUtil.getOneSignalAppId(this)
63+
// If cached app id is null use the default, otherwise use cached.
64+
if (appId == null) {
65+
appId = getString(R.string.onesignal_app_id)
66+
SharedPreferenceUtil.cacheOneSignalAppId(this, appId)
67+
}
68+
69+
OneSignalNotificationSender.setAppId(appId)
70+
71+
// Initialize OneSignal asynchronously on background thread to avoid ANR
72+
applicationScope.launch {
73+
OneSignal.initWithContextSuspend(this@MainApplicationKT, appId)
74+
Log.d(Tag.LOG_TAG, "OneSignal async init completed")
75+
76+
// Set up all OneSignal listeners after successful async initialization
77+
setupOneSignalListeners()
78+
79+
// Request permission - this will internally switch to Main thread for UI operations
80+
OneSignal.Notifications.requestPermission(true)
81+
82+
Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT)
83+
}
84+
}
85+
86+
private fun setupOneSignalListeners() {
87+
OneSignal.InAppMessages.addLifecycleListener(object : IInAppMessageLifecycleListener {
88+
override fun onWillDisplay(@NonNull event: IInAppMessageWillDisplayEvent) {
89+
Log.v(Tag.LOG_TAG, "onWillDisplayInAppMessage")
90+
}
91+
92+
override fun onDidDisplay(@NonNull event: IInAppMessageDidDisplayEvent) {
93+
Log.v(Tag.LOG_TAG, "onDidDisplayInAppMessage")
94+
}
95+
96+
override fun onWillDismiss(@NonNull event: IInAppMessageWillDismissEvent) {
97+
Log.v(Tag.LOG_TAG, "onWillDismissInAppMessage")
98+
}
99+
100+
override fun onDidDismiss(@NonNull event: IInAppMessageDidDismissEvent) {
101+
Log.v(Tag.LOG_TAG, "onDidDismissInAppMessage")
102+
}
103+
})
104+
105+
OneSignal.InAppMessages.addClickListener(object : IInAppMessageClickListener {
106+
override fun onClick(event: IInAppMessageClickEvent) {
107+
Log.v(Tag.LOG_TAG, "INotificationClickListener.inAppMessageClicked")
108+
}
109+
})
110+
111+
OneSignal.Notifications.addClickListener(object : INotificationClickListener {
112+
override fun onClick(event: INotificationClickEvent) {
113+
Log.v(Tag.LOG_TAG, "INotificationClickListener.onClick fired" +
114+
" with event: " + event)
115+
}
116+
})
117+
118+
OneSignal.Notifications.addForegroundLifecycleListener(object : INotificationLifecycleListener {
119+
override fun onWillDisplay(@NonNull event: INotificationWillDisplayEvent) {
120+
Log.v(Tag.LOG_TAG, "INotificationLifecycleListener.onWillDisplay fired" +
121+
" with event: " + event)
122+
123+
val notification: IDisplayableNotification = event.notification
124+
125+
//Prevent OneSignal from displaying the notification immediately on return. Spin
126+
//up a new thread to mimic some asynchronous behavior, when the async behavior (which
127+
//takes 2 seconds) completes, then the notification can be displayed.
128+
event.preventDefault()
129+
val r = Runnable {
130+
try {
131+
Thread.sleep(SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION.toLong())
132+
} catch (ignored: InterruptedException) {
133+
}
134+
135+
notification.display()
136+
}
137+
138+
val t = Thread(r)
139+
t.start()
140+
}
141+
})
142+
143+
OneSignal.User.addObserver(object : IUserStateObserver {
144+
override fun onUserStateChange(@NonNull state: UserChangedState) {
145+
val currentUserState: UserState = state.current
146+
Log.v(Tag.LOG_TAG, "onUserStateChange fired " + currentUserState.toJSONObject())
147+
}
148+
})
149+
150+
OneSignal.InAppMessages.paused = true
151+
OneSignal.Location.isShared = false
152+
}
153+
154+
companion object {
155+
private const val SLEEP_TIME_TO_MIMIC_ASYNC_OPERATION = 2000
156+
}
157+
}

Examples/OneSignalDemo/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ buildscript {
1313
}
1414
dependencies {
1515
classpath 'com.android.tools.build:gradle:8.8.2'
16+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
1617
classpath 'com.google.gms:google-services:4.3.10'
1718
classpath 'com.huawei.agconnect:agcp:1.9.1.304'
1819

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/IOneSignal.kt

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,100 @@ interface IOneSignal {
130130
* data is not cleared.
131131
*/
132132
fun logout()
133+
134+
// Suspend versions of property accessors and methods to avoid blocking threads
135+
136+
/**
137+
* Initialize the OneSignal SDK, suspend until initialization is completed
138+
*
139+
* @param context The Android context the SDK should use.
140+
* @param appId The application ID the OneSignal SDK is bound to.
141+
*
142+
* @return true if the SDK could be successfully initialized, false otherwise.
143+
*/
144+
suspend fun initWithContextSuspend(
145+
context: Context,
146+
appId: String? = null,
147+
): Boolean
148+
149+
/**
150+
* Get the session manager without blocking the calling thread.
151+
* Suspends until the SDK is initialized.
152+
*/
153+
suspend fun getSession(): ISessionManager
154+
155+
/**
156+
* Get the notifications manager without blocking the calling thread.
157+
* Suspends until the SDK is initialized.
158+
*/
159+
suspend fun getNotifications(): INotificationsManager
160+
161+
/**
162+
* Get the location manager without blocking the calling thread.
163+
* Suspends until the SDK is initialized.
164+
*/
165+
suspend fun getLocation(): ILocationManager
166+
167+
/**
168+
* Get the in-app messages manager without blocking the calling thread.
169+
* Suspends until the SDK is initialized.
170+
*/
171+
suspend fun getInAppMessages(): IInAppMessagesManager
172+
173+
/**
174+
* Get the user manager without blocking the calling thread.
175+
* Suspends until the SDK is initialized.
176+
*/
177+
suspend fun getUser(): IUserManager
178+
179+
// Suspend versions of configuration properties for thread-safe access
180+
181+
/**
182+
* Get the consent required flag in a thread-safe manner.
183+
*/
184+
suspend fun getConsentRequired(): Boolean
185+
186+
/**
187+
* Set the consent required flag in a thread-safe manner.
188+
*/
189+
suspend fun setConsentRequired(required: Boolean)
190+
191+
/**
192+
* Get the consent given flag in a thread-safe manner.
193+
*/
194+
suspend fun getConsentGiven(): Boolean
195+
196+
/**
197+
* Set the consent given flag in a thread-safe manner.
198+
*/
199+
suspend fun setConsentGiven(value: Boolean)
200+
201+
/**
202+
* Get the disable GMS missing prompt flag in a thread-safe manner.
203+
*/
204+
suspend fun getDisableGMSMissingPrompt(): Boolean
205+
206+
/**
207+
* Set the disable GMS missing prompt flag in a thread-safe manner.
208+
*/
209+
suspend fun setDisableGMSMissingPrompt(value: Boolean)
210+
211+
/**
212+
* Login a user with external ID and optional JWT token (suspend version).
213+
* Handles initialization automatically.
214+
*
215+
* @param externalId The external ID of the user that is to be logged in.
216+
* @param jwtBearerToken The optional JWT bearer token generated by your backend to establish
217+
* trust for the login operation. Required when identity verification has been enabled.
218+
* See [Identity Verification | OneSignal](https://documentation.onesignal.com/docs/identity-verification)
219+
*/
220+
suspend fun loginSuspend(
221+
externalId: String,
222+
jwtBearerToken: String? = null,
223+
)
224+
225+
/**
226+
* Logout the current user (suspend version).
227+
*/
228+
suspend fun logoutSuspend()
133229
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,23 @@ object OneSignal {
135135
oneSignal.initWithContext(context, appId)
136136
}
137137

138+
/**
139+
* Initialize the OneSignal SDK asynchronously. This should be called during startup of the application.
140+
* This method provides a suspended version that returns a boolean indicating success.
141+
* Uses Dispatchers.IO internally to prevent ANRs and optimize for I/O operations.
142+
*
143+
* @param context Application context is recommended for SDK operations
144+
* @param appId The application ID the OneSignal SDK is bound to.
145+
* @return Boolean indicating if initialization was successful.
146+
*/
147+
@JvmStatic
148+
suspend fun initWithContextSuspend(
149+
context: Context,
150+
appId: String? = null,
151+
): Boolean {
152+
return oneSignal.initWithContextSuspend(context, appId)
153+
}
154+
138155
/**
139156
* Login to OneSignal under the user identified by the [externalId] provided. The act of
140157
* logging a user into the OneSignal SDK will switch the [User] context to that specific user.
@@ -208,6 +225,27 @@ object OneSignal {
208225
return oneSignal.initWithContext(context)
209226
}
210227

228+
/**
229+
* Login a user with external ID and optional JWT token (suspend version).
230+
*
231+
* @param externalId External user ID for login
232+
* @param jwtBearerToken Optional JWT token for authentication
233+
*/
234+
@JvmStatic
235+
suspend fun loginSuspend(
236+
externalId: String,
237+
jwtBearerToken: String? = null,
238+
) {
239+
oneSignal.login(externalId, jwtBearerToken)
240+
}
241+
242+
/**
243+
* Logout the current user (suspend version).
244+
*/
245+
suspend fun logoutSuspend() {
246+
oneSignal.logout()
247+
}
248+
211249
/**
212250
* Used to retrieve services from the SDK when constructor dependency injection is not an
213251
* option.

0 commit comments

Comments
 (0)