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
@@ -1,9 +1,15 @@
@file:Suppress("GlobalCoroutineUsage")

package com.onesignal.common.threading

import com.onesignal.debug.internal.logging.Logging
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.concurrent.thread

/**
* Modernized ThreadUtils that leverages OneSignalDispatchers for better thread management.
Expand All @@ -24,8 +30,27 @@ import kotlinx.coroutines.withContext
*
*/
fun suspendifyOnMain(block: suspend () -> Unit) {
OneSignalDispatchers.launchOnIO {
withContext(Dispatchers.Main) { block() }
if (ThreadingMode.useBackgroundThreading) {
OneSignalDispatchers.launchOnIO {
try {
withContext(Dispatchers.Main) { block() }
} catch (e: Exception) {
Logging.error("Exception in suspendifyOnMain", e)
}
}
return
}

thread {
try {
runBlocking {
withContext(Dispatchers.Main) {
block()
}
}
} catch (e: Exception) {
Logging.error("Exception on thread with switch to main", e)
}
}
}

Expand Down Expand Up @@ -86,24 +111,36 @@ fun suspendifyWithCompletion(
block: suspend () -> Unit,
onComplete: (() -> Unit)? = null,
) {
if (useIO) {
OneSignalDispatchers.launchOnIO {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithCompletion", e)
if (ThreadingMode.useBackgroundThreading) {
if (useIO) {
OneSignalDispatchers.launchOnIO {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithCompletion", e)
}
}
}
} else {
OneSignalDispatchers.launchOnDefault {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithCompletion", e)
} else {
OneSignalDispatchers.launchOnDefault {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithCompletion", e)
}
}
}
return
}

GlobalScope.launch(if (useIO) Dispatchers.IO else Dispatchers.Default) {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithCompletion", e)
}
}
}

Expand All @@ -122,26 +159,39 @@ fun suspendifyWithErrorHandling(
onError: ((Exception) -> Unit)? = null,
onComplete: (() -> Unit)? = null,
) {
if (useIO) {
OneSignalDispatchers.launchOnIO {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithErrorHandling", e)
onError?.invoke(e)
if (ThreadingMode.useBackgroundThreading) {
if (useIO) {
OneSignalDispatchers.launchOnIO {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithErrorHandling", e)
onError?.invoke(e)
}
}
}
} else {
OneSignalDispatchers.launchOnDefault {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithErrorHandling", e)
onError?.invoke(e)
} else {
OneSignalDispatchers.launchOnDefault {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithErrorHandling", e)
onError?.invoke(e)
}
}
}
return
}

GlobalScope.launch(if (useIO) Dispatchers.IO else Dispatchers.Default) {
try {
block()
onComplete?.invoke()
} catch (e: Exception) {
Logging.error("Exception in suspendifyWithErrorHandling", e)
onError?.invoke(e)
}
}
}

Expand All @@ -153,7 +203,23 @@ fun suspendifyWithErrorHandling(
* @return Job that can be used to wait for completion with .join()
*/
fun launchOnIO(block: suspend () -> Unit): Job {
return OneSignalDispatchers.launchOnIO(block)
return if (ThreadingMode.useBackgroundThreading) {
OneSignalDispatchers.launchOnIO {
try {
block()
} catch (e: Exception) {
Logging.error("Exception in launchOnIO", e)
}
}
} else {
GlobalScope.launch(Dispatchers.IO) {
try {
block()
} catch (e: Exception) {
Logging.error("Exception in launchOnIO", e)
}
}
}
}

/**
Expand All @@ -164,5 +230,21 @@ fun launchOnIO(block: suspend () -> Unit): Job {
* @return Job that can be used to wait for completion with .join()
*/
fun launchOnDefault(block: suspend () -> Unit): kotlinx.coroutines.Job {
return OneSignalDispatchers.launchOnDefault(block)
return if (ThreadingMode.useBackgroundThreading) {
OneSignalDispatchers.launchOnDefault {
try {
block()
} catch (e: Exception) {
Logging.error("Exception in launchOnDefault", e)
}
}
} else {
GlobalScope.launch(Dispatchers.Default) {
try {
block()
} catch (e: Exception) {
Logging.error("Exception in launchOnDefault", e)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.onesignal.common.threading

import com.onesignal.debug.internal.logging.Logging

/**
* Global threading mode switch that can be refreshed from remote config.
*/
internal object ThreadingMode {
@Volatile
var useBackgroundThreading: Boolean = false

fun updateUseBackgroundThreading(
enabled: Boolean,
source: String,
) {
val previous = useBackgroundThreading
useBackgroundThreading = enabled

if (previous != enabled) {
Logging.info("OneSignal: ThreadingMode changed to useBackgroundThreading=$enabled (source=$source)")
} else {
Logging.debug("OneSignal: ThreadingMode unchanged (useBackgroundThreading=$enabled, source=$source)")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import com.onesignal.core.internal.device.IDeviceService
import com.onesignal.core.internal.device.IInstallIdService
import com.onesignal.core.internal.device.impl.DeviceService
import com.onesignal.core.internal.device.impl.InstallIdService
import com.onesignal.core.internal.features.FeatureManager
import com.onesignal.core.internal.features.IFeatureManager
import com.onesignal.core.internal.http.IHttpClient
import com.onesignal.core.internal.http.impl.HttpClient
import com.onesignal.core.internal.http.impl.HttpConnectionFactory
Expand Down Expand Up @@ -57,6 +59,7 @@ internal class CoreModule : IModule {

// Params (Config)
builder.register<ConfigModelStore>().provides<ConfigModelStore>()
builder.register<FeatureManager>().provides<IFeatureManager>()
builder.register<ParamsBackendService>().provides<IParamsBackendService>()
builder.register<ConfigModelStoreListener>().provides<IStartableService>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal class ParamsObject(
var locationShared: Boolean? = null,
var requiresUserPrivacyConsent: Boolean? = null,
var opRepoExecutionInterval: Long? = null,
val features: List<String> = emptyList(),
var influenceParams: InfluenceParamsObject,
var fcmParams: FCMParamsObject,
val remoteLoggingParams: RemoteLoggingParamsObject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ internal class ParamsBackendService(
)
}

val features =
responseJson.optJSONArray("features")
?.let { featuresJson ->
buildList {
for (i in 0 until featuresJson.length()) {
val featureName = featuresJson.optString(i, "")
if (featureName.isNotBlank()) {
add(featureName)
}
}
}
} ?: emptyList()

return ParamsObject(
googleProjectNumber = responseJson.safeString("android_sender_id"),
enterprise = responseJson.safeBool("enterp"),
Expand All @@ -84,6 +97,7 @@ internal class ParamsBackendService(
requiresUserPrivacyConsent = responseJson.safeBool("requires_user_privacy_consent"),
// TODO: New
opRepoExecutionInterval = responseJson.safeLong("oprepo_execution_interval"),
features = features,
influenceParams = influenceParams ?: InfluenceParamsObject(),
fcmParams = fcmParams ?: FCMParamsObject(),
remoteLoggingParams = remoteLoggingParams ?: RemoteLoggingParamsObject(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,16 @@ class ConfigModel : Model() {
setBooleanProperty(::clearGroupOnSummaryClick.name, value)
}

/**
* Remote feature switches controlled by backend.
* Presence of a feature name indicates enabled.
*/
var features: List<String>
get() = getListProperty(::features.name) { emptyList() }
set(value) {
setListProperty(::features.name, value)
}

/**
* The outcomes parameters
*/
Expand Down Expand Up @@ -329,6 +339,24 @@ class ConfigModel : Model() {

return null
}

override fun createListForProperty(
property: String,
jsonArray: JSONArray,
): List<*>? {
if (property == ::features.name) {
return buildList {
for (i in 0 until jsonArray.length()) {
val featureName = jsonArray.optString(i, "")
if (featureName.isNotBlank()) {
add(featureName)
}
}
}
}

return null
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ internal class ConfigModelStoreListener(
params.locationShared?.let { config.locationShared = it }
params.requiresUserPrivacyConsent?.let { config.consentRequired = it }
params.opRepoExecutionInterval?.let { config.opRepoExecutionInterval = it }
config.features = params.features
params.influenceParams.notificationLimit?.let { config.influenceParams.notificationLimit = it }
params.influenceParams.indirectNotificationAttributionWindow?.let { config.influenceParams.indirectNotificationAttributionWindow = it }
params.influenceParams.iamLimit?.let { config.influenceParams.iamLimit = it }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.onesignal.core.internal.features

/**
* Controls when remote config changes for a feature are applied.
*/
internal enum class FeatureActivationMode {
/**
* Apply config changes immediately during the current app run.
*/
IMMEDIATE,

/**
* Latch value at startup; apply remote changes on next app run.
*/
APP_STARTUP,
}

/**
* Backend-driven feature switches used by the SDK.
*/
internal enum class FeatureFlag(
val key: String,
val activationMode: FeatureActivationMode,
) {
// Threading mode is selected once per app startup to avoid mixed-mode behavior mid-session.
BACKGROUND_THREADING("BACKGROUND_THREADING", FeatureActivationMode.APP_STARTUP),
}
Loading
Loading