Skip to content
Open
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
Expand Up @@ -41,7 +41,11 @@ import com.onesignal.user.state.UserState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class MainApplicationKT : MultiDexApplication() {

Expand Down Expand Up @@ -80,9 +84,43 @@ class MainApplicationKT : MultiDexApplication() {
OneSignal.Notifications.requestPermission(true)

Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT)

delay(3000)
}
// crashApp()
// forceANR()
}

private fun forceANR() {
try {
android.os.Handler(android.os.Looper.getMainLooper()).post {
Log.d(Tag.LOG_TAG, "Starting infinite loop on main thread to trigger ANR.")
// This will block the main thread indefinitely, triggering an ANR
// The ANR detector will detect it after 5 seconds if OneSignal code is in the stack trace
while (true) {
Log.d(Tag.LOG_TAG, "Blocking main thread - ANR test")
// Small sleep to prevent excessive CPU usage, but still blocks the thread
Thread.sleep(100)
}
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
}

private fun crashApp() {
val sdf = SimpleDateFormat(
"MMM dd, yyyy HH:mm:ss",
Locale.getDefault()
)

crashApp()
val currentTimeMillis = System.currentTimeMillis()
val date = Date(currentTimeMillis)
val formattedDate = sdf.format(date)
throw RuntimeException("test crash from AR $formattedDate")
}

private fun setupOneSignalListeners() {
OneSignal.InAppMessages.addLifecycleListener(object : IInAppMessageLifecycleListener {
override fun onWillDisplay(@NonNull event: IInAppMessageWillDisplayEvent) {
Expand Down
16 changes: 9 additions & 7 deletions OneSignalSDK/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ buildscript {
huaweiAgconnectVersion = '1.9.1.304'
huaweiHMSPushVersion = '6.3.0.304'
huaweiHMSLocationVersion = '4.0.0.300'
kotlinVersion = '1.9.25'
dokkaVersion = '1.9.10' // Dokka version compatible with Kotlin 1.9.25
kotlinVersion = '2.2.0'
dokkaVersion = '1.9.10'
coroutinesVersion = '1.7.3'
kotestVersion = '5.8.0'
ioMockVersion = '1.13.2'
Expand All @@ -25,6 +25,10 @@ buildscript {
ktlintVersion = '0.50.0' // Used by Spotless for Kotlin formatting (compatible with Kotlin 1.7.10)
spotlessVersion = '6.25.0'
tdunningJsonForTest = '1.0' // DO NOT upgrade for tests, using an old version so it matches AOSP
// OpenTelemetry versions
opentelemetryBomVersion = '1.55.0'
opentelemetrySemconvVersion = '1.37.0'
opentelemetryDiskBufferingVersion = '1.51.0-alpha'

sharedRepos = {
google()
Expand All @@ -45,11 +49,9 @@ buildscript {
]
}

buildscript {
repositories sharedRepos
dependencies {
classpath sharedDeps
}
repositories sharedRepos
dependencies {
classpath sharedDeps
}
}

Expand Down
2 changes: 1 addition & 1 deletion OneSignalSDK/detekt/detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ comments:
UndocumentedPublicFunction:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/testhelpers/**']

EndOfSentenceFormat:
active: false
endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$)
Expand Down
2 changes: 2 additions & 0 deletions OneSignalSDK/onesignal/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ dependencies {
}
}

// Otel module dependency
implementation(project(':OneSignal:otel'))
testImplementation(project(':OneSignal:testhelpers'))

testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
Expand Down
7 changes: 6 additions & 1 deletion OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- Override otel module's minSdk requirement (26) since we have runtime checks -->
<!-- The otel module is only used on SDK 26+, so this is safe -->
<uses-sdk tools:overrideLibrary="com.onesignal.otel" />

<!-- Required so the device can access the internet. -->
<uses-permission android:name="android.permission.INTERNET" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.onesignal.core.internal.purchases.impl.TrackGooglePurchase
import com.onesignal.core.internal.startup.IStartableService
import com.onesignal.core.internal.time.ITime
import com.onesignal.core.internal.time.impl.Time
import com.onesignal.debug.internal.crash.OneSignalCrashUploaderWrapper
import com.onesignal.inAppMessages.IInAppMessagesManager
import com.onesignal.inAppMessages.internal.MisconfiguredIAMManager
import com.onesignal.location.ILocationManager
Expand Down Expand Up @@ -81,6 +82,9 @@ internal class CoreModule : IModule {
// Purchase Tracking
builder.register<TrackGooglePurchase>().provides<IStartableService>()

// Crash Uploader (crash handler is initialized directly in OneSignalImp for early initialization)
builder.register<OneSignalCrashUploaderWrapper>().provides<IStartableService>()

// Register dummy services in the event they are not configured. These dummy services
// will throw an error message if the associated functionality is attempted to be used.
builder.register<MisconfiguredNotificationsManager>().provides<INotificationsManager>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.onesignal.core.internal.backend

import org.json.JSONArray

interface IParamsBackendService {
internal interface IParamsBackendService {
/**
* Retrieve the configuration parameters for the [appId] and optional [subscriptionId].
*
Expand All @@ -20,7 +20,8 @@ interface IParamsBackendService {
): ParamsObject
}

class ParamsObject(
@Suppress("LongParameterList")
internal class ParamsObject(
var googleProjectNumber: String? = null,
var enterprise: Boolean? = null,
var useIdentityVerification: Boolean? = null,
Expand All @@ -36,9 +37,10 @@ class ParamsObject(
var opRepoExecutionInterval: Long? = null,
var influenceParams: InfluenceParamsObject,
var fcmParams: FCMParamsObject,
val remoteLoggingParams: RemoteLoggingParamsObject,
)

class InfluenceParamsObject(
internal class InfluenceParamsObject(
val indirectNotificationAttributionWindow: Int? = null,
val notificationLimit: Int? = null,
val indirectIAMAttributionWindow: Int? = null,
Expand All @@ -48,8 +50,12 @@ class InfluenceParamsObject(
val isUnattributedEnabled: Boolean? = null,
)

class FCMParamsObject(
internal class FCMParamsObject(
val projectId: String? = null,
val appId: String? = null,
val apiKey: String? = null,
)

internal class RemoteLoggingParamsObject(
val logLevel: com.onesignal.debug.LogLevel? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.onesignal.core.internal.backend.FCMParamsObject
import com.onesignal.core.internal.backend.IParamsBackendService
import com.onesignal.core.internal.backend.InfluenceParamsObject
import com.onesignal.core.internal.backend.ParamsObject
import com.onesignal.core.internal.backend.RemoteLoggingParamsObject
import com.onesignal.core.internal.http.CacheKeys
import com.onesignal.core.internal.http.IHttpClient
import com.onesignal.core.internal.http.impl.OptionalHeaders
Expand Down Expand Up @@ -57,6 +58,16 @@ internal class ParamsBackendService(
)
}

// Process Remote Logging params
var remoteLoggingParams: RemoteLoggingParamsObject? = null
responseJson.expandJSONObject("remote_logging") {
val logLevel = parseLogLevel(it)
remoteLoggingParams =
RemoteLoggingParamsObject(
logLevel = logLevel,
)
}

return ParamsObject(
googleProjectNumber = responseJson.safeString("android_sender_id"),
enterprise = responseJson.safeBool("enterp"),
Expand All @@ -75,6 +86,7 @@ internal class ParamsBackendService(
opRepoExecutionInterval = responseJson.safeLong("oprepo_execution_interval"),
influenceParams = influenceParams ?: InfluenceParamsObject(),
fcmParams = fcmParams ?: FCMParamsObject(),
remoteLoggingParams = remoteLoggingParams ?: RemoteLoggingParamsObject(),
)
}

Expand Down Expand Up @@ -122,4 +134,38 @@ internal class ParamsBackendService(
isUnattributedEnabled,
)
}

/**
* Parse LogLevel from JSON. Supports both string (enum name) and int (ordinal) formats.
*/
@Suppress("ReturnCount", "TooGenericExceptionCaught", "SwallowedException")
private fun parseLogLevel(json: JSONObject): LogLevel? {
// Try string format first (e.g., "ERROR", "WARN", "NONE")
val logLevelString = json.safeString("log_level") ?: json.safeString("logLevel")
if (logLevelString != null) {
try {
return LogLevel.valueOf(logLevelString.uppercase())
} catch (e: IllegalArgumentException) {
Logging.warn("Invalid log level string: $logLevelString")
}
}

// Try int format (ordinal: 0=NONE, 1=FATAL, 2=ERROR, etc.)
val logLevelInt = json.safeInt("log_level") ?: json.safeInt("logLevel")
if (logLevelInt != null) {
try {
return LogLevel.fromInt(logLevelInt)
} catch (e: Exception) {
Logging.warn("Invalid log level int: $logLevelInt")
}
}

// Backward compatibility: support old "enable" boolean field
val enable = json.safeBool("enable")
if (enable != null) {
return if (enable) LogLevel.ERROR else LogLevel.NONE
}

return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ class ConfigModel : Model() {
val fcmParams: FCMConfigModel
get() = getAnyProperty(::fcmParams.name) { FCMConfigModel(this, ::fcmParams.name) } as FCMConfigModel

val remoteLoggingParams: RemoteLoggingConfigModel
get() = getAnyProperty(::remoteLoggingParams.name) { RemoteLoggingConfigModel(this, ::remoteLoggingParams.name) } as RemoteLoggingConfigModel

override fun createModelForProperty(
property: String,
jsonObject: JSONObject,
Expand All @@ -317,6 +320,12 @@ class ConfigModel : Model() {
return model
}

if (property == ::remoteLoggingParams.name) {
val model = RemoteLoggingConfigModel(this, ::remoteLoggingParams.name)
model.initializeFromJson(jsonObject)
return model
}

return null
}
}
Expand Down Expand Up @@ -425,3 +434,24 @@ class FCMConfigModel(parentModel: Model, parentProperty: String) : Model(parentM
setOptStringProperty(::apiKey.name, value)
}
}

/**
* Configuration related to OneSignal's remote logging.
*/
class RemoteLoggingConfigModel(
parentModel: Model,
parentProperty: String,
) : Model(parentModel, parentProperty) {
/**
* The minimum log level to send to OneSignal's server.
* If null, defaults to ERROR level for client-side logging.
* If NONE, no logs (including errors) will be sent remotely.
*
* Log levels: NONE < FATAL < ERROR < WARN < INFO < DEBUG < VERBOSE
*/
var logLevel: com.onesignal.debug.LogLevel?
get() = getOptEnumProperty<com.onesignal.debug.LogLevel>(::logLevel.name)
set(value) {
setOptEnumProperty(::logLevel.name, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package com.onesignal.core.internal.config
import com.onesignal.common.modeling.SimpleModelStore
import com.onesignal.common.modeling.SingletonModelStore
import com.onesignal.core.internal.preferences.IPreferencesService
const val CONFIG_NAME_SPACE = "config"

open class ConfigModelStore(prefs: IPreferencesService) : SingletonModelStore<ConfigModel>(
SimpleModelStore({ ConfigModel() }, "config", prefs),
SimpleModelStore({ ConfigModel() }, CONFIG_NAME_SPACE, prefs),
)
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ internal class ConfigModelStoreListener(
params.influenceParams.isIndirectEnabled?.let { config.influenceParams.isIndirectEnabled = it }
params.influenceParams.isUnattributedEnabled?.let { config.influenceParams.isUnattributedEnabled = it }

params.remoteLoggingParams.logLevel?.let { config.remoteLoggingParams.logLevel = it }

_configModelStore.replace(config, ModelChangeTags.HYDRATE)
success = true
} catch (ex: BackendException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import java.net.UnknownHostException
import java.util.Scanner
import javax.net.ssl.HttpsURLConnection

internal const val HTTP_SDK_VERSION_HEADER_KEY = "SDK-Version"
internal val HTTP_SDK_VERSION_HEADER_VALUE = "onesignal/android/${OneSignalUtils.sdkVersion}"

internal class HttpClient(
private val _connectionFactory: IHttpConnectionFactory,
private val _prefs: IPreferencesService,
Expand Down Expand Up @@ -131,7 +134,7 @@ internal class HttpClient(
con.useCaches = false
con.connectTimeout = timeout
con.readTimeout = timeout
con.setRequestProperty("SDK-Version", "onesignal/android/" + OneSignalUtils.sdkVersion)
con.setRequestProperty(HTTP_SDK_VERSION_HEADER_KEY, HTTP_SDK_VERSION_HEADER_VALUE)

if (OneSignalWrapper.sdkType != null && OneSignalWrapper.sdkVersion != null) {
con.setRequestProperty("SDK-Wrapper", "onesignal/${OneSignalWrapper.sdkType}/${OneSignalWrapper.sdkVersion}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ interface ITime {
* current time and midnight, January 1, 1970 UTC).
*/
val currentTimeMillis: Long

/**
* Returns how long the app has been running.
*/
val processUptimeMillis: Long
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.onesignal.core.internal.time.impl

import android.os.Build
import android.os.SystemClock
import androidx.annotation.RequiresApi
import com.onesignal.core.internal.time.ITime

internal class Time : ITime {
override val currentTimeMillis: Long
get() = System.currentTimeMillis()
override val processUptimeMillis: Long
@RequiresApi(Build.VERSION_CODES.N)
get() = SystemClock.uptimeMillis() - android.os.Process.getStartUptimeMillis()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.onesignal.debug.internal.crash

/**
* Constants for ANR (Application Not Responding) detection configuration.
*/
internal object AnrConstants {
/**
* Default ANR threshold in milliseconds.
* Android's default ANR threshold is 5 seconds (5000ms).
* An ANR is reported when the main thread is unresponsive for this duration.
*/
const val DEFAULT_ANR_THRESHOLD_MS: Long = 5_000L

/**
* Default check interval in milliseconds.
* The ANR detector checks the main thread responsiveness every 2 seconds.
*/
const val DEFAULT_CHECK_INTERVAL_MS: Long = 2_000L
}
Loading
Loading