Skip to content
Draft
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
5 changes: 3 additions & 2 deletions OneSignalSDK/detekt/detekt-baseline-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@
<ID>PrintStackTrace:TrackGooglePurchase.kt$TrackGooglePurchase.&lt;no name provided>$t</ID>
<ID>RethrowCaughtException:OSDatabase.kt$OSDatabase$throw e</ID>
<ID>ReturnCount:AppIdResolution.kt$fun resolveAppId( inputAppId: String?, configModel: ConfigModel, preferencesService: IPreferencesService, ): AppIdResolution</ID>
<ID>ReturnCount:AppIdResolutionV2.kt$internal fun resolveAppIdV2( inputAppId: String?, configModel: ConfigModel, preferencesService: IPreferencesService, ): AppIdResolution</ID>
<ID>ReturnCount:BackgroundManager.kt$BackgroundManager$override fun cancelRunBackgroundServices(): Boolean</ID>
<ID>ReturnCount:ConfigModel.kt$ConfigModel$override fun createModelForProperty( property: String, jsonObject: JSONObject, ): Model?</ID>
<ID>ReturnCount:ExecutorsIvExtensions.kt$internal fun resolveIvBackendParams( op: Operation, onesignalId: String, jwtTokenStore: JwtTokenStore, ivBehaviorActive: Boolean, ): IvBackendParams</ID>
Expand All @@ -327,6 +328,8 @@
<ID>ReturnCount:Model.kt$Model$protected fun getOptIntProperty( name: String, create: (() -> Int?)? = null, ): Int?</ID>
<ID>ReturnCount:Model.kt$Model$protected fun getOptLongProperty( name: String, create: (() -> Long?)? = null, ): Long?</ID>
<ID>ReturnCount:Model.kt$Model$protected inline fun &lt;reified T : Enum&lt;T>> getOptEnumProperty(name: String): T?</ID>
<ID>ReturnCount:OneSignalImp.kt$OneSignalImp$override suspend fun initWithContextSuspend( context: Context?, appId: String?, ): Boolean</ID>
<ID>ReturnCount:OneSignalImpV2.kt$OneSignalImpV2$override suspend fun initWithContextSuspend( context: Context?, appId: String?, ): Boolean</ID>
<ID>ReturnCount:OperationModelStore.kt$OperationModelStore$override fun create(jsonObject: JSONObject?): Operation?</ID>
<ID>ReturnCount:OperationModelStore.kt$OperationModelStore$private fun isValidOperation(jsonObject: JSONObject): Boolean</ID>
<ID>ReturnCount:OperationRepo.kt$OperationRepo$private fun shouldSuppressAnonymousOp(op: Operation): Boolean</ID>
Expand Down Expand Up @@ -640,7 +643,5 @@
<ID>UnusedPrivateMember:OperationRepo.kt$OperationRepo$private val _time: ITime</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'updateUserJwt'")</ID>
<ID>UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw initFailureException ?: IllegalStateException("Initialization failed. Cannot proceed.")</ID>
<ID>UseCheckOrError:OneSignalImpV2.kt$OneSignalImpV2$throw IllegalStateException("'initWithContext failed' before 'updateUserJwt'")</ID>
<ID>UseCheckOrError:OneSignalImpV2.kt$OneSignalImpV2$throw initFailureException ?: IllegalStateException("Initialization failed. Cannot proceed.")</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ interface IOneSignal {
/**
* The user manager for accessing user-scoped
* management.
*
* Before init succeeds this is a stand-in: listener and setting registrations are replayed once
* init succeeds, other calls are logged and ignored.
*/
val user: IUserManager

Expand Down Expand Up @@ -79,13 +82,15 @@ interface IOneSignal {
* Initialize the OneSignal SDK. This should be called during startup of the application.
*
* @param context The Android context the SDK should use.
* @param appId The application ID the OneSignal SDK is bound to.
* @param appId The application ID the OneSignal SDK is bound to. If null or invalid, the cached
* app ID is used; if none is cached, initialization is skipped and an error is logged.
*
* @return true if the SDK could be successfully initialized, false otherwise.
* @return true once initialization has started (or is already running or done), false for a
* null [context] or an immediate startup failure. Check [isInitialized] for the outcome.
*/
fun initWithContext(
context: Context,
appId: String,
context: Context?,
appId: String?,
): Boolean

/**
Expand Down Expand Up @@ -165,42 +170,43 @@ interface IOneSignal {
* Initialize the OneSignal SDK, suspend until initialization is completed
*
* @param context The Android context the SDK should use.
* @param appId The application ID the OneSignal SDK is bound to.
* @param appId The application ID the OneSignal SDK is bound to. If null or invalid, the cached
* app ID is used; if none is cached, initialization is skipped and an error is logged.
*
* @return true if the SDK could be successfully initialized, false otherwise.
*/
suspend fun initWithContextSuspend(
context: Context,
context: Context?,
appId: String? = null,
): Boolean

/**
* Get the session manager without blocking the calling thread.
* Suspends until the SDK is initialized.
* Suspends while initialization is in progress; if the SDK is not initialized, returns a stand-in (see [user]).
*/
suspend fun getSession(): ISessionManager

/**
* Get the notifications manager without blocking the calling thread.
* Suspends until the SDK is initialized.
* Suspends while initialization is in progress; if the SDK is not initialized, returns a stand-in (see [user]).
*/
suspend fun getNotifications(): INotificationsManager

/**
* Get the location manager without blocking the calling thread.
* Suspends until the SDK is initialized.
* Suspends while initialization is in progress; if the SDK is not initialized, returns a stand-in (see [user]).
*/
suspend fun getLocation(): ILocationManager

/**
* Get the in-app messages manager without blocking the calling thread.
* Suspends until the SDK is initialized.
* Suspends while initialization is in progress; if the SDK is not initialized, returns a stand-in (see [user]).
*/
suspend fun getInAppMessages(): IInAppMessagesManager

/**
* Get the user manager without blocking the calling thread.
* Suspends until the SDK is initialized.
* Suspends while initialization is in progress; if the SDK is not initialized, returns a stand-in (see [user]).
*/
suspend fun getUser(): IUserManager

Expand Down Expand Up @@ -266,8 +272,8 @@ interface IOneSignal {
suspend fun logoutSuspend()

/**
* Update the JWT bearer token associated with [externalId] (suspend version). Suspends
* until SDK initialization is complete, then stores the JWT and wakes the operation queue.
* Update the JWT bearer token associated with [externalId] (suspend version). Suspends while
* initialization is in progress, then stores the JWT; if the SDK is not initialized it logs instead.
*
* @param externalId The external ID the JWT belongs to.
* @param token The new JWT bearer token issued by your backend.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,41 +37,44 @@ object OneSignal {
get() = oneSignal.sdkVersion

/**
* The user manager for accessing user-scoped management. Initialized only after [initWithContext]
* has been called, and initialized with a device-scoped user until (or if) [login] has been
* called.
* The user manager for accessing user-scoped management, with a device-scoped user until (or
* if) [login] is called. Before [initWithContext] succeeds this is a stand-in: listener and setting registrations are
* replayed once init succeeds, other calls are logged and ignored.
*/
@JvmStatic
val User: IUserManager
get() = oneSignal.user

/**
* The session manager for accessing session-scoped management. Initialized only after [initWithContext]
* has been called.
* The session manager for accessing session-scoped management.
* Before [initWithContext] succeeds this is a stand-in: outcome calls are logged and ignored.
*/
@JvmStatic
val Session: ISessionManager
get() = oneSignal.session

/**
* The notification manager for accessing device-scoped notification management. Initialized
* only after [initWithContext] has been called.
* The notification manager for accessing device-scoped notification management.
* Before [initWithContext] succeeds this is a stand-in: listener and setting registrations are
* replayed once init succeeds, other calls are logged and ignored.
*/
@JvmStatic
val Notifications: INotificationsManager
get() = oneSignal.notifications

/**
* The location manager for accessing device-scoped location management. Initialized
* only after [initWithContext] has been called.
* The location manager for accessing device-scoped location management.
* Before [initWithContext] succeeds this is a stand-in: listener and setting registrations are
* replayed once init succeeds, other calls are logged and ignored.
*/
@JvmStatic
val Location: ILocationManager
get() = oneSignal.location

/**
* The In App Messaging manager for accessing device-scoped IAP management. Initialized
* only after [initWithContext] has been called.
* The In App Messaging manager for accessing device-scoped IAP management.
* Before [initWithContext] succeeds this is a stand-in: listener and setting registrations are
* replayed once init succeeds, other calls are logged and ignored.
*/
@JvmStatic
val InAppMessages: IInAppMessagesManager
Expand Down Expand Up @@ -125,12 +128,13 @@ object OneSignal {
* Initialize the OneSignal SDK. This should be called during startup of the application.
*
* @param context The Android context the SDK should use.
* @param appId The application ID the OneSignal SDK is bound to.
* @param appId The application ID the OneSignal SDK is bound to. If null or invalid, the cached
* app ID is used; if none is cached, initialization is skipped and an error is logged.
*/
@JvmStatic
fun initWithContext(
context: Context,
appId: String,
context: Context?,
appId: String?,
) {
oneSignal.initWithContext(context, appId)
}
Expand All @@ -141,12 +145,13 @@ object OneSignal {
* Uses Dispatchers.IO internally to prevent ANRs and optimize for I/O operations.
*
* @param context Application context is recommended for SDK operations
* @param appId The application ID the OneSignal SDK is bound to.
* @param appId The application ID the OneSignal SDK is bound to. If null or invalid, the cached
* app ID is used; if none is cached, initialization is skipped and an error is logged.
* @return Boolean indicating if initialization was successful.
*/
@JvmStatic
suspend fun initWithContextSuspend(
context: Context,
context: Context?,
appId: String? = null,
): Boolean {
return oneSignal.initWithContextSuspend(context, appId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ import com.onesignal.debug.internal.logging.Logging
internal class StartupService(
private val services: ServiceProvider,
) {
private val bootstrapped = mutableSetOf<IBootstrapService>()

// Retryable after a failure: services that already bootstrapped are skipped, so none subscribes twice.
fun bootstrap() {
services.getAllServices<IBootstrapService>().forEach { it.bootstrap() }
services.getAllServices<IBootstrapService>().forEach {
if (it !in bootstrapped) {
it.bootstrap()
bootstrapped += it
}
}
}

// schedule to start all startable services using OneSignal dispatcher
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.onesignal.internal

import com.onesignal.debug.internal.logging.Logging
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking

/**
* Init state for [OneSignalImpV2] and the waits on it. Never throws: callers that need the SDK
* get `false` and a log line when init has not started or has failed.
*/
internal class InitGate {
private val lock = Any()

// Replaced by every new attempt, so a retry after FAILED never reuses a completed signal.
@Volatile
private var completion = CompletableDeferred<Unit>()

@Volatile
var state: InitState = InitState.NOT_STARTED
private set

// Created by the call that started the attempt, so its stack points at the app's init call.
@Volatile
var failure: Exception? = null
private set

@Volatile
private var failureTraceLogged = false

val isSuccess: Boolean get() = state == InitState.SUCCESS

/** Starts a new attempt and returns true, or returns false if one is running or has succeeded. */
fun tryStart(): Boolean =
synchronized(lock) {
if (state.isSDKAccessible()) return false
failure = IllegalStateException("OneSignal initWithContext failed.")
failureTraceLogged = false
state = InitState.IN_PROGRESS
completion = CompletableDeferred()
true
}

fun addFailureCause(cause: Throwable) {
failure?.addSuppressed(cause)
}

// State and signal change together, so a waiter can't wake up holding the next attempt's signal.
fun complete(terminal: InitState) {
require(terminal == InitState.SUCCESS || terminal == InitState.FAILED) {
"complete requires a terminal state, got $terminal"
}
synchronized(lock) {
state = terminal
completion.complete(Unit)
}
}

/** Waits out in-progress attempts, including a retry that starts while waiting. True only on SUCCESS. */
suspend fun await(operation: SdkOperation? = null): Boolean {
while (true) {
val (observed, signal) = synchronized(lock) { state to completion }
when (observed) {
InitState.SUCCESS -> return true
InitState.NOT_STARTED, InitState.FAILED -> return false
InitState.IN_PROGRESS -> awaitAttempt(signal, operation)
}
}
}

/** Blocking form of [awaitReady]. On the main thread this blocks until init finishes. */
fun isReady(
operation: SdkOperation,
member: String? = null,
log: Boolean = true,
): Boolean {
if (isSuccess) return true
// Dispatcher-less: resumes on the caller's own event loop, never on a busy IO worker.
return runBlocking { awaitReady(operation, member, log) }
}

suspend fun awaitReady(
operation: SdkOperation,
member: String? = null,
log: Boolean = true,
): Boolean {
if (await(operation)) return true
if (log) logNotReady(target(operation, member))
return false
}

fun notReadyMessage(target: String): String =
if (state == InitState.FAILED) {
"'$target' ignored: initWithContext failed."
} else {
"'$target' ignored: call initWithContext with a valid appId first."
}

fun logQueued(
operation: SdkOperation,
member: String,
) {
Logging.warn("'${target(operation, member)}' queued until initWithContext succeeds.")
}

private fun logNotReady(target: String) {
val message = notReadyMessage(target)
val trace = failure.takeIf { state == InitState.FAILED && !failureTraceLogged }
if (trace == null) {
Logging.error(message)
return
}
failureTraceLogged = true
Logging.error(message, trace)
}

private suspend fun awaitAttempt(
signal: CompletableDeferred<Unit>,
operation: SdkOperation?,
) {
Logging.debug("Waiting for init to complete...")
val startTime = System.currentTimeMillis()
signal.await()
val elapsed = System.currentTimeMillis() - startTime
val before = operation?.let { " before '$it'" } ?: ""
Logging.debug("OneSignalImpV2 initialization completed$before (took ${elapsed}ms)")
}
}

private fun target(
operation: SdkOperation,
member: String?,
) = member?.let { "$operation.$it" } ?: "$operation"
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ package com.onesignal.internal
internal enum class InitState {
/**
* SDK initialization has not yet started.
* Calling SDK-dependent methods in this state will throw an exception.
* SDK-dependent calls log and return a default; they never throw.
*/
NOT_STARTED,

/**
* SDK initialization is currently in progress.
* Calls that require initialization will block (via a latch) until this completes.
* Calls that require initialization wait (block or suspend) until this completes.
*/
IN_PROGRESS,

Expand All @@ -26,8 +26,8 @@ internal enum class InitState {
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.
* SDK initialization has failed (e.g., no usable app ID, locked storage).
* SDK-dependent calls log and return a default until a retry of initWithContext succeeds.
*/
FAILED,

Expand Down
Loading
Loading