Skip to content

Commit c779475

Browse files
abdulraqeeb33AR Abdul Azeez
andauthored
feat: SDK-4176: gate background threading behind remote feature flag (#2595)
Co-authored-by: AR Abdul Azeez <abdul@onesignal.com>
1 parent b164424 commit c779475

File tree

18 files changed

+780
-89
lines changed

18 files changed

+780
-89
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt

Lines changed: 118 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
@file:Suppress("GlobalCoroutineUsage")
2+
13
package com.onesignal.common.threading
24

35
import com.onesignal.debug.internal.logging.Logging
46
import kotlinx.coroutines.Dispatchers
7+
import kotlinx.coroutines.GlobalScope
58
import kotlinx.coroutines.Job
9+
import kotlinx.coroutines.launch
10+
import kotlinx.coroutines.runBlocking
611
import kotlinx.coroutines.withContext
12+
import kotlin.concurrent.thread
713

814
/**
915
* Modernized ThreadUtils that leverages OneSignalDispatchers for better thread management.
@@ -24,8 +30,27 @@ import kotlinx.coroutines.withContext
2430
*
2531
*/
2632
fun suspendifyOnMain(block: suspend () -> Unit) {
27-
OneSignalDispatchers.launchOnIO {
28-
withContext(Dispatchers.Main) { block() }
33+
if (ThreadingMode.useBackgroundThreading) {
34+
OneSignalDispatchers.launchOnIO {
35+
try {
36+
withContext(Dispatchers.Main) { block() }
37+
} catch (e: Exception) {
38+
Logging.error("Exception in suspendifyOnMain", e)
39+
}
40+
}
41+
return
42+
}
43+
44+
thread {
45+
try {
46+
runBlocking {
47+
withContext(Dispatchers.Main) {
48+
block()
49+
}
50+
}
51+
} catch (e: Exception) {
52+
Logging.error("Exception on thread with switch to main", e)
53+
}
2954
}
3055
}
3156

@@ -86,24 +111,36 @@ fun suspendifyWithCompletion(
86111
block: suspend () -> Unit,
87112
onComplete: (() -> Unit)? = null,
88113
) {
89-
if (useIO) {
90-
OneSignalDispatchers.launchOnIO {
91-
try {
92-
block()
93-
onComplete?.invoke()
94-
} catch (e: Exception) {
95-
Logging.error("Exception in suspendifyWithCompletion", e)
114+
if (ThreadingMode.useBackgroundThreading) {
115+
if (useIO) {
116+
OneSignalDispatchers.launchOnIO {
117+
try {
118+
block()
119+
onComplete?.invoke()
120+
} catch (e: Exception) {
121+
Logging.error("Exception in suspendifyWithCompletion", e)
122+
}
96123
}
97-
}
98-
} else {
99-
OneSignalDispatchers.launchOnDefault {
100-
try {
101-
block()
102-
onComplete?.invoke()
103-
} catch (e: Exception) {
104-
Logging.error("Exception in suspendifyWithCompletion", e)
124+
} else {
125+
OneSignalDispatchers.launchOnDefault {
126+
try {
127+
block()
128+
onComplete?.invoke()
129+
} catch (e: Exception) {
130+
Logging.error("Exception in suspendifyWithCompletion", e)
131+
}
105132
}
106133
}
134+
return
135+
}
136+
137+
GlobalScope.launch(if (useIO) Dispatchers.IO else Dispatchers.Default) {
138+
try {
139+
block()
140+
onComplete?.invoke()
141+
} catch (e: Exception) {
142+
Logging.error("Exception in suspendifyWithCompletion", e)
143+
}
107144
}
108145
}
109146

@@ -122,26 +159,39 @@ fun suspendifyWithErrorHandling(
122159
onError: ((Exception) -> Unit)? = null,
123160
onComplete: (() -> Unit)? = null,
124161
) {
125-
if (useIO) {
126-
OneSignalDispatchers.launchOnIO {
127-
try {
128-
block()
129-
onComplete?.invoke()
130-
} catch (e: Exception) {
131-
Logging.error("Exception in suspendifyWithErrorHandling", e)
132-
onError?.invoke(e)
162+
if (ThreadingMode.useBackgroundThreading) {
163+
if (useIO) {
164+
OneSignalDispatchers.launchOnIO {
165+
try {
166+
block()
167+
onComplete?.invoke()
168+
} catch (e: Exception) {
169+
Logging.error("Exception in suspendifyWithErrorHandling", e)
170+
onError?.invoke(e)
171+
}
133172
}
134-
}
135-
} else {
136-
OneSignalDispatchers.launchOnDefault {
137-
try {
138-
block()
139-
onComplete?.invoke()
140-
} catch (e: Exception) {
141-
Logging.error("Exception in suspendifyWithErrorHandling", e)
142-
onError?.invoke(e)
173+
} else {
174+
OneSignalDispatchers.launchOnDefault {
175+
try {
176+
block()
177+
onComplete?.invoke()
178+
} catch (e: Exception) {
179+
Logging.error("Exception in suspendifyWithErrorHandling", e)
180+
onError?.invoke(e)
181+
}
143182
}
144183
}
184+
return
185+
}
186+
187+
GlobalScope.launch(if (useIO) Dispatchers.IO else Dispatchers.Default) {
188+
try {
189+
block()
190+
onComplete?.invoke()
191+
} catch (e: Exception) {
192+
Logging.error("Exception in suspendifyWithErrorHandling", e)
193+
onError?.invoke(e)
194+
}
145195
}
146196
}
147197

@@ -153,7 +203,23 @@ fun suspendifyWithErrorHandling(
153203
* @return Job that can be used to wait for completion with .join()
154204
*/
155205
fun launchOnIO(block: suspend () -> Unit): Job {
156-
return OneSignalDispatchers.launchOnIO(block)
206+
return if (ThreadingMode.useBackgroundThreading) {
207+
OneSignalDispatchers.launchOnIO {
208+
try {
209+
block()
210+
} catch (e: Exception) {
211+
Logging.error("Exception in launchOnIO", e)
212+
}
213+
}
214+
} else {
215+
GlobalScope.launch(Dispatchers.IO) {
216+
try {
217+
block()
218+
} catch (e: Exception) {
219+
Logging.error("Exception in launchOnIO", e)
220+
}
221+
}
222+
}
157223
}
158224

159225
/**
@@ -164,5 +230,21 @@ fun launchOnIO(block: suspend () -> Unit): Job {
164230
* @return Job that can be used to wait for completion with .join()
165231
*/
166232
fun launchOnDefault(block: suspend () -> Unit): kotlinx.coroutines.Job {
167-
return OneSignalDispatchers.launchOnDefault(block)
233+
return if (ThreadingMode.useBackgroundThreading) {
234+
OneSignalDispatchers.launchOnDefault {
235+
try {
236+
block()
237+
} catch (e: Exception) {
238+
Logging.error("Exception in launchOnDefault", e)
239+
}
240+
}
241+
} else {
242+
GlobalScope.launch(Dispatchers.Default) {
243+
try {
244+
block()
245+
} catch (e: Exception) {
246+
Logging.error("Exception in launchOnDefault", e)
247+
}
248+
}
249+
}
168250
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.onesignal.common.threading
2+
3+
import com.onesignal.debug.internal.logging.Logging
4+
5+
/**
6+
* Global threading mode switch that can be refreshed from remote config.
7+
*/
8+
internal object ThreadingMode {
9+
@Volatile
10+
var useBackgroundThreading: Boolean = false
11+
12+
fun updateUseBackgroundThreading(
13+
enabled: Boolean,
14+
source: String,
15+
) {
16+
val previous = useBackgroundThreading
17+
useBackgroundThreading = enabled
18+
19+
if (previous != enabled) {
20+
Logging.info("OneSignal: ThreadingMode changed to useBackgroundThreading=$enabled (source=$source)")
21+
} else {
22+
Logging.debug("OneSignal: ThreadingMode unchanged (useBackgroundThreading=$enabled, source=$source)")
23+
}
24+
}
25+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import com.onesignal.core.internal.device.IDeviceService
1616
import com.onesignal.core.internal.device.IInstallIdService
1717
import com.onesignal.core.internal.device.impl.DeviceService
1818
import com.onesignal.core.internal.device.impl.InstallIdService
19+
import com.onesignal.core.internal.features.FeatureManager
20+
import com.onesignal.core.internal.features.IFeatureManager
1921
import com.onesignal.core.internal.http.IHttpClient
2022
import com.onesignal.core.internal.http.impl.HttpClient
2123
import com.onesignal.core.internal.http.impl.HttpConnectionFactory
@@ -57,6 +59,7 @@ internal class CoreModule : IModule {
5759

5860
// Params (Config)
5961
builder.register<ConfigModelStore>().provides<ConfigModelStore>()
62+
builder.register<FeatureManager>().provides<IFeatureManager>()
6063
builder.register<ParamsBackendService>().provides<IParamsBackendService>()
6164
builder.register<ConfigModelStoreListener>().provides<IStartableService>()
6265

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/IParamsBackendService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ internal class ParamsObject(
3535
var locationShared: Boolean? = null,
3636
var requiresUserPrivacyConsent: Boolean? = null,
3737
var opRepoExecutionInterval: Long? = null,
38+
val features: List<String> = emptyList(),
3839
var influenceParams: InfluenceParamsObject,
3940
var fcmParams: FCMParamsObject,
4041
val remoteLoggingParams: RemoteLoggingParamsObject,

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ internal class ParamsBackendService(
6868
)
6969
}
7070

71+
val features =
72+
responseJson.optJSONArray("features")
73+
?.let { featuresJson ->
74+
buildList {
75+
for (i in 0 until featuresJson.length()) {
76+
val featureName = featuresJson.optString(i, "")
77+
if (featureName.isNotBlank()) {
78+
add(featureName)
79+
}
80+
}
81+
}
82+
} ?: emptyList()
83+
7184
return ParamsObject(
7285
googleProjectNumber = responseJson.safeString("android_sender_id"),
7386
enterprise = responseJson.safeBool("enterp"),
@@ -84,6 +97,7 @@ internal class ParamsBackendService(
8497
requiresUserPrivacyConsent = responseJson.safeBool("requires_user_privacy_consent"),
8598
// TODO: New
8699
opRepoExecutionInterval = responseJson.safeLong("oprepo_execution_interval"),
100+
features = features,
87101
influenceParams = influenceParams ?: InfluenceParamsObject(),
88102
fcmParams = fcmParams ?: FCMParamsObject(),
89103
remoteLoggingParams = remoteLoggingParams ?: RemoteLoggingParamsObject(),

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ class ConfigModel : Model() {
290290
setBooleanProperty(::clearGroupOnSummaryClick.name, value)
291291
}
292292

293+
/**
294+
* Remote feature switches controlled by backend.
295+
* Presence of a feature name indicates enabled.
296+
*/
297+
var features: List<String>
298+
get() = getListProperty(::features.name) { emptyList() }
299+
set(value) {
300+
setListProperty(::features.name, value)
301+
}
302+
293303
/**
294304
* The outcomes parameters
295305
*/
@@ -329,6 +339,24 @@ class ConfigModel : Model() {
329339

330340
return null
331341
}
342+
343+
override fun createListForProperty(
344+
property: String,
345+
jsonArray: JSONArray,
346+
): List<*>? {
347+
if (property == ::features.name) {
348+
return buildList {
349+
for (i in 0 until jsonArray.length()) {
350+
val featureName = jsonArray.optString(i, "")
351+
if (featureName.isNotBlank()) {
352+
add(featureName)
353+
}
354+
}
355+
}
356+
}
357+
358+
return null
359+
}
332360
}
333361

334362
/**

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ internal class ConfigModelStoreListener(
9595
params.locationShared?.let { config.locationShared = it }
9696
params.requiresUserPrivacyConsent?.let { config.consentRequired = it }
9797
params.opRepoExecutionInterval?.let { config.opRepoExecutionInterval = it }
98+
config.features = params.features
9899
params.influenceParams.notificationLimit?.let { config.influenceParams.notificationLimit = it }
99100
params.influenceParams.indirectNotificationAttributionWindow?.let { config.influenceParams.indirectNotificationAttributionWindow = it }
100101
params.influenceParams.iamLimit?.let { config.influenceParams.iamLimit = it }
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.onesignal.core.internal.features
2+
3+
/**
4+
* Controls when remote config changes for a feature are applied.
5+
*/
6+
internal enum class FeatureActivationMode {
7+
/**
8+
* Apply config changes immediately during the current app run.
9+
*/
10+
IMMEDIATE,
11+
12+
/**
13+
* Latch value at startup; apply remote changes on next app run.
14+
*/
15+
APP_STARTUP,
16+
}
17+
18+
/**
19+
* Backend-driven feature switches used by the SDK.
20+
*/
21+
internal enum class FeatureFlag(
22+
val key: String,
23+
val activationMode: FeatureActivationMode,
24+
) {
25+
// Threading mode is selected once per app startup to avoid mixed-mode behavior mid-session.
26+
BACKGROUND_THREADING("BACKGROUND_THREADING", FeatureActivationMode.APP_STARTUP),
27+
}

0 commit comments

Comments
 (0)