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
4 changes: 4 additions & 0 deletions OneSignalSDK/detekt/detekt-baseline-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
<ID>MagicNumber:OSDatabase.kt$OSDatabase$9</ID>
<ID>MagicNumber:OneSignalDispatchers.kt$OneSignalDispatchers$1024</ID>
<ID>MagicNumber:OneSignalImp.kt$OneSignalImp$8</ID>
<ID>MagicNumber:OneSignalImpV2.kt$OneSignalImpV2$8</ID>
<ID>MagicNumber:OperationRepo.kt$OperationRepo$1_000</ID>
<ID>MagicNumber:OutcomeEventsController.kt$OutcomeEventsController$1000</ID>
<ID>MagicNumber:PermissionsActivity.kt$PermissionsActivity$23</ID>
Expand Down Expand Up @@ -418,6 +419,7 @@
<ID>TooManyFunctions:OSDatabase.kt$OSDatabase : SQLiteOpenHelperIDatabase</ID>
<ID>TooManyFunctions:OneSignal.kt$OneSignal$OneSignal</ID>
<ID>TooManyFunctions:OneSignalImp.kt$OneSignalImp : IOneSignalIServiceProvider</ID>
<ID>TooManyFunctions:OneSignalImpV2.kt$OneSignalImpV2 : IOneSignalIServiceProvider</ID>
<ID>TooManyFunctions:OperationRepo.kt$OperationRepo : IOperationRepoIStartableService</ID>
<ID>TooManyFunctions:OutcomeEventsController.kt$OutcomeEventsController : IOutcomeEventsControllerIStartableServiceISessionLifecycleHandler</ID>
<ID>TooManyFunctions:PreferencesService.kt$PreferencesService : IPreferencesServiceIStartableService</ID>
Expand Down Expand Up @@ -638,5 +640,7 @@
<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 @@ -4,7 +4,7 @@ import android.content.Context
import com.onesignal.common.services.IServiceProvider
import com.onesignal.debug.IDebugManager
import com.onesignal.inAppMessages.IInAppMessagesManager
import com.onesignal.internal.OneSignalImp
import com.onesignal.internal.NeverCrashKillSwitch
import com.onesignal.location.ILocationManager
import com.onesignal.notifications.INotificationsManager
import com.onesignal.session.ISessionManager
Expand Down Expand Up @@ -376,8 +376,9 @@ object OneSignal {
fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) =
oneSignal.removeUserJwtInvalidatedListener(listener)

@Suppress("MemberNameEqualsClassName")
private val oneSignal: IOneSignal by lazy {
OneSignalImp()
NeverCrashKillSwitch.create()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context
import androidx.startup.Initializer
import com.onesignal.common.threading.OneSignalDispatchers
import com.onesignal.core.internal.application.IApplicationService
import com.onesignal.internal.NeverCrashKillSwitch

/**
* Registers activity lifecycle observation at process start via androidx.startup, before the host
Expand All @@ -16,12 +17,13 @@ import com.onesignal.core.internal.application.IApplicationService
* full lifecycle regardless of when init later happens.
*
* This intentionally does NOT initialize the SDK or require an App ID; it only attaches lifecycle
* observation and starts pure dispatcher warmup. App ID / consent gating remains in the runtime
* `initWithContext` path.
* observation, starts pure dispatcher warmup, and reads the cached init kill switch. App ID / consent
* gating remains in the runtime `initWithContext` path.
*/
class ActivityLifecycleInitializer : Initializer<IApplicationService> {
override fun create(context: Context): IApplicationService {
OneSignalDispatchers.prewarm()
NeverCrashKillSwitch.evaluate(context)
val applicationService = ApplicationService.getInstance()
(context.applicationContext as? Application)?.let { application ->
applicationService.attachToApplication(application)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.onesignal.internal

import android.content.Context
import com.onesignal.IOneSignal
import com.onesignal.common.AndroidUtils
import com.onesignal.core.internal.config.CONFIG_NAME_SPACE
import com.onesignal.core.internal.config.ConfigModel
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
import com.onesignal.core.internal.preferences.PreferenceStores
import com.onesignal.debug.internal.logging.Logging
import org.json.JSONArray

/**
* Chooses the [IOneSignal] implementation once per process, from the feature flags cached by the
* previous run, so a remote change applies on the next cold start. Any failure keeps the new behavior.
*/
internal object NeverCrashKillSwitch {
// Present means off, so installs whose init never succeeds (and never fetch flags) keep the safe default.
const val FLAG_KEY = "sdk_never_crash_disabled"

// Local testing only: true forces the legacy implementation, false the new one, null reads the flag.
private val localOverride: Boolean? = null

@Volatile
var useLegacy: Boolean = false
private set

@Volatile
var evaluated: Boolean = false
private set

// Runs on the main thread before Application.onCreate, so it must never throw.
@Suppress("TooGenericExceptionCaught")
fun evaluate(context: Context) {
useLegacy = localOverride ?: try {
isDisabledByCachedFlags(context)
} catch (t: Throwable) {
Logging.warn("OneSignal: could not read cached feature flags, keeping the default init behavior", t)
false
}
evaluated = true
}

fun create(): IOneSignal =
when {
!evaluated -> {
Logging.debug("OneSignal: kill switch not evaluated before first use, keeping the default init behavior")
OneSignalImpV2()
}
useLegacy -> {
Logging.warn("OneSignal: $FLAG_KEY is on, using the legacy init behavior")
OneSignalImp()
}
else -> OneSignalImpV2()
}

internal fun isDisabledByCachedFlags(context: Context): Boolean {
// Credential-encrypted prefs throw on access before the user unlocks the device.
val keys = if (AndroidUtils.isAndroidUserUnlocked(context)) cachedFeatureKeys(context) else null
return keys?.any { it.equals(FLAG_KEY, ignoreCase = true) } == true
}

private fun cachedFeatureKeys(context: Context): List<String>? {
val stored =
context
.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE)
.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + CONFIG_NAME_SPACE, null)
val keys = stored?.let { JSONArray(it).optJSONObject(0)?.optJSONArray(ConfigModel::sdkRemoteFeatureFlags.name) }
return keys?.let { array -> (0 until array.length()).map { array.optString(it) } }
}

internal fun resetForTest() {
useLegacy = false
evaluated = false
}
}
Loading
Loading