diff --git a/OneSignalSDK/build.gradle b/OneSignalSDK/build.gradle index c02338492e..eab205d258 100644 --- a/OneSignalSDK/build.gradle +++ b/OneSignalSDK/build.gradle @@ -14,8 +14,8 @@ buildscript { huaweiAgconnectVersion = '1.9.1.304' huaweiHMSPushVersion = '6.3.0.304' huaweiHMSLocationVersion = '4.0.0.300' - kotlinVersion = '2.2.0' - dokkaVersion = '1.9.10' + kotlinVersion = '1.9.25' + dokkaVersion = '1.9.10' // Dokka version compatible with Kotlin 1.9.25 coroutinesVersion = '1.7.3' kotestVersion = '5.8.0' ioMockVersion = '1.13.2' @@ -25,10 +25,6 @@ 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() @@ -49,9 +45,11 @@ buildscript { ] } - repositories sharedRepos - dependencies { - classpath sharedDeps + buildscript { + repositories sharedRepos + dependencies { + classpath sharedDeps + } } } diff --git a/OneSignalSDK/detekt/detekt-baseline-core.xml b/OneSignalSDK/detekt/detekt-baseline-core.xml index 797b08f41e..20c78da602 100644 --- a/OneSignalSDK/detekt/detekt-baseline-core.xml +++ b/OneSignalSDK/detekt/detekt-baseline-core.xml @@ -6,11 +6,10 @@ ComplexCondition:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$args.size == 4 && args[0] == Int::class.javaPrimitiveType && args[1] == String::class.java && args[2] == String::class.java && args[3] == Bundle::class.java && returnType == Bundle::class.java ComplexCondition:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$args.size == 4 && args[0] == Int::class.javaPrimitiveType && args[1] == String::class.java && args[2] == String::class.java && args[3] == String::class.java ComplexMethod:ConfigModelStoreListener.kt$ConfigModelStoreListener$private fun fetchParams() - ComplexMethod:HttpClient.kt$HttpClient$@OptIn(DelicateCoroutinesApi::class) private suspend fun makeRequestIODispatcher( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse + ComplexMethod:HttpClient.kt$HttpClient$private suspend fun makeRequestIODispatcher( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse ComplexMethod:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse ComplexMethod:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private suspend fun createUser( createUserOperation: LoginUserOperation, operations: List<Operation>, ): ExecutionResponse ComplexMethod:OSDatabase.kt$OSDatabase$@Synchronized private fun internalOnUpgrade( db: SQLiteDatabase, oldVersion: Int, newVersion: Int, ) - ComplexMethod:OneSignalImp.kt$OneSignalImp$override fun initWithContext( context: Context, appId: String?, ): Boolean ComplexMethod:OperationModelStore.kt$OperationModelStore$override fun create(jsonObject: JSONObject?): Operation? ComplexMethod:OperationRepo.kt$OperationRepo$internal suspend fun executeOperations(ops: List<OperationQueueItem>) ComplexMethod:PreferencesService.kt$PreferencesService$private fun get( store: String, key: String, type: Class<*>, defValue: Any?, ): Any? @@ -139,7 +138,6 @@ ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _propertiesModelStore: PropertiesModelStore ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _userBackend: IUserBackendService ConstructorParameterNaming:UserBackendService.kt$UserBackendService$private val _httpClient: IHttpClient - ConstructorParameterNaming:UserManager.kt$UserManager$private val _customEventController: ICustomEventController ConstructorParameterNaming:UserManager.kt$UserManager$private val _identityModelStore: IdentityModelStore ConstructorParameterNaming:UserManager.kt$UserManager$private val _languageContext: ILanguageContext ConstructorParameterNaming:UserManager.kt$UserManager$private val _propertiesModelStore: PropertiesModelStore @@ -159,30 +157,26 @@ ForbiddenComment:HttpClient.kt$HttpClient$// TODO: SHOULD RETURN OK INSTEAD OF NOT_MODIFIED TO MAKE TRANSPARENT? ForbiddenComment:IPreferencesService.kt$PreferenceOneSignalKeys$* (String) The serialized IAMs TODO: This isn't currently used, determine if actually needed for cold start IAM fetch delay ForbiddenComment:IUserBackendService.kt$IUserBackendService$// TODO: Change to send only the push subscription, optimally - ForbiddenComment:OneSignalImp.kt$OneSignalImp$// TODO: Set JWT Token for all future requests. - ForbiddenComment:OneSignalImp.kt$OneSignalImp$// TODO: remove JWT Token for all future requests. + ForbiddenComment:LoginHelper.kt$LoginHelper$// TODO: Set JWT Token for all future requests. + ForbiddenComment:LogoutHelper.kt$LogoutHelper$// TODO: remove JWT Token for all future requests. ForbiddenComment:OperationRepo.kt$OperationRepo$// TODO: Need to provide callback for app to reset JWT. For now, fail with no retry. ForbiddenComment:ParamsBackendService.kt$ParamsBackendService$// TODO: New ForbiddenComment:PermissionsActivity.kt$PermissionsActivity$// TODO after we remove IAM from being an activity window we may be able to remove this handler ForbiddenComment:PermissionsActivity.kt$PermissionsActivity$// TODO improve this method - ForbiddenComment:PermissionsActivity.kt$PermissionsActivity.Companion$// TODO this will be removed once the handled is deleted + ForbiddenComment:PermissionsViewModel.kt$PermissionsViewModel.Companion$// TODO this will be removed once the handler is deleted ForbiddenComment:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$// TODO: whenever the end-user changes users, we need to add the read-your-write token here, currently no code to handle the re-fetch IAMs ForbiddenComment:TrackGooglePurchase.kt$TrackGooglePurchase$// TODO: Handle very large list. Test for continuationToken != null then call getPurchases again FunctionOnlyReturningConstant:AndroidUtils.kt$AndroidUtils$@Keep fun opaqueHasClass(_class: Class<*>): Boolean FunctionParameterNaming:AndroidUtils.kt$AndroidUtils$_class: Class<*> FunctionParameterNaming:JSONUtils.kt$JSONUtils$`object`: Any - GlobalCoroutineUsage:HttpClient.kt$HttpClient$GlobalScope.launch(Dispatchers.IO) { var httpResponse = -1 var con: HttpURLConnection? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { TrafficStats.setThreadStatsTag(THREAD_ID) } try { con = _connectionFactory.newHttpURLConnection(url) // https://github.com/OneSignal/OneSignal-Android-SDK/issues/1465 // Android 4.4 and older devices fail to register to onesignal.com to due it's TLS1.2+ requirement if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1 && con is HttpsURLConnection) { val conHttps = con conHttps.sslSocketFactory = TLS12SocketFactory( conHttps.sslSocketFactory, ) } con.useCaches = false con.connectTimeout = timeout con.readTimeout = timeout 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}") } con.setRequestProperty("Accept", OS_ACCEPT_HEADER) val subscriptionId = _configModelStore.model.pushSubscriptionId if (subscriptionId != null && subscriptionId.isNotEmpty()) { con.setRequestProperty("OneSignal-Subscription-Id", subscriptionId) } con.setRequestProperty("OneSignal-Install-Id", _installIdService.getId().toString()) if (jsonBody != null) { con.doInput = true } if (method != null) { con.setRequestProperty("Content-Type", "application/json; charset=UTF-8") con.requestMethod = method con.doOutput = true } logHTTPSent(con.requestMethod, con.url, jsonBody, con.requestProperties) if (jsonBody != null) { val strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody) val sendBytes = strJsonBody.toByteArray(charset("UTF-8")) con.setFixedLengthStreamingMode(sendBytes.size) val outputStream = con.outputStream outputStream.write(sendBytes) } // H E A D E R S if (headers?.cacheKey != null) { val eTag = _prefs.getString( PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_ETAG_PREFIX + headers.cacheKey, ) if (eTag != null) { con.setRequestProperty("If-None-Match", eTag) Logging.debug("HttpClient: Adding header if-none-match: $eTag") } } if (headers?.rywToken != null) { con.setRequestProperty("OneSignal-RYW-Token", headers.rywToken.toString()) } if (headers?.retryCount != null) { con.setRequestProperty("Onesignal-Retry-Count", headers.retryCount.toString()) } if (headers?.sessionDuration != null) { con.setRequestProperty("OneSignal-Session-Duration", headers.sessionDuration.toString()) } // Network request is made from getResponseCode() httpResponse = con.responseCode val retryAfter = retryAfterFromResponse(con) val retryLimit = retryLimitFromResponse(con) val newDelayUntil = _time.currentTimeMillis + (retryAfter ?: 0) * 1_000 if (newDelayUntil > delayNewRequestsUntil) delayNewRequestsUntil = newDelayUntil when (httpResponse) { HttpURLConnection.HTTP_NOT_MODIFIED -> { val cachedResponse = _prefs.getString( PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_HTTP_CACHE_PREFIX + headers?.cacheKey, ) Logging.debug( "HttpClient: Got Response = ${method ?: "GET"} ${con.url} - Using Cached response due to 304: " + cachedResponse, ) // TODO: SHOULD RETURN OK INSTEAD OF NOT_MODIFIED TO MAKE TRANSPARENT? retVal = HttpResponse(httpResponse, cachedResponse, retryAfterSeconds = retryAfter, retryLimit = retryLimit) } HttpURLConnection.HTTP_ACCEPTED, HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_OK -> { val inputStream = con.inputStream val scanner = Scanner(inputStream, "UTF-8") val json = if (scanner.useDelimiter("\\A").hasNext()) scanner.next() else "" scanner.close() Logging.debug( "HttpClient: Got Response = ${method ?: "GET"} ${con.url} - STATUS: $httpResponse - Body: " + json, ) if (headers?.cacheKey != null) { val eTag = con.getHeaderField("etag") if (eTag != null) { Logging.debug("HttpClient: Got Response = Response has etag of $eTag so caching the response.") _prefs.saveString( PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_ETAG_PREFIX + headers.cacheKey, eTag, ) _prefs.saveString( PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_OS_HTTP_CACHE_PREFIX + headers.cacheKey, json, ) } } retVal = HttpResponse(httpResponse, json, retryAfterSeconds = retryAfter, retryLimit = retryLimit) } else -> { Logging.debug("HttpClient: Got Response = ${method ?: "GET"} ${con.url} - FAILED STATUS: $httpResponse") var inputStream = con.errorStream if (inputStream == null) { inputStream = con.inputStream } var jsonResponse: String? = null if (inputStream != null) { val scanner = Scanner(inputStream, "UTF-8") jsonResponse = if (scanner.useDelimiter("\\A").hasNext()) scanner.next() else "" scanner.close() Logging.warn("HttpClient: Got Response = $method - STATUS: $httpResponse - Body: $jsonResponse") } else { Logging.warn("HttpClient: Got Response = $method - STATUS: $httpResponse - No response body!") } retVal = HttpResponse(httpResponse, jsonResponse, retryAfterSeconds = retryAfter, retryLimit = retryLimit) } } } catch (t: Throwable) { if (t is ConnectException || t is UnknownHostException) { Logging.info("HttpClient: Could not send last request, device is offline. Throwable: " + t.javaClass.name) } else { Logging.warn("HttpClient: $method Error thrown from network stack. ", t) } retVal = HttpResponse(httpResponse, null, t) } finally { con?.disconnect() } } - GlobalCoroutineUsage:PreferencesService.kt$PreferencesService$GlobalScope.async(Dispatchers.IO) { var lastSyncTime = _time.currentTimeMillis while (true) { try { // go through all outstanding items to process for (storeKey in prefsToApply.keys) { val storeMap = prefsToApply[storeKey]!! val prefsToWrite = getSharedPrefsByName(storeKey) if (prefsToWrite == null) { // the assumption here is there is no context yet, but will be. So ensure // we wake up to try again and persist the preference. waiter.wake() continue } val editor = prefsToWrite.edit() synchronized(storeMap) { for (key in storeMap.keys) { when (val value = storeMap[key]) { is String -> editor.putString(key, value as String?) is Boolean -> editor.putBoolean(key, (value as Boolean?)!!) is Int -> editor.putInt(key, (value as Int?)!!) is Long -> editor.putLong(key, (value as Long?)!!) is Set<*> -> editor.putStringSet(key, value as Set<String?>?) null -> editor.remove(key) } } storeMap.clear() } editor.apply() } // potentially delay to prevent this from constant IO if a bunch of // preferences are set sequentially. val newTime = _time.currentTimeMillis val delay = lastSyncTime - newTime + WRITE_CALL_DELAY_TO_BUFFER_MS lastSyncTime = newTime if (delay > 0) { delay(delay) } // wait to be woken up for the next pass waiter.waitForWake() } catch (e: Throwable) { Logging.log(LogLevel.ERROR, "Error with Preference work loop", e) } } } - GlobalCoroutineUsage:RecoverFromDroppedLoginBug.kt$RecoverFromDroppedLoginBug$GlobalScope.launch(Dispatchers.IO) { _operationRepo.awaitInitialized() if (isInBadState()) { Logging.warn( "User with externalId:" + "${_identityModelStore.model.externalId} " + "was in a bad state, causing it to not update on OneSignal's " + "backend! We are recovering and replaying all unsent " + "operations now.", ) recoverByAddingBackDroppedLoginOperation() } } InstanceOfCheckForException:HttpClient.kt$HttpClient$t is ConnectException InstanceOfCheckForException:HttpClient.kt$HttpClient$t is UnknownHostException LongMethod:ApplicationService.kt$ApplicationService$override suspend fun waitUntilSystemConditionsAvailable(): Boolean LongMethod:ConfigModelStoreListener.kt$ConfigModelStoreListener$private fun fetchParams() - LongMethod:HttpClient.kt$HttpClient$@OptIn(DelicateCoroutinesApi::class) private suspend fun makeRequestIODispatcher( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse + LongMethod:HttpClient.kt$HttpClient$private suspend fun makeRequestIODispatcher( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse LongMethod:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse LongMethod:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private suspend fun createUser( createUserOperation: LoginUserOperation, operations: List<Operation>, ): ExecutionResponse LongMethod:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private suspend fun loginUser( loginUserOp: LoginUserOperation, operations: List<Operation>, ): ExecutionResponse - LongMethod:OneSignalImp.kt$OneSignalImp$override fun initWithContext( context: Context, appId: String?, ): Boolean LongMethod:OperationRepo.kt$OperationRepo$internal suspend fun executeOperations(ops: List<OperationQueueItem>) LongMethod:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendAndCreateOutcomeEvent( name: String, weight: Float, // Note: this is optional sessionTime: Long, influences: List<Influence>, ): OutcomeEvent? LongMethod:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendUniqueOutcomeEvent( name: String, sessionInfluences: List<Influence>, ): OutcomeEvent? @@ -197,14 +191,15 @@ LongMethod:TrackGooglePurchase.kt$TrackGooglePurchase$private fun queryBoughtItems() LongMethod:TrackGooglePurchase.kt$TrackGooglePurchase$private fun sendPurchases( skusToAdd: ArrayList<String>, newPurchaseTokens: ArrayList<String>, ) LongMethod:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse - LongParameterList:ICustomEventBackendService.kt$ICustomEventBackendService$( appId: String, onesignalId: String, externalId: String?, timestamp: Long, eventName: String, eventProperties: String?, metadata: CustomEventMetadata, ) LongParameterList:IDatabase.kt$IDatabase$( table: String, columns: Array<String>? = null, whereClause: String? = null, whereArgs: Array<String>? = null, groupBy: String? = null, having: String? = null, orderBy: String? = null, limit: String? = null, action: (ICursor) -> Unit, ) LongParameterList:IOutcomeEventsBackendService.kt$IOutcomeEventsBackendService$( appId: String, userId: String, subscriptionId: String, deviceType: String, direct: Boolean?, event: OutcomeEvent, ) + LongParameterList:IParamsBackendService.kt$ParamsObject$( var googleProjectNumber: String? = null, var enterprise: Boolean? = null, var useIdentityVerification: Boolean? = null, var notificationChannels: JSONArray? = null, var firebaseAnalytics: Boolean? = null, var restoreTTLFilter: Boolean? = null, var clearGroupOnSummaryClick: Boolean? = null, var receiveReceiptEnabled: Boolean? = null, var disableGMSMissingPrompt: Boolean? = null, var unsubscribeWhenNotificationsDisabled: Boolean? = null, var locationShared: Boolean? = null, var requiresUserPrivacyConsent: Boolean? = null, var opRepoExecutionInterval: Long? = null, var influenceParams: InfluenceParamsObject, var fcmParams: FCMParamsObject, ) LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, ) LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, ) LongParameterList:OutcomeEventsController.kt$OutcomeEventsController$( private val _session: ISessionService, private val _influenceManager: IInfluenceManager, private val _outcomeEventsCache: IOutcomeEventsRepository, private val _outcomeEventsPreferences: IOutcomeEventsPreferences, private val _outcomeEventsBackend: IOutcomeEventsBackendService, private val _configModelStore: ConfigModelStore, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _deviceService: IDeviceService, private val _time: ITime, ) LongParameterList:SubscriptionObject.kt$SubscriptionObject$( val id: String? = null, val type: SubscriptionObjectType? = null, val token: String? = null, val enabled: Boolean? = null, val notificationTypes: Int? = null, val sdk: String? = null, val deviceModel: String? = null, val deviceOS: String? = null, val rooted: Boolean? = null, val netType: Int? = null, val carrier: String? = null, val appVersion: String? = null, ) LongParameterList:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$( private val _subscriptionBackend: ISubscriptionBackendService, private val _deviceService: IDeviceService, private val _applicationService: IApplicationService, private val _subscriptionModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, ) + LongParameterList:UserSwitcher.kt$UserSwitcher$( private val preferencesService: IPreferencesService, private val operationRepo: IOperationRepo, private val services: ServiceProvider, private val idManager: IDManager = IDManager, private val identityModelStore: IdentityModelStore, private val propertiesModelStore: PropertiesModelStore, private val subscriptionModelStore: SubscriptionModelStore, private val configModel: ConfigModel, private val oneSignalUtils: OneSignalUtils = OneSignalUtils, private val carrierName: String? = null, private val deviceOS: String? = null, private val androidUtils: AndroidUtils = AndroidUtils, private val appContextProvider: () -> Context, ) LoopWithTooManyJumpStatements:ModelStore.kt$ModelStore$for (index in jsonArray.length() - 1 downTo 0) { val newModel = create(jsonArray.getJSONObject(index)) ?: continue /* * NOTE: Migration fix for bug introduced in 5.1.12 * The following check is intended for the operation model store. * When the call to this method moved out of the operation model store's initializer, * duplicate operations could be cached. * See https://github.com/OneSignal/OneSignal-Android-SDK/pull/2099 */ val hasExisting = models.any { it.id == newModel.id } if (hasExisting) { Logging.debug("ModelStore<$name>: load - operation.id: ${newModel.id} already exists in the store.") continue } models.add(0, newModel) // listen for changes to this model newModel.subscribe(this) } MagicNumber:ApplicationService.kt$ApplicationService$50 MagicNumber:BackgroundManager.kt$BackgroundManager$5000 @@ -231,6 +226,7 @@ MagicNumber:OSDatabase.kt$OSDatabase$7 MagicNumber:OSDatabase.kt$OSDatabase$8 MagicNumber:OSDatabase.kt$OSDatabase$9 + MagicNumber:OneSignalDispatchers.kt$OneSignalDispatchers$1024 MagicNumber:OperationRepo.kt$OperationRepo$1_000 MagicNumber:OutcomeEventsController.kt$OutcomeEventsController$1000 MagicNumber:PermissionsActivity.kt$PermissionsActivity$23 @@ -277,11 +273,11 @@ PrintStackTrace:DeviceUtils.kt$DeviceUtils$t PrintStackTrace:JSONUtils.kt$JSONUtils$e PrintStackTrace:OSDatabase.kt$OSDatabase$e - PrintStackTrace:OneSignalImp.kt$OneSignalImp$e PrintStackTrace:OutcomeTableProvider.kt$OutcomeTableProvider$e PrintStackTrace:TrackGooglePurchase.kt$TrackGooglePurchase$e PrintStackTrace:TrackGooglePurchase.kt$TrackGooglePurchase.<no name provided>$t RethrowCaughtException:OSDatabase.kt$OSDatabase$throw e + ReturnCount:AppIdResolution.kt$fun resolveAppId( inputAppId: String?, configModel: ConfigModel, preferencesService: IPreferencesService, ): AppIdResolution ReturnCount:BackgroundManager.kt$BackgroundManager$override fun cancelRunBackgroundServices(): Boolean ReturnCount:ConfigModel.kt$ConfigModel$override fun createModelForProperty( property: String, jsonObject: JSONObject, ): Model? ReturnCount:HttpClient.kt$HttpClient$private suspend fun makeRequest( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse @@ -295,12 +291,12 @@ ReturnCount:Model.kt$Model$protected fun getOptIntProperty( name: String, create: (() -> Int?)? = null, ): Int? ReturnCount:Model.kt$Model$protected fun getOptLongProperty( name: String, create: (() -> Long?)? = null, ): Long? ReturnCount:Model.kt$Model$protected inline fun <reified T : Enum<T>> getOptEnumProperty(name: String): T? - ReturnCount:OneSignalImp.kt$OneSignalImp$override fun initWithContext( context: Context, appId: String?, ): Boolean ReturnCount:OperationModelStore.kt$OperationModelStore$override fun create(jsonObject: JSONObject?): Operation? ReturnCount:OperationModelStore.kt$OperationModelStore$private fun isValidOperation(jsonObject: JSONObject): Boolean ReturnCount:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendAndCreateOutcomeEvent( name: String, weight: Float, // Note: this is optional sessionTime: Long, influences: List<Influence>, ): OutcomeEvent? ReturnCount:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendUniqueOutcomeEvent( name: String, sessionInfluences: List<Influence>, ): OutcomeEvent? - ReturnCount:PermissionsActivity.kt$PermissionsActivity$private fun shouldShowSettings(permission: String): Boolean + ReturnCount:PermissionsViewModel.kt$PermissionsViewModel$private fun shouldShowSettings( permission: String, shouldShowRationaleAfter: Boolean, ): Boolean + ReturnCount:PermissionsViewModel.kt$PermissionsViewModel$suspend fun initialize( activity: Activity, permissionType: String?, androidPermission: String?, ): Boolean ReturnCount:PreferenceStoreFix.kt$PreferenceStoreFix$fun ensureNoObfuscatedPrefStore(context: Context) ReturnCount:PreferencesService.kt$PreferencesService$private fun get( store: String, key: String, type: Class<*>, defValue: Any?, ): Any? ReturnCount:PropertiesModelStoreListener.kt$PropertiesModelStoreListener$override fun getUpdateOperation( model: PropertiesModel, path: String, property: String, oldValue: Any?, newValue: Any?, ): Operation? @@ -312,7 +308,6 @@ SpreadOperator:AndroidUtils.kt$AndroidUtils$(*packageInfo.requestedPermissions) SpreadOperator:ServiceRegistration.kt$ServiceRegistrationReflection$(*paramList.toTypedArray()) StringLiteralDuplication:OSDatabase.kt$OSDatabase$"Error closing transaction! " - StringLiteralDuplication:OneSignalImp.kt$OneSignalImp$"Must call 'initWithContext' before use" StringLiteralDuplication:OutcomesDbContract.kt$OutcomesDbContract$"CREATE TABLE " SwallowedException:AlertDialogPrepromptForAndroidSettings.kt$AlertDialogPrepromptForAndroidSettings$ex: BadTokenException SwallowedException:AndroidUtils.kt$AndroidUtils$e: PackageManager.NameNotFoundException @@ -322,17 +317,21 @@ SwallowedException:JSONUtils.kt$JSONUtils$t: Throwable SwallowedException:PermissionsActivity.kt$PermissionsActivity$e: ClassNotFoundException SwallowedException:PreferencesService.kt$PreferencesService$ex: Exception + SwallowedException:SyncJobService.kt$SyncJobService$e: Exception SwallowedException:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$t: Throwable + ThrowsCount:OneSignalImp.kt$OneSignalImp$private suspend fun waitUntilInitInternal(operationName: String? = null) TooGenericExceptionCaught:AndroidUtils.kt$AndroidUtils$e: Throwable TooGenericExceptionCaught:DeviceUtils.kt$DeviceUtils$t: Throwable TooGenericExceptionCaught:HttpClient.kt$HttpClient$e: Throwable TooGenericExceptionCaught:HttpClient.kt$HttpClient$t: Throwable TooGenericExceptionCaught:JSONUtils.kt$JSONUtils$t: Throwable TooGenericExceptionCaught:Logging.kt$Logging$t: Throwable + TooGenericExceptionCaught:OneSignalDispatchers.kt$OneSignalDispatchers$e: Exception TooGenericExceptionCaught:OperationRepo.kt$OperationRepo$e: Throwable TooGenericExceptionCaught:PreferenceStoreFix.kt$PreferenceStoreFix$e: Throwable TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$e: Throwable TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$ex: Exception + TooGenericExceptionCaught:SyncJobService.kt$SyncJobService$e: Exception TooGenericExceptionCaught:ThreadUtils.kt$e: Exception TooGenericExceptionCaught:TrackGooglePurchase.kt$TrackGooglePurchase$e: Throwable TooGenericExceptionCaught:TrackGooglePurchase.kt$TrackGooglePurchase$t: Throwable @@ -346,14 +345,11 @@ TooGenericExceptionThrown:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$throw Exception("Unrecognized operation: $startingOp") TooGenericExceptionThrown:Model.kt$Model$throw Exception("If parent model is set, parent property must also be set.") TooGenericExceptionThrown:Model.kt$Model$throw Exception("If parent property is set, parent model must also be set.") - TooGenericExceptionThrown:OneSignalImp.kt$OneSignalImp$throw Exception( "Must call 'initWithContext' before use", ) - TooGenericExceptionThrown:OneSignalImp.kt$OneSignalImp$throw Exception("Must call 'initWithContext' before 'login'") - TooGenericExceptionThrown:OneSignalImp.kt$OneSignalImp$throw Exception("Must call 'initWithContext' before 'logout'") TooGenericExceptionThrown:OperationModelStore.kt$OperationModelStore$throw Exception("Unrecognized operation: $operationName") TooGenericExceptionThrown:OperationRepo.kt$OperationRepo$throw Exception("Both comparison keys can not be blank!") TooGenericExceptionThrown:OperationRepo.kt$OperationRepo$throw Exception("Could not find executor for operation ${startingOp.operation.name}") TooGenericExceptionThrown:PermissionsActivity.kt$PermissionsActivity$throw RuntimeException( "Could not find callback class for PermissionActivity: $className", ) - TooGenericExceptionThrown:PermissionsActivity.kt$PermissionsActivity$throw RuntimeException("Missing handler for permissionRequestType: $permissionRequestType") + TooGenericExceptionThrown:PermissionsViewModel.kt$PermissionsViewModel$throw RuntimeException("Missing handler for permissionRequestType: $type") TooGenericExceptionThrown:PreferencesService.kt$PreferencesService$throw Exception("Store not found: $store") TooGenericExceptionThrown:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$throw Exception("Unrecognized operation(s)! Attempted operations:\n$operations") TooGenericExceptionThrown:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$throw Exception("Unrecognized operation: $startingOp") @@ -370,14 +366,16 @@ TooManyFunctions:ApplicationService.kt$ApplicationService : IApplicationServiceActivityLifecycleCallbacksOnGlobalLayoutListener TooManyFunctions:BackgroundManager.kt$BackgroundManager : IApplicationLifecycleHandlerIBackgroundManagerIStartableService TooManyFunctions:HttpClient.kt$HttpClient : IHttpClient + TooManyFunctions:IOneSignal.kt$IOneSignal TooManyFunctions:IUserManager.kt$IUserManager TooManyFunctions:InfluenceManager.kt$InfluenceManager : IInfluenceManagerISessionLifecycleHandler TooManyFunctions:JSONObjectExtensions.kt$com.onesignal.common.JSONObjectExtensions.kt - TooManyFunctions:JSONUtils.kt$JSONUtils$JSONUtils TooManyFunctions:Logging.kt$Logging$Logging TooManyFunctions:Model.kt$Model : IEventNotifier TooManyFunctions:ModelStore.kt$ModelStore<TModel> : IEventNotifierIModelStoreIModelChangedHandler TooManyFunctions:OSDatabase.kt$OSDatabase : SQLiteOpenHelperIDatabase + TooManyFunctions:OneSignal.kt$OneSignal$OneSignal + TooManyFunctions:OneSignalImp.kt$OneSignalImp : IOneSignalIServiceProvider TooManyFunctions:OperationRepo.kt$OperationRepo : IOperationRepoIStartableService TooManyFunctions:OutcomeEventsController.kt$OutcomeEventsController : IOutcomeEventsControllerIStartableServiceISessionLifecycleHandler TooManyFunctions:PreferencesService.kt$PreferencesService : IPreferencesServiceIStartableService @@ -386,6 +384,7 @@ UndocumentedPublicClass:AlertDialogPrepromptForAndroidSettings.kt$AlertDialogPrepromptForAndroidSettings$Callback UndocumentedPublicClass:AndroidUtils.kt$AndroidUtils UndocumentedPublicClass:AndroidUtils.kt$AndroidUtils$SchemaType + UndocumentedPublicClass:AppIdResolution.kt$AppIdResolution UndocumentedPublicClass:ApplicationService.kt$ApplicationService : IApplicationServiceActivityLifecycleCallbacksOnGlobalLayoutListener UndocumentedPublicClass:ConfigModel.kt$ConfigModel : Model UndocumentedPublicClass:ConfigModelStore.kt$ConfigModelStore : SingletonModelStore @@ -409,6 +408,10 @@ UndocumentedPublicClass:IOperationExecutor.kt$ExecutionResponse UndocumentedPublicClass:IOperationExecutor.kt$ExecutionResult UndocumentedPublicClass:IOutcomeEvent.kt$IOutcomeEvent + UndocumentedPublicClass:IParamsBackendService.kt$FCMParamsObject + UndocumentedPublicClass:IParamsBackendService.kt$IParamsBackendService + UndocumentedPublicClass:IParamsBackendService.kt$InfluenceParamsObject + UndocumentedPublicClass:IParamsBackendService.kt$ParamsObject UndocumentedPublicClass:IPreferencesService.kt$PreferenceOneSignalKeys UndocumentedPublicClass:IPreferencesService.kt$PreferencePlayerPurchasesKeys UndocumentedPublicClass:IPreferencesService.kt$PreferenceStores @@ -426,10 +429,11 @@ UndocumentedPublicClass:JSONConverter.kt$JSONConverter UndocumentedPublicClass:JSONUtils.kt$JSONUtils UndocumentedPublicClass:Logging.kt$Logging + UndocumentedPublicClass:LoginHelper.kt$LoginHelper + UndocumentedPublicClass:LogoutHelper.kt$LogoutHelper UndocumentedPublicClass:MigrationRecovery.kt$MigrationRecovery : IMigrationRecovery UndocumentedPublicClass:NetworkUtils.kt$NetworkUtils UndocumentedPublicClass:NetworkUtils.kt$NetworkUtils$ResponseStatusType - UndocumentedPublicClass:OSPrimaryCoroutineScope.kt$OSPrimaryCoroutineScope UndocumentedPublicClass:OneSignalDbContract.kt$OneSignalDbContract UndocumentedPublicClass:OneSignalDbContract.kt$OneSignalDbContract$InAppMessageTable : BaseColumns UndocumentedPublicClass:OneSignalDbContract.kt$OneSignalDbContract$NotificationTable : BaseColumns @@ -437,7 +441,6 @@ UndocumentedPublicClass:OneSignalWrapper.kt$OneSignalWrapper UndocumentedPublicClass:Operation.kt$GroupComparisonType UndocumentedPublicClass:OptionalHeaders.kt$OptionalHeaders - UndocumentedPublicClass:PermissionsActivity.kt$PermissionsActivity : Activity UndocumentedPublicClass:PreferenceStoreFix.kt$PreferenceStoreFix UndocumentedPublicClass:PropertiesDeltasObject.kt$PropertiesDeltasObject UndocumentedPublicClass:PropertiesDeltasObject.kt$PurchaseObject @@ -458,8 +461,10 @@ UndocumentedPublicClass:SyncJobService.kt$SyncJobService : JobService UndocumentedPublicClass:TimeUtils.kt$TimeUtils UndocumentedPublicClass:UserRefreshService.kt$UserRefreshService : IStartableServiceISessionLifecycleHandler + UndocumentedPublicClass:UserSwitcher.kt$UserSwitcher UndocumentedPublicClass:ViewUtils.kt$ViewUtils UndocumentedPublicFunction:AlertDialogPrepromptForAndroidSettings.kt$AlertDialogPrepromptForAndroidSettings$fun show( activity: Activity, titlePrefix: String, previouslyDeniedPostfix: String, callback: Callback, ) + UndocumentedPublicFunction:AlertDialogPrepromptForAndroidSettings.kt$AlertDialogPrepromptForAndroidSettings$fun show( activity: Activity, titlePrefix: String, previouslyDeniedPostfix: String, callback: Callback, dismissCallback: (() -> Unit)?, ) UndocumentedPublicFunction:AlertDialogPrepromptForAndroidSettings.kt$AlertDialogPrepromptForAndroidSettings.Callback$fun onAccept() UndocumentedPublicFunction:AlertDialogPrepromptForAndroidSettings.kt$AlertDialogPrepromptForAndroidSettings.Callback$fun onDecline() UndocumentedPublicFunction:AndroidUtils.kt$AndroidUtils$@Keep fun opaqueHasClass(_class: Class<*>): Boolean @@ -482,6 +487,7 @@ UndocumentedPublicFunction:AndroidUtils.kt$AndroidUtils$fun openURLInBrowser( appContext: Context, url: String, ) UndocumentedPublicFunction:AndroidUtils.kt$AndroidUtils$fun openURLInBrowserIntent(uri: Uri): Intent UndocumentedPublicFunction:AndroidUtils.kt$AndroidUtils.SchemaType.Companion$fun fromString(text: String?): SchemaType? + UndocumentedPublicFunction:AppIdResolution.kt$fun resolveAppId( inputAppId: String?, configModel: ConfigModel, preferencesService: IPreferencesService, ): AppIdResolution UndocumentedPublicFunction:ApplicationService.kt$ApplicationService$fun decorViewReady( activity: Activity, runnable: Runnable, ) UndocumentedPublicFunction:DateUtils.kt$DateUtils$fun iso8601Format(): SimpleDateFormat UndocumentedPublicFunction:DeviceUtils.kt$DeviceUtils$fun getCarrierName(appContext: Context): String? @@ -538,6 +544,8 @@ UndocumentedPublicFunction:Logging.kt$Logging$@JvmStatic fun warn( message: String, throwable: Throwable? = null, ) UndocumentedPublicFunction:Logging.kt$Logging$fun addListener(listener: ILogListener) UndocumentedPublicFunction:Logging.kt$Logging$fun removeListener(listener: ILogListener) + UndocumentedPublicFunction:LoginHelper.kt$LoginHelper$suspend fun login( externalId: String, jwtBearerToken: String? = null, ) + UndocumentedPublicFunction:LogoutHelper.kt$LogoutHelper$fun logout() UndocumentedPublicFunction:Model.kt$Model$fun <T> setListProperty( name: String, value: List<T>, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, ) UndocumentedPublicFunction:Model.kt$Model$fun <T> setMapModelProperty( name: String, value: MapModel<T>, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, ) UndocumentedPublicFunction:Model.kt$Model$fun <T> setOptListProperty( name: String, value: List<T>?, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, ) @@ -564,7 +572,8 @@ UndocumentedPublicFunction:NewRecordsState.kt$NewRecordsState$fun add(key: String) UndocumentedPublicFunction:NewRecordsState.kt$NewRecordsState$fun canAccess(key: String): Boolean UndocumentedPublicFunction:NewRecordsState.kt$NewRecordsState$fun isInMissingRetryWindow(key: String): Boolean - UndocumentedPublicFunction:OSPrimaryCoroutineScope.kt$OSPrimaryCoroutineScope$suspend fun waitForIdle() + UndocumentedPublicFunction:OneSignalDispatchers.kt$OneSignalDispatchers$fun launchOnDefault(block: suspend () -> Unit): Job + UndocumentedPublicFunction:OneSignalDispatchers.kt$OneSignalDispatchers$fun launchOnIO(block: suspend () -> Unit): Job UndocumentedPublicFunction:OneSignalUtils.kt$OneSignalUtils$fun isValidEmail(email: String): Boolean UndocumentedPublicFunction:OneSignalUtils.kt$OneSignalUtils$fun isValidPhoneNumber(number: String): Boolean UndocumentedPublicFunction:PushSubscriptionChangedState.kt$PushSubscriptionChangedState$fun toJSONObject(): JSONObject @@ -579,6 +588,9 @@ UndocumentedPublicFunction:TimeUtils.kt$TimeUtils$fun getTimeZoneOffset(): Int UndocumentedPublicFunction:UserChangedState.kt$UserChangedState$fun toJSONObject(): JSONObject UndocumentedPublicFunction:UserState.kt$UserState$fun toJSONObject(): JSONObject + UndocumentedPublicFunction:UserSwitcher.kt$UserSwitcher$fun createAndSwitchToNewUser( suppressBackendOperation: Boolean = false, modify: ((identityModel: IdentityModel, propertiesModel: PropertiesModel) -> Unit)? = null, ) + UndocumentedPublicFunction:UserSwitcher.kt$UserSwitcher$fun createPushSubscriptionFromLegacySync( legacyPlayerId: String, legacyUserSyncJSON: JSONObject, configModel: ConfigModel, subscriptionModelStore: SubscriptionModelStore, appContext: Context, ): Boolean + UndocumentedPublicFunction:UserSwitcher.kt$UserSwitcher$fun initUser(forceCreateUser: Boolean) UndocumentedPublicFunction:ViewUtils.kt$ViewUtils$fun dpToPx(dp: Int): Int UndocumentedPublicFunction:ViewUtils.kt$ViewUtils$fun getCutoutAndStatusBarInsets(activity: Activity): IntArray UndocumentedPublicFunction:ViewUtils.kt$ViewUtils$fun getFullbleedWindowWidth(activity: Activity): Int @@ -588,7 +600,11 @@ UnusedPrivateMember:AndroidUtils.kt$AndroidUtils$var requestPermission: String? = null UnusedPrivateMember:ApplicationService.kt$ApplicationService$val listenerKey = "decorViewReady:$runnable" UnusedPrivateMember:JSONUtils.kt$JSONUtils$`object`: Any + UnusedPrivateMember:LoginHelper.kt$LoginHelper$jwtBearerToken: String? = null UnusedPrivateMember:OSDatabase.kt$OSDatabase.Companion$private const val FLOAT_TYPE = " FLOAT" UnusedPrivateMember:OperationRepo.kt$OperationRepo$private val _time: ITime + UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'login'") + UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw IllegalStateException("'initWithContext failed' before 'logout'") + UseCheckOrError:OneSignalImp.kt$OneSignalImp$throw initFailureException ?: IllegalStateException("Initialization failed. Cannot proceed.") diff --git a/OneSignalSDK/detekt/detekt-config.yml b/OneSignalSDK/detekt/detekt-config.yml index de24a4b2b2..12c24e6464 100644 --- a/OneSignalSDK/detekt/detekt-config.yml +++ b/OneSignalSDK/detekt/detekt-config.yml @@ -91,7 +91,7 @@ comments: UndocumentedPublicFunction: active: true excludes: ['**/test/**', '**/androidTest/**', '**/testhelpers/**'] - + EndOfSentenceFormat: active: false endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$) diff --git a/OneSignalSDK/onesignal/core/build.gradle b/OneSignalSDK/onesignal/core/build.gradle index 8dd5c206da..6f90bb1224 100644 --- a/OneSignalSDK/onesignal/core/build.gradle +++ b/OneSignalSDK/onesignal/core/build.gradle @@ -82,8 +82,6 @@ dependencies { } } - // Otel module dependency - implementation(project(':OneSignal:otel')) testImplementation(project(':OneSignal:testhelpers')) testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") diff --git a/OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml b/OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml index 7d0c8323f0..285ce5c588 100644 --- a/OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml +++ b/OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml @@ -1,9 +1,4 @@ - - - - - + diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt index 9f400bb559..0cf3b0bdd1 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt @@ -61,7 +61,7 @@ object JSONUtils { try { val value = jsonObject.opt(key) if (value is JSONArray || value is JSONObject) { - Logging.warn("Omitting key '$key'! sendTags DO NOT supported nested values!") + Logging.error("Omitting key '$key'! sendTags DO NOT supported nested values!") } else if (jsonObject.isNull(key) || "" == value) { result[key] = "" } else { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt index 8897bb13a6..9083cddade 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt @@ -33,7 +33,6 @@ 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 @@ -82,9 +81,6 @@ internal class CoreModule : IModule { // Purchase Tracking builder.register().provides() - // Crash Uploader (crash handler is initialized directly in OneSignalImp for early initialization) - builder.register().provides() - // 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().provides() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/IParamsBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/IParamsBackendService.kt index 8773a23af3..514cc798bc 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/IParamsBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/IParamsBackendService.kt @@ -2,7 +2,7 @@ package com.onesignal.core.internal.backend import org.json.JSONArray -internal interface IParamsBackendService { +interface IParamsBackendService { /** * Retrieve the configuration parameters for the [appId] and optional [subscriptionId]. * @@ -20,8 +20,7 @@ internal interface IParamsBackendService { ): ParamsObject } -@Suppress("LongParameterList") -internal class ParamsObject( +class ParamsObject( var googleProjectNumber: String? = null, var enterprise: Boolean? = null, var useIdentityVerification: Boolean? = null, @@ -37,10 +36,9 @@ internal class ParamsObject( var opRepoExecutionInterval: Long? = null, var influenceParams: InfluenceParamsObject, var fcmParams: FCMParamsObject, - val remoteLoggingParams: RemoteLoggingParamsObject, ) -internal class InfluenceParamsObject( +class InfluenceParamsObject( val indirectNotificationAttributionWindow: Int? = null, val notificationLimit: Int? = null, val indirectIAMAttributionWindow: Int? = null, @@ -50,13 +48,8 @@ internal class InfluenceParamsObject( val isUnattributedEnabled: Boolean? = null, ) -internal class FCMParamsObject( +class FCMParamsObject( val projectId: String? = null, val appId: String? = null, val apiKey: String? = null, ) - -internal class RemoteLoggingParamsObject( - val logLevel: com.onesignal.debug.LogLevel? = null, - val isEnabled: Boolean = logLevel != null, -) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt index dfaaa027dc..85dd452d41 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt @@ -11,7 +11,6 @@ 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 @@ -58,16 +57,6 @@ internal class ParamsBackendService( ) } - // Process Remote Logging params - var remoteLoggingParams: RemoteLoggingParamsObject? = null - responseJson.expandJSONObject("logging_config") { - val logLevel = LogLevel.fromString(it.safeString("log_level")) - remoteLoggingParams = - RemoteLoggingParamsObject( - logLevel = logLevel, - ) - } - return ParamsObject( googleProjectNumber = responseJson.safeString("android_sender_id"), enterprise = responseJson.safeBool("enterp"), @@ -86,7 +75,6 @@ internal class ParamsBackendService( opRepoExecutionInterval = responseJson.safeLong("oprepo_execution_interval"), influenceParams = influenceParams ?: InfluenceParamsObject(), fcmParams = fcmParams ?: FCMParamsObject(), - remoteLoggingParams = remoteLoggingParams ?: RemoteLoggingParamsObject(), ) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/background/impl/BackgroundManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/background/impl/BackgroundManager.kt index 01c6c81934..eddb183784 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/background/impl/BackgroundManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/background/impl/BackgroundManager.kt @@ -186,7 +186,7 @@ internal class BackgroundManager( } catch (e: NullPointerException) { // Catch for buggy Oppo devices // https://github.com/OneSignal/OneSignal-Android-SDK/issues/487 - Logging.info( + Logging.error( "scheduleSyncServiceAsJob called JobScheduler.jobScheduler which " + "triggered an internal null Android error. Skipping job.", e, diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt index bd06e4c3e4..74d31c4669 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt @@ -1,7 +1,6 @@ package com.onesignal.core.internal.config import com.onesignal.common.modeling.Model -import com.onesignal.core.internal.http.OneSignalService.ONESIGNAL_API_BASE_URL import org.json.JSONArray import org.json.JSONObject @@ -37,7 +36,7 @@ class ConfigModel : Model() { * The API URL String. */ var apiUrl: String - get() = getStringProperty(::apiUrl.name) { ONESIGNAL_API_BASE_URL } + get() = getStringProperty(::apiUrl.name) { "https://api.onesignal.com/" } set(value) { setStringProperty(::apiUrl.name, value) } @@ -302,9 +301,6 @@ 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, @@ -321,12 +317,6 @@ class ConfigModel : Model() { return model } - if (property == ::remoteLoggingParams.name) { - val model = RemoteLoggingConfigModel(this, ::remoteLoggingParams.name) - model.initializeFromJson(jsonObject) - return model - } - return null } } @@ -435,34 +425,3 @@ 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(::logLevel.name) - set(value) { - setOptEnumProperty(::logLevel.name, value) - } - - /** - * Whether remote logging is enabled. - * Set by backend config hydration — true when the server sends a valid log_level, false otherwise. - */ - var isEnabled: Boolean - get() = getBooleanProperty(::isEnabled.name) { false } - set(value) { - setBooleanProperty(::isEnabled.name, value) - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModelStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModelStore.kt index 801a85e903..687a8547b0 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModelStore.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModelStore.kt @@ -3,8 +3,7 @@ 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( - SimpleModelStore({ ConfigModel() }, CONFIG_NAME_SPACE, prefs), + SimpleModelStore({ ConfigModel() }, "config", prefs), ) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt index 581943bc58..87d7eae6b0 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt @@ -103,9 +103,6 @@ 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 } - config.remoteLoggingParams.isEnabled = params.remoteLoggingParams.isEnabled - _configModelStore.replace(config, ModelChangeTags.HYDRATE) success = true } catch (ex: BackendException) { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/OneSignalService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/OneSignalService.kt deleted file mode 100644 index b7533961de..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/OneSignalService.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.onesignal.core.internal.http - -/** Central API base URL used by all SDK HTTP traffic, including Otel log export. */ -object OneSignalService { -// const val ONESIGNAL_API_BASE_URL = "https://api.staging.onesignal.com/" - const val ONESIGNAL_API_BASE_URL = "https://api.onesignal.com/" -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt index 747b0b7085..00748d428e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt @@ -29,9 +29,6 @@ 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, @@ -96,7 +93,7 @@ internal class HttpClient( return@withTimeout makeRequestIODispatcher(url, method, jsonBody, timeout, headers) } } catch (e: TimeoutCancellationException) { - Logging.info("HttpClient: Request timed out: $url", e) + Logging.error("HttpClient: Request timed out: $url", e) return HttpResponse(0, null, e) } catch (e: Throwable) { return HttpResponse(0, null, e) @@ -138,7 +135,7 @@ internal class HttpClient( con.useCaches = false con.connectTimeout = timeout con.readTimeout = timeout - con.setRequestProperty(HTTP_SDK_VERSION_HEADER_KEY, HTTP_SDK_VERSION_HEADER_VALUE) + con.setRequestProperty("SDK-Version", "onesignal/android/" + OneSignalUtils.sdkVersion) if (OneSignalWrapper.sdkType != null && OneSignalWrapper.sdkVersion != null) { con.setRequestProperty("SDK-Wrapper", "onesignal/${OneSignalWrapper.sdkType}/${OneSignalWrapper.sdkVersion}") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt index 78983cc7fc..1861261506 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt @@ -264,7 +264,7 @@ internal class OperationRepo( ExecutionResult.FAIL_NORETRY, ExecutionResult.FAIL_CONFLICT, -> { - Logging.warn("Operation execution failed without retry: $operations") + Logging.error("Operation execution failed without retry: $operations") // on failure we remove the operation from the store and wake any waiters ops.forEach { _operationModelStore.remove(it.operation.id) } ops.forEach { it.waiter?.wake(false) } @@ -279,7 +279,7 @@ internal class OperationRepo( } } ExecutionResult.FAIL_RETRY -> { - Logging.info("Operation execution failed, retrying: $operations") + Logging.error("Operation execution failed, retrying: $operations") // add back all operations to the front of the queue to be re-executed. synchronized(queue) { ops.reversed().forEach { @@ -341,7 +341,7 @@ internal class OperationRepo( val delayForOnRetries = retries * _configModelStore.model.opRepoDefaultFailRetryBackoff val delayFor = max(delayForOnRetries, retryAfterSecondsNonNull * 1_000) if (delayFor < 1) return - Logging.debug("Operations being delay for: $delayFor ms") + Logging.error("Operations being delay for: $delayFor ms") withTimeoutOrNull(delayFor) { retryWaiter.waitForWake() } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/ITime.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/ITime.kt index 8f1824d481..ff35096efd 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/ITime.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/ITime.kt @@ -10,9 +10,4 @@ interface ITime { * current time and midnight, January 1, 1970 UTC). */ val currentTimeMillis: Long - - /** - * Returns how long the app has been running. - */ - val processUptimeMillis: Long } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/impl/Time.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/impl/Time.kt index 753ef124d5..231f37edf3 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/impl/Time.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/impl/Time.kt @@ -1,14 +1,8 @@ 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() } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/LogLevel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/LogLevel.kt index e88922909c..9c3f99e877 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/LogLevel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/LogLevel.kt @@ -49,19 +49,5 @@ enum class LogLevel { fun fromInt(value: Int): LogLevel { return values()[value] } - - /** - * Parses a [LogLevel] from its string name (case-insensitive). - * Returns `null` if the string is null or not a valid level name. - */ - @JvmStatic - fun fromString(value: String?): LogLevel? { - if (value == null) return null - return try { - valueOf(value.uppercase()) - } catch (_: IllegalArgumentException) { - null - } - } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/AnrConstants.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/AnrConstants.kt deleted file mode 100644 index 3f0e115eb2..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/AnrConstants.kt +++ /dev/null @@ -1,19 +0,0 @@ -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 -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactory.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactory.kt deleted file mode 100644 index 568134287f..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactory.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.content.Context -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider -import com.onesignal.otel.IOtelCrashHandler -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.OtelFactory - -/** - * Factory for creating Otel-based crash handlers. - * Callers must verify [OtelSdkSupport.isSupported] before calling [createCrashHandler]. - * - * Uses minimal dependencies - only Context and logger. - * Platform provider uses OtelIdResolver internally which reads from SharedPreferences. - */ -internal object OneSignalCrashHandlerFactory { - /** - * Creates an Otel crash handler. Must only be called on supported devices - * (SDK >= [OtelSdkSupport.MIN_SDK_VERSION]). - * - * @param context Android context for creating platform provider - * @param logger Logger instance (can be shared with other components) - * @throws IllegalArgumentException if called on an unsupported SDK - */ - fun createCrashHandler( - context: Context, - logger: IOtelLogger, - ): IOtelCrashHandler { - require(OtelSdkSupport.isSupported) { - "createCrashHandler called on unsupported SDK (< ${OtelSdkSupport.MIN_SDK_VERSION})" - } - - Logging.info("OneSignal: Creating Otel crash handler (SDK >= ${OtelSdkSupport.MIN_SDK_VERSION})") - val platformProvider = createAndroidOtelPlatformProvider(context) - return OtelFactory.createCrashHandler(platformProvider, logger) - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt deleted file mode 100644 index 2f9f7c9c3a..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.onesignal.debug.internal.crash - -import com.onesignal.common.threading.suspendifyOnThread -import com.onesignal.core.internal.application.IApplicationService -import com.onesignal.core.internal.startup.IStartableService -import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger -import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider -import com.onesignal.otel.OtelFactory -import com.onesignal.otel.crash.OtelCrashUploader - -/** - * Android-specific wrapper for OtelCrashUploader that implements IStartableService. - * - * This is a thin adapter layer that: - * 1. Takes Android-specific services as dependencies - * 2. Creates platform-agnostic implementations (IOtelPlatformProvider, IOtelLogger) - * 3. Wraps the platform-agnostic OtelCrashUploader for Android service architecture - * - * The OtelCrashUploader itself is fully platform-agnostic and can be used directly - * in KMP projects by providing platform-specific implementations of: - * - IOtelPlatformProvider (inject all platform values) - * - IOtelLogger (platform logging interface) - * - * Example KMP usage: - * ```kotlin - * val platformProvider = MyPlatformProvider(...) // iOS/Android specific - * val logger = MyPlatformLogger() // iOS/Android specific - * val uploader = OtelFactory.createCrashUploader(platformProvider, logger) - * // Use uploader.start() in a coroutine - * ``` - */ -internal class OneSignalCrashUploaderWrapper( - private val applicationService: IApplicationService, -) : IStartableService { - private val uploader: OtelCrashUploader by lazy { - // Create Android-specific platform provider (injects Android values) - val platformProvider = createAndroidOtelPlatformProvider( - applicationService.appContext - ) - // Create Android-specific logger (delegates to Android Logging) - val logger = AndroidOtelLogger() - // Create platform-agnostic uploader using factory - OtelFactory.createCrashUploader(platformProvider, logger) - } - - @Suppress("TooGenericExceptionCaught") - override fun start() { - if (!OtelSdkSupport.isSupported) return - suspendifyOnThread { - try { - uploader.start() - } catch (t: Throwable) { - com.onesignal.debug.internal.logging.Logging.warn( - "OneSignal: Crash uploader failed to start: ${t.message}", - t, - ) - } - } - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelAnrDetector.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelAnrDetector.kt deleted file mode 100644 index d7ad6960a1..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelAnrDetector.kt +++ /dev/null @@ -1,221 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.os.Handler -import android.os.Looper -import com.onesignal.otel.IOtelCrashReporter -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryCrash -import com.onesignal.otel.OtelFactory -import com.onesignal.otel.crash.IOtelAnrDetector -import kotlinx.coroutines.runBlocking -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicLong - -/** - * Android-specific implementation of ANR detection. - * - * Uses a watchdog pattern to monitor the main thread: - * - Posts a message to the main thread every check interval - * - If the main thread doesn't respond within the ANR threshold, reports an ANR - * - Captures the main thread's stack trace when ANR is detected - * - * This is a standalone component that can be initialized independently of the crash handler. - * It creates its own crash reporter to save ANR reports. - */ -internal class OtelAnrDetector( - openTelemetryCrash: IOtelOpenTelemetryCrash, - private val logger: IOtelLogger, - private val anrThresholdMs: Long = AnrConstants.DEFAULT_ANR_THRESHOLD_MS, - private val checkIntervalMs: Long = AnrConstants.DEFAULT_CHECK_INTERVAL_MS, -) : IOtelAnrDetector { - private val crashReporter: IOtelCrashReporter = OtelFactory.createCrashReporter(openTelemetryCrash, logger) - private val mainHandler = Handler(Looper.getMainLooper()) - private val isMonitoring = AtomicBoolean(false) - private val lastResponseTime = AtomicLong(System.currentTimeMillis()) - private val lastAnrReportTime = AtomicLong(0L) - private var watchdogThread: Thread? = null - private var watchdogRunnable: Runnable? = null - private var mainThreadRunnable: Runnable? = null - - companion object { - private const val TAG = "OtelAnrDetector" - - // Minimum time between ANR reports (to avoid duplicate reports for the same ANR) - private const val MIN_TIME_BETWEEN_ANR_REPORTS_MS = 30_000L // 30 seconds - } - - override fun start() { - if (isMonitoring.getAndSet(true)) { - logger.warn("$TAG: Already monitoring for ANRs, skipping start") - return - } - - logger.info("$TAG: Starting ANR detection (threshold: ${anrThresholdMs}ms, check interval: ${checkIntervalMs}ms)") - - setupRunnables() - startWatchdogThread() - - logger.info("$TAG: ✅ ANR detection started successfully") - } - - @Suppress("TooGenericExceptionCaught") - private fun setupRunnables() { - // Runnable that runs on the main thread to indicate it's responsive - mainThreadRunnable = Runnable { - lastResponseTime.set(System.currentTimeMillis()) - } - - // Runnable that runs on the watchdog thread to check for ANRs - watchdogRunnable = Runnable { - while (isMonitoring.get()) { - try { - checkForAnr() - } catch (e: InterruptedException) { - // Thread was interrupted, stop monitoring - logger.info("$TAG: Watchdog thread interrupted, stopping ANR detection") - break - } catch (t: Throwable) { - logger.error("$TAG: Error in ANR watchdog: ${t.message} - ${t.javaClass.simpleName}") - } - } - } - } - - private fun checkForAnr() { - val runnable = mainThreadRunnable ?: return - mainHandler.post(runnable) - - // Wait for the check interval - Thread.sleep(checkIntervalMs) - - // Check if main thread responded - val timeSinceLastResponse = System.currentTimeMillis() - lastResponseTime.get() - if (timeSinceLastResponse > anrThresholdMs) { - handleAnrDetected(timeSinceLastResponse) - } else { - handleMainThreadResponsive() - } - } - - private fun handleAnrDetected(timeSinceLastResponse: Long) { - // Main thread hasn't responded - ANR detected! - val now = System.currentTimeMillis() - val timeSinceLastReport = now - lastAnrReportTime.get() - - // Only report if enough time has passed since last report (avoid duplicates) - if (timeSinceLastReport > MIN_TIME_BETWEEN_ANR_REPORTS_MS) { - logger.warn("$TAG: ⚠️ ANR detected! Main thread unresponsive for ${timeSinceLastResponse}ms") - lastAnrReportTime.set(now) - reportAnr(timeSinceLastResponse) - } else { - logger.debug("$TAG: ANR still ongoing (${timeSinceLastResponse}ms), but already reported recently (${timeSinceLastReport}ms ago)") - } - } - - private fun handleMainThreadResponsive() { - // Main thread is responsive - reset ANR report time so we can detect new ANRs - if (lastAnrReportTime.get() > 0) { - lastAnrReportTime.set(0L) - logger.debug("$TAG: Main thread recovered, ready to detect new ANRs") - } - } - - private fun startWatchdogThread() { - // Start the watchdog thread - watchdogThread = Thread(watchdogRunnable, "OneSignal-ANR-Watchdog") - watchdogThread?.isDaemon = true - watchdogThread?.start() - } - - override fun stop() { - if (!isMonitoring.getAndSet(false)) { - logger.warn("$TAG: Not monitoring, skipping stop") - return - } - - logger.info("$TAG: Stopping ANR detection...") - - // Interrupt the watchdog thread to stop it - watchdogThread?.interrupt() - watchdogThread = null - watchdogRunnable = null - // Remove pending callbacks before nulling to prevent execution after stop - mainThreadRunnable?.let { mainHandler.removeCallbacks(it) } - mainThreadRunnable = null - - logger.info("$TAG: ✅ ANR detection stopped") - } - - @Suppress("TooGenericExceptionCaught") - private fun reportAnr(unresponsiveDurationMs: Long) { - try { - logger.info("$TAG: Checking if ANR is OneSignal-related (unresponsive for ${unresponsiveDurationMs}ms)") - - // Get the main thread's stack trace - val mainThread = Looper.getMainLooper().thread - val stackTrace = mainThread.stackTrace - - // Only report if OneSignal is at fault (uses centralized utility from otel module) - val isOneSignalAtFault = com.onesignal.otel.crash.isOneSignalAtFault(stackTrace) - - if (!isOneSignalAtFault) { - logger.debug("$TAG: ANR is not OneSignal-related, skipping report") - return - } - - logger.info("$TAG: OneSignal-related ANR detected, reporting...") - - // Create an ANR exception with the stack trace - val anrException = ApplicationNotRespondingException( - "Application Not Responding: Main thread blocked for ${unresponsiveDurationMs}ms", - stackTrace - ) - - // Report it as a crash (but mark it as ANR) - runBlocking { - crashReporter.saveCrash(mainThread, anrException) - } - - logger.info("$TAG: ✅ ANR report saved successfully") - } catch (t: Throwable) { - logger.error("$TAG: Failed to report ANR: ${t.message} - ${t.javaClass.simpleName}") - } - } - - /** - * Custom exception type for ANRs. - * This allows us to distinguish ANRs from regular crashes in the crash reporting system. - */ - private class ApplicationNotRespondingException( - message: String, - stackTrace: Array - ) : RuntimeException(message) { - init { - this.stackTrace = stackTrace - } - } -} - -// Use the centralized isOneSignalAtFault from otel module - -/** - * Factory function to create an ANR detector for Android. - * This is in the core module since it needs to access Android-specific classes. - */ - -internal fun createAnrDetector( - platformProvider: com.onesignal.otel.IOtelPlatformProvider, - logger: IOtelLogger, - anrThresholdMs: Long = AnrConstants.DEFAULT_ANR_THRESHOLD_MS, - checkIntervalMs: Long = AnrConstants.DEFAULT_CHECK_INTERVAL_MS, -): IOtelAnrDetector { - // Use the factory to create crash local instance (keeps implementation details internal) - val crashLocal = OtelFactory.createCrashLocalTelemetry(platformProvider) - - return OtelAnrDetector( - crashLocal, - logger, - anrThresholdMs, - checkIntervalMs - ) -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelSdkSupport.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelSdkSupport.kt deleted file mode 100644 index 47fc0034de..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelSdkSupport.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.os.Build - -/** - * Centralizes the SDK version requirement for Otel-based features - * (crash reporting, ANR detection, remote log shipping). - * - * [isSupported] is writable internally so that unit tests can override - * the device-level gate without Robolectric @Config gymnastics. - */ -internal object OtelSdkSupport { - /** Otel libraries require Android O (API 26) or above. */ - const val MIN_SDK_VERSION = Build.VERSION_CODES.O // 26 - - /** - * Whether the current device meets the minimum SDK requirement. - * Production code should treat this as read-only; tests may flip it via [reset]/direct set. - */ - var isSupported: Boolean = Build.VERSION.SDK_INT >= MIN_SDK_VERSION - internal set - - /** Restores the runtime-detected value — call in test teardown. */ - fun reset() { - isSupported = Build.VERSION.SDK_INT >= MIN_SDK_VERSION - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt index 673db1b8da..a4db03407a 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt @@ -6,12 +6,6 @@ import com.onesignal.core.internal.application.IApplicationService import com.onesignal.debug.ILogListener import com.onesignal.debug.LogLevel import com.onesignal.debug.OneSignalLogEvent -import com.onesignal.otel.IOtelOpenTelemetryRemote -import com.onesignal.otel.OtelLoggingHelper -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import java.io.PrintWriter import java.io.StringWriter import java.util.concurrent.CopyOnWriteArraySet @@ -23,38 +17,6 @@ object Logging { private val logListeners = CopyOnWriteArraySet() - /** - * Optional Otel remote telemetry for logging SDK events. - * Set this when remote logging is enabled. - */ - @Volatile - private var otelRemoteTelemetry: IOtelOpenTelemetryRemote? = null - - /** - * Function to check if a specific log level should be sent remotely. - * Set this to dynamically check remote logging configuration based on log level. - */ - @Volatile - private var shouldSendLogLevel: (LogLevel) -> Boolean = { false } - - /** - * Sets the Otel remote telemetry instance and log level check function. - * This should be called when remote logging is enabled. - * - * @param telemetry The Otel remote telemetry instance - * @param shouldSend Function that returns true if a log level should be sent remotely - */ - fun setOtelTelemetry( - telemetry: IOtelOpenTelemetryRemote?, - shouldSend: (LogLevel) -> Boolean = { false }, - ) { - otelRemoteTelemetry = telemetry - shouldSendLogLevel = shouldSend - } - - // Coroutine scope for async Otel logging (non-blocking) - private val otelLoggingScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - @JvmStatic var logLevel = LogLevel.WARN @@ -131,7 +93,6 @@ object Logging { logToLogcat(level, fullMessage, throwable) showVisualLogging(level, fullMessage, throwable) callLogListeners(level, fullMessage, throwable) - logToOtel(level, fullMessage, throwable) } private fun logToLogcat( @@ -199,42 +160,6 @@ object Logging { } } - /** - * Logs to Otel remote telemetry if enabled. - * This is non-blocking and runs asynchronously. - */ - @Suppress("TooGenericExceptionCaught", "ReturnCount") - private fun logToOtel( - level: LogLevel, - message: String, - throwable: Throwable?, - ) { - val telemetry = otelRemoteTelemetry ?: return - - // Skip NONE level - if (level == LogLevel.NONE) return - - // Check if this log level should be sent remotely - if (!shouldSendLogLevel(level)) return - - // Log asynchronously (non-blocking) - otelLoggingScope.launch { - try { - OtelLoggingHelper.logToOtel( - telemetry = telemetry, - level = level.name, - message = message, - exceptionType = throwable?.javaClass?.name, - exceptionMessage = throwable?.message, - exceptionStacktrace = throwable?.stackTraceToString(), - ) - } catch (t: Throwable) { - // Don't log Otel errors to Otel (would cause infinite loop) - android.util.Log.e(TAG, "Failed to log to Otel: ${t.message}", t) - } - } - } - fun addListener(listener: ILogListener) { logListeners.add(listener) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/AndroidOtelLogger.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/AndroidOtelLogger.kt deleted file mode 100644 index 0452a8dca3..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/AndroidOtelLogger.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.onesignal.debug.internal.logging.otel.android - -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.otel.IOtelLogger - -/** - * Android-specific implementation of IOtelLogger. - * Delegates to the existing Logging object. - */ -internal class AndroidOtelLogger : IOtelLogger { - override fun error(message: String) { - Logging.error(message) - } - - override fun warn(message: String) { - Logging.warn(message) - } - - override fun info(message: String) { - Logging.info(message) - } - - override fun debug(message: String) { - Logging.debug(message) - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelIdResolver.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelIdResolver.kt deleted file mode 100644 index b205fffd9f..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelIdResolver.kt +++ /dev/null @@ -1,247 +0,0 @@ -package com.onesignal.debug.internal.logging.otel.android - -import android.content.Context -import com.onesignal.common.IDManager -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 com.onesignal.user.internal.backend.IdentityConstants -import org.json.JSONArray -import org.json.JSONObject - -/** - * Resolves OneSignal IDs from SharedPreferences with fallback strategies. - * This class encapsulates all the logic for reading IDs from ConfigModelStore and legacy SharedPreferences, - * making it easier to maintain and test. - * - * Note: Data is read fresh from SharedPreferences each time (not cached) to ensure test reliability - * and correctness. The performance impact is minimal since these methods are not called frequently. - */ -@Suppress("TooManyFunctions") // This class intentionally groups related ID resolution functions -internal class OtelIdResolver( - private val context: Context?, -) { - companion object { - /** - * Hardcoded error appId prefix when appId cannot be resolved. - */ - private const val ERROR_APP_ID_RESOLVE = "00000000-0000-4000-a000-000000000000" - private const val ERROR_APP_ID_PREFIX_UNKNOWN = "e1100000-0000-4000-a000-000000000000" - private const val ERROR_APP_ID_PREFIX_NO_APPID_IN_CONFIG = "e1100000-0000-4000-a000-000000000001" - private const val ERROR_APP_ID_PREFIX_NO_CONFIG_STORE = "e1100000-0000-4000-a000-000000000002" - private const val ERROR_APP_ID_PREFIX_NO_APPID_IN_CONFIG_STORE = "e1100000-0000-4000-a000-000000000003" - private const val ERROR_APP_ID_PREFIX_NO_CONTEXT = "e1100000-0000-4000-a000-000000000004" - } - - // Get SharedPreferences instance (fresh each time to avoid caching issues in tests) - private fun getSharedPreferences(): android.content.SharedPreferences? { - return context?.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - - // Read ConfigModelStore JSON (fresh read each time for testability) - // In production, this is called multiple times per resolver instance, but the performance impact is minimal - // and this ensures test reliability - @Suppress("TooGenericExceptionCaught", "SwallowedException") - private fun readConfigModel(): JSONObject? { - return try { - val configStoreJson = getSharedPreferences()?.getString( - PreferenceOneSignalKeys.MODEL_STORE_PREFIX + com.onesignal.core.internal.config.CONFIG_NAME_SPACE, - null - ) - - if (configStoreJson != null && configStoreJson.isNotEmpty()) { - val jsonArray = JSONArray(configStoreJson) - if (jsonArray.length() > 0) { - jsonArray.getJSONObject(0) - } else { - null - } - } else { - null - } - } catch (e: Exception) { - null - } - } - - // Check if ConfigModelStore exists but is empty (to distinguish from "not found") - @Suppress("TooGenericExceptionCaught", "SwallowedException") - private fun hasEmptyConfigStore(): Boolean { - return try { - val configStoreJson = getSharedPreferences()?.getString( - PreferenceOneSignalKeys.MODEL_STORE_PREFIX + com.onesignal.core.internal.config.CONFIG_NAME_SPACE, - null - ) - if (configStoreJson != null && configStoreJson.isNotEmpty()) { - val jsonArray = JSONArray(configStoreJson) - jsonArray.length() == 0 - } else { - false - } - } catch (e: Exception) { - false - } - } - - /** - * Resolves appId with the following fallback chain: - * 1. Try ConfigModelStore in SharedPreferences (MODEL_STORE_config) - * 2. Try legacy OneSignal SharedPreferences - * 3. Return error appId with affix if all fail - */ - @Suppress("TooGenericExceptionCaught") - fun resolveAppId(): String { - return try { - val configModel = readConfigModel() - val appIdFromConfig = extractAppIdFromConfig(configModel) - appIdFromConfig ?: resolveAppIdFromLegacy(configModel) - } catch (e: Exception) { - Logging.error("Trying resolve the app Id${e.message}") - ERROR_APP_ID_RESOLVE - } - } - - private fun extractAppIdFromConfig(configModel: JSONObject?): String? { - if (configModel == null) return null - val appIdProperty = ConfigModel::appId - return if (configModel.has(appIdProperty.name)) { - val appId = configModel.getString(appIdProperty.name) - appId.ifEmpty { null } - } else { - null - } - } - - @Suppress("TooGenericExceptionCaught", "SwallowedException", "NestedBlockDepth") - private fun resolveAppIdFromLegacy(configModel: JSONObject?): String { - // Second: fall back to legacy OneSignal SharedPreferences - val legacyAppId = try { - getSharedPreferences()?.getString(PreferenceOneSignalKeys.PREFS_LEGACY_APP_ID, null) - ?.takeIf { it.isNotEmpty() } - } catch (e: Exception) { - null - } - - return legacyAppId ?: run { - // Third: return error appId with affix - return when { - context == null -> ERROR_APP_ID_PREFIX_NO_CONTEXT - hasEmptyConfigStore() -> ERROR_APP_ID_PREFIX_NO_APPID_IN_CONFIG_STORE // Store exists but is empty array - configModel == null -> ERROR_APP_ID_PREFIX_NO_CONFIG_STORE // Store doesn't exist - !configModel.has("appId") -> ERROR_APP_ID_PREFIX_NO_APPID_IN_CONFIG // Store exists but no appId field - else -> ERROR_APP_ID_PREFIX_UNKNOWN - } - } - } - - /** - * Resolves onesignalId with the following fallback chain: - * 1. Try IdentityModelStore in SharedPreferences (MODEL_STORE_identity) - * 2. Return null if all fail - */ - @Suppress("TooGenericExceptionCaught", "SwallowedException", "NestedBlockDepth") - fun resolveOnesignalId(): String? { - return try { - val identityStoreJson = getSharedPreferences()?.getString( - PreferenceOneSignalKeys.MODEL_STORE_PREFIX + com.onesignal.user.internal.identity.IDENTITY_NAME_SPACE, - null - ) - - if (identityStoreJson != null && identityStoreJson.isNotEmpty()) { - extractOnesignalIdFromJson(identityStoreJson) - } else { - null - } - } catch (e: Exception) { - null - } - } - - private fun extractOnesignalIdFromJson(identityStoreJson: String): String? { - val jsonArray = JSONArray(identityStoreJson) - if (jsonArray.length() > 0) { - val identityModel = jsonArray.getJSONObject(0) - if (identityModel.has(IdentityConstants.ONESIGNAL_ID)) { - val onesignalId = identityModel.getString(IdentityConstants.ONESIGNAL_ID) - return onesignalId.takeIf { it.isNotEmpty() && !IDManager.isLocalId(it) } - } - } - return null - } - - /** - * Resolves pushSubscriptionId from cached ConfigModelStore. - * Returns null if not found or if it's a local ID. - */ - @Suppress("TooGenericExceptionCaught", "SwallowedException") - fun resolvePushSubscriptionId(): String? { - return try { - val configModel = readConfigModel() - val pushSubscriptionIdProperty = ConfigModel::pushSubscriptionId - if (configModel != null && configModel.has(pushSubscriptionIdProperty.name)) { - val pushSubscriptionId = configModel.getString(pushSubscriptionIdProperty.name) - pushSubscriptionId.takeIf { it.isNotEmpty() && !IDManager.isLocalId(pushSubscriptionId) } - } else { - null - } - } catch (e: Exception) { - null - } - } - - /** - * Resolves whether remote logging is enabled from cached ConfigModelStore. - * Enabled is derived from the presence of a valid logLevel: - * - "logging_config": {} → no logLevel → disabled (not on allowlist) - * - "logging_config": {"log_level": "ERROR"} → has logLevel → enabled (on allowlist) - * Returns false if not found, empty, or on error (disabled by default on first launch). - */ - @Suppress("TooGenericExceptionCaught", "SwallowedException") - fun resolveRemoteLoggingEnabled(): Boolean { - return try { - val logLevel = resolveRemoteLogLevel() - logLevel != null && logLevel != com.onesignal.debug.LogLevel.NONE - } catch (e: Exception) { - false - } - } - - /** - * Resolves remote log level from cached ConfigModelStore. - * Returns null if not found or if there's an error. - */ - @Suppress("TooGenericExceptionCaught", "SwallowedException", "NestedBlockDepth") - fun resolveRemoteLogLevel(): com.onesignal.debug.LogLevel? { - return try { - val configModel = readConfigModel() - val remoteLoggingParamsProperty = ConfigModel::remoteLoggingParams - if (configModel != null && configModel.has(remoteLoggingParamsProperty.name)) { - extractLogLevelFromParams(configModel.getJSONObject(remoteLoggingParamsProperty.name)) - } else { - null - } - } catch (e: Exception) { - null - } - } - - private fun extractLogLevelFromParams(remoteLoggingParams: JSONObject): com.onesignal.debug.LogLevel? = - com.onesignal.debug.LogLevel.fromString( - if (remoteLoggingParams.has("logLevel")) remoteLoggingParams.getString("logLevel") else null - ) - - /** - * Resolves install ID from SharedPreferences. - * Returns "InstallId-Null" if not found, "InstallId-NotFound" if there's an error. - */ - @Suppress("TooGenericExceptionCaught", "SwallowedException") - fun resolveInstallId(): String { - return try { - val installIdString = getSharedPreferences()?.getString(PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID, "InstallId-Null") - installIdString ?: "InstallId-Null" - } catch (e: Exception) { - "InstallId-NotFound" - } - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt deleted file mode 100644 index eebf9469c0..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.onesignal.debug.internal.logging.otel.android - -import android.app.ActivityManager -import android.content.Context -import android.os.Build -import com.onesignal.common.OneSignalUtils -import com.onesignal.common.OneSignalWrapper -import com.onesignal.core.internal.http.OneSignalService -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.otel.IOtelPlatformProvider - -/** - * Configuration for AndroidOtelPlatformProvider. - */ -internal data class OtelPlatformProviderConfig( - val crashStoragePath: String, - val appPackageId: String, - val appVersion: String, - val context: Context? = null, - val getIsInForeground: (() -> Boolean?)? = null, -) - -/** - * Android-specific implementation of IOtelPlatformProvider. - * Reads all values directly from SharedPreferences and system services. - * No SDK service dependencies required. - * - * All IDs (appId, onesignalId, pushSubscriptionId) are resolved from SharedPreferences via OtelIdResolver. - * Remote log level defaults to ERROR if not found in config. - */ -internal class OtelPlatformProvider( - config: OtelPlatformProviderConfig, -) : IOtelPlatformProvider { - override val appPackageId: String = config.appPackageId - override val appVersion: String = config.appVersion - private val context: Context? = config.context - private val getIsInForeground: (() -> Boolean?)? = config.getIsInForeground - private val idResolver = OtelIdResolver(context) - - // Top-level attributes (static, calculated once) - override suspend fun getInstallId(): String = idResolver.resolveInstallId() - - override val sdkBase: String = "android" - - override val sdkBaseVersion: String = OneSignalUtils.sdkVersion - - override val deviceManufacturer: String = Build.MANUFACTURER - - override val deviceModel: String = Build.MODEL - - override val osName: String = "Android" - - override val osVersion: String = Build.VERSION.RELEASE - - override val osBuildId: String = Build.ID - - override val sdkWrapper: String? = OneSignalWrapper.sdkType - - override val sdkWrapperVersion: String? = OneSignalWrapper.sdkVersion - - // Per-event attributes - IDs are cached (calculated once), appState is dynamic (calculated per access) - override val appId: String? by lazy { - idResolver.resolveAppId() - } - - override val onesignalId: String? by lazy { - idResolver.resolveOnesignalId() - } - - override val pushSubscriptionId: String? by lazy { - idResolver.resolvePushSubscriptionId() - } - - // https://opentelemetry.io/docs/specs/semconv/registry/attributes/android/ - override val appState: String - @Suppress("TooGenericExceptionCaught", "SwallowedException") - get() = try { - // Try to get from ApplicationService if available - getIsInForeground?.invoke()?.let { isForeground -> - if (isForeground) "foreground" else "background" - } ?: run { - // Fall back to ActivityManager if Context is available - context?.let { ctx -> - @Suppress("TooGenericExceptionCaught", "SwallowedException") - try { - val activityManager = ctx.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager - val runningAppProcesses = activityManager?.runningAppProcesses - val currentProcess = runningAppProcesses?.find { it.pid == android.os.Process.myPid() } - when (currentProcess?.importance) { - ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE -> "foreground" - else -> "background" - } - } catch (e: Exception) { - "unknown" - } - } ?: "unknown" - } - } catch (e: Exception) { - "unknown" - } - - // https://opentelemetry.io/docs/specs/semconv/system/process-metrics/#metric-processuptime - override val processUptime: Long - get() = android.os.SystemClock.uptimeMillis() - android.os.Process.getStartUptimeMillis() - - // https://opentelemetry.io/docs/specs/semconv/general/attributes/#general-thread-attributes - override val currentThreadName: String - get() = Thread.currentThread().name - - override val crashStoragePath: String by lazy { - val path = config.crashStoragePath - Logging.info("OneSignal: Crash logs stored at: $path") - path - } - - override val minFileAgeForReadMillis: Long = 5_000 - - // Cached from SharedPreferences on first access and held for the session. - // Mid-session config updates are handled by OtelLifecycleManager reading - // from ConfigModel directly, not from these cached values. - override val isRemoteLoggingEnabled: Boolean by lazy { - idResolver.resolveRemoteLoggingEnabled() - } - - // Cached from SharedPreferences on first access and held for the session. - // Mid-session config updates are handled by OtelLifecycleManager reading - // from ConfigModel directly, not from these cached values. - @Suppress("TooGenericExceptionCaught", "SwallowedException") - override val remoteLogLevel: String? by lazy { - try { - idResolver.resolveRemoteLogLevel()?.name - } catch (e: Exception) { - null - } - } - - override val appIdForHeaders: String - get() = appId ?: "" - - override val apiBaseUrl: String = OneSignalService.ONESIGNAL_API_BASE_URL -} - -/** - * Factory function to create AndroidOtelPlatformProvider without service dependencies. - * Reads all values directly from SharedPreferences and system services. - */ -internal fun createAndroidOtelPlatformProvider( - context: Context, -): OtelPlatformProvider { - val crashStoragePath = context.cacheDir.path + java.io.File.separator + - "onesignal" + java.io.File.separator + - "otel" + java.io.File.separator + - "crashes" - - return OtelPlatformProvider( - OtelPlatformProviderConfig( - crashStoragePath = crashStoragePath, - appPackageId = context.packageName, - appVersion = com.onesignal.common.AndroidUtils.getAppVersion(context) ?: "unknown", - context = context, - ) - ) -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index afd7ab39d8..1ccf96809b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -53,8 +53,6 @@ import com.onesignal.user.internal.subscriptions.SubscriptionType import org.json.JSONObject internal class OneSignalImp : IOneSignal, IServiceProvider { - private var otelManager: OtelLifecycleManager? = null - override val sdkVersion: String = OneSignalUtils.sdkVersion override var isInitialized: Boolean = false @@ -204,8 +202,6 @@ internal class OneSignalImp : IOneSignal, IServiceProvider { Logging.log(LogLevel.DEBUG, "initWithContext: SDK initializing") - otelManager = OtelLifecycleManager(context).also { it.initializeFromCachedConfig() } - PreferenceStoreFix.ensureNoObfuscatedPrefStore(context) // start the application service. This is called explicitly first because we want @@ -222,8 +218,6 @@ internal class OneSignalImp : IOneSignal, IServiceProvider { sessionModel = services.getService().model operationRepo = services.getService() - otelManager?.subscribeToConfigStore(services.getService()) - var forceCreateUser = false // initWithContext is called by our internal services/receivers/activities but they do not provide diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OtelConfigEvaluator.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OtelConfigEvaluator.kt deleted file mode 100644 index ea8b862ae5..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OtelConfigEvaluator.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.onesignal.internal - -import com.onesignal.debug.LogLevel - -/** - * Snapshot of the Otel-relevant fields from remote config. - * Used by [OtelConfigEvaluator] to diff old vs new config. - */ -internal data class OtelConfig( - val isEnabled: Boolean, - val logLevel: LogLevel?, -) { - companion object { - val DISABLED = OtelConfig(isEnabled = false, logLevel = null) - } -} - -/** - * Describes what the [OtelLifecycleManager] should do after a config change. - */ -internal sealed class OtelConfigAction { - /** Nothing changed that affects Otel features. */ - object NoChange : OtelConfigAction() - - /** Otel features should be started at the given [logLevel]. */ - data class Enable(val logLevel: LogLevel) : OtelConfigAction() - - /** The remote log level changed while features remain enabled. */ - data class UpdateLogLevel(val oldLevel: LogLevel, val newLevel: LogLevel) : OtelConfigAction() - - /** Otel features should be stopped/torn down. */ - object Disable : OtelConfigAction() -} - -/** - * Pure, side-effect-free evaluator that compares old and new [OtelConfig] - * and returns the [OtelConfigAction] the lifecycle manager should execute. - * - * Designed to be fully unit-testable without mocks. - */ -internal object OtelConfigEvaluator { - /** - * @param old the previous config snapshot, or null on first evaluation (cold start). - * @param new the freshly-arrived config snapshot. - */ - fun evaluate(old: OtelConfig?, new: OtelConfig): OtelConfigAction { - val wasEnabled = old?.isEnabled == true - val isNowEnabled = new.isEnabled - - return when { - // Transition: off -> on - !wasEnabled && isNowEnabled -> { - val level = new.logLevel ?: LogLevel.ERROR - OtelConfigAction.Enable(level) - } - // Transition: on -> off - wasEnabled && !isNowEnabled -> OtelConfigAction.Disable - // Stays enabled but log level changed - wasEnabled && isNowEnabled && old?.logLevel != new.logLevel -> { - val oldLevel = old?.logLevel ?: LogLevel.ERROR - val newLevel = new.logLevel ?: LogLevel.ERROR - OtelConfigAction.UpdateLogLevel(oldLevel, newLevel) - } - // Everything else: no meaningful change - else -> OtelConfigAction.NoChange - } - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OtelLifecycleManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OtelLifecycleManager.kt deleted file mode 100644 index 1b8b97b58b..0000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OtelLifecycleManager.kt +++ /dev/null @@ -1,240 +0,0 @@ -package com.onesignal.internal - -import android.content.Context -import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler -import com.onesignal.common.modeling.ModelChangeTags -import com.onesignal.common.modeling.ModelChangedArgs -import com.onesignal.core.internal.config.ConfigModel -import com.onesignal.core.internal.config.ConfigModelStore -import com.onesignal.debug.LogLevel -import com.onesignal.debug.internal.crash.AnrConstants -import com.onesignal.debug.internal.crash.OneSignalCrashHandlerFactory -import com.onesignal.debug.internal.crash.OtelSdkSupport -import com.onesignal.debug.internal.crash.createAnrDetector -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger -import com.onesignal.debug.internal.logging.otel.android.OtelPlatformProvider -import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider -import com.onesignal.otel.IOtelCrashHandler -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryRemote -import com.onesignal.otel.IOtelPlatformProvider -import com.onesignal.otel.OtelFactory -import com.onesignal.otel.crash.IOtelAnrDetector - -/** - * Owns the lifecycle of all Otel-based observability features and reacts - * to remote config changes so features can be enabled, disabled, or - * have their log level updated mid-session. - * - * Subscribes to [ConfigModelStore] via [ISingletonModelStoreChangeHandler] - * so that when fresh remote config arrives (HYDRATE), Otel features are - * automatically started, stopped, or updated. - * - * Thread safety: methods are synchronized on [lock] so that concurrent - * calls from initEssentials (main) and the config store callback (IO) are safe. - * - * All factory parameters default to the real implementations, so production - * callers can use `OtelLifecycleManager(context)`. Tests can override any - * factory to inject mocks or throwing stubs. - */ -@Suppress("TooManyFunctions") -internal class OtelLifecycleManager( - private val context: Context, - private val crashHandlerFactory: (Context, IOtelLogger) -> IOtelCrashHandler = - { ctx, log -> OneSignalCrashHandlerFactory.createCrashHandler(ctx, log) }, - private val anrDetectorFactory: (IOtelPlatformProvider, IOtelLogger, Long, Long) -> IOtelAnrDetector = - { pp, log, threshold, interval -> createAnrDetector(pp, log, threshold, interval) }, - private val remoteTelemetryFactory: (IOtelPlatformProvider) -> IOtelOpenTelemetryRemote = - { pp -> OtelFactory.createRemoteTelemetry(pp) }, - private val platformProviderFactory: (Context) -> OtelPlatformProvider = - { ctx -> createAndroidOtelPlatformProvider(ctx) }, - private val loggerFactory: () -> IOtelLogger = { AndroidOtelLogger() }, -) : ISingletonModelStoreChangeHandler { - private val lock = Any() - - private val platformProvider: OtelPlatformProvider by lazy { - platformProviderFactory(context) - } - - private val logger: IOtelLogger by lazy { loggerFactory() } - - private var crashHandler: IOtelCrashHandler? = null - private var anrDetector: IOtelAnrDetector? = null - private var remoteTelemetry: IOtelOpenTelemetryRemote? = null - private var currentConfig: OtelConfig? = null - - /** - * Called once from [OneSignalImp.initEssentials] at cold start. - * Reads the cached config from SharedPreferences and boots - * whichever features are already enabled. - */ - @Suppress("TooGenericExceptionCaught") - fun initializeFromCachedConfig() { - if (!OtelSdkSupport.isSupported) { - Logging.info("OneSignal: Device SDK < ${OtelSdkSupport.MIN_SDK_VERSION}, Otel not supported — skipping all Otel features") - return - } - - try { - val cachedConfig = readCurrentCachedConfig() - synchronized(lock) { - val action = OtelConfigEvaluator.evaluate(old = currentConfig, new = cachedConfig) - applyAction(action, cachedConfig) - } - } catch (t: Throwable) { - Logging.warn("OneSignal: Failed to initialize Otel from cached config: ${t.message}", t) - } - } - - /** - * Subscribes this manager to config store change events. - * Call after the IoC container is bootstrapped (i.e. after [bootstrapServices]). - */ - fun subscribeToConfigStore(configModelStore: ConfigModelStore) { - configModelStore.subscribe(this) - } - - // ------------------------------------------------------------------ - // ISingletonModelStoreChangeHandler - // ------------------------------------------------------------------ - - @Suppress("TooGenericExceptionCaught") - override fun onModelReplaced(model: ConfigModel, tag: String) { - if (tag != ModelChangeTags.HYDRATE) return - if (!OtelSdkSupport.isSupported) return - - try { - val logLevel = model.remoteLoggingParams.logLevel - val isEnabled = model.remoteLoggingParams.isEnabled - val newConfig = OtelConfig(isEnabled = isEnabled, logLevel = logLevel) - synchronized(lock) { - val action = OtelConfigEvaluator.evaluate(old = currentConfig, new = newConfig) - applyAction(action, newConfig) - } - } catch (t: Throwable) { - Logging.warn("OneSignal: Failed to refresh Otel from remote config: ${t.message}", t) - } - } - - override fun onModelUpdated(args: ModelChangedArgs, tag: String) { - // We only care about full model replacements (HYDRATE), not individual property changes. - } - - // ------------------------------------------------------------------ - // Internal - // ------------------------------------------------------------------ - - private fun readCurrentCachedConfig(): OtelConfig { - val enabled = platformProvider.isRemoteLoggingEnabled - val level = LogLevel.fromString(platformProvider.remoteLogLevel) - return OtelConfig(isEnabled = enabled, logLevel = level) - } - - /** Must be called while holding [lock]. */ - @Suppress("TooGenericExceptionCaught") - private fun applyAction(action: OtelConfigAction, newConfig: OtelConfig) { - when (action) { - is OtelConfigAction.Enable -> enableFeatures(newConfig.logLevel ?: LogLevel.ERROR) - is OtelConfigAction.Disable -> disableFeatures() - is OtelConfigAction.UpdateLogLevel -> updateLogLevel(action.newLevel) - is OtelConfigAction.NoChange -> { - Logging.debug("OneSignal: Otel config unchanged, no action needed") - } - } - currentConfig = newConfig - } - - @Suppress("TooGenericExceptionCaught") - private fun enableFeatures(logLevel: LogLevel) { - Logging.info("OneSignal: Enabling Otel features at level $logLevel") - - try { - startCrashHandler() - } catch (t: Throwable) { - Logging.warn("OneSignal: Failed to start crash handler: ${t.message}", t) - } - - try { - startAnrDetector() - } catch (t: Throwable) { - Logging.warn("OneSignal: Failed to start ANR detector: ${t.message}", t) - } - - try { - startOtelLogging(logLevel) - } catch (t: Throwable) { - Logging.warn("OneSignal: Failed to start Otel logging: ${t.message}", t) - } - } - - @Suppress("TooGenericExceptionCaught") - private fun disableFeatures() { - Logging.info("OneSignal: Disabling Otel features") - - try { - anrDetector?.stop() - anrDetector = null - } catch (t: Throwable) { - Logging.warn("OneSignal: Error stopping ANR detector: ${t.message}", t) - } - - try { - crashHandler?.unregister() - crashHandler = null - } catch (t: Throwable) { - Logging.warn("OneSignal: Error unregistering crash handler: ${t.message}", t) - } - - try { - Logging.setOtelTelemetry(null, { false }) - remoteTelemetry?.shutdown() - remoteTelemetry = null - } catch (t: Throwable) { - Logging.warn("OneSignal: Error disabling Otel logging: ${t.message}", t) - } - } - - @Suppress("TooGenericExceptionCaught") - private fun updateLogLevel(newLevel: LogLevel) { - Logging.info("OneSignal: Updating Otel log level to $newLevel") - try { - startOtelLogging(newLevel) - } catch (t: Throwable) { - Logging.warn("OneSignal: Failed to update Otel log level: ${t.message}", t) - } - } - - private fun startCrashHandler() { - if (crashHandler != null) return - val handler = crashHandlerFactory(context, logger) - handler.initialize() - crashHandler = handler - Logging.info("OneSignal: Crash handler initialized — logs at: ${platformProvider.crashStoragePath}") - } - - private fun startAnrDetector() { - if (anrDetector != null) return - val detector = anrDetectorFactory( - platformProvider, - logger, - AnrConstants.DEFAULT_ANR_THRESHOLD_MS, - AnrConstants.DEFAULT_CHECK_INTERVAL_MS, - ) - detector.start() - anrDetector = detector - Logging.info("OneSignal: ANR detector started") - } - - @Suppress("TooGenericExceptionCaught") - private fun startOtelLogging(logLevel: LogLevel) { - remoteTelemetry?.shutdown() - val telemetry = remoteTelemetryFactory(platformProvider) - remoteTelemetry = telemetry - val shouldSend: (LogLevel) -> Boolean = { level -> - logLevel != LogLevel.NONE && level <= logLevel - } - Logging.setOtelTelemetry(telemetry, shouldSend) - Logging.info("OneSignal: Otel logging active at level $logLevel") - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsController.kt index 17137a0665..70758efe36 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsController.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsController.kt @@ -80,9 +80,9 @@ internal class OutcomeEventsController( val err = "OutcomeEventsController.sendSavedOutcomeEvent: Sending outcome with name: ${event.outcomeId} failed with status code: ${ex.statusCode} and response: ${ex.response}" if (responseType == NetworkUtils.ResponseStatusType.RETRYABLE) { - Logging.info("$err Outcome event was cached and will be reattempted on app cold start") + Logging.warn("$err Outcome event was cached and will be reattempted on app cold start") } else { - Logging.warn("$err Outcome event will be omitted!") + Logging.error("$err Outcome event will be omitted!") _outcomeEventsCache.deleteOldOutcomeEvent(event) } } @@ -223,13 +223,13 @@ internal class OutcomeEventsController( val err = "OutcomeEventsController.sendAndCreateOutcomeEvent: Sending outcome with name: $name failed with status code: ${ex.statusCode} and response: ${ex.response}" if (responseType == NetworkUtils.ResponseStatusType.RETRYABLE) { - Logging.info("$err Outcome event was cached and will be reattempted on app cold start") + Logging.warn("$err Outcome event was cached and will be reattempted on app cold start") // Only if we need to save and retry the outcome, then we will save the timestamp for future sending eventParams.timestamp = timestampSeconds _outcomeEventsCache.saveOutcomeEvent(eventParams) } else { - Logging.warn("$err Outcome event will be omitted!") + Logging.error("$err Outcome event will be omitted!") _outcomeEventsCache.deleteOldOutcomeEvent(eventParams) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionListener.kt index 2f4f3f8ce2..acef72d3c5 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionListener.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/session/impl/SessionListener.kt @@ -56,7 +56,7 @@ internal class SessionListener( // Time is erroneous if below 1 second or over a day if (durationInSeconds < 1L || durationInSeconds > SECONDS_IN_A_DAY) { - Logging.info("SessionListener.onSessionEnded sending duration of $durationInSeconds seconds") + Logging.error("SessionListener.onSessionEnded sending duration of $durationInSeconds seconds") } _operationRepo.enqueue( diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt index 35ff97298f..911c4ba71b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt @@ -4,8 +4,6 @@ import com.onesignal.common.modeling.SimpleModelStore import com.onesignal.common.modeling.SingletonModelStore import com.onesignal.core.internal.preferences.IPreferencesService -const val IDENTITY_NAME_SPACE = "identity" - open class IdentityModelStore(prefs: IPreferencesService) : SingletonModelStore( - SimpleModelStore({ IdentityModel() }, IDENTITY_NAME_SPACE, prefs), + SimpleModelStore({ IdentityModel() }, "identity", prefs), ) diff --git a/OneSignalSDK/onesignal/core/src/test/AndroidManifest.xml b/OneSignalSDK/onesignal/core/src/test/AndroidManifest.xml deleted file mode 100644 index 8768713b9a..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt index 4f9c377348..ca6ce9b308 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt @@ -95,13 +95,13 @@ class LoggingTests : FunSpec({ test("removeListener nested") { // Given val calls = ArrayList() - lateinit var listener: ILogListener - listener = ILogListener { logEvent -> - calls += logEvent.entry - // Remove self from listeners - Logging.removeListener(listener) - } - Logging.addListener(listener) + var listener: ILogListener? = null + listener = + ILogListener { + calls += it.entry + Logging.removeListener(listener!!) + } + Logging.addListener(listener!!) // When Logging.debug("test") diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactoryTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactoryTest.kt deleted file mode 100644 index 5eaaa714d7..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactoryTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.content.Context -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger -import com.onesignal.otel.IOtelCrashHandler -import com.onesignal.otel.IOtelLogger -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.mockk.mockk -import org.robolectric.annotation.Config - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class OneSignalCrashHandlerFactoryTest : FunSpec({ - lateinit var appContext: Context - lateinit var logger: AndroidOtelLogger - // Save original handler to restore after tests - val originalHandler: Thread.UncaughtExceptionHandler? = Thread.getDefaultUncaughtExceptionHandler() - - beforeAny { - appContext = ApplicationProvider.getApplicationContext() - logger = AndroidOtelLogger() - } - - afterEach { - // Restore original uncaught exception handler after each test - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - } - - test("createCrashHandler should return IOtelCrashHandler") { - val handler = OneSignalCrashHandlerFactory.createCrashHandler(appContext, logger) - - handler.shouldBeInstanceOf() - } - - test("createCrashHandler should create handler that can be initialized") { - val handler = OneSignalCrashHandlerFactory.createCrashHandler(appContext, logger) - - handler shouldNotBe null - handler.initialize() - } - - test("createCrashHandler should accept mock logger") { - val mockLogger = mockk(relaxed = true) - - val handler = OneSignalCrashHandlerFactory.createCrashHandler(appContext, mockLogger) - - handler shouldNotBe null - handler.shouldBeInstanceOf() - } - - test("handler should be idempotent when initialized multiple times") { - val handler = OneSignalCrashHandlerFactory.createCrashHandler(appContext, logger) - - handler.initialize() - handler.initialize() // Should not throw - - handler shouldNotBe null - } - - test("createCrashHandler should work with different contexts") { - val context1: Context = ApplicationProvider.getApplicationContext() - val context2: Context = ApplicationProvider.getApplicationContext() - - val handler1 = OneSignalCrashHandlerFactory.createCrashHandler(context1, logger) - val handler2 = OneSignalCrashHandlerFactory.createCrashHandler(context2, logger) - - handler1 shouldNotBe null - handler2 shouldNotBe null - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapperTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapperTest.kt deleted file mode 100644 index 942c02af20..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapperTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.content.Context -import android.content.SharedPreferences -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.core.internal.application.IApplicationService -import com.onesignal.core.internal.config.ConfigModel -import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys -import com.onesignal.core.internal.preferences.PreferenceStores -import com.onesignal.core.internal.startup.IStartableService -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import org.json.JSONArray -import org.json.JSONObject -import org.robolectric.annotation.Config -import com.onesignal.core.internal.config.CONFIG_NAME_SPACE as configNameSpace - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class OneSignalCrashUploaderWrapperTest : FunSpec({ - - lateinit var appContext: Context - lateinit var sharedPreferences: SharedPreferences - - beforeAny { - appContext = ApplicationProvider.getApplicationContext() - sharedPreferences = appContext.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - - afterEach { - sharedPreferences.edit().clear().commit() - } - - test("should implement IStartableService interface") { - val mockApplicationService = mockk(relaxed = true) - every { mockApplicationService.appContext } returns appContext - - val wrapper = OneSignalCrashUploaderWrapper(mockApplicationService) - - wrapper.shouldBeInstanceOf() - } - - test("start should complete without error when remote logging is disabled") { - // Configure remote logging as disabled (NONE) - val remoteLoggingParams = JSONObject().put("logLevel", "NONE") - val configModel = JSONObject().put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - sharedPreferences.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, JSONArray().put(configModel).toString()) - .commit() - - val mockApplicationService = mockk(relaxed = true) - every { mockApplicationService.appContext } returns appContext - - val wrapper = OneSignalCrashUploaderWrapper(mockApplicationService) - - // Should return early without error when remote logging is disabled - runBlocking { wrapper.start() } - } - - test("start should complete without error when no crash reports exist") { - // Configure remote logging as enabled - val remoteLoggingParams = JSONObject().put("logLevel", "ERROR") - val configModel = JSONObject().put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - sharedPreferences.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, JSONArray().put(configModel).toString()) - .commit() - - val mockApplicationService = mockk(relaxed = true) - every { mockApplicationService.appContext } returns appContext - - val wrapper = OneSignalCrashUploaderWrapper(mockApplicationService) - - // Should complete without error even when no crash reports exist - runBlocking { wrapper.start() } - } - - test("start can be called multiple times safely") { - val mockApplicationService = mockk(relaxed = true) - every { mockApplicationService.appContext } returns appContext - - val wrapper = OneSignalCrashUploaderWrapper(mockApplicationService) - - // Multiple calls should not throw - runBlocking { - wrapper.start() - wrapper.start() - } - } - - test("wrapper should be non-null after creation") { - val mockApplicationService = mockk(relaxed = true) - every { mockApplicationService.appContext } returns appContext - - val wrapper = OneSignalCrashUploaderWrapper(mockApplicationService) - - wrapper shouldNotBe null - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelAnrDetectorTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelAnrDetectorTest.kt deleted file mode 100644 index 25cac810c6..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelAnrDetectorTest.kt +++ /dev/null @@ -1,221 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.os.Build -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryCrash -import com.onesignal.otel.IOtelPlatformProvider -import com.onesignal.otel.crash.IOtelAnrDetector -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.robolectric.annotation.Config - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class OtelAnrDetectorTest : FunSpec({ - - val mockPlatformProvider = mockk(relaxed = true) - val mockLogger = mockk(relaxed = true) - val mockCrashTelemetry = mockk(relaxed = true) - - fun setupDefaultMocks() { - every { mockPlatformProvider.sdkBase } returns "android" - every { mockPlatformProvider.sdkBaseVersion } returns "1.0.0" - every { mockPlatformProvider.appPackageId } returns "com.test.app" - every { mockPlatformProvider.appVersion } returns "1.0" - every { mockPlatformProvider.deviceManufacturer } returns "Test" - every { mockPlatformProvider.deviceModel } returns "TestDevice" - every { mockPlatformProvider.osName } returns "Android" - every { mockPlatformProvider.osVersion } returns "10" - every { mockPlatformProvider.osBuildId } returns "TEST123" - every { mockPlatformProvider.sdkWrapper } returns null - every { mockPlatformProvider.sdkWrapperVersion } returns null - every { mockPlatformProvider.appId } returns "test-app-id" - every { mockPlatformProvider.onesignalId } returns null - every { mockPlatformProvider.pushSubscriptionId } returns null - every { mockPlatformProvider.appState } returns "foreground" - every { mockPlatformProvider.processUptime } returns 100L - every { mockPlatformProvider.currentThreadName } returns "main" - every { mockPlatformProvider.crashStoragePath } returns "/test/path" - every { mockPlatformProvider.minFileAgeForReadMillis } returns 5000L - every { mockPlatformProvider.remoteLogLevel } returns "ERROR" - every { mockPlatformProvider.appIdForHeaders } returns "test-app-id" - every { mockPlatformProvider.apiBaseUrl } returns "https://api.onesignal.com" - coEvery { mockPlatformProvider.getInstallId() } returns "test-install-id" - } - - beforeEach { - setupDefaultMocks() - } - - // ===== Factory Function Tests ===== - - test("createAnrDetector should return IOtelAnrDetector") { - // When - val detector = createAnrDetector(mockPlatformProvider, mockLogger) - - // Then - detector.shouldBeInstanceOf() - } - - test("createAnrDetector should create detector with default thresholds") { - // When - val detector = createAnrDetector(mockPlatformProvider, mockLogger) - - // Then - detector shouldNotBe null - } - - test("createAnrDetector should accept custom thresholds") { - // When - val detector = createAnrDetector( - mockPlatformProvider, - mockLogger, - anrThresholdMs = 10_000L, - checkIntervalMs = 2_000L - ) - - // Then - detector shouldNotBe null - } - - // ===== Start/Stop Tests ===== - - test("start should log info messages") { - // Given - val detector = createAnrDetector(mockPlatformProvider, mockLogger) - - // When - detector.start() - - // Then - verify { mockLogger.info(match { it.contains("Starting ANR detection") }) } - - // Cleanup - detector.stop() - } - - test("stop should log info messages") { - // Given - val detector = createAnrDetector(mockPlatformProvider, mockLogger) - detector.start() - - // When - detector.stop() - - // Then - verify { mockLogger.info(match { it.contains("Stopping ANR detection") }) } - } - - test("start should warn when already monitoring") { - // Given - val detector = createAnrDetector(mockPlatformProvider, mockLogger) - detector.start() - - // When - start again - detector.start() - - // Then - verify { mockLogger.warn(match { it.contains("Already monitoring") }) } - - // Cleanup - detector.stop() - } - - test("stop should warn when not monitoring") { - // Given - val detector = createAnrDetector(mockPlatformProvider, mockLogger) - - // When - stop without starting - detector.stop() - - // Then - verify { mockLogger.warn(match { it.contains("Not monitoring") }) } - } - - test("start and stop can be called multiple times safely") { - // Given - val detector = createAnrDetector(mockPlatformProvider, mockLogger) - - // When - detector.start() - detector.stop() - detector.start() - detector.stop() - - // Then - no exceptions thrown - } - - // ===== OtelAnrDetector Internal Tests ===== - - test("OtelAnrDetector should implement IOtelAnrDetector") { - // Given - val detector = OtelAnrDetector(mockCrashTelemetry, mockLogger) - - // Then - detector.shouldBeInstanceOf() - } - - test("OtelAnrDetector should accept custom thresholds") { - // When - val detector = OtelAnrDetector( - mockCrashTelemetry, - mockLogger, - anrThresholdMs = 15_000L, - checkIntervalMs = 3_000L - ) - - // Then - detector shouldNotBe null - } - - test("OtelAnrDetector start should initialize watchdog thread") { - // Given - val detector = OtelAnrDetector( - mockCrashTelemetry, - mockLogger, - anrThresholdMs = 100_000L, // Very long threshold to prevent actual ANR detection - checkIntervalMs = 100_000L // Very long interval - ) - - // When - detector.start() - - // Then - verify { mockLogger.info(match { it.contains("ANR detection started successfully") }) } - - // Cleanup - detector.stop() - } - - test("OtelAnrDetector stop should stop watchdog thread") { - // Given - val detector = OtelAnrDetector( - mockCrashTelemetry, - mockLogger, - anrThresholdMs = 100_000L, - checkIntervalMs = 100_000L - ) - detector.start() - - // When - detector.stop() - - // Then - verify { mockLogger.info(match { it.contains("ANR detection stopped") }) } - } - - // ===== AnrConstants Tests ===== - - test("AnrConstants should have reasonable defaults") { - // Then - AnrConstants.DEFAULT_ANR_THRESHOLD_MS shouldBe 5_000L - AnrConstants.DEFAULT_CHECK_INTERVAL_MS shouldBe 2_000L - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelIntegrationTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelIntegrationTest.kt deleted file mode 100644 index f7cf09c7dd..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelIntegrationTest.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.content.Context -import android.content.SharedPreferences -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -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.otel.android.AndroidOtelLogger -import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider -import com.onesignal.otel.IOtelCrashHandler -import com.onesignal.otel.IOtelPlatformProvider -import com.onesignal.otel.OtelFactory -import com.onesignal.user.internal.backend.IdentityConstants -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeInstanceOf -import kotlinx.coroutines.runBlocking -import org.json.JSONArray -import org.json.JSONObject -import org.robolectric.annotation.Config -import com.onesignal.core.internal.config.CONFIG_NAME_SPACE as configNameSpace -import com.onesignal.user.internal.identity.IDENTITY_NAME_SPACE as identityNameSpace - -// Helper extension for shouldBeOneOf -private infix fun T.shouldBeOneOf(expected: List) { - val isInList = expected.contains(this) - if (!isInList) { - throw AssertionError("Expected $this to be one of $expected") - } -} - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class OtelIntegrationTest : FunSpec({ - var appContext: Context? = null - var sharedPreferences: SharedPreferences? = null - - beforeAny { - if (appContext == null) { - appContext = ApplicationProvider.getApplicationContext() - sharedPreferences = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - } - - beforeEach { - // Ensure sharedPreferences is initialized - if (sharedPreferences == null) { - appContext = ApplicationProvider.getApplicationContext() - sharedPreferences = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - // Clear and set up SharedPreferences with test data - sharedPreferences!!.edit().clear().commit() - - // Set up ConfigModelStore data - val configModel = JSONObject().apply { - put(ConfigModel::appId.name, "test-app-id") - put(ConfigModel::pushSubscriptionId.name, "test-subscription-id") - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "ERROR") - } - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - - // Set up IdentityModelStore data - val identityModel = JSONObject().apply { - put(IdentityConstants.ONESIGNAL_ID, "test-onesignal-id") - } - val identityArray = JSONArray().apply { - put(identityModel) - } - - sharedPreferences.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - .putString(PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID, "test-install-id") - .commit() - } - - afterEach { - sharedPreferences!!.edit().clear().commit() - } - - test("AndroidOtelPlatformProvider should provide correct Android values") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - - provider.shouldBeInstanceOf() - provider.sdkBase shouldBe "android" - provider.appPackageId shouldBe appContext!!.packageName // Use actual package name from context - provider.osName shouldBe "Android" - provider.deviceManufacturer shouldBe Build.MANUFACTURER - provider.deviceModel shouldBe Build.MODEL - provider.osVersion shouldBe Build.VERSION.RELEASE - provider.osBuildId shouldBe Build.ID - - runBlocking { - provider.getInstallId() shouldNotBe null - } - } - - test("AndroidOtelPlatformProvider should provide per-event values") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - - provider.appId shouldBe "test-app-id" - provider.onesignalId shouldBe "test-onesignal-id" - provider.pushSubscriptionId shouldBe "test-subscription-id" - provider.appState shouldBeOneOf listOf("foreground", "background", "unknown") - (provider.processUptime > 0) shouldBe true - provider.currentThreadName shouldBe Thread.currentThread().name - } - - test("AndroidOtelLogger should delegate to Logging") { - val logger = AndroidOtelLogger() - - logger.shouldBeInstanceOf() - // Should not throw - logger.debug("test") - logger.info("test") - logger.warn("test") - logger.error("test") - } - - test("OtelFactory should create crash handler with Android provider") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - val logger = AndroidOtelLogger() - - val handler = OtelFactory.createCrashHandler(provider, logger) - - handler shouldNotBe null - handler.shouldBeInstanceOf() - handler.initialize() // Should not throw - } - - test("OneSignalCrashHandlerFactory should create working crash handler") { - // Note: OneSignalCrashHandlerFactory may need to be updated to use the new approach - // For now, we'll test the direct creation - val provider = createAndroidOtelPlatformProvider(appContext!!) - val logger = AndroidOtelLogger() - val handler = OtelFactory.createCrashHandler(provider, logger) - - handler shouldNotBe null - handler.shouldBeInstanceOf() - handler.initialize() // Should not throw - } - - test("AndroidOtelPlatformProvider should provide crash storage path") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - - provider.crashStoragePath.contains("onesignal") shouldBe true - provider.crashStoragePath.contains("otel") shouldBe true - provider.crashStoragePath.contains("crashes") shouldBe true - provider.minFileAgeForReadMillis shouldBe 5000L - } - - test("AndroidOtelPlatformProvider should handle remote logging config") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - - provider.remoteLogLevel shouldBe "ERROR" - provider.appIdForHeaders shouldBe "test-app-id" - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelSdkSupportTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelSdkSupportTest.kt deleted file mode 100644 index f7660108e8..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelSdkSupportTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.os.Build -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import org.robolectric.annotation.Config - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class OtelSdkSupportTest : FunSpec({ - - afterEach { - OtelSdkSupport.reset() - } - - test("isSupported is true on SDK >= 26") { - OtelSdkSupport.reset() - OtelSdkSupport.isSupported shouldBe true - } - - test("isSupported can be overridden to false for testing") { - OtelSdkSupport.isSupported = false - OtelSdkSupport.isSupported shouldBe false - } - - test("reset restores runtime-detected value") { - OtelSdkSupport.isSupported = false - OtelSdkSupport.isSupported shouldBe false - - OtelSdkSupport.reset() - OtelSdkSupport.isSupported shouldBe true - } - - test("MIN_SDK_VERSION is 26") { - OtelSdkSupport.MIN_SDK_VERSION shouldBe 26 - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt deleted file mode 100644 index 6bde1defb8..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt +++ /dev/null @@ -1,232 +0,0 @@ -package com.onesignal.debug.internal.logging - -import android.os.Build -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.debug.LogLevel -import com.onesignal.otel.IOtelOpenTelemetryRemote -import io.kotest.core.spec.style.FunSpec -import io.mockk.mockk -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.robolectric.annotation.Config - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class LoggingOtelTest : FunSpec({ - val mockTelemetry = mockk(relaxed = true) - - beforeEach { - // Reset Logging state - Logging.setOtelTelemetry(null, { false }) - - // Setup default mock behavior - relaxed mock automatically returns mocks for suspend functions - // The return type (LogRecordBuilder) is handled by the relaxed mock, but we can't verify it - // directly due to type visibility. We'll test behavior instead. - } - - test("setOtelTelemetry should store telemetry and enabled check function") { - // Given - val shouldSend = { _: LogLevel -> true } - - // When - Logging.setOtelTelemetry(mockTelemetry, shouldSend) - - // Then - verify it's set (we'll test it works by logging) - Logging.info("test") - - // Wait for async logging - runBlocking { - delay(100) - } - - // Then - verify it doesn't crash (integration test) - // Note: We can't verify exact calls due to OpenTelemetry type visibility - } - - test("logToOtel should work when remote logging is enabled") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - - // When - Logging.info("test message") - - // Wait for async logging - runBlocking { - delay(100) - } - - // Then - should not crash (integration test) - // The actual Otel call is verified in otel module tests - } - - test("logToOtel should NOT crash when remote logging is disabled") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> false }) - - // When - Logging.info("test message") - - // Wait for async logging - runBlocking { - delay(100) - } - - // Then - should not crash - } - - test("logToOtel should NOT crash when telemetry is null") { - // Given - Logging.setOtelTelemetry(null, { _: LogLevel -> true }) - - // When - Logging.info("test message") - - // Wait for async logging - runBlocking { - delay(100) - } - - // Then - should not crash - } - - test("logToOtel should handle all log levels without crashing") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - - // When - Logging.verbose("verbose message") - Logging.debug("debug message") - Logging.info("info message") - Logging.warn("warn message") - Logging.error("error message") - Logging.fatal("fatal message") - - // Wait for async logging - runBlocking { - delay(200) - } - - // Then - should not crash for any level - } - - test("logToOtel should NOT log NONE level") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - - // When - Logging.log(LogLevel.NONE, "none message") - - // Wait for async logging - runBlocking { - delay(100) - } - - // Then - should not crash, NONE level is skipped - } - - test("logToOtel should handle exceptions in logs") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - val exception = RuntimeException("test exception") - - // When - Logging.error("error with exception", exception) - - // Wait for async logging - runBlocking { - delay(100) - } - - // Then - should not crash, exception details are included - } - - test("logToOtel should handle null exception message") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - val exception = RuntimeException() - - // When - Logging.error("error with null exception message", exception) - - // Wait for async logging - runBlocking { - delay(100) - } - - // Then - should not crash - } - - test("logToOtel should handle Otel errors gracefully") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - // Note: We can't mock getLogger() to throw due to OpenTelemetry type visibility, - // but the real implementation in Logging.logToOtel() handles errors gracefully - - // When - Logging.info("test message") - - // Wait for async logging - runBlocking { - delay(100) - } - - // Then - should not crash, error handling is tested in integration tests - } - - test("logToOtel should use dynamic remote logging check") { - // Given - var isEnabled = false - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> isEnabled }) - - // When - initially disabled - Logging.info("message 1") - runBlocking { delay(50) } - - // When - enable remote logging - isEnabled = true - Logging.info("message 2") - runBlocking { delay(50) } - - // When - disable again - isEnabled = false - Logging.info("message 3") - runBlocking { delay(50) } - - // Then - should not crash, dynamic check works - } - - test("logToOtel should handle multiple rapid log calls") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - - // When - rapid fire logging - repeat(10) { - Logging.info("message $it") - } - - // Wait for async logging - runBlocking { - delay(200) - } - - // Then - should not crash - } - - test("logToOtel should work with different message formats") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - - // When - Logging.info("simple message") - Logging.info("message with numbers: 12345") - Logging.info("message with special chars: !@#$%") - Logging.info("message with unicode: 测试 🚀") - - // Wait for async logging - runBlocking { - delay(200) - } - - // Then - should not crash - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingTest.kt deleted file mode 100644 index 92d8d69885..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingTest.kt +++ /dev/null @@ -1,360 +0,0 @@ -package com.onesignal.debug.internal.logging - -import android.os.Build -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.debug.ILogListener -import com.onesignal.debug.LogLevel -import com.onesignal.debug.OneSignalLogEvent -import com.onesignal.otel.IOtelOpenTelemetryRemote -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.robolectric.annotation.Config - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class LoggingTest : FunSpec({ - - val originalLogLevel = Logging.logLevel - val originalVisualLogLevel = Logging.visualLogLevel - - beforeEach { - // Reset Logging state - Logging.logLevel = LogLevel.WARN - Logging.visualLogLevel = LogLevel.NONE - Logging.setOtelTelemetry(null) { false } - } - - afterEach { - // Restore original state - Logging.logLevel = originalLogLevel - Logging.visualLogLevel = originalVisualLogLevel - Logging.setOtelTelemetry(null) { false } - } - - // ===== Log Level Tests ===== - - test("default logLevel should be WARN") { - // Reset to default - Logging.logLevel = LogLevel.WARN - - // Then - Logging.logLevel shouldBe LogLevel.WARN - } - - test("default visualLogLevel should be NONE") { - // Reset to default - Logging.visualLogLevel = LogLevel.NONE - - // Then - Logging.visualLogLevel shouldBe LogLevel.NONE - } - - test("logLevel can be changed") { - // When - Logging.logLevel = LogLevel.DEBUG - - // Then - Logging.logLevel shouldBe LogLevel.DEBUG - } - - test("visualLogLevel can be changed") { - // When - Logging.visualLogLevel = LogLevel.INFO - - // Then - Logging.visualLogLevel shouldBe LogLevel.INFO - } - - // ===== atLogLevel Tests ===== - - test("atLogLevel returns true when level is at or below logLevel") { - // Given - Logging.logLevel = LogLevel.WARN - - // Then - Logging.atLogLevel(LogLevel.WARN) shouldBe true - Logging.atLogLevel(LogLevel.ERROR) shouldBe true - Logging.atLogLevel(LogLevel.FATAL) shouldBe true - } - - test("atLogLevel returns false when level is above logLevel") { - // Given - Logging.logLevel = LogLevel.WARN - Logging.visualLogLevel = LogLevel.NONE - - // Then - Logging.atLogLevel(LogLevel.INFO) shouldBe false - Logging.atLogLevel(LogLevel.DEBUG) shouldBe false - Logging.atLogLevel(LogLevel.VERBOSE) shouldBe false - } - - test("atLogLevel considers visualLogLevel too") { - // Given - Logging.logLevel = LogLevel.NONE - Logging.visualLogLevel = LogLevel.INFO - - // Then - INFO should pass because visualLogLevel is INFO - Logging.atLogLevel(LogLevel.INFO) shouldBe true - } - - // ===== Log Methods Tests ===== - - test("verbose method should not throw") { - // Given - Logging.logLevel = LogLevel.VERBOSE - - // When & Then - should not throw - Logging.verbose("Test message") - Logging.verbose("Test message with throwable", RuntimeException("test")) - } - - test("debug method should not throw") { - // Given - Logging.logLevel = LogLevel.DEBUG - - // When & Then - should not throw - Logging.debug("Test message") - Logging.debug("Test message with throwable", RuntimeException("test")) - } - - test("info method should not throw") { - // Given - Logging.logLevel = LogLevel.INFO - - // When & Then - should not throw - Logging.info("Test message") - Logging.info("Test message with throwable", RuntimeException("test")) - } - - test("warn method should not throw") { - // Given - Logging.logLevel = LogLevel.WARN - - // When & Then - should not throw - Logging.warn("Test message") - Logging.warn("Test message with throwable", RuntimeException("test")) - } - - test("error method should not throw") { - // Given - Logging.logLevel = LogLevel.ERROR - - // When & Then - should not throw - Logging.error("Test message") - Logging.error("Test message with throwable", RuntimeException("test")) - } - - test("fatal method should not throw") { - // Given - Logging.logLevel = LogLevel.FATAL - - // When & Then - should not throw - Logging.fatal("Test message") - Logging.fatal("Test message with throwable", RuntimeException("test")) - } - - test("log method with level and message should not throw") { - // When & Then - should not throw - Logging.log(LogLevel.INFO, "Test message") - } - - test("log method with level, message, and throwable should not throw") { - // When & Then - should not throw - Logging.log(LogLevel.ERROR, "Test message", RuntimeException("test")) - } - - // ===== Log Listener Tests ===== - - test("addListener should register listener") { - // Given - val mockListener = mockk(relaxed = true) - val eventSlot = slot() - every { mockListener.onLogEvent(capture(eventSlot)) } returns Unit - - Logging.addListener(mockListener) - Logging.logLevel = LogLevel.INFO - - // When - Logging.info("Test listener message") - - // Then - verify { mockListener.onLogEvent(any()) } - eventSlot.captured.level shouldBe LogLevel.INFO - eventSlot.captured.entry.contains("Test listener message") shouldBe true - - // Cleanup - Logging.removeListener(mockListener) - } - - test("removeListener should unregister listener") { - // Given - val mockListener = mockk(relaxed = true) - Logging.addListener(mockListener) - Logging.removeListener(mockListener) - Logging.logLevel = LogLevel.INFO - - // When - Logging.info("Test message after removal") - - // Then - listener should not be called - verify(exactly = 0) { mockListener.onLogEvent(any()) } - } - - test("listener should receive throwable in message") { - // Given - val mockListener = mockk(relaxed = true) - val eventSlot = slot() - every { mockListener.onLogEvent(capture(eventSlot)) } returns Unit - - Logging.addListener(mockListener) - Logging.logLevel = LogLevel.ERROR - - // When - val exception = RuntimeException("Test exception message") - Logging.error("Test error", exception) - - // Then - verify { mockListener.onLogEvent(any()) } - eventSlot.captured.entry.contains("Test error") shouldBe true - eventSlot.captured.entry.contains("Test exception message") shouldBe true - - // Cleanup - Logging.removeListener(mockListener) - } - - // ===== Otel Integration Tests ===== - - test("setOtelTelemetry should set telemetry instance") { - // Given - val mockTelemetry = mockk(relaxed = true) - - // When - Logging.setOtelTelemetry(mockTelemetry) { true } - - // Then - no exception thrown - } - - test("setOtelTelemetry with null should clear telemetry") { - // Given - val mockTelemetry = mockk(relaxed = true) - Logging.setOtelTelemetry(mockTelemetry) { true } - - // When - Logging.setOtelTelemetry(null) { false } - - // Then - no exception thrown - } - - test("log with Otel configured should not throw") { - // Given - Using relaxed mock that doesn't require OpenTelemetry classes - val mockTelemetry = mockk(relaxed = true) - - Logging.setOtelTelemetry(mockTelemetry) { level -> level >= LogLevel.ERROR } - Logging.logLevel = LogLevel.ERROR - - // When & Then - should not throw - Logging.error("Test Otel error message") - runBlocking { delay(100) } - } - - test("log with Otel telemetry set to null should not throw") { - // Given - Logging.setOtelTelemetry(null) { true } - Logging.logLevel = LogLevel.ERROR - - // When & Then - should not throw - Logging.error("Test error - telemetry is null") - } - - test("log with NONE level and Otel configured should not throw") { - // Given - val mockTelemetry = mockk(relaxed = true) - Logging.setOtelTelemetry(mockTelemetry) { true } - - // When & Then - should not throw - Logging.log(LogLevel.NONE, "Should not be logged") - } - - // ===== Message Formatting Tests ===== - - test("log message should include thread name") { - // Given - val mockListener = mockk(relaxed = true) - val eventSlot = slot() - every { mockListener.onLogEvent(capture(eventSlot)) } returns Unit - - Logging.addListener(mockListener) - Logging.logLevel = LogLevel.INFO - - // When - Logging.info("Test message") - - // Then - message should contain thread name in brackets - eventSlot.captured.entry.contains("[") shouldBe true - eventSlot.captured.entry.contains("]") shouldBe true - - // Cleanup - Logging.removeListener(mockListener) - } - - // ===== Thread Safety Tests ===== - - test("multiple listeners can be added safely") { - // Given - val listener1 = mockk(relaxed = true) - val listener2 = mockk(relaxed = true) - val listener3 = mockk(relaxed = true) - - Logging.addListener(listener1) - Logging.addListener(listener2) - Logging.addListener(listener3) - Logging.logLevel = LogLevel.INFO - - // When - Logging.info("Test message to multiple listeners") - - // Then - all listeners should receive the event - verify { listener1.onLogEvent(any()) } - verify { listener2.onLogEvent(any()) } - verify { listener3.onLogEvent(any()) } - - // Cleanup - Logging.removeListener(listener1) - Logging.removeListener(listener2) - Logging.removeListener(listener3) - } - - test("removing non-existent listener should not throw") { - // Given - val mockListener = mockk(relaxed = true) - - // When & Then - should not throw - Logging.removeListener(mockListener) - } - - // ===== All Log Levels Tests ===== - - test("all log levels should work correctly") { - // Given - Logging.logLevel = LogLevel.VERBOSE - val logLevels = listOf( - LogLevel.VERBOSE to { msg: String -> Logging.verbose(msg) }, - LogLevel.DEBUG to { msg: String -> Logging.debug(msg) }, - LogLevel.INFO to { msg: String -> Logging.info(msg) }, - LogLevel.WARN to { msg: String -> Logging.warn(msg) }, - LogLevel.ERROR to { msg: String -> Logging.error(msg) }, - LogLevel.FATAL to { msg: String -> Logging.fatal(msg) } - ) - - // When & Then - none should throw - logLevels.forEach { (level, logFn) -> - logFn("Test message for level $level") - } - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/AndroidOtelLoggerTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/AndroidOtelLoggerTest.kt deleted file mode 100644 index 67336bd367..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/AndroidOtelLoggerTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.onesignal.debug.internal.logging.otel.android - -import com.onesignal.debug.LogLevel -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.otel.IOtelLogger -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.types.shouldBeInstanceOf - -class AndroidOtelLoggerTest : FunSpec({ - // Save original log level to restore after tests - val originalLogLevel = Logging.logLevel - - beforeEach { - // Disable logging during tests to avoid polluting test output - Logging.logLevel = LogLevel.NONE - } - - afterEach { - // Restore original log level - Logging.logLevel = originalLogLevel - } - - test("should implement IOtelLogger interface") { - val logger = AndroidOtelLogger() - - logger.shouldBeInstanceOf() - } - - test("error should not throw") { - val logger = AndroidOtelLogger() - - // Should not throw - logger.error("test error message") - } - - test("warn should not throw") { - val logger = AndroidOtelLogger() - - // Should not throw - logger.warn("test warn message") - } - - test("info should not throw") { - val logger = AndroidOtelLogger() - - // Should not throw - logger.info("test info message") - } - - test("debug should not throw") { - val logger = AndroidOtelLogger() - - // Should not throw - logger.debug("test debug message") - } - - test("should handle empty messages") { - val logger = AndroidOtelLogger() - - // Should not throw with empty messages - logger.error("") - logger.warn("") - logger.info("") - logger.debug("") - } - - test("should handle messages with special characters") { - val logger = AndroidOtelLogger() - - // Should not throw with special characters - logger.error("Error: \n\t special chars: @#$%^&*()") - logger.info("Unicode: 日本語 中文 한국어") - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelIdResolverTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelIdResolverTest.kt deleted file mode 100644 index 86be0f189a..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelIdResolverTest.kt +++ /dev/null @@ -1,1051 +0,0 @@ -package com.onesignal.debug.internal.logging.otel.android - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.common.IDManager.LOCAL_PREFIX -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.LogLevel -import com.onesignal.user.internal.backend.IdentityConstants -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import org.json.JSONArray -import org.json.JSONObject -import com.onesignal.core.internal.config.CONFIG_NAME_SPACE as configNameSpace -import com.onesignal.user.internal.identity.IDENTITY_NAME_SPACE as identityNameSpace - -@RobolectricTest -class OtelIdResolverTest : FunSpec({ - - var appContext: Context? = null - var sharedPreferences: SharedPreferences? = null - - // Helper function to ensure SharedPreferences data is written and verified - fun writeAndVerifyConfigData(configArray: JSONArray) { - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - } - - // Helper function to ensure SharedPreferences identity data is written and verified - fun writeAndVerifyIdentityData(identityArray: JSONArray) { - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - editor.commit() - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, null) - if (verifyData == null || verifyData != identityArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - } - - beforeAny { - if (appContext == null) { - appContext = ApplicationProvider.getApplicationContext() - sharedPreferences = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - // AGGRESSIVE CLEANUP: Clear ALL SharedPreferences before any test runs - // This ensures clean state even if previous test classes left data behind - sharedPreferences!!.edit().clear().commit() - try { - val otherPrefs = appContext!!.getSharedPreferences("com.onesignal", Context.MODE_PRIVATE) - otherPrefs.edit().clear().commit() - } catch (e: Exception) { - // Ignore any errors during cleanup - } - } - - beforeEach { - // Ensure appContext is initialized - if (appContext == null) { - appContext = ApplicationProvider.getApplicationContext() - } - - // Get a FRESH SharedPreferences instance for each test to avoid caching issues - // This ensures we're not reading stale data from previous tests - sharedPreferences = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - - // AGGRESSIVE CLEANUP: Clear ALL SharedPreferences to ensure complete isolation - sharedPreferences!!.edit().clear().commit() - - // Also clear any other potential SharedPreferences files - try { - val otherPrefs = appContext!!.getSharedPreferences("com.onesignal", Context.MODE_PRIVATE) - otherPrefs.edit().clear().commit() - } catch (e: Exception) { - // Ignore any errors during cleanup - } - } - - afterEach { - // Clean up after each test - sharedPreferences!!.edit().clear().commit() - - // Also clear any other potential SharedPreferences files - try { - val otherPrefs = appContext!!.getSharedPreferences("com.onesignal", Context.MODE_PRIVATE) - otherPrefs.edit().clear().commit() - } catch (e: Exception) { - // Ignore any errors during cleanup - } - } - - afterSpec { - // Final cleanup after all tests in this spec - if (appContext != null) { - try { - val prefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - prefs.edit().clear().commit() - - val otherPrefs = appContext!!.getSharedPreferences("com.onesignal", Context.MODE_PRIVATE) - otherPrefs.edit().clear().commit() - } catch (e: Exception) { - // Ignore any errors during cleanup - } - } - } - - // ===== resolveAppId Tests ===== - - test("resolveAppId returns appId from ConfigModelStore when available") { - // Given - val configModel = JSONObject().apply { - put(ConfigModel::appId.name, "test-app-id-123") - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveAppId() - - // Then - result shouldBe "test-app-id-123" - } - - test("resolveAppId returns empty string appId as null and falls back to legacy") { - // Given - val configModel = JSONObject().apply { - put(ConfigModel::appId.name, "") - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .putString(PreferenceOneSignalKeys.PREFS_LEGACY_APP_ID, "legacy-app-id") - .commit() - - // Ensure commit is complete before creating resolver - Thread.sleep(10) - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveAppId() - - // Then - result shouldBe "legacy-app-id" - } - - test("resolveAppId falls back to legacy SharedPreferences when ConfigModelStore has no appId") { - // Given - val configModel = JSONObject() // No appId field - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .putString(PreferenceOneSignalKeys.PREFS_LEGACY_APP_ID, "legacy-app-id") - .commit() - - // Ensure commit is complete before creating resolver - Thread.sleep(10) - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveAppId() - - // Then - result shouldBe "legacy-app-id" - } - - test("resolveAppId returns error appId when ConfigModelStore is null") { - // Given - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveAppId() - - // Then - ERROR_APP_ID_PREFIX_NO_CONFIG_STORE - result shouldBe "e1100000-0000-4000-a000-000000000002" - } - - test("resolveAppId returns error appId when ConfigModelStore is empty array") { - // Given - val configArray = JSONArray() // Empty array - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveAppId() - - // Then - ERROR_APP_ID_PREFIX_NO_APPID_IN_CONFIG_STORE - result shouldBe "e1100000-0000-4000-a000-000000000003" - } - - test("resolveAppId returns error appId when context is null") { - // Given - val resolver = OtelIdResolver(null) - - // When - val result = resolver.resolveAppId() - - // Then - ERROR_APP_ID_PREFIX_NO_CONTEXT - result shouldBe "e1100000-0000-4000-a000-000000000004" - } - - test("resolveAppId handles JSON parsing exceptions gracefully") { - // Given - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, "invalid-json") - .commit() - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveAppId() - - // Then - JSON parse error results in null configModel, so ERROR_APP_ID_PREFIX_NO_CONFIG_STORE - result shouldBe "e1100000-0000-4000-a000-000000000002" - } - - // ===== resolveOnesignalId Tests ===== - - test("resolveOnesignalId returns onesignalId from IdentityModelStore when available") { - // Given - val identityModel = JSONObject().apply { - put(IdentityConstants.ONESIGNAL_ID, "test-onesignal-id-123") - } - val identityArray = JSONArray().apply { - put(identityModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, null) - if (verifyData == null || verifyData != identityArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveOnesignalId() - - // Then - result shouldBe "test-onesignal-id-123" - } - - test("resolveOnesignalId returns null when onesignalId is empty string") { - // Given - val identityModel = JSONObject().apply { - put(IdentityConstants.ONESIGNAL_ID, "") - } - val identityArray = JSONArray().apply { - put(identityModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, null) - if (verifyData == null || verifyData != identityArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveOnesignalId() - - // Then - result shouldBe null - } - - test("resolveOnesignalId returns null when onesignalId is a local ID") { - // Given - val localId = "${LOCAL_PREFIX}test-id" - val identityModel = JSONObject().apply { - put(IdentityConstants.ONESIGNAL_ID, localId) - } - val identityArray = JSONArray().apply { - put(identityModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, null) - if (verifyData == null || verifyData != identityArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveOnesignalId() - - // Then - result shouldBe null - } - - test("resolveOnesignalId returns null when IdentityModelStore has no onesignalId field") { - // Given - val identityModel = JSONObject() // No ONESIGNAL_ID field - val identityArray = JSONArray().apply { - put(identityModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, null) - if (verifyData == null || verifyData != identityArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveOnesignalId() - - // Then - result shouldBe null - } - - test("resolveOnesignalId returns null when IdentityModelStore is null") { - // Given - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveOnesignalId() - - // Then - result shouldBe null - } - - test("resolveOnesignalId returns null when IdentityModelStore is empty array") { - // Given - val identityArray = JSONArray() // Empty array - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, null) - if (verifyData == null || verifyData != identityArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveOnesignalId() - - // Then - result shouldBe null - } - - test("resolveOnesignalId handles JSON parsing exceptions gracefully") { - // Given - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, "invalid-json") - .commit() - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveOnesignalId() - - // Then - result shouldBe null - } - - // ===== resolvePushSubscriptionId Tests ===== - - test("resolvePushSubscriptionId returns pushSubscriptionId from ConfigModelStore when available") { - // Given - val configModel = JSONObject().apply { - put(ConfigModel::pushSubscriptionId.name, "test-push-sub-id-123") - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolvePushSubscriptionId() - - // Then - result shouldBe "test-push-sub-id-123" - } - - test("resolvePushSubscriptionId returns null when pushSubscriptionId is empty string") { - // Given - val configModel = JSONObject().apply { - put(ConfigModel::pushSubscriptionId.name, "") - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolvePushSubscriptionId() - - // Then - result shouldBe null - } - - test("resolvePushSubscriptionId returns null when pushSubscriptionId is a local ID") { - // Given - val localId = "${LOCAL_PREFIX}test-id" - val configModel = JSONObject().apply { - put(ConfigModel::pushSubscriptionId.name, localId) - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolvePushSubscriptionId() - - // Then - result shouldBe null - } - - test("resolvePushSubscriptionId returns null when ConfigModelStore has no pushSubscriptionId field") { - // Given - val configModel = JSONObject() // No pushSubscriptionId field - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolvePushSubscriptionId() - - // Then - result shouldBe null - } - - test("resolvePushSubscriptionId returns null when ConfigModelStore is null") { - // Given - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolvePushSubscriptionId() - - // Then - result shouldBe null - } - - test("resolvePushSubscriptionId handles JSON parsing exceptions gracefully") { - // Given - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, "invalid-json") - .commit() - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolvePushSubscriptionId() - - // Then - result shouldBe null - } - - // ===== resolveRemoteLoggingEnabled Tests ===== - // Enabled is derived from presence of a valid logLevel: - // "logging_config": {} → disabled (not on allowlist) - // "logging_config": {"log_level": "ERROR"} → enabled (on allowlist) - - test("resolveRemoteLoggingEnabled returns true when logLevel is ERROR") { - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "ERROR") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLoggingEnabled() shouldBe true - } - - test("resolveRemoteLoggingEnabled returns true when logLevel is WARN") { - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "WARN") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLoggingEnabled() shouldBe true - } - - test("resolveRemoteLoggingEnabled returns false when logLevel is NONE") { - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "NONE") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - test("resolveRemoteLoggingEnabled returns false when logLevel field is missing (empty logging_config)") { - val remoteLoggingParams = JSONObject() - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - test("resolveRemoteLoggingEnabled returns false when remoteLoggingParams is missing") { - val configModel = JSONObject() - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - test("resolveRemoteLoggingEnabled returns false when no config exists") { - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - test("resolveRemoteLoggingEnabled returns false when logLevel is invalid") { - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "INVALID_LEVEL") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - // ===== resolveRemoteLogLevel Tests ===== - - test("resolveRemoteLogLevel returns LogLevel from ConfigModelStore when available") { - // Given - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "ERROR") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveRemoteLogLevel() - - // Then - result shouldBe LogLevel.ERROR - } - - test("resolveRemoteLogLevel returns LogLevel case-insensitively") { - // Given - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "warn") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveRemoteLogLevel() - - // Then - result shouldBe LogLevel.WARN - } - - test("resolveRemoteLogLevel returns null when logLevel field is missing") { - // Given - val remoteLoggingParams = JSONObject() // No logLevel field - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveRemoteLogLevel() - - // Then - result shouldBe null - } - - test("resolveRemoteLogLevel returns null when remoteLoggingParams field is missing") { - // Given - val configModel = JSONObject() // No remoteLoggingParams field - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveRemoteLogLevel() - - // Then - result shouldBe null - } - - test("resolveRemoteLogLevel returns null when logLevel is invalid") { - // Given - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "INVALID_LEVEL") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveRemoteLogLevel() - - // Then - result shouldBe null - } - - test("resolveRemoteLogLevel returns null when ConfigModelStore is null") { - // Given - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveRemoteLogLevel() - - // Then - result shouldBe null - } - - test("resolveRemoteLogLevel handles JSON parsing exceptions gracefully") { - // Given - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, "invalid-json") - .commit() - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveRemoteLogLevel() - - // Then - result shouldBe null - } - - // ===== extractLogLevelFromParams Tests (via resolveRemoteLogLevel / resolveRemoteLoggingEnabled) ===== - // These test the exact JSON shapes received from the backend. - - test("extractLogLevelFromParams: {logLevel:NONE, isEnabled:false} returns NONE and disabled") { - val remoteLoggingParams = JSONObject("""{"logLevel":"NONE","isEnabled":false}""") - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - writeAndVerifyConfigData(configArray) - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLogLevel() shouldBe LogLevel.NONE - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - test("extractLogLevelFromParams: {logLevel:ERROR, isEnabled:true} returns ERROR and enabled") { - val remoteLoggingParams = JSONObject("""{"logLevel":"ERROR","isEnabled":true}""") - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - writeAndVerifyConfigData(configArray) - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLogLevel() shouldBe LogLevel.ERROR - resolver.resolveRemoteLoggingEnabled() shouldBe true - } - - test("extractLogLevelFromParams: {isEnabled:false} with no logLevel returns null and disabled") { - val remoteLoggingParams = JSONObject("""{"isEnabled":false}""") - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - writeAndVerifyConfigData(configArray) - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLogLevel() shouldBe null - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - test("extractLogLevelFromParams: empty object {} returns null and disabled") { - val remoteLoggingParams = JSONObject("""{}""") - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - writeAndVerifyConfigData(configArray) - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLogLevel() shouldBe null - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - test("extractLogLevelFromParams: {logLevel:WARN} without isEnabled returns WARN and enabled") { - val remoteLoggingParams = JSONObject("""{"logLevel":"WARN"}""") - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - writeAndVerifyConfigData(configArray) - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLogLevel() shouldBe LogLevel.WARN - resolver.resolveRemoteLoggingEnabled() shouldBe true - } - - test("extractLogLevelFromParams: {logLevel:error} lowercase returns ERROR (case-insensitive)") { - val remoteLoggingParams = JSONObject("""{"logLevel":"error","isEnabled":true}""") - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - writeAndVerifyConfigData(configArray) - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLogLevel() shouldBe LogLevel.ERROR - resolver.resolveRemoteLoggingEnabled() shouldBe true - } - - test("extractLogLevelFromParams: {logLevel:INVALID} returns null and disabled") { - val remoteLoggingParams = JSONObject("""{"logLevel":"INVALID","isEnabled":true}""") - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - writeAndVerifyConfigData(configArray) - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLogLevel() shouldBe null - resolver.resolveRemoteLoggingEnabled() shouldBe false - } - - test("extractLogLevelFromParams: isEnabled field does not influence logLevel resolution") { - val remoteLoggingParams = JSONObject("""{"logLevel":"ERROR","isEnabled":false}""") - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - writeAndVerifyConfigData(configArray) - - val resolver = OtelIdResolver(appContext!!) - resolver.resolveRemoteLogLevel() shouldBe LogLevel.ERROR - resolver.resolveRemoteLoggingEnabled() shouldBe true - } - - // ===== resolveInstallId Tests ===== - - test("resolveInstallId returns installId from SharedPreferences when available") { - // Given - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID, "test-install-id-123") - .commit() - - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveInstallId() - - // Then - result shouldBe "test-install-id-123" - } - - test("resolveInstallId returns default InstallId-Null when not found") { - // Given - val resolver = OtelIdResolver(appContext!!) - - // When - val result = resolver.resolveInstallId() - - // Then - result shouldBe "InstallId-Null" - } - - test("resolveInstallId returns InstallId-NotFound when exception occurs") { - // Given - val mockContext = mockk(relaxed = true) - val mockSharedPreferences = mockk(relaxed = true) - every { mockContext.getSharedPreferences(any(), any()) } throws RuntimeException("Test exception") - - val resolver = OtelIdResolver(mockContext) - - // When - val result = resolver.resolveInstallId() - - // Then - result shouldBe "InstallId-NotFound" - } - - // ===== Caching Tests ===== - - test("cachedConfigModel is reused across multiple resolve calls") { - // Given - val configModel = JSONObject().apply { - put(ConfigModel::appId.name, "test-app-id") - put(ConfigModel::pushSubscriptionId.name, "test-push-id") - } - val configArray = JSONArray().apply { - put(configModel) - } - // Write data and ensure it's committed - val editor = sharedPreferences!!.edit() - editor.putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - editor.commit() // Use commit() to ensure synchronous write - - // Get a fresh SharedPreferences instance to ensure we read the latest data - val freshPrefs = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - val verifyData = freshPrefs.getString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, null) - if (verifyData == null || verifyData != configArray.toString()) { - throw AssertionError("Failed to write SharedPreferences data - test isolation issue") - } - - val resolver = OtelIdResolver(appContext!!) - - // When - resolve multiple IDs - val appId1 = resolver.resolveAppId() - val pushId1 = resolver.resolvePushSubscriptionId() - val appId2 = resolver.resolveAppId() - val pushId2 = resolver.resolvePushSubscriptionId() - - // Then - should return same values (cached) - appId1 shouldBe "test-app-id" - pushId1 shouldBe "test-push-id" - appId2 shouldBe "test-app-id" - pushId2 shouldBe "test-push-id" - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProviderTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProviderTest.kt deleted file mode 100644 index f47e3b65ab..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProviderTest.kt +++ /dev/null @@ -1,903 +0,0 @@ -package com.onesignal.debug.internal.logging.otel.android - -import android.content.Context -import android.content.SharedPreferences -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.common.OneSignalUtils -import com.onesignal.common.OneSignalWrapper -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.LogLevel -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.user.internal.backend.IdentityConstants -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.string.shouldContain -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import kotlinx.coroutines.runBlocking -import org.json.JSONArray -import org.json.JSONObject -import org.robolectric.annotation.Config -import com.onesignal.core.internal.config.CONFIG_NAME_SPACE as configNameSpace -import com.onesignal.user.internal.identity.IDENTITY_NAME_SPACE as identityNameSpace - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class OtelPlatformProviderTest : FunSpec({ - - var appContext: Context? = null - var sharedPreferences: SharedPreferences? = null - - beforeAny { - if (appContext == null) { - appContext = ApplicationProvider.getApplicationContext() - sharedPreferences = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - } - - beforeEach { - // Ensure sharedPreferences is initialized - if (sharedPreferences == null) { - appContext = ApplicationProvider.getApplicationContext() - sharedPreferences = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - // Clear SharedPreferences and reset wrapper - sharedPreferences!!.edit().clear().commit() - OneSignalWrapper.sdkType = null - OneSignalWrapper.sdkVersion = null - Logging.logLevel = LogLevel.NONE - } - - afterEach { - // Clean up - sharedPreferences!!.edit().clear().commit() - OneSignalWrapper.sdkType = null - OneSignalWrapper.sdkVersion = null - } - - // ===== Static Properties Tests ===== - - test("sdkBase returns android") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.sdkBase - - // Then - result shouldBe "android" - } - - test("sdkBaseVersion returns OneSignalUtils.sdkVersion") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.sdkBaseVersion - - // Then - result shouldBe OneSignalUtils.sdkVersion - } - - test("appPackageId returns context.packageName") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.appPackageId - - // Then - result shouldBe appContext!!.packageName - } - - test("appVersion returns AndroidUtils.getAppVersion") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.appVersion - - // Then - result shouldNotBe null - result shouldNotBe "" - } - - test("deviceManufacturer returns Build.MANUFACTURER") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.deviceManufacturer - - // Then - result shouldBe Build.MANUFACTURER - } - - test("deviceModel returns Build.MODEL") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.deviceModel - - // Then - result shouldBe Build.MODEL - } - - test("osName returns Android") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.osName - - // Then - result shouldBe "Android" - } - - test("osVersion returns Build.VERSION.RELEASE") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.osVersion - - // Then - result shouldBe Build.VERSION.RELEASE - } - - test("osBuildId returns Build.ID") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.osBuildId - - // Then - result shouldBe Build.ID - } - - test("sdkWrapper returns OneSignalWrapper.sdkType") { - // Given - OneSignalWrapper.sdkType = "Unity" - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.sdkWrapper - - // Then - result shouldBe "Unity" - } - - test("sdkWrapper returns null when not set") { - // Given - OneSignalWrapper.sdkType = null - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.sdkWrapper - - // Then - result shouldBe null - } - - test("sdkWrapperVersion returns OneSignalWrapper.sdkVersion") { - // Given - OneSignalWrapper.sdkVersion = "1.0.0" - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.sdkWrapperVersion - - // Then - result shouldBe "1.0.0" - } - - test("sdkWrapperVersion returns null when not set") { - // Given - OneSignalWrapper.sdkVersion = null - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.sdkWrapperVersion - - // Then - result shouldBe null - } - - // ===== Lazy ID Properties Tests ===== - - test("appId returns resolved appId from OtelIdResolver") { - // Given - val configModel = JSONObject().apply { - put(ConfigModel::appId.name, "test-app-id-123") - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.appId - - // Then - result shouldBe "test-app-id-123" - } - - test("appId returns error UUID when not available") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.appId - - // Then - should return error appId (not null, but error UUID prefix) - result shouldNotBe null - result shouldContain "e1100000-0000-4000-a000-" - } - - test("onesignalId returns resolved onesignalId from OtelIdResolver") { - // Given - val identityModel = JSONObject().apply { - put(IdentityConstants.ONESIGNAL_ID, "test-onesignal-id-123") - } - val identityArray = JSONArray().apply { - put(identityModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.onesignalId - - // Then - result shouldBe "test-onesignal-id-123" - } - - test("onesignalId returns null when not available") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.onesignalId - - // Then - result shouldBe null - } - - test("pushSubscriptionId returns resolved pushSubscriptionId from OtelIdResolver") { - // Given - val configModel = JSONObject().apply { - put(ConfigModel::pushSubscriptionId.name, "test-push-sub-id-123") - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.pushSubscriptionId - - // Then - result shouldBe "test-push-sub-id-123" - } - - test("pushSubscriptionId returns null when not available") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.pushSubscriptionId - - // Then - result shouldBe null - } - - // ===== appState Tests ===== - - test("appState returns foreground when getIsInForeground returns true") { - // Given - val getIsInForeground: () -> Boolean? = { true } - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = appContext, - getIsInForeground = getIsInForeground - ) - val provider = OtelPlatformProvider(config) - - // When - val result = provider.appState - - // Then - result shouldBe "foreground" - } - - test("appState returns background when getIsInForeground returns false") { - // Given - val getIsInForeground: () -> Boolean? = { false } - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = appContext, - getIsInForeground = getIsInForeground - ) - val provider = OtelPlatformProvider(config) - - // When - val result = provider.appState - - // Then - result shouldBe "background" - } - - test("appState falls back to ActivityManager when getIsInForeground is null") { - // Given - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = appContext, - getIsInForeground = null - ) - val provider = OtelPlatformProvider(config) - - // When - val result = provider.appState - - // Then - should return a valid state (foreground, background, or unknown) - result shouldBeOneOf listOf("foreground", "background", "unknown") - } - - test("appState returns unknown when context is null and getIsInForeground is null") { - // Given - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = null, - getIsInForeground = null - ) - val provider = OtelPlatformProvider(config) - - // When - val result = provider.appState - - // Then - result shouldBe "unknown" - } - - test("appState handles exceptions gracefully and returns unknown") { - // Given - val mockContext = mockk(relaxed = true) - every { mockContext.getSystemService(any()) } throws RuntimeException("Test exception") - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = mockContext, - getIsInForeground = null - ) - val provider = OtelPlatformProvider(config) - - // When - val result = provider.appState - - // Then - result shouldBe "unknown" - } - - // ===== processUptime Tests ===== - - test("processUptime returns uptime in milliseconds") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.processUptime - - // Then - (result >= 0) shouldBe true - (result < 1000000.0) shouldBe true // Reasonable upper bound - } - - // ===== currentThreadName Tests ===== - - test("currentThreadName returns current thread name") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.currentThreadName - - // Then - result shouldNotBe null - result shouldNotBe "" - } - - // ===== crashStoragePath Tests ===== - - test("crashStoragePath returns configured path") { - // Given - val expectedPath = "/test/crash/path" - val config = OtelPlatformProviderConfig( - crashStoragePath = expectedPath, - appPackageId = "com.test", - appVersion = "1.0" - ) - val provider = OtelPlatformProvider(config) - - // When - val result = provider.crashStoragePath - - // Then - result shouldBe expectedPath - } - - test("crashStoragePath logs info message on first access") { - // Given - val logSlot = slot() - val expectedPath = "/test/crash/path" - val config = OtelPlatformProviderConfig( - crashStoragePath = expectedPath, - appPackageId = "com.test", - appVersion = "1.0" - ) - val provider = OtelPlatformProvider(config) - - // When - val result = provider.crashStoragePath - - // Then - result shouldBe expectedPath - // Note: We can't easily verify Logging.info was called without mocking Logging, - // but the behavior is tested by ensuring the path is returned correctly - } - - test("createAndroidOtelPlatformProvider sets correct crashStoragePath") { - // Given & When - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // Then - provider.crashStoragePath shouldContain "onesignal" - provider.crashStoragePath shouldContain "otel" - provider.crashStoragePath shouldContain "crashes" - } - - // ===== minFileAgeForReadMillis Tests ===== - - test("minFileAgeForReadMillis returns default value") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.minFileAgeForReadMillis - - // Then - result shouldBe 5_000L - } - - // ===== isRemoteLoggingEnabled Tests ===== - // Derived from logLevel presence: empty logging_config → disabled, has log_level → enabled - - test("isRemoteLoggingEnabled returns false when no config exists") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - provider.isRemoteLoggingEnabled shouldBe false - } - - test("isRemoteLoggingEnabled returns true when config has logLevel ERROR") { - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "ERROR") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - provider.isRemoteLoggingEnabled shouldBe true - } - - test("isRemoteLoggingEnabled returns false when logging_config is empty (no logLevel)") { - val remoteLoggingParams = JSONObject() - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - provider.isRemoteLoggingEnabled shouldBe false - } - - test("isRemoteLoggingEnabled returns false when logLevel is NONE") { - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "NONE") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - provider.isRemoteLoggingEnabled shouldBe false - } - - test("isRemoteLoggingEnabled returns false when exception occurs") { - val mockContext = mockk(relaxed = true) - every { mockContext.getSharedPreferences(any(), any()) } throws RuntimeException("Test exception") - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = mockContext - ) - val provider = OtelPlatformProvider(config) - provider.isRemoteLoggingEnabled shouldBe false - } - - // ===== remoteLogLevel Tests ===== - - test("remoteLogLevel returns null when no config exists (disabled)") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.remoteLogLevel - - // Then - result shouldBe null - } - - test("remoteLogLevel returns null when logging_config is empty (disabled)") { - // Given - val remoteLoggingParams = JSONObject() - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.remoteLogLevel - - // Then - result shouldBe null - } - - test("remoteLogLevel returns configLevel name when available") { - // Given - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "WARN") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.remoteLogLevel - - // Then - result shouldBe "WARN" - } - - test("remoteLogLevel returns ERROR when configLevel is ERROR") { - // Given - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "ERROR") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.remoteLogLevel - - // Then - result shouldBe "ERROR" - } - - test("remoteLogLevel returns NONE when configLevel is NONE") { - // Given - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "NONE") - } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.remoteLogLevel - - // Then - result shouldBe "NONE" - } - - test("remoteLogLevel returns null when exception occurs") { - // Given - val mockContext = mockk(relaxed = true) - every { mockContext.getSharedPreferences(any(), any()) } throws RuntimeException("Test exception") - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = mockContext - ) - val provider = OtelPlatformProvider(config) - - // When - val result = provider.remoteLogLevel - - // Then - result shouldBe null - } - - // ===== appIdForHeaders Tests ===== - - test("appIdForHeaders returns appId when available") { - // Given - val configModel = JSONObject().apply { - put(ConfigModel::appId.name, "test-app-id-123") - } - val configArray = JSONArray().apply { - put(configModel) - } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.appIdForHeaders - - // Then - result shouldBe "test-app-id-123" - } - - test("appIdForHeaders returns empty string when appId is null") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = provider.appIdForHeaders - - // Then - even with error appId, it should return something (not empty) - result shouldNotBe null - } - - // ===== apiBaseUrl Tests ===== - - test("apiBaseUrl returns the core module base URL") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - - provider.apiBaseUrl shouldBe com.onesignal.core.internal.http.OneSignalService.ONESIGNAL_API_BASE_URL - } - - // ===== getInstallId Tests ===== - - test("getInstallId returns installId from SharedPreferences") { - // Given - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID, "test-install-id-123") - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = runBlocking { provider.getInstallId() } - - // Then - result shouldBe "test-install-id-123" - } - - test("getInstallId returns default when not found") { - // Given - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // When - val result = runBlocking { provider.getInstallId() } - - // Then - result shouldBe "InstallId-Null" - } - - // ===== Factory Function Tests ===== - - test("createAndroidOtelPlatformProvider creates provider with correct config") { - // Given & When - val provider = createAndroidOtelPlatformProvider(appContext!!) - - // Then - provider.appPackageId shouldBe appContext!!.packageName - provider.sdkBase shouldBe "android" - provider.osName shouldBe "Android" - } - - // ===== Fresh install / all-missing scenario ===== - - test("fresh install: all lazy properties return safe defaults without crashing") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - - provider.appId shouldContain "e1100000-0000-4000-a000-" - provider.onesignalId shouldBe null - provider.pushSubscriptionId shouldBe null - provider.isRemoteLoggingEnabled shouldBe false - provider.remoteLogLevel shouldBe null - provider.appIdForHeaders shouldNotBe null - provider.sdkBase shouldBe "android" - provider.osName shouldBe "Android" - provider.crashStoragePath shouldContain "onesignal" - } - - test("lazy properties cache the initial value and ignore later SharedPreferences changes") { - val provider = createAndroidOtelPlatformProvider(appContext!!) - - provider.isRemoteLoggingEnabled shouldBe false - provider.remoteLogLevel shouldBe null - - val remoteLoggingParams = JSONObject().apply { put("logLevel", "ERROR") } - val configModel = JSONObject().apply { - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { put(configModel) } - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .commit() - - provider.isRemoteLoggingEnabled shouldBe false - provider.remoteLogLevel shouldBe null - } - - test("getIsInForeground callback throws — appState returns unknown") { - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = appContext, - getIsInForeground = { throw RuntimeException("callback boom") } - ) - val provider = OtelPlatformProvider(config) - provider.appState shouldBe "unknown" - } - - test("getIsInForeground returns null — falls back to ActivityManager") { - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = appContext, - getIsInForeground = { null } - ) - val provider = OtelPlatformProvider(config) - provider.appState shouldBeOneOf listOf("foreground", "background", "unknown") - } - - test("null context and null callback — all provider properties return safe defaults") { - val config = OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = null, - getIsInForeground = null - ) - val provider = OtelPlatformProvider(config) - - provider.appState shouldBe "unknown" - provider.appPackageId shouldBe "com.test" - provider.appVersion shouldBe "1.0" - provider.crashStoragePath shouldBe "/test/path" - provider.isRemoteLoggingEnabled shouldBe false - provider.remoteLogLevel shouldBe null - } - - test("corrupted SharedPreferences JSON — isRemoteLoggingEnabled returns false") { - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, "not valid json {{{") - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - provider.isRemoteLoggingEnabled shouldBe false - provider.remoteLogLevel shouldBe null - } - - test("corrupted SharedPreferences JSON — appId returns error UUID") { - sharedPreferences!!.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, "not valid json {{{") - .commit() - - val provider = createAndroidOtelPlatformProvider(appContext!!) - provider.appId shouldContain "e1100000-0000-4000-a000-" - } - - // ===== Factory Function Tests ===== - - test("createAndroidOtelPlatformProvider handles null appVersion gracefully") { - // Given - val mockContext = mockk(relaxed = true) - val mockPackageManager = mockk(relaxed = true) - every { mockContext.packageName } returns "com.test" - every { mockContext.cacheDir } returns appContext!!.cacheDir - every { mockContext.packageManager } returns mockPackageManager - every { mockContext.getSharedPreferences(any(), any()) } returns sharedPreferences - // Make getPackageInfo throw NameNotFoundException to simulate missing package - every { mockPackageManager.getPackageInfo(any(), any()) } throws android.content.pm.PackageManager.NameNotFoundException() - - // When - val provider: OtelPlatformProvider = createAndroidOtelPlatformProvider(mockContext) - - // Then - provider.appVersion shouldBe "unknown" - } -}) - -// Helper extension for shouldBeOneOf -private infix fun T.shouldBeOneOf(expected: List) { - val isInList = expected.contains(this) - if (!isInList) { - throw AssertionError("Expected $this to be one of $expected") - } -} diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelConfigEvaluatorTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelConfigEvaluatorTest.kt deleted file mode 100644 index 6fd5478cdd..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelConfigEvaluatorTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.onesignal.internal - -import com.onesignal.debug.LogLevel -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.shouldBeInstanceOf - -class OtelConfigEvaluatorTest : FunSpec({ - - // ---- null -> enabled ---- - - test("null old config and new enabled returns Enable with the configured level") { - val result = OtelConfigEvaluator.evaluate( - old = null, - new = OtelConfig(isEnabled = true, logLevel = LogLevel.WARN), - ) - result.shouldBeInstanceOf() - result.logLevel shouldBe LogLevel.WARN - } - - test("null old config and new enabled with null logLevel defaults to ERROR") { - val result = OtelConfigEvaluator.evaluate( - old = null, - new = OtelConfig(isEnabled = true, logLevel = null), - ) - result.shouldBeInstanceOf() - result.logLevel shouldBe LogLevel.ERROR - } - - // ---- null -> disabled ---- - - test("null old config and new disabled returns NoChange") { - val result = OtelConfigEvaluator.evaluate( - old = null, - new = OtelConfig(isEnabled = false, logLevel = null), - ) - result shouldBe OtelConfigAction.NoChange - } - - // ---- disabled -> enabled ---- - - test("disabled to enabled returns Enable") { - val result = OtelConfigEvaluator.evaluate( - old = OtelConfig.DISABLED, - new = OtelConfig(isEnabled = true, logLevel = LogLevel.INFO), - ) - result.shouldBeInstanceOf() - result.logLevel shouldBe LogLevel.INFO - } - - // ---- enabled -> disabled ---- - - test("enabled to disabled returns Disable") { - val result = OtelConfigEvaluator.evaluate( - old = OtelConfig(isEnabled = true, logLevel = LogLevel.ERROR), - new = OtelConfig(isEnabled = false, logLevel = null), - ) - result shouldBe OtelConfigAction.Disable - } - - // ---- enabled -> enabled (level changed) ---- - - test("enabled to enabled with different log level returns UpdateLogLevel") { - val result = OtelConfigEvaluator.evaluate( - old = OtelConfig(isEnabled = true, logLevel = LogLevel.ERROR), - new = OtelConfig(isEnabled = true, logLevel = LogLevel.WARN), - ) - result.shouldBeInstanceOf() - result.oldLevel shouldBe LogLevel.ERROR - result.newLevel shouldBe LogLevel.WARN - } - - test("enabled with null level to enabled with explicit level returns UpdateLogLevel") { - val result = OtelConfigEvaluator.evaluate( - old = OtelConfig(isEnabled = true, logLevel = null), - new = OtelConfig(isEnabled = true, logLevel = LogLevel.WARN), - ) - result.shouldBeInstanceOf() - result.oldLevel shouldBe LogLevel.ERROR - result.newLevel shouldBe LogLevel.WARN - } - - // ---- enabled -> enabled (same level) ---- - - test("enabled to enabled with same level returns NoChange") { - val result = OtelConfigEvaluator.evaluate( - old = OtelConfig(isEnabled = true, logLevel = LogLevel.ERROR), - new = OtelConfig(isEnabled = true, logLevel = LogLevel.ERROR), - ) - result shouldBe OtelConfigAction.NoChange - } - - // ---- disabled -> disabled ---- - - test("disabled to disabled returns NoChange") { - val result = OtelConfigEvaluator.evaluate( - old = OtelConfig.DISABLED, - new = OtelConfig.DISABLED, - ) - result shouldBe OtelConfigAction.NoChange - } -}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelLifecycleManagerFaultTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelLifecycleManagerFaultTest.kt deleted file mode 100644 index 7cd5fb6418..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelLifecycleManagerFaultTest.kt +++ /dev/null @@ -1,311 +0,0 @@ -package com.onesignal.internal - -import android.content.Context -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.common.modeling.ModelChangeTags -import com.onesignal.core.internal.config.ConfigModel -import com.onesignal.debug.LogLevel -import com.onesignal.debug.internal.crash.OtelSdkSupport -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.debug.internal.logging.otel.android.OtelPlatformProvider -import com.onesignal.debug.internal.logging.otel.android.OtelPlatformProviderConfig -import com.onesignal.otel.IOtelCrashHandler -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryRemote -import com.onesignal.otel.IOtelPlatformProvider -import com.onesignal.otel.crash.IOtelAnrDetector -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.robolectric.annotation.Config - -/** - * Fault injection tests that prove all try/catch(Throwable) wrappers in - * [OtelLifecycleManager] actually catch and suppress exceptions, and that - * a failure in one feature does not prevent others from starting. - */ -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class OtelLifecycleManagerFaultTest : FunSpec({ - - lateinit var context: Context - lateinit var mockCrashHandler: IOtelCrashHandler - lateinit var mockAnrDetector: IOtelAnrDetector - lateinit var mockTelemetry: IOtelOpenTelemetryRemote - lateinit var mockLogger: IOtelLogger - lateinit var mockPlatformProvider: OtelPlatformProvider - - beforeEach { - context = ApplicationProvider.getApplicationContext() - OtelSdkSupport.isSupported = true - - mockCrashHandler = mockk(relaxed = true) - mockAnrDetector = mockk(relaxed = true) - mockTelemetry = mockk(relaxed = true) - mockLogger = mockk(relaxed = true) - mockPlatformProvider = OtelPlatformProvider( - OtelPlatformProviderConfig( - crashStoragePath = "/test/path", - appPackageId = "com.test", - appVersion = "1.0", - context = context, - ) - ) - } - - afterEach { - OtelSdkSupport.reset() - Logging.setOtelTelemetry(null) { false } - } - - fun createManager( - crashFactory: (Context, IOtelLogger) -> IOtelCrashHandler = { _, _ -> mockCrashHandler }, - anrFactory: (IOtelPlatformProvider, IOtelLogger, Long, Long) -> IOtelAnrDetector = { _, _, _, _ -> mockAnrDetector }, - telemetryFactory: (IOtelPlatformProvider) -> IOtelOpenTelemetryRemote = { mockTelemetry }, - ppFactory: (Context) -> OtelPlatformProvider = { mockPlatformProvider }, - ): OtelLifecycleManager = - OtelLifecycleManager( - context = context, - crashHandlerFactory = crashFactory, - anrDetectorFactory = anrFactory, - remoteTelemetryFactory = telemetryFactory, - platformProviderFactory = ppFactory, - loggerFactory = { mockLogger }, - ) - - // ------------------------------------------------------------------ - // Factory-level fault injection: factory itself throws - // ------------------------------------------------------------------ - - test("crash handler factory throws — ANR and logging still start") { - var telemetryCreated = false - val manager = createManager( - crashFactory = { _, _ -> throw RuntimeException("crash factory boom") }, - telemetryFactory = { telemetryCreated = true; mockTelemetry }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockAnrDetector.start() } - telemetryCreated shouldBe true - } - - test("ANR factory throws — crash handler and logging still start") { - var telemetryCreated = false - val manager = createManager( - anrFactory = { _, _, _, _ -> throw RuntimeException("anr factory boom") }, - telemetryFactory = { telemetryCreated = true; mockTelemetry }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockCrashHandler.initialize() } - telemetryCreated shouldBe true - } - - test("telemetry factory throws — crash handler and ANR still start") { - val manager = createManager( - telemetryFactory = { throw RuntimeException("telemetry factory boom") }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockCrashHandler.initialize() } - verify(exactly = 1) { mockAnrDetector.start() } - } - - test("all three factories throw — no exception propagates") { - val manager = createManager( - crashFactory = { _, _ -> throw RuntimeException("crash") }, - anrFactory = { _, _, _, _ -> throw RuntimeException("anr") }, - telemetryFactory = { throw RuntimeException("telemetry") }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - } - - // ------------------------------------------------------------------ - // Initialize-level fault injection: object created but init throws - // ------------------------------------------------------------------ - - test("crash handler initialize() throws — ANR and logging still start") { - every { mockCrashHandler.initialize() } throws RuntimeException("init boom") - var telemetryCreated = false - - val manager = createManager( - telemetryFactory = { telemetryCreated = true; mockTelemetry }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockAnrDetector.start() } - telemetryCreated shouldBe true - } - - test("ANR detector start() throws — crash handler and logging still start") { - every { mockAnrDetector.start() } throws RuntimeException("start boom") - var telemetryCreated = false - - val manager = createManager( - telemetryFactory = { telemetryCreated = true; mockTelemetry }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockCrashHandler.initialize() } - telemetryCreated shouldBe true - } - - // ------------------------------------------------------------------ - // Disable-level fault injection: shutdown/stop/unregister throws - // ------------------------------------------------------------------ - - test("ANR stop() throws during disable — crash unregister and telemetry shutdown still run") { - every { mockAnrDetector.stop() } throws RuntimeException("stop boom") - - val manager = createManager() - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = false, logLevel = null), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockCrashHandler.unregister() } - verify(exactly = 1) { mockTelemetry.shutdown() } - } - - test("crash handler unregister() throws during disable — telemetry shutdown still runs") { - every { mockCrashHandler.unregister() } throws RuntimeException("unregister boom") - - val manager = createManager() - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = false, logLevel = null), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockTelemetry.shutdown() } - } - - test("telemetry shutdown() throws during disable — no exception propagates") { - every { mockTelemetry.shutdown() } throws RuntimeException("shutdown boom") - - val manager = createManager() - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = false, logLevel = null), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockAnrDetector.stop() } - verify(exactly = 1) { mockCrashHandler.unregister() } - } - - // ------------------------------------------------------------------ - // Platform provider fault injection - // ------------------------------------------------------------------ - - test("platform provider factory throws — initializeFromCachedConfig does not propagate") { - val manager = createManager( - ppFactory = { throw RuntimeException("provider boom") }, - ) - manager.initializeFromCachedConfig() - } - - // ------------------------------------------------------------------ - // UpdateLogLevel fault injection - // ------------------------------------------------------------------ - - test("telemetry factory throws during log level update — no exception propagates") { - var callCount = 0 - val manager = createManager( - telemetryFactory = { - callCount++ - if (callCount > 1) throw RuntimeException("second create boom") - mockTelemetry - }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.WARN), ModelChangeTags.HYDRATE) - } - - // ------------------------------------------------------------------ - // Idempotency: calling enable twice doesn't double-create - // ------------------------------------------------------------------ - - test("enable called twice does not create duplicate crash handler or ANR detector") { - val manager = createManager() - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = false, logLevel = null), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.WARN), ModelChangeTags.HYDRATE) - - verify(exactly = 2) { mockCrashHandler.initialize() } - verify(exactly = 2) { mockAnrDetector.start() } - } - - // ------------------------------------------------------------------ - // Verify mock interactions in happy path - // ------------------------------------------------------------------ - - test("enable creates all three features and disable tears all down") { - val manager = createManager() - - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - verify(exactly = 1) { mockCrashHandler.initialize() } - verify(exactly = 1) { mockAnrDetector.start() } - - manager.onModelReplaced(configWith(isEnabled = false, logLevel = null), ModelChangeTags.HYDRATE) - verify(exactly = 1) { mockCrashHandler.unregister() } - verify(exactly = 1) { mockAnrDetector.stop() } - verify { mockTelemetry.shutdown() } - } - - test("update log level shuts down old telemetry and creates new one") { - var createCount = 0 - val telemetry1 = mockk(relaxed = true) - val telemetry2 = mockk(relaxed = true) - val manager = createManager( - telemetryFactory = { - createCount++ - if (createCount == 1) telemetry1 else telemetry2 - }, - ) - - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.WARN), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { telemetry1.shutdown() } - createCount shouldBe 2 - } - - // ------------------------------------------------------------------ - // Error type coverage: OutOfMemoryError, StackOverflowError - // ------------------------------------------------------------------ - - test("OutOfMemoryError from factory does not propagate") { - val manager = createManager( - crashFactory = { _, _ -> throw OutOfMemoryError("oom") }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockAnrDetector.start() } - } - - test("StackOverflowError from factory does not propagate") { - val manager = createManager( - anrFactory = { _, _, _, _ -> throw StackOverflowError("stack overflow") }, - ) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - - verify(exactly = 1) { mockCrashHandler.initialize() } - } - - // ------------------------------------------------------------------ - // initializeFromCachedConfig fault injection - // ------------------------------------------------------------------ - - test("initializeFromCachedConfig catches factory failure and does not propagate") { - val manager = createManager( - crashFactory = { _, _ -> throw RuntimeException("crash") }, - anrFactory = { _, _, _, _ -> throw RuntimeException("anr") }, - telemetryFactory = { throw RuntimeException("telemetry") }, - ) - manager.initializeFromCachedConfig() - } -}) - -private fun configWith(isEnabled: Boolean, logLevel: LogLevel?): ConfigModel { - val config = ConfigModel() - config.remoteLoggingParams.isEnabled = isEnabled - logLevel?.let { config.remoteLoggingParams.logLevel = it } - return config -} diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelLifecycleManagerTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelLifecycleManagerTest.kt deleted file mode 100644 index c5c2380346..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelLifecycleManagerTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.onesignal.internal - -import android.content.Context -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.common.modeling.ModelChangeTags -import com.onesignal.core.internal.config.ConfigModel -import com.onesignal.debug.LogLevel -import com.onesignal.debug.internal.crash.OtelSdkSupport -import com.onesignal.debug.internal.logging.Logging -import io.kotest.core.spec.style.FunSpec -import org.robolectric.annotation.Config - -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class OtelLifecycleManagerTest : FunSpec({ - lateinit var context: Context - - beforeEach { - context = ApplicationProvider.getApplicationContext() - OtelSdkSupport.isSupported = true - } - - afterEach { - OtelSdkSupport.reset() - } - - test("initializeFromCachedConfig does not crash when SDK unsupported") { - OtelSdkSupport.isSupported = false - val manager = OtelLifecycleManager(context) - manager.initializeFromCachedConfig() - } - - test("initializeFromCachedConfig does not throw on supported SDK") { - val manager = OtelLifecycleManager(context) - manager.initializeFromCachedConfig() - } - - test("onModelReplaced does not crash when SDK unsupported") { - OtelSdkSupport.isSupported = false - val manager = OtelLifecycleManager(context) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - } - - test("onModelReplaced ignores non-HYDRATE tags") { - val manager = OtelLifecycleManager(context) - manager.initializeFromCachedConfig() - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.NORMAL) - } - - test("onModelReplaced enable then disable does not throw") { - val manager = OtelLifecycleManager(context) - manager.initializeFromCachedConfig() - - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = false, logLevel = null), ModelChangeTags.HYDRATE) - } - - test("onModelReplaced updates log level without throwing") { - val manager = OtelLifecycleManager(context) - manager.initializeFromCachedConfig() - - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.WARN), ModelChangeTags.HYDRATE) - } - - test("onModelReplaced with same config is no-op") { - val manager = OtelLifecycleManager(context) - manager.initializeFromCachedConfig() - - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - } - - test("disable clears Otel telemetry from Logging") { - val manager = OtelLifecycleManager(context) - manager.initializeFromCachedConfig() - - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = false, logLevel = null), ModelChangeTags.HYDRATE) - - Logging.info("test message after otel disabled") - } - - test("full lifecycle: init -> enable -> update level -> disable -> re-enable") { - val manager = OtelLifecycleManager(context) - manager.initializeFromCachedConfig() - - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.ERROR), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.WARN), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.INFO), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = false, logLevel = null), ModelChangeTags.HYDRATE) - manager.onModelReplaced(configWith(isEnabled = true, logLevel = LogLevel.DEBUG), ModelChangeTags.HYDRATE) - } -}) - -private fun configWith(isEnabled: Boolean, logLevel: LogLevel?): ConfigModel { - val config = ConfigModel() - config.remoteLoggingParams.isEnabled = isEnabled - logLevel?.let { config.remoteLoggingParams.logLevel = it } - return config -} diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt index c3b7e72d80..64ae3a9b82 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt @@ -446,7 +446,7 @@ internal class InAppMessagesManager( Logging.debug("InAppMessagesManager.attemptToShowInAppMessage: $messageDisplayQueue") // If there are IAMs in the queue and nothing showing, show first in the queue if (paused) { - Logging.debug( + Logging.warn( "InAppMessagesManager.attemptToShowInAppMessage: In app messaging is currently paused, in app messages will not be shown!", ) } else if (messageDisplayQueue.isEmpty()) { diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt index 9bbd738d55..79d9a76099 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/backend/impl/InAppBackendService.kt @@ -202,7 +202,7 @@ internal class InAppBackendService( statusCode: Int, response: String?, ) { - Logging.info("Encountered a $statusCode error while attempting in-app message $requestType request: $response") + Logging.error("Encountered a $statusCode error while attempting in-app message $requestType request: $response") } private suspend fun attemptFetchWithRetries( diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/InAppDisplayer.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/InAppDisplayer.kt index 7bf7c14fb2..9c7115cad3 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/InAppDisplayer.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/InAppDisplayer.kt @@ -149,7 +149,7 @@ internal class InAppDisplayer( } catch (e: Exception) { // Need to check error message to only catch MissingWebViewPackageException as it isn't public if (e.message != null && e.message!!.contains("No WebView installed")) { - Logging.info("Error setting up WebView: ", e) + Logging.error("Error setting up WebView: ", e) } else { throw e } diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/InAppMessageView.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/InAppMessageView.kt index f4b8f1263a..a86a8bed6b 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/InAppMessageView.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/InAppMessageView.kt @@ -460,7 +460,7 @@ internal class InAppMessageView( */ suspend fun dismissAndAwaitNextMessage() { if (draggableRelativeLayout == null) { - Logging.info("No host presenter to trigger dismiss animation, counting as dismissed already") + Logging.error("No host presenter to trigger dismiss animation, counting as dismissed already") dereferenceViews() return } diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/hydrators/InAppHydrator.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/hydrators/InAppHydrator.kt index a860357bb6..0518dc5c72 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/hydrators/InAppHydrator.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/hydrators/InAppHydrator.kt @@ -30,7 +30,7 @@ internal class InAppHydrator( try { val content = InAppMessageContent(jsonObject) if (content.contentHtml == null) { - Logging.info("displayMessage:OnSuccess: No HTML retrieved from loadMessageContent") + Logging.debug("displayMessage:OnSuccess: No HTML retrieved from loadMessageContent") return null } diff --git a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/LocationManager.kt b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/LocationManager.kt index bd26095c5a..903183d369 100644 --- a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/LocationManager.kt +++ b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/LocationManager.kt @@ -102,7 +102,7 @@ internal class LocationManager( if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (!hasFinePermissionGranted && !hasCoarsePermissionGranted) { // Permission missing on manifest - Logging.info("Location permissions not added on AndroidManifest file < M") + Logging.error("Location permissions not added on AndroidManifest file < M") return@withContext false } diff --git a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/HmsLocationController.kt b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/HmsLocationController.kt index 19c11038ad..e2e219fcd5 100644 --- a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/HmsLocationController.kt +++ b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/HmsLocationController.kt @@ -50,7 +50,7 @@ internal class HmsLocationController( hmsFusedLocationClient = com.huawei.hms.location.LocationServices.getFusedLocationProviderClient(_applicationService.appContext) } catch (e: Exception) { - Logging.warn("Huawei LocationServices getFusedLocationProviderClient failed! $e") + Logging.error("Huawei LocationServices getFusedLocationProviderClient failed! $e") wasSuccessful = false return@withLock } @@ -75,7 +75,7 @@ internal class HmsLocationController( }, ) .addOnFailureListener { e -> - Logging.warn("Huawei LocationServices getLastLocation failed!", e) + Logging.error("Huawei LocationServices getLastLocation failed!", e) waiter.wake(false) } wasSuccessful = waiter.waitForWake() @@ -133,7 +133,7 @@ internal class HmsLocationController( }, ) .addOnFailureListener { e -> - Logging.warn("Huawei LocationServices getLastLocation failed!", e) + Logging.error("Huawei LocationServices getLastLocation failed!", e) waiter.wake() } waiter.waitForWake() diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/bridges/OneSignalHmsEventBridge.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/bridges/OneSignalHmsEventBridge.kt index d4599af876..7a9a8fef54 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/bridges/OneSignalHmsEventBridge.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/bridges/OneSignalHmsEventBridge.kt @@ -87,7 +87,7 @@ object OneSignalHmsEventBridge { data = messageDataJSON.toString() } catch (e: JSONException) { - Logging.warn("OneSignalHmsEventBridge error when trying to create RemoteMessage data JSON") + Logging.error("OneSignalHmsEventBridge error when trying to create RemoteMessage data JSON") } // HMS notification with Message Type being Message won't trigger Activity reverse trampolining logic diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/channels/impl/NotificationChannelManager.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/channels/impl/NotificationChannelManager.kt index 45577fc7c9..f5e9fe6e74 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/channels/impl/NotificationChannelManager.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/channels/impl/NotificationChannelManager.kt @@ -116,7 +116,7 @@ internal class NotificationChannelManager( ledColor = BigInteger(ledc, 16) channel.lightColor = ledColor.toInt() } catch (t: Throwable) { - Logging.warn("Couldn't convert ARGB Hex value to BigInteger:", t) + Logging.error("Couldn't convert ARGB Hex value to BigInteger:", t) } } channel.enableLights(payload.optInt("led", 1) == 1) @@ -211,7 +211,7 @@ internal class NotificationChannelManager( } catch (e: NullPointerException) { // Catch issue caused by "Attempt to invoke virtual method 'boolean android.app.NotificationChannel.isDeleted()' on a null object reference" // https://github.com/OneSignal/OneSignal-Android-SDK/issues/1291 - Logging.warn("Error when trying to delete notification channel: " + e.message) + Logging.error("Error when trying to delete notification channel: " + e.message) } // Delete old channels - Payload will include all changes for the app. Any extra OS_ ones must diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/OSWorkManagerHelper.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/OSWorkManagerHelper.kt index 9dfab0e6d7..a0c9397984 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/OSWorkManagerHelper.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/common/OSWorkManagerHelper.kt @@ -26,7 +26,7 @@ object OSWorkManagerHelper { This aims to catch the IllegalStateException "WorkManager is not initialized properly..." - https://androidx.tech/artifacts/work/work-runtime/2.8.1-source/androidx/work/impl/WorkManagerImpl.java.html */ - Logging.warn("OSWorkManagerHelper.getInstance failed, attempting to initialize: ", e) + Logging.error("OSWorkManagerHelper.getInstance failed, attempting to initialize: ", e) initializeWorkManager(context) WorkManager.getInstance(context) } @@ -51,7 +51,7 @@ object OSWorkManagerHelper { 1. We lost the race with another call to WorkManager.initialize outside of OneSignal. 2. It is possible for some other unexpected error is thrown from WorkManager. */ - Logging.warn("OSWorkManagerHelper initializing WorkManager failed: ", e) + Logging.error("OSWorkManagerHelper initializing WorkManager failed: ", e) } } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt index 056540cac9..66c750e3c0 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt @@ -429,7 +429,7 @@ internal class NotificationRepository( } } } catch (t: Throwable) { - Logging.warn("Error clearing oldest notifications over limit! ", t) + Logging.error("Error clearing oldest notifications over limit! ", t) } } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt index 2fe22d6aa8..86ac61fe58 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt @@ -85,9 +85,9 @@ internal class NotificationGenerationProcessor( }.join() } } catch (to: TimeoutCancellationException) { - Logging.info("remoteNotificationReceived timed out, continuing with wantsToDisplay=$wantsToDisplay.", to) + Logging.error("remoteNotificationReceived timed out, continuing with wantsToDisplay=$wantsToDisplay.", to) } catch (t: Throwable) { - Logging.info("remoteNotificationReceived threw an exception. Displaying normal OneSignal notification.", t) + Logging.error("remoteNotificationReceived threw an exception. Displaying normal OneSignal notification.", t) } var shouldDisplay = @@ -120,7 +120,7 @@ internal class NotificationGenerationProcessor( } catch (to: TimeoutCancellationException) { Logging.info("notificationWillShowInForegroundHandler timed out, continuing with wantsToDisplay=$wantsToDisplay.", to) } catch (t: Throwable) { - Logging.info( + Logging.error( "notificationWillShowInForegroundHandler threw an exception. Displaying normal OneSignal notification.", t, ) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt index 1235267aba..eb4b3cac52 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/lifecycle/impl/NotificationLifecycleService.kt @@ -149,7 +149,7 @@ internal class NotificationLifecycleService( deviceType, ) } catch (ex: BackendException) { - Logging.info("Notification opened confirmation failed with statusCode: ${ex.statusCode} response: ${ex.response}") + Logging.error("Notification opened confirmation failed with statusCode: ${ex.statusCode} response: ${ex.response}") } } } @@ -266,17 +266,20 @@ internal class NotificationLifecycleService( val intent = intentGenerator.getIntentVisible() if (intent != null) { - Logging.debug("SDK running startActivity with Intent: $intent") + Logging.info("SDK running startActivity with Intent: $intent") activity.startActivity(intent) } else { - Logging.debug("SDK not showing an Activity automatically due to it's settings.") + Logging.info("SDK not showing an Activity automatically due to it's settings.") } } catch (e: JSONException) { - Logging.error("Could not parse JSON to open notification activity.", e) + Logging.error("Could not parse JSON to open notification activity.") + e.printStackTrace() } catch (e: ActivityNotFoundException) { - Logging.warn("No activity found to handle notification open intent.", e) + Logging.error("No activity found to handle notification open intent.") + e.printStackTrace() } catch (e: Exception) { - Logging.error("Could not open notification activity.", e) + Logging.error("Could not open notification activity.") + e.printStackTrace() } } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/pushtoken/PushTokenManager.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/pushtoken/PushTokenManager.kt index 5813d156bb..4dffeec5c5 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/pushtoken/PushTokenManager.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/pushtoken/PushTokenManager.kt @@ -18,11 +18,11 @@ internal class PushTokenManager( override suspend fun retrievePushToken(): PushTokenResponse { when (_deviceService.jetpackLibraryStatus) { IDeviceService.JetpackLibraryStatus.MISSING -> { - Logging.info("Could not find the Jetpack/AndroidX. Please make sure it has been correctly added to your project.") + Logging.fatal("Could not find the Jetpack/AndroidX. Please make sure it has been correctly added to your project.") pushTokenStatus = SubscriptionStatus.MISSING_JETPACK_LIBRARY } IDeviceService.JetpackLibraryStatus.OUTDATED -> { - Logging.info( + Logging.fatal( "The included Jetpack/AndroidX Library is too old or incomplete.", ) pushTokenStatus = SubscriptionStatus.OUTDATED_JETPACK_LIBRARY diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/receivereceipt/impl/ReceiveReceiptProcessor.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/receivereceipt/impl/ReceiveReceiptProcessor.kt index e15937d1d3..ad7827d11a 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/receivereceipt/impl/ReceiveReceiptProcessor.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/receivereceipt/impl/ReceiveReceiptProcessor.kt @@ -20,7 +20,7 @@ internal class ReceiveReceiptProcessor( try { _backend.updateNotificationAsReceived(appId, notificationId, subscriptionId, deviceType) } catch (ex: BackendException) { - Logging.info("Receive receipt failed with statusCode: ${ex.statusCode} response: ${ex.response}") + Logging.error("Receive receipt failed with statusCode: ${ex.statusCode} response: ${ex.response}") } } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorADM.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorADM.kt index 6970196777..98d1611302 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorADM.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorADM.kt @@ -38,13 +38,13 @@ internal class PushRegistratorADM( result = if (registrationId != null) { - Logging.debug("ADM registered with ID:$registrationId") + Logging.error("ADM registered with ID:$registrationId") IPushRegistrator.RegisterResult( registrationId, SubscriptionStatus.SUBSCRIBED, ) } else { - Logging.info("com.onesignal.ADMMessageHandler timed out, please check that your have the receiver, service, and your package name matches(NOTE: Case Sensitive) per the OneSignal instructions.") + Logging.error("com.onesignal.ADMMessageHandler timed out, please check that your have the receiver, service, and your package name matches(NOTE: Case Sensitive) per the OneSignal instructions.") IPushRegistrator.RegisterResult( null, SubscriptionStatus.ERROR, diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt index ac24ca3558..d1d53fcdc6 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorAbstractGoogle.kt @@ -57,12 +57,12 @@ internal abstract class PushRegistratorAbstractGoogle( } if (!_deviceService.hasFCMLibrary) { - Logging.warn("The Firebase FCM library is missing! Please make sure to include it in your project.") + Logging.fatal("The Firebase FCM library is missing! Please make sure to include it in your project.") return IPushRegistrator.RegisterResult(null, SubscriptionStatus.MISSING_FIREBASE_FCM_LIBRARY) } return if (!isValidProjectNumber(_configModelStore.model.googleProjectNumber)) { - Logging.warn( + Logging.error( "Missing Google Project number!\nPlease enter a Google Project number / Sender ID on under App Settings > Android > Configuration on the OneSignal dashboard.", ) IPushRegistrator.RegisterResult( @@ -84,14 +84,14 @@ internal abstract class PushRegistratorAbstractGoogle( registerInBackground(senderId) } else { _upgradePrompt.showUpdateGPSDialog() - Logging.warn("'Google Play services' app not installed or disabled on the device.") + Logging.error("'Google Play services' app not installed or disabled on the device.") IPushRegistrator.RegisterResult( null, SubscriptionStatus.OUTDATED_GOOGLE_PLAY_SERVICES_APP, ) } } catch (t: Throwable) { - Logging.warn( + Logging.error( "Could not register with $providerName due to an issue with your AndroidManifest.xml or with 'Google Play services'.", t, ) @@ -140,7 +140,7 @@ internal abstract class PushRegistratorAbstractGoogle( // Wrapping with new Exception so the current line is included in the stack trace. val exception = Exception(e) if (currentRetry >= REGISTRATION_RETRY_COUNT - 1) { - Logging.info("Retry count of $REGISTRATION_RETRY_COUNT exceed! Could not get a $providerName Token.", exception) + Logging.error("Retry count of $REGISTRATION_RETRY_COUNT exceed! Could not get a $providerName Token.", exception) } else { Logging.info("'Google Play services' returned $exceptionMessage error. Current retry count: $currentRetry", exception) @@ -152,12 +152,12 @@ internal abstract class PushRegistratorAbstractGoogle( } else { // Wrapping with new Exception so the current line is included in the stack trace. val exception = Exception(e) - Logging.warn("Error Getting $providerName Token", exception) + Logging.error("Error Getting $providerName Token", exception) return IPushRegistrator.RegisterResult(null, pushStatus) } } catch (t: Throwable) { - Logging.warn("Unknown error getting $providerName Token", t) + Logging.error("Unknown error getting $providerName Token", t) return IPushRegistrator.RegisterResult( null, SubscriptionStatus.FIREBASE_FCM_ERROR_MISC_EXCEPTION, diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorHMS.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorHMS.kt index d637242cdc..b568c34e9d 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorHMS.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/registration/impl/PushRegistratorHMS.kt @@ -78,13 +78,13 @@ internal class PushRegistratorHMS( } return if (pushToken != null) { - Logging.debug("HMS registered with ID:$pushToken") + Logging.error("HMS registered with ID:$pushToken") IPushRegistrator.RegisterResult( pushToken, SubscriptionStatus.SUBSCRIBED, ) } else { - Logging.warn("HmsMessageServiceOneSignal.onNewToken timed out.") + Logging.error("HmsMessageServiceOneSignal.onNewToken timed out.") IPushRegistrator.RegisterResult( null, SubscriptionStatus.HMS_TOKEN_TIMEOUT, diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreProcessor.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreProcessor.kt index bc2ba38c7d..d00de6ce8d 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreProcessor.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreProcessor.kt @@ -30,7 +30,7 @@ internal class NotificationRestoreProcessor( _badgeCountUpdater.update() } catch (t: Throwable) { - Logging.warn("Error restoring notification records! ", t) + Logging.error("Error restoring notification records! ", t) } } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreWorkManager.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreWorkManager.kt index 5a95221ccd..bb2b567850 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreWorkManager.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreWorkManager.kt @@ -16,14 +16,13 @@ internal class NotificationRestoreWorkManager : INotificationRestoreWorkManager // Notifications will never be force removed when the app's process is running, // so we only need to restore at most once per cold start of the app. private var restored = false - private val lock = Any() override fun beginEnqueueingWork( context: Context, shouldDelay: Boolean, ) { // Only allow one piece of work to be enqueued. - synchronized(lock) { + synchronized(restored) { if (restored) { return } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/services/ADMMessageHandler.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/services/ADMMessageHandler.kt index 5434bb13d7..cc8d9c2e2e 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/services/ADMMessageHandler.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/services/ADMMessageHandler.kt @@ -34,10 +34,10 @@ class ADMMessageHandler : ADMMessageHandlerBase("ADMMessageHandler") { } override fun onRegistrationError(error: String) { - Logging.info("ADM:onRegistrationError: $error") + Logging.error("ADM:onRegistrationError: $error") if ("INVALID_SENDER" == error) { - Logging.info( + Logging.error( "Please double check that you have a matching package name (NOTE: Case Sensitive), api_key.txt, and the apk was signed with the same Keystore and Alias.", ) } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/services/ADMMessageHandlerJob.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/services/ADMMessageHandlerJob.kt index f309538d23..c707333743 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/services/ADMMessageHandlerJob.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/services/ADMMessageHandlerJob.kt @@ -50,9 +50,9 @@ class ADMMessageHandlerJob : ADMMessageHandlerJobBase() { context: Context?, error: String?, ) { - Logging.info("ADM:onRegistrationError: $error") + Logging.error("ADM:onRegistrationError: $error") if ("INVALID_SENDER" == error) { - Logging.info( + Logging.error( "Please double check that you have a matching package name (NOTE: Case Sensitive), api_key.txt, and the apk was signed with the same Keystore and Alias.", ) } diff --git a/OneSignalSDK/onesignal/otel/.gitignore b/OneSignalSDK/onesignal/otel/.gitignore deleted file mode 100644 index 796b96d1c4..0000000000 --- a/OneSignalSDK/onesignal/otel/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/OneSignalSDK/onesignal/otel/build.gradle b/OneSignalSDK/onesignal/otel/build.gradle deleted file mode 100644 index 7860f04201..0000000000 --- a/OneSignalSDK/onesignal/otel/build.gradle +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'com.diffplug.spotless' - id 'io.gitlab.arturbosch.detekt' -} - -android { - namespace 'com.onesignal.otel' - compileSdkVersion rootProject.buildVersions.compileSdkVersion - - defaultConfig { - minSdkVersion 26 - consumerProguardFiles "consumer-rules.pro" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - original { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - release { - minifyEnabled false - } - unity { - minifyEnabled false - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - freeCompilerArgs += ['-module-name', namespace] - } -} - -ext { - projectName = "OneSignal SDK Otel" - projectDescription = "OneSignal Android SDK - OpenTelemetry Module" -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" - - implementation platform("io.opentelemetry:opentelemetry-bom:$rootProject.opentelemetryBomVersion") - implementation('io.opentelemetry:opentelemetry-api') - implementation('io.opentelemetry:opentelemetry-sdk') - implementation('io.opentelemetry:opentelemetry-exporter-otlp') - implementation("io.opentelemetry.semconv:opentelemetry-semconv:$rootProject.opentelemetrySemconvVersion") - implementation("io.opentelemetry.contrib:opentelemetry-disk-buffering:$rootProject.opentelemetryDiskBufferingVersion") - - testImplementation(project(':OneSignal:testhelpers')) - testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") - testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") - testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") - testImplementation("io.mockk:mockk:$ioMockVersion") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") -} - -apply from: '../detekt.gradle' -apply from: '../spotless.gradle' diff --git a/OneSignalSDK/onesignal/otel/consumer-rules.pro b/OneSignalSDK/onesignal/otel/consumer-rules.pro deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/OneSignalSDK/onesignal/otel/proguard-rules.pro b/OneSignalSDK/onesignal/otel/proguard-rules.pro deleted file mode 100644 index f1b424510d..0000000000 --- a/OneSignalSDK/onesignal/otel/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/OneSignalSDK/onesignal/otel/src/main/AndroidManifest.xml b/OneSignalSDK/onesignal/otel/src/main/AndroidManifest.xml deleted file mode 100644 index 8bdb7e14b3..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelCrashHandler.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelCrashHandler.kt deleted file mode 100644 index 93b31fc75f..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelCrashHandler.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.onesignal.otel - -/** - * Platform-agnostic crash handler interface. - * This should be initialized as early as possible and be independent of service architecture. - */ -interface IOtelCrashHandler { - /** - * Initialize the crash handler. This should be called as early as possible, - * before any other initialization that might crash. - */ - fun initialize() - - /** - * Unregisters this crash handler, restoring the previous default handler. - * Safe to call even if [initialize] was never called (no-op in that case). - */ - fun unregister() -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelCrashReporter.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelCrashReporter.kt deleted file mode 100644 index 4ab4a7e8b6..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelCrashReporter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.onesignal.otel - -/** - * Platform-agnostic crash reporter interface. - */ -interface IOtelCrashReporter { - suspend fun saveCrash(thread: Thread, throwable: Throwable) -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelLogger.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelLogger.kt deleted file mode 100644 index 510ffab2eb..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelLogger.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.onesignal.otel - -/** - * Platform-agnostic logger interface for the Otel module. - * Implementations should be provided by the platform (Android/iOS). - */ -interface IOtelLogger { - /** - * Logs an error message. - * - * @param message The error message to log - */ - fun error(message: String) - - /** - * Logs a warning message. - * - * @param message The warning message to log - */ - fun warn(message: String) - - /** - * Logs an informational message. - * - * @param message The info message to log - */ - fun info(message: String) - - /** - * Logs a debug message. - * - * @param message The debug message to log - */ - fun debug(message: String) -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelOpenTelemetry.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelOpenTelemetry.kt deleted file mode 100644 index 156df29ffd..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelOpenTelemetry.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.onesignal.otel - -import io.opentelemetry.api.logs.LogRecordBuilder -import io.opentelemetry.sdk.common.CompletableResultCode -import io.opentelemetry.sdk.logs.export.LogRecordExporter - -/** - * Platform-agnostic OpenTelemetry interface. - */ -interface IOtelOpenTelemetry { - /** - * Gets a LogRecordBuilder for creating log records. - * This is a suspend function as it may need to initialize the SDK on first call. - * - * @return A LogRecordBuilder instance for building log records - */ - suspend fun getLogger(): LogRecordBuilder - - /** - * Forces a flush of all pending log records. - * This ensures all buffered logs are exported immediately. - * - * @return A CompletableResultCode indicating the flush operation result - */ - suspend fun forceFlush(): CompletableResultCode - - /** - * Shuts down the underlying OpenTelemetry SDK, flushing pending data - * and releasing resources (exporters, logger providers, etc.). - * After this call the instance must not be reused. - */ - fun shutdown() -} - -/** - * Interface for crash-specific OpenTelemetry (local file storage). - */ -interface IOtelOpenTelemetryCrash : IOtelOpenTelemetry - -/** - * Interface for remote OpenTelemetry (network export). - */ -interface IOtelOpenTelemetryRemote : IOtelOpenTelemetry { - val logExporter: LogRecordExporter -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt deleted file mode 100644 index 98978ee19b..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.onesignal.otel - -/** - * Platform-agnostic provider interface for injecting platform-specific values. - * All Android/iOS specific values should be provided through this interface. - */ -interface IOtelPlatformProvider { - // Top-level attributes (static, calculated once) - /** - * Gets the installation ID for this device. - * This is an async operation as it may need to generate a new ID if one doesn't exist. - * - * @return The installation ID as a string - */ - suspend fun getInstallId(): String - val sdkBase: String - val sdkBaseVersion: String - val appPackageId: String - val appVersion: String - val deviceManufacturer: String - val deviceModel: String - val osName: String - val osVersion: String - val osBuildId: String - val sdkWrapper: String? - val sdkWrapperVersion: String? - - // Per-event attributes (dynamic, calculated per event) - val appId: String? - val onesignalId: String? - val pushSubscriptionId: String? - val appState: String // "foreground" or "background" - val processUptime: Long // in milliseconds - val currentThreadName: String - - // Crash-specific configuration - val crashStoragePath: String - val minFileAgeForReadMillis: Long - - // Remote logging configuration - /** - * Whether remote logging (crash reporting, ANR detection, remote log shipping) is enabled. - * Derived from the presence of a valid log_level in logging_config: - * - "logging_config": {} → false (not on allowlist) - * - "logging_config": {"log_level": "ERROR"} → true (on allowlist) - * Defaults to false on first launch (before remote config is cached). - */ - val isRemoteLoggingEnabled: Boolean - - /** - * The minimum log level to send remotely as a string (e.g., "ERROR", "WARN"). - * Null when logging_config is empty or not yet cached (disabled). - * Valid values: "NONE", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "VERBOSE" - */ - val remoteLogLevel: String? - val appIdForHeaders: String - - /** - * Base URL for the OneSignal API (e.g. "https://api.onesignal.com"). - * The Otel exporter appends "sdk/otel/v1/logs" to this. - * Sourced from the core module so all SDK traffic hits the same host. - */ - val apiBaseUrl: String -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt deleted file mode 100644 index 3e470f9422..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.onesignal.otel - -import com.onesignal.otel.attributes.OtelFieldsPerEvent -import com.onesignal.otel.attributes.OtelFieldsTopLevel -import com.onesignal.otel.config.OtelConfigCrashFile -import com.onesignal.otel.config.OtelConfigRemoteOneSignal -import com.onesignal.otel.config.OtelConfigShared -import io.opentelemetry.api.logs.LogRecordBuilder -import io.opentelemetry.sdk.OpenTelemetrySdk -import io.opentelemetry.sdk.common.CompletableResultCode -import java.util.concurrent.TimeUnit -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -internal fun LogRecordBuilder.setAllAttributes(attributes: Map): LogRecordBuilder { - attributes.forEach { this.setAttribute(it.key, it.value) } - return this -} - -/** - * Extension function to set all attributes from an Attributes object. - * Made public so it can be used from other modules (e.g., core module for logging). - */ -fun LogRecordBuilder.setAllAttributes(attributes: io.opentelemetry.api.common.Attributes): LogRecordBuilder { - attributes.forEach { key, value -> - val keyString = key.key - when (value) { - is String -> this.setAttribute(keyString, value) - is Long -> this.setAttribute(keyString, value) - is Double -> this.setAttribute(keyString, value) - is Boolean -> this.setAttribute(keyString, value) - else -> this.setAttribute(keyString, value.toString()) - } - } - return this -} - -internal abstract class OneSignalOpenTelemetryBase( - private val osTopLevelFields: OtelFieldsTopLevel, - private val osPerEventFields: OtelFieldsPerEvent, -) : IOtelOpenTelemetry { - private val lock = Any() - private var sdkCachedValue: OpenTelemetrySdk? = null - - protected suspend fun getSdk(): OpenTelemetrySdk { - val attributes = osTopLevelFields.getAttributes() - synchronized(lock) { - var localSdk = sdkCachedValue - if (localSdk != null) { - return localSdk - } - - localSdk = getSdkInstance(attributes) - sdkCachedValue = localSdk - return localSdk - } - } - - protected abstract fun getSdkInstance(attributes: Map): OpenTelemetrySdk - - override suspend fun forceFlush(): CompletableResultCode { - val sdkLoggerProvider = getSdk().sdkLoggerProvider - return suspendCoroutine { - it.resume( - sdkLoggerProvider.forceFlush().join(FORCE_FLUSH_TIMEOUT_SECONDS, TimeUnit.SECONDS) - ) - } - } - - @Suppress("TooGenericExceptionCaught") - override fun shutdown() { - synchronized(lock) { - try { - sdkCachedValue?.shutdown() - } catch (_: Throwable) { - // Best-effort cleanup — never propagate Otel teardown failures - } - sdkCachedValue = null - } - } - - companion object { - private const val FORCE_FLUSH_TIMEOUT_SECONDS = 10L - } - - override suspend fun getLogger(): LogRecordBuilder = - getSdk() - .sdkLoggerProvider - .loggerBuilder("loggerBuilder") - .build() - .logRecordBuilder() - .setAllAttributes(osPerEventFields.getAttributes()) -} - -internal class OneSignalOpenTelemetryRemote( - private val platformProvider: IOtelPlatformProvider, - osTopLevelFields: OtelFieldsTopLevel, - osPerEventFields: OtelFieldsPerEvent, -) : OneSignalOpenTelemetryBase(osTopLevelFields, osPerEventFields), - IOtelOpenTelemetryRemote { - - private val appId: String get() = platformProvider.appIdForHeaders - - val extraHttpHeaders: Map by lazy { - mapOf( - "X-OneSignal-App-Id" to appId, - "X-OneSignal-SDK-Version" to platformProvider.sdkBaseVersion, - ) - } - - private val apiBaseUrl: String get() = platformProvider.apiBaseUrl - - override val logExporter by lazy { - OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create(extraHttpHeaders, appId, apiBaseUrl) - } - - override fun getSdkInstance(attributes: Map): OpenTelemetrySdk = - OpenTelemetrySdk - .builder() - .setLoggerProvider( - OtelConfigRemoteOneSignal.SdkLoggerProviderConfig.create( - OtelConfigShared.ResourceConfig.create(attributes), - extraHttpHeaders, - appId, - apiBaseUrl, - ) - ).build() -} - -internal class OneSignalOpenTelemetryCrashLocal( - private val platformProvider: IOtelPlatformProvider, - osTopLevelFields: OtelFieldsTopLevel, - osPerEventFields: OtelFieldsPerEvent, -) : OneSignalOpenTelemetryBase(osTopLevelFields, osPerEventFields), - IOtelOpenTelemetryCrash { - override fun getSdkInstance(attributes: Map): OpenTelemetrySdk = - OpenTelemetrySdk - .builder() - .setLoggerProvider( - OtelConfigCrashFile.SdkLoggerProviderConfig.create( - OtelConfigShared.ResourceConfig.create( - attributes - ), - platformProvider.crashStoragePath, - platformProvider.minFileAgeForReadMillis, - ) - ).build() -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelFactory.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelFactory.kt deleted file mode 100644 index c4e46e6630..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelFactory.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.onesignal.otel - -import com.onesignal.otel.attributes.OtelFieldsPerEvent -import com.onesignal.otel.attributes.OtelFieldsTopLevel -import com.onesignal.otel.crash.OtelCrashHandler -import com.onesignal.otel.crash.OtelCrashReporter -import com.onesignal.otel.crash.OtelCrashUploader - -/** - * Factory class for creating Otel components. - * This allows for fast initialization of the crash handler with all dependencies - * pre-populated. - */ -object OtelFactory { - /** - * Creates a fully configured crash handler that can be initialized immediately. - * All fields are pre-populated for fast initialization. - * - * This method composes other factory methods to create the crash handler, - * ensuring consistency and reducing duplication. - */ - fun createCrashHandler( - platformProvider: IOtelPlatformProvider, - logger: IOtelLogger, - ): IOtelCrashHandler { - val crashLocal = createCrashLocalTelemetry(platformProvider) - val crashReporter = createCrashReporter(crashLocal, logger) - return OtelCrashHandler(crashReporter, logger) - } - - /** - * Creates a crash uploader for sending crash reports to the server. - * - * This is platform-agnostic and can be used in KMP projects. - * All platform-specific values must be provided through IOtelPlatformProvider. - * - * @param platformProvider Platform-specific provider that injects all required values - * @param logger Platform-specific logger implementation - * @return Platform-agnostic crash uploader that can be used on Android/iOS - */ - fun createCrashUploader( - platformProvider: IOtelPlatformProvider, - logger: IOtelLogger, - ): OtelCrashUploader { - val topLevelFields = OtelFieldsTopLevel(platformProvider) - val perEventFields = OtelFieldsPerEvent(platformProvider) - val remote = OneSignalOpenTelemetryRemote( - platformProvider, - topLevelFields, - perEventFields - ) - return OtelCrashUploader(remote, platformProvider, logger) - } - - /** - * Creates a remote OpenTelemetry instance for logging SDK events. - * - * This is platform-agnostic and can be used in KMP projects. - * All platform-specific values must be provided through IOtelPlatformProvider. - * - * @param platformProvider Platform-specific provider that injects all required values - * @return Platform-agnostic remote telemetry instance for logging - */ - fun createRemoteTelemetry( - platformProvider: IOtelPlatformProvider, - ): IOtelOpenTelemetryRemote { - val topLevelFields = OtelFieldsTopLevel(platformProvider) - val perEventFields = OtelFieldsPerEvent(platformProvider) - return OneSignalOpenTelemetryRemote( - platformProvider, - topLevelFields, - perEventFields - ) - } - - /** - * Creates a local OpenTelemetry crash instance for saving crash reports locally. - * - * This is platform-agnostic and can be used in KMP projects. - * All platform-specific values must be provided through IOtelPlatformProvider. - * - * @param platformProvider Platform-specific provider that injects all required values - * @return Platform-agnostic crash local telemetry instance - */ - fun createCrashLocalTelemetry( - platformProvider: IOtelPlatformProvider, - ): IOtelOpenTelemetryCrash { - val topLevelFields = OtelFieldsTopLevel(platformProvider) - val perEventFields = OtelFieldsPerEvent(platformProvider) - return OneSignalOpenTelemetryCrashLocal( - platformProvider, - topLevelFields, - perEventFields - ) - } - - /** - * Creates a crash reporter for saving crash reports. - * - * This is platform-agnostic and can be used in KMP projects. - * - * @param openTelemetryCrash The crash telemetry instance to use - * @param logger Platform-specific logger implementation - * @return Platform-agnostic crash reporter - */ - fun createCrashReporter( - openTelemetryCrash: IOtelOpenTelemetryCrash, - logger: IOtelLogger, - ): IOtelCrashReporter { - return OtelCrashReporter(openTelemetryCrash, logger) - } -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelLoggingHelper.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelLoggingHelper.kt deleted file mode 100644 index 8b1c85c7b0..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelLoggingHelper.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.onesignal.otel - -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.logs.Severity -import java.time.Instant - -/** - * Helper class for logging to Otel from the Logging class. - * This abstracts away OpenTelemetry-specific types so the core module - * doesn't need direct OpenTelemetry dependencies. - */ -object OtelLoggingHelper { - /** - * Logs a message to Otel remote telemetry. - * This method handles all OpenTelemetry-specific types internally. - * - * @param telemetry The Otel remote telemetry instance - * @param level The log level as a string (VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL) - * @param message The log message - * @param exceptionType Optional exception type - * @param exceptionMessage Optional exception message - * @param exceptionStacktrace Optional exception stacktrace - */ - suspend fun logToOtel( - telemetry: IOtelOpenTelemetryRemote, - level: String, - message: String, - exceptionType: String? = null, - exceptionMessage: String? = null, - exceptionStacktrace: String? = null, - ) { - val severity = when (level.uppercase()) { - "VERBOSE" -> Severity.TRACE - "DEBUG" -> Severity.DEBUG - "INFO" -> Severity.INFO - "WARN" -> Severity.WARN - "ERROR" -> Severity.ERROR - "FATAL" -> Severity.FATAL - else -> Severity.INFO - } - - val attributes = Attributes.builder() - .put("log.message", message) - .put("log.level", level) - .apply { - if (exceptionType != null) { - put("exception.type", exceptionType) - } - if (exceptionMessage != null) { - put("exception.message", exceptionMessage) - } - if (exceptionStacktrace != null) { - put("exception.stacktrace", exceptionStacktrace) - } - } - .build() - - val logRecordBuilder = telemetry.getLogger() - logRecordBuilder.setAllAttributes(attributes) - logRecordBuilder.setSeverity(severity) - logRecordBuilder.setBody(message) - logRecordBuilder.setTimestamp(Instant.now()) - logRecordBuilder.emit() - } -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/attributes/OtelFieldsPerEvent.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/attributes/OtelFieldsPerEvent.kt deleted file mode 100644 index 2d2cb20026..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/attributes/OtelFieldsPerEvent.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.onesignal.otel.attributes - -import com.onesignal.otel.IOtelPlatformProvider -import com.squareup.wire.internal.toUnmodifiableMap -import java.util.UUID - -/** - * Purpose: Fields to be included in each individual Otel event. - * These can change during runtime. - */ -internal class OtelFieldsPerEvent( - private val platformProvider: IOtelPlatformProvider, -) { - fun getAttributes(): Map { - val attributes: MutableMap = mutableMapOf() - - attributes["log.record.uid"] = recordId.toString() - - attributes - .putIfValueNotNull("ossdk.app_id", platformProvider.appId) - .putIfValueNotNull("ossdk.onesignal_id", platformProvider.onesignalId) - .putIfValueNotNull("ossdk.push_subscription_id", platformProvider.pushSubscriptionId) - - // Use platform-agnostic attribute name (works for both Android and iOS) - attributes["app.state"] = platformProvider.appState - attributes["process.uptime"] = platformProvider.processUptime.toString() - attributes["thread.name"] = platformProvider.currentThreadName - - return attributes.toUnmodifiableMap() - } - - // idempotency so the backend can filter on duplicate events - // https://opentelemetry.io/docs/specs/semconv/general/logs/#general-log-identification-attributes - private val recordId: UUID get() = UUID.randomUUID() -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/attributes/OtelFieldsTopLevel.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/attributes/OtelFieldsTopLevel.kt deleted file mode 100644 index 8021535f67..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/attributes/OtelFieldsTopLevel.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.onesignal.otel.attributes - -import com.onesignal.otel.IOtelPlatformProvider -import com.squareup.wire.internal.toUnmodifiableMap - -/** - * Purpose: Fields to be included in every Otel request that goes out. - * Requirements: Only include fields that can NOT change during runtime, - * as these are only fetched once. (Calculated fields are ok) - */ -internal class OtelFieldsTopLevel( - private val platformProvider: IOtelPlatformProvider, -) { - suspend fun getAttributes(): Map { - val attributes: MutableMap = - mutableMapOf( - "ossdk.install_id" to platformProvider.getInstallId(), - "ossdk.sdk_base" to platformProvider.sdkBase, - "ossdk.sdk_base_version" to platformProvider.sdkBaseVersion, - "ossdk.app_package_id" to platformProvider.appPackageId, - "ossdk.app_version" to platformProvider.appVersion, - "device.manufacturer" to platformProvider.deviceManufacturer, - "device.model.identifier" to platformProvider.deviceModel, - "os.name" to platformProvider.osName, - "os.version" to platformProvider.osVersion, - "os.build_id" to platformProvider.osBuildId, - ) - - attributes - .putIfValueNotNull("ossdk.sdk_wrapper", platformProvider.sdkWrapper) - .putIfValueNotNull("ossdk.sdk_wrapper_version", platformProvider.sdkWrapperVersion) - - return attributes.toUnmodifiableMap() - } -} - -internal fun MutableMap.putIfValueNotNull(key: K, value: V?): MutableMap { - if (value != null) { - this[key] = value - } - return this -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigCrashFile.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigCrashFile.kt deleted file mode 100644 index aa99748589..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigCrashFile.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.onesignal.otel.config - -import io.opentelemetry.contrib.disk.buffering.exporters.LogRecordToDiskExporter -import io.opentelemetry.contrib.disk.buffering.storage.impl.FileLogRecordStorage -import io.opentelemetry.contrib.disk.buffering.storage.impl.FileStorageConfiguration -import io.opentelemetry.sdk.logs.SdkLoggerProvider -import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor -import java.io.File -import kotlin.time.Duration.Companion.hours - -internal class OtelConfigCrashFile { - internal object SdkLoggerProviderConfig { - // NOTE: Only use such as small maxFileAgeForWrite for - // crashes, as we want to send them as soon as possible - // without having to wait too long for buffers. - private const val MAX_FILE_AGE_FOR_WRITE_MILLIS = 2_000L - - fun getFileLogRecordStorage( - rootDir: String, - minFileAgeForReadMillis: Long - ): FileLogRecordStorage = - FileLogRecordStorage.create( - File(rootDir), - FileStorageConfiguration - .builder() - .setMaxFileAgeForWriteMillis(MAX_FILE_AGE_FOR_WRITE_MILLIS) - .setMinFileAgeForReadMillis(minFileAgeForReadMillis) - .setMaxFileAgeForReadMillis(72.hours.inWholeMilliseconds) - .build() - ) - - fun create( - resource: io.opentelemetry.sdk.resources.Resource, - rootDir: String, - minFileAgeForReadMillis: Long, - ): SdkLoggerProvider { - val logToDiskExporter = - LogRecordToDiskExporter - .builder(getFileLogRecordStorage(rootDir, minFileAgeForReadMillis)) - .build() - return SdkLoggerProvider - .builder() - .setResource(resource) - .addLogRecordProcessor( - BatchLogRecordProcessor.builder(logToDiskExporter).build() - ).setLogLimits(OtelConfigShared.LogLimitsConfig::logLimits) - .build() - } - } -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt deleted file mode 100644 index b6d877dda8..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.onesignal.otel.config - -import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter -import io.opentelemetry.sdk.logs.SdkLoggerProvider -import io.opentelemetry.sdk.logs.export.LogRecordExporter -import java.time.Duration - -internal class OtelConfigRemoteOneSignal { - companion object { - const val OTEL_PATH = "sdk/otel" - - fun buildEndpoint(apiBaseUrl: String, appId: String): String = - "$apiBaseUrl$OTEL_PATH/v1/logs?app_id=$appId" - } - - object LogRecordExporterConfig { - private const val EXPORTER_TIMEOUT_SECONDS = 10L - - fun otlpHttpLogRecordExporter( - headers: Map, - endpoint: String, - ): LogRecordExporter { - val builder = OtlpHttpLogRecordExporter.builder() - headers.forEach { builder.addHeader(it.key, it.value) } - builder - .setEndpoint(endpoint) - .setTimeout(Duration.ofSeconds(EXPORTER_TIMEOUT_SECONDS)) - return builder.build() - } - } - - object SdkLoggerProviderConfig { - fun create( - resource: io.opentelemetry.sdk.resources.Resource, - extraHttpHeaders: Map, - appId: String, - apiBaseUrl: String, - ): SdkLoggerProvider = - SdkLoggerProvider - .builder() - .setResource(resource) - .addLogRecordProcessor( - OtelConfigShared.LogRecordProcessorConfig.batchLogRecordProcessor( - HttpRecordBatchExporter.create(extraHttpHeaders, appId, apiBaseUrl) - ) - ).setLogLimits(OtelConfigShared.LogLimitsConfig::logLimits) - .build() - } - - object HttpRecordBatchExporter { - fun create(extraHttpHeaders: Map, appId: String, apiBaseUrl: String) = - LogRecordExporterConfig.otlpHttpLogRecordExporter( - extraHttpHeaders, - buildEndpoint(apiBaseUrl, appId) - ) - } -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigShared.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigShared.kt deleted file mode 100644 index f54b3d5590..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigShared.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.onesignal.otel.config - -import io.opentelemetry.sdk.logs.LogLimits -import io.opentelemetry.sdk.logs.LogRecordProcessor -import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor -import io.opentelemetry.sdk.logs.export.LogRecordExporter -import io.opentelemetry.sdk.resources.Resource -import io.opentelemetry.sdk.resources.ResourceBuilder -import io.opentelemetry.semconv.ServiceAttributes -import java.time.Duration - -internal fun ResourceBuilder.putAll(attributes: Map): ResourceBuilder { - attributes.forEach { this.put(it.key, it.value) } - return this -} - -internal class OtelConfigShared { - object ResourceConfig { - fun create(attributes: Map): Resource = - Resource - .getDefault() - .toBuilder() - .put(ServiceAttributes.SERVICE_NAME, "OneSignalDeviceSDK") - .putAll(attributes) - .build() - } - - object LogRecordProcessorConfig { - private const val MAX_QUEUE_SIZE = 100 - private const val MAX_EXPORT_BATCH_SIZE = 100 - private const val EXPORTER_TIMEOUT_SECONDS = 30L - private const val SCHEDULE_DELAY_SECONDS = 1L - - fun batchLogRecordProcessor(logRecordExporter: LogRecordExporter): LogRecordProcessor = - BatchLogRecordProcessor - .builder(logRecordExporter) - .setMaxQueueSize(MAX_QUEUE_SIZE) - .setMaxExportBatchSize(MAX_EXPORT_BATCH_SIZE) - .setExporterTimeout(Duration.ofSeconds(EXPORTER_TIMEOUT_SECONDS)) - .setScheduleDelay(Duration.ofSeconds(SCHEDULE_DELAY_SECONDS)) - .build() - } - - object LogLimitsConfig { - private const val MAX_NUMBER_OF_ATTRIBUTES = 128 - - // We want a high value max length as the exception.stacktrace - // value can be lengthly. - private const val MAX_ATTRIBUTE_VALUE_LENGTH = 32000 - - fun logLimits(): LogLimits = - LogLimits - .builder() - .setMaxNumberOfAttributes(MAX_NUMBER_OF_ATTRIBUTES) - .setMaxAttributeValueLength(MAX_ATTRIBUTE_VALUE_LENGTH) - .build() - } -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/IOtelAnrDetector.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/IOtelAnrDetector.kt deleted file mode 100644 index b7b5027ee8..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/IOtelAnrDetector.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.onesignal.otel.crash - -/** - * Platform-agnostic interface for ANR (Application Not Responding) detection. - * - * ANRs occur when the main thread is blocked for too long (typically >5 seconds on Android). - * Unlike crashes, ANRs don't throw exceptions - they're detected by monitoring thread responsiveness. - */ -interface IOtelAnrDetector { - /** - * Starts monitoring for ANRs. - * This should be called early in the app lifecycle, ideally right after crash handler initialization. - */ - fun start() - - /** - * Stops monitoring for ANRs. - * Should be called when the app is shutting down or when monitoring is no longer needed. - */ - fun stop() -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt deleted file mode 100644 index 9581c069ea..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.onesignal.otel.crash - -import com.onesignal.otel.IOtelCrashReporter -import com.onesignal.otel.IOtelLogger -import kotlinx.coroutines.runBlocking - -/** - * Purpose: Writes any crashes involving OneSignal to a file where they can - * later be send to OneSignal to help improve reliability. - * NOTE: For future refactors, code is written assuming this is a singleton - * - * This should be initialized as early as possible, before any other initialization - * that might crash. All fields must be pre-populated before initialization. - */ -internal class OtelCrashHandler( - private val crashReporter: IOtelCrashReporter, - private val logger: IOtelLogger, -) : Thread.UncaughtExceptionHandler, com.onesignal.otel.IOtelCrashHandler { - private var existingHandler: Thread.UncaughtExceptionHandler? = null - private val seenThrowables: MutableList = mutableListOf() - - @Volatile - private var initialized = false - - override fun initialize() { - if (initialized) { - logger.warn("OtelCrashHandler already initialized, skipping") - return - } - logger.info("OtelCrashHandler: Setting up uncaught exception handler...") - existingHandler = Thread.getDefaultUncaughtExceptionHandler() - Thread.setDefaultUncaughtExceptionHandler(this) - initialized = true - logger.info("OtelCrashHandler: ✅ Successfully initialized and registered as default uncaught exception handler") - } - - override fun unregister() { - if (!initialized) { - logger.debug("OtelCrashHandler: Not initialized, nothing to unregister") - return - } - logger.info("OtelCrashHandler: Unregistering — restoring previous exception handler") - Thread.setDefaultUncaughtExceptionHandler(existingHandler) - existingHandler = null - initialized = false - } - - override fun uncaughtException(thread: Thread, throwable: Throwable) { - // Ensure we never attempt to process the same throwable instance - // more than once. This would only happen if there was another crash - // handler and was faulty in a specific way. - synchronized(seenThrowables) { - if (seenThrowables.contains(throwable)) { - logger.warn("OtelCrashHandler: Ignoring duplicate throwable instance") - return - } - seenThrowables.add(throwable) - } - - logger.info("OtelCrashHandler: Uncaught exception detected - ${throwable.javaClass.simpleName}: ${throwable.message}") - - // Check if this is an ANR exception (though standalone ANR detector already handles ANRs) - // This would only catch ANRs if they're thrown as exceptions, which is rare - val isAnr = throwable.javaClass.simpleName.contains("ApplicationNotResponding", ignoreCase = true) || - throwable.message?.contains("Application Not Responding", ignoreCase = true) == true - - // NOTE: Future improvements: - // - Catch anything we may throw and print only to logcat - // - Send a stop command to OneSignalCrashUploader, give a bit of time to finish - // and then call existingHandler. This way the app doesn't have to open a 2nd - // time to get the crash report and should help prevent duplicated reports. - // NOTE: ANRs are typically detected by the standalone OtelAnrDetector, which only - // reports OneSignal-related ANRs. This handler would only catch ANRs if they're - // thrown as exceptions (unlikely), and we still check if OneSignal is at fault. - if (!isAnr && !isOneSignalAtFault(throwable)) { - logger.debug("OtelCrashHandler: Crash is not OneSignal-related, delegating to existing handler") - existingHandler?.uncaughtException(thread, throwable) - return - } - - if (isAnr) { - logger.info("OtelCrashHandler: ANR exception caught (unusual - ANRs are usually detected by standalone detector)") - } - - logger.info("OtelCrashHandler: OneSignal-related crash detected, saving crash report...") - - /** - * NOTE: The order and running sequentially is important as: - * The existingHandler.uncaughtException can immediately terminate the - * process, either directly (if this is Android's - * KillApplicationHandler) OR the app's handler / 3rd party SDK (either - * directly or more likely, by it calling Android's - * KillApplicationHandler). - * Given this, we can't parallelize the existingHandler work with ours. - * The safest thing is to try to finish our work as fast as possible - * (including ensuring our logging write buffers are flushed) then call - * the existingHandler so any crash handlers the app also has gets the - * crash even too. - * - * NOTE: addShutdownHook() isn't a workaround as it doesn't fire for - * Process.killProcess, which KillApplicationHandler calls. - */ - try { - runBlocking { crashReporter.saveCrash(thread, throwable) } - logger.info("OtelCrashHandler: Crash report saved successfully") - } catch (t: Throwable) { - logger.error("OtelCrashHandler: Failed to save crash report: ${t.message} - ${t.javaClass.simpleName}") - } - logger.info("OtelCrashHandler: Delegating to existing crash handler") - existingHandler?.uncaughtException(thread, throwable) - } -} - -/** - * Checks if a throwable's stack trace indicates OneSignal is at fault. - * Centralized logic used by both crash handler and ANR detector. - */ -internal fun isOneSignalAtFault(throwable: Throwable): Boolean = - isOneSignalAtFault(throwable.stackTrace) - -/** - * Helper function to check if a stack trace indicates OneSignal is at fault. - * Centralized logic used by both crash handler and ANR detector. - * Made public so it can be accessed from core module. - */ -fun isOneSignalAtFault(stackTrace: Array): Boolean = - stackTrace.any { it.className.startsWith("com.onesignal") } diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashReporter.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashReporter.kt deleted file mode 100644 index b875f906cb..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashReporter.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.onesignal.otel.crash - -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryCrash -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.logs.Severity -import java.time.Instant - -internal class OtelCrashReporter( - private val openTelemetry: IOtelOpenTelemetryCrash, - private val logger: IOtelLogger, -) : com.onesignal.otel.IOtelCrashReporter { - companion object { - private const val OTEL_EXCEPTION_TYPE = "exception.type" - private const val OTEL_EXCEPTION_MESSAGE = "exception.message" - private const val OTEL_EXCEPTION_STACKTRACE = "exception.stacktrace" - private const val OTEL_EXCEPTION_THREAD_NAME = "ossdk.exception.thread.name" - } - - override suspend fun saveCrash(thread: Thread, throwable: Throwable) { - try { - logger.info("OtelCrashReporter: Starting to save crash report for ${throwable.javaClass.simpleName}") - - val attributes = - Attributes - .builder() - .put(OTEL_EXCEPTION_MESSAGE, throwable.message ?: "") - .put(OTEL_EXCEPTION_STACKTRACE, throwable.stackTraceToString()) - .put(OTEL_EXCEPTION_TYPE, throwable.javaClass.name) - // This matches the top level thread.name today, but it may not - // always if things are refactored to use a different thread. - .put(OTEL_EXCEPTION_THREAD_NAME, thread.name) - .build() - - logger.debug("OtelCrashReporter: Creating log record with attributes...") - openTelemetry - .getLogger() - .setAllAttributes(attributes) - .setSeverity(Severity.FATAL) - .setTimestamp(Instant.now()) - .emit() - - logger.debug("OtelCrashReporter: Flushing crash report to disk...") - openTelemetry.forceFlush() - - // Note: forceFlush() returns CompletableResultCode which is async - // We wait for it in the implementation, so if we get here, it succeeded - logger.info("OtelCrashReporter: ✅ Crash report saved and flushed successfully to disk") - } catch (e: RuntimeException) { - // If we fail to log the crash, at least try to log the failure - logger.error("OtelCrashReporter: Failed to save crash report: ${e.message} - ${e.javaClass.simpleName}") - throw e // Re-throw so caller knows it failed - } catch (e: java.io.IOException) { - // Handle IO errors specifically - logger.error("OtelCrashReporter: IO error saving crash report: ${e.message}") - throw e - } catch (e: IllegalStateException) { - // Handle illegal state errors - logger.error("OtelCrashReporter: Illegal state error saving crash report: ${e.message}") - throw e - } - } -} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashUploader.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashUploader.kt deleted file mode 100644 index d9091b8a32..0000000000 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashUploader.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.onesignal.otel.crash - -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryRemote -import com.onesignal.otel.IOtelPlatformProvider -import com.onesignal.otel.config.OtelConfigCrashFile -import io.opentelemetry.sdk.logs.data.LogRecordData -import kotlinx.coroutines.delay -import java.util.concurrent.TimeUnit - -/** - * Purpose: This reads a local crash report files created by OneSignal's - * crash handler and sends them to OneSignal on the app's next start. - * - * This is fully platform-agnostic and can be used in KMP projects. - * All platform-specific values are injected through IOtelPlatformProvider. - * - * Dependencies (all platform-agnostic): - * - IOtelOpenTelemetryRemote: For network export (created via OtelFactory) - * - IOtelPlatformProvider: Injects all platform values (Android/iOS) - * - IOtelLogger: Platform logging interface (Android/iOS) - * - * Usage: - * ```kotlin - * val uploader = OtelFactory.createCrashUploader(platformProvider, logger) - * coroutineScope.launch { - * uploader.start() - * } - * ``` - */ -class OtelCrashUploader( - private val openTelemetryRemote: IOtelOpenTelemetryRemote, - private val platformProvider: IOtelPlatformProvider, - private val logger: IOtelLogger, -) { - companion object { - const val SEND_TIMEOUT_SECONDS = 30L - } - - private fun getReports() = - OtelConfigCrashFile.SdkLoggerProviderConfig - .getFileLogRecordStorage( - platformProvider.crashStoragePath, - platformProvider.minFileAgeForReadMillis - ).iterator() - - /** - * Starts the crash uploader process. - * This will periodically check for crash reports on disk and upload them to OneSignal. - * If remote logging is disabled (NONE level), this function returns immediately without doing anything. - */ - suspend fun start() { - val remoteLogLevel = platformProvider.remoteLogLevel - if (remoteLogLevel == null || remoteLogLevel == "NONE") { - logger.info("OtelCrashUploader: remote logging disabled (level: $remoteLogLevel)") - return - } - - logger.info("OtelCrashUploader: starting") - internalStart() - } - - /** - * NOTE: sendCrashReports is called twice for the these reasons: - * 1. We want to send crash reports as soon as possible. - * - App may crash quickly after starting a 2nd time. - * 2. Reports could be delayed until the 2nd start after a crash - * - Otel doesn't let you read a file it could be writing so we must - * wait a minium amount of time after a crash to ensure we get the - * report from the last crash. - */ - suspend fun internalStart() { - sendCrashReports(getReports()) - delay(platformProvider.minFileAgeForReadMillis) - sendCrashReports(getReports()) - } - - private fun sendCrashReports(reports: Iterator>) { - val networkExporter = openTelemetryRemote.logExporter - var failed = false - // NOTE: next() will delete the previous report, so we only want to send - // another one if there isn't an issue making network calls. - while (reports.hasNext() && !failed) { - val future = networkExporter.export(reports.next()) - logger.debug("Sending OneSignal crash report") - val result = future.join(SEND_TIMEOUT_SECONDS, TimeUnit.SECONDS) - failed = !result.isSuccess - logger.debug("Done OneSignal crash report, failed: $failed") - } - } -} diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt deleted file mode 100644 index 5bc57abf30..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt +++ /dev/null @@ -1,181 +0,0 @@ -package com.onesignal.otel - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.logs.LogRecordBuilder -import kotlinx.coroutines.runBlocking - -class OneSignalOpenTelemetryTest : FunSpec({ - val mockPlatformProvider = mockk(relaxed = true) - - fun setupDefaultMocks() { - coEvery { mockPlatformProvider.getInstallId() } returns "test-install-id" - every { mockPlatformProvider.sdkBase } returns "android" - every { mockPlatformProvider.sdkBaseVersion } returns "5.0.0" - every { mockPlatformProvider.appPackageId } returns "com.test.app" - every { mockPlatformProvider.appVersion } returns "1.0.0" - every { mockPlatformProvider.deviceManufacturer } returns "TestManufacturer" - every { mockPlatformProvider.deviceModel } returns "TestModel" - every { mockPlatformProvider.osName } returns "Android" - every { mockPlatformProvider.osVersion } returns "13" - every { mockPlatformProvider.osBuildId } returns "TEST123" - every { mockPlatformProvider.sdkWrapper } returns null - every { mockPlatformProvider.sdkWrapperVersion } returns null - every { mockPlatformProvider.appId } returns "test-app-id" - every { mockPlatformProvider.appIdForHeaders } returns "test-app-id" - every { mockPlatformProvider.onesignalId } returns "test-onesignal-id" - every { mockPlatformProvider.pushSubscriptionId } returns "test-subscription-id" - every { mockPlatformProvider.appState } returns "foreground" - every { mockPlatformProvider.processUptime } returns 100L - every { mockPlatformProvider.currentThreadName } returns "main" - every { mockPlatformProvider.crashStoragePath } returns "/test/path" - every { mockPlatformProvider.minFileAgeForReadMillis } returns 5000L - every { mockPlatformProvider.remoteLogLevel } returns "ERROR" - every { mockPlatformProvider.apiBaseUrl } returns "https://api.onesignal.com" - } - - beforeEach { - clearMocks(mockPlatformProvider) - setupDefaultMocks() - } - - // ===== Remote Telemetry Tests ===== - - test("createRemoteTelemetry should return IOtelOpenTelemetryRemote") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - remoteTelemetry.shouldBeInstanceOf() - } - - test("remote telemetry should have logExporter") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - remoteTelemetry.logExporter shouldNotBe null - } - - test("remote telemetry getLogger should return LogRecordBuilder") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - runBlocking { - val logger = remoteTelemetry.getLogger() - logger.shouldBeInstanceOf() - } - } - - test("remote telemetry forceFlush should not throw") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - runBlocking { - // Should not throw - remoteTelemetry.forceFlush() - } - } - - // ===== Crash Local Telemetry Tests ===== - - test("createCrashLocalTelemetry should return IOtelOpenTelemetryCrash") { - // Use temp directory for crash storage - val tempDir = System.getProperty("java.io.tmpdir") + "/otel-test-" + System.currentTimeMillis() - java.io.File(tempDir).mkdirs() - every { mockPlatformProvider.crashStoragePath } returns tempDir - - try { - val crashTelemetry = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) - - crashTelemetry.shouldBeInstanceOf() - } finally { - java.io.File(tempDir).deleteRecursively() - } - } - - test("crash telemetry getLogger should return LogRecordBuilder") { - val tempDir = System.getProperty("java.io.tmpdir") + "/otel-test-" + System.currentTimeMillis() - java.io.File(tempDir).mkdirs() - every { mockPlatformProvider.crashStoragePath } returns tempDir - - try { - val crashTelemetry = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) - - runBlocking { - val logger = crashTelemetry.getLogger() - logger.shouldBeInstanceOf() - } - } finally { - java.io.File(tempDir).deleteRecursively() - } - } - - // ===== LogRecordBuilder Extension Tests ===== - - test("setAllAttributes with Map should set all string attributes") { - val mockBuilder = mockk(relaxed = true) - val attributes = mapOf( - "key1" to "value1", - "key2" to "value2" - ) - - mockBuilder.setAllAttributes(attributes) - - io.mockk.verify { mockBuilder.setAttribute("key1", "value1") } - io.mockk.verify { mockBuilder.setAttribute("key2", "value2") } - } - - test("setAllAttributes with Attributes should handle different types") { - val mockBuilder = mockk(relaxed = true) - val attributes = Attributes.builder() - .put("string.key", "string-value") - .put("long.key", 123L) - .put("double.key", 45.67) - .put("boolean.key", true) - .build() - - mockBuilder.setAllAttributes(attributes) - - io.mockk.verify { mockBuilder.setAttribute("string.key", "string-value") } - io.mockk.verify { mockBuilder.setAttribute("long.key", 123L) } - io.mockk.verify { mockBuilder.setAttribute("double.key", 45.67) } - io.mockk.verify { mockBuilder.setAttribute("boolean.key", true) } - } - - // ===== SDK Caching Tests ===== - - test("remote telemetry should cache SDK instance") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - runBlocking { - val logger1 = remoteTelemetry.getLogger() - val logger2 = remoteTelemetry.getLogger() - - // Both calls should succeed (SDK is cached internally) - logger1 shouldNotBe null - logger2 shouldNotBe null - } - } - - // ===== Integration with Factory Tests ===== - - test("factory should create independent instances") { - val remote1 = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - val remote2 = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - remote1 shouldNotBe remote2 - } - - test("factory should work with null optional fields") { - every { mockPlatformProvider.appId } returns null - every { mockPlatformProvider.onesignalId } returns null - every { mockPlatformProvider.pushSubscriptionId } returns null - every { mockPlatformProvider.sdkWrapper } returns null - every { mockPlatformProvider.sdkWrapperVersion } returns null - - // Should not throw - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - remoteTelemetry shouldNotBe null - } -}) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelFactoryTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelFactoryTest.kt deleted file mode 100644 index 56f2ce5cc4..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelFactoryTest.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.onesignal.otel - -import com.onesignal.otel.crash.OtelCrashUploader -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk - -class OtelFactoryTest : FunSpec({ - val mockPlatformProvider = mockk(relaxed = true) - val mockLogger = mockk(relaxed = true) - - beforeEach { - // Setup default values - every { mockPlatformProvider.sdkBase } returns "android" - every { mockPlatformProvider.sdkBaseVersion } returns "1.0.0" - every { mockPlatformProvider.appPackageId } returns "com.test.app" - every { mockPlatformProvider.appVersion } returns "1.0" - every { mockPlatformProvider.deviceManufacturer } returns "Test" - every { mockPlatformProvider.deviceModel } returns "TestDevice" - every { mockPlatformProvider.osName } returns "Android" - every { mockPlatformProvider.osVersion } returns "10" - every { mockPlatformProvider.osBuildId } returns "TEST123" - every { mockPlatformProvider.sdkWrapper } returns null - every { mockPlatformProvider.sdkWrapperVersion } returns null - every { mockPlatformProvider.appId } returns null - every { mockPlatformProvider.onesignalId } returns null - every { mockPlatformProvider.pushSubscriptionId } returns null - every { mockPlatformProvider.appState } returns "foreground" - every { mockPlatformProvider.processUptime } returns 100L - every { mockPlatformProvider.currentThreadName } returns "main" - every { mockPlatformProvider.crashStoragePath } returns "/test/path" - every { mockPlatformProvider.minFileAgeForReadMillis } returns 5000L - every { mockPlatformProvider.remoteLogLevel } returns "ERROR" - every { mockPlatformProvider.appIdForHeaders } returns "test-app-id" - every { mockPlatformProvider.apiBaseUrl } returns "https://api.onesignal.com" - coEvery { mockPlatformProvider.getInstallId() } returns "test-install-id" - } - - // ===== createCrashHandler Tests ===== - - test("createCrashHandler should return IOtelCrashHandler") { - // When - val handler = OtelFactory.createCrashHandler(mockPlatformProvider, mockLogger) - - // Then - handler.shouldBeInstanceOf() - } - - test("createCrashHandler should create handler with correct dependencies") { - // When - val handler = OtelFactory.createCrashHandler(mockPlatformProvider, mockLogger) - - // Then - handler shouldNotBe null - // Handler should be initializable - handler.initialize() - } - - test("createCrashHandler should create handler that can be initialized multiple times") { - // Given - val handler = OtelFactory.createCrashHandler(mockPlatformProvider, mockLogger) - - // When - handler.initialize() - handler.initialize() // Should not throw - - // Then - no exception thrown - } - - // ===== createCrashUploader Tests ===== - - test("createCrashUploader should return OtelCrashUploader") { - // When - val uploader = OtelFactory.createCrashUploader(mockPlatformProvider, mockLogger) - - // Then - uploader shouldNotBe null - uploader.shouldBeInstanceOf() - } - - test("createCrashUploader should create uploader with correct dependencies") { - // When - val uploader = OtelFactory.createCrashUploader(mockPlatformProvider, mockLogger) - - // Then - uploader shouldNotBe null - } - - // ===== createRemoteTelemetry Tests ===== - - test("createRemoteTelemetry should return IOtelOpenTelemetryRemote") { - // When - val telemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - // Then - telemetry shouldNotBe null - telemetry.shouldBeInstanceOf() - } - - test("createRemoteTelemetry should have logExporter") { - // When - val telemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - // Then - telemetry.logExporter shouldNotBe null - } - - // ===== createCrashLocalTelemetry Tests ===== - - test("createCrashLocalTelemetry should return IOtelOpenTelemetryCrash") { - // When - val telemetry = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) - - // Then - telemetry shouldNotBe null - telemetry.shouldBeInstanceOf() - } - - test("createCrashLocalTelemetry should be different instance from remote") { - // When - val localTelemetry = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - - // Then - localTelemetry shouldNotBe remoteTelemetry - } - - // ===== createCrashReporter Tests ===== - - test("createCrashReporter should return IOtelCrashReporter") { - // Given - val crashTelemetry = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) - - // When - val reporter = OtelFactory.createCrashReporter(crashTelemetry, mockLogger) - - // Then - reporter shouldNotBe null - reporter.shouldBeInstanceOf() - } - - test("createCrashReporter should work with different telemetry instances") { - // Given - val crashTelemetry1 = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) - val crashTelemetry2 = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) - - // When - val reporter1 = OtelFactory.createCrashReporter(crashTelemetry1, mockLogger) - val reporter2 = OtelFactory.createCrashReporter(crashTelemetry2, mockLogger) - - // Then - reporter1 shouldNotBe null - reporter2 shouldNotBe null - reporter1 shouldNotBe reporter2 - } - - // ===== Integration Tests ===== - - test("createCrashHandler uses platform provider values correctly") { - // Given - every { mockPlatformProvider.appId } returns "test-app-id" - every { mockPlatformProvider.onesignalId } returns "test-onesignal-id" - - // When - val handler = OtelFactory.createCrashHandler(mockPlatformProvider, mockLogger) - - // Then - handler shouldNotBe null - handler.initialize() // Should work with provided values - } - - test("createCrashUploader uses platform provider values correctly") { - // Given - every { mockPlatformProvider.appId } returns "test-app-id" - every { mockPlatformProvider.crashStoragePath } returns "/custom/path" - - // When - val uploader = OtelFactory.createCrashUploader(mockPlatformProvider, mockLogger) - - // Then - uploader shouldNotBe null - } - - test("all factory methods work with null appId") { - // Given - every { mockPlatformProvider.appId } returns null - - // When & Then - should not throw - val handler = OtelFactory.createCrashHandler(mockPlatformProvider, mockLogger) - handler shouldNotBe null - - val uploader = OtelFactory.createCrashUploader(mockPlatformProvider, mockLogger) - uploader shouldNotBe null - - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) - remoteTelemetry shouldNotBe null - - val localTelemetry = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) - localTelemetry shouldNotBe null - } -}) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelLoggingHelperTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelLoggingHelperTest.kt deleted file mode 100644 index 16b195754e..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelLoggingHelperTest.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.onesignal.otel - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import io.opentelemetry.api.logs.LogRecordBuilder -import io.opentelemetry.api.logs.Severity -import kotlinx.coroutines.runBlocking - -class OtelLoggingHelperTest : FunSpec({ - val mockTelemetry = mockk(relaxed = true) - val mockLogRecordBuilder = mockk(relaxed = true) - - beforeEach { - coEvery { mockTelemetry.getLogger() } returns mockLogRecordBuilder - } - - test("logToOtel should set correct severity for VERBOSE level") { - val severitySlot = slot() - every { mockLogRecordBuilder.setSeverity(capture(severitySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "VERBOSE", "test message") - } - - severitySlot.captured shouldBe Severity.TRACE - } - - test("logToOtel should set correct severity for DEBUG level") { - val severitySlot = slot() - every { mockLogRecordBuilder.setSeverity(capture(severitySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "DEBUG", "test message") - } - - severitySlot.captured shouldBe Severity.DEBUG - } - - test("logToOtel should set correct severity for INFO level") { - val severitySlot = slot() - every { mockLogRecordBuilder.setSeverity(capture(severitySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "INFO", "test message") - } - - severitySlot.captured shouldBe Severity.INFO - } - - test("logToOtel should set correct severity for WARN level") { - val severitySlot = slot() - every { mockLogRecordBuilder.setSeverity(capture(severitySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "WARN", "test message") - } - - severitySlot.captured shouldBe Severity.WARN - } - - test("logToOtel should set correct severity for ERROR level") { - val severitySlot = slot() - every { mockLogRecordBuilder.setSeverity(capture(severitySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "ERROR", "test message") - } - - severitySlot.captured shouldBe Severity.ERROR - } - - test("logToOtel should set correct severity for FATAL level") { - val severitySlot = slot() - every { mockLogRecordBuilder.setSeverity(capture(severitySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "FATAL", "test message") - } - - severitySlot.captured shouldBe Severity.FATAL - } - - test("logToOtel should default to INFO for unknown level") { - val severitySlot = slot() - every { mockLogRecordBuilder.setSeverity(capture(severitySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "UNKNOWN", "test message") - } - - severitySlot.captured shouldBe Severity.INFO - } - - test("logToOtel should set body with message") { - val bodySlot = slot() - every { mockLogRecordBuilder.setBody(capture(bodySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "INFO", "my test message") - } - - bodySlot.captured shouldBe "my test message" - } - - test("logToOtel should emit the log record") { - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "INFO", "test message") - } - - verify { mockLogRecordBuilder.emit() } - } - - test("logToOtel should include exception attributes when provided") { - runBlocking { - OtelLoggingHelper.logToOtel( - telemetry = mockTelemetry, - level = "ERROR", - message = "error occurred", - exceptionType = "java.lang.RuntimeException", - exceptionMessage = "something went wrong", - exceptionStacktrace = "at com.test.Class.method(Class.kt:10)" - ) - } - - coVerify { mockTelemetry.getLogger() } - verify { mockLogRecordBuilder.emit() } - } - - test("logToOtel should handle case-insensitive log levels") { - val severitySlot = slot() - every { mockLogRecordBuilder.setSeverity(capture(severitySlot)) } returns mockLogRecordBuilder - - runBlocking { - OtelLoggingHelper.logToOtel(mockTelemetry, "error", "test message") - } - - severitySlot.captured shouldBe Severity.ERROR - } -}) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt deleted file mode 100644 index 6fec492830..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.onesignal.otel.attributes - -import com.onesignal.otel.IOtelPlatformProvider -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.collections.shouldContain -import io.kotest.matchers.collections.shouldNotContain -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.mockk.clearMocks -import io.mockk.every -import io.mockk.mockk - -class OtelFieldsPerEventTest : FunSpec({ - val mockPlatformProvider = mockk(relaxed = true) - val fields = OtelFieldsPerEvent(mockPlatformProvider) - - fun setupDefaultMocks( - appId: String? = "test-app-id", - onesignalId: String? = "test-onesignal-id", - pushSubscriptionId: String? = "test-subscription-id", - appState: String = "foreground", - processUptime: Long = 100, - threadName: String = "main-thread" - ) { - every { mockPlatformProvider.appId } returns appId - every { mockPlatformProvider.onesignalId } returns onesignalId - every { mockPlatformProvider.pushSubscriptionId } returns pushSubscriptionId - every { mockPlatformProvider.appState } returns appState - every { mockPlatformProvider.processUptime } returns processUptime - every { mockPlatformProvider.currentThreadName } returns threadName - } - - beforeEach { clearMocks(mockPlatformProvider) } - - test("getAttributes should include all per-event fields when all values present") { - setupDefaultMocks() - - val attributes = fields.getAttributes() - - attributes.keys shouldContain "log.record.uid" - attributes["log.record.uid"] shouldNotBe null - attributes["ossdk.app_id"] shouldBe "test-app-id" - attributes["ossdk.onesignal_id"] shouldBe "test-onesignal-id" - attributes["ossdk.push_subscription_id"] shouldBe "test-subscription-id" - attributes["app.state"] shouldBe "foreground" - attributes["process.uptime"] shouldBe "100" - attributes["thread.name"] shouldBe "main-thread" - } - - test("getAttributes should exclude null optional fields") { - setupDefaultMocks(appId = null, onesignalId = null, pushSubscriptionId = null, appState = "background") - - val attributes = fields.getAttributes() - - attributes.keys shouldNotContain "ossdk.app_id" - attributes.keys shouldNotContain "ossdk.onesignal_id" - attributes.keys shouldNotContain "ossdk.push_subscription_id" - attributes["app.state"] shouldBe "background" - } - - test("getAttributes should generate unique record IDs on each call") { - setupDefaultMocks() - - val uid1 = fields.getAttributes()["log.record.uid"] - val uid2 = fields.getAttributes()["log.record.uid"] - - uid1 shouldNotBe uid2 - } -}) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsTopLevelTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsTopLevelTest.kt deleted file mode 100644 index 6f6463475b..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsTopLevelTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.onesignal.otel.attributes - -import com.onesignal.otel.IOtelPlatformProvider -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.collections.shouldNotContain -import io.kotest.matchers.shouldBe -import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.runBlocking - -class OtelFieldsTopLevelTest : FunSpec({ - val mockPlatformProvider = mockk(relaxed = true) - val fields = OtelFieldsTopLevel(mockPlatformProvider) - - fun setupDefaultMocks( - installId: String = "test-install-id", - sdkWrapper: String? = null, - sdkWrapperVersion: String? = null - ) { - coEvery { mockPlatformProvider.getInstallId() } returns installId - every { mockPlatformProvider.sdkBase } returns "android" - every { mockPlatformProvider.sdkBaseVersion } returns "1.0.0" - every { mockPlatformProvider.appPackageId } returns "com.test.app" - every { mockPlatformProvider.appVersion } returns "1.0" - every { mockPlatformProvider.deviceManufacturer } returns "TestManufacturer" - every { mockPlatformProvider.deviceModel } returns "TestModel" - every { mockPlatformProvider.osName } returns "Android" - every { mockPlatformProvider.osVersion } returns "10" - every { mockPlatformProvider.osBuildId } returns "TEST123" - every { mockPlatformProvider.sdkWrapper } returns sdkWrapper - every { mockPlatformProvider.sdkWrapperVersion } returns sdkWrapperVersion - } - - beforeEach { clearMocks(mockPlatformProvider) } - - test("getAttributes should include all required top-level fields") { - setupDefaultMocks() - - runBlocking { - val attributes = fields.getAttributes() - - attributes["ossdk.install_id"] shouldBe "test-install-id" - attributes["ossdk.sdk_base"] shouldBe "android" - attributes["ossdk.sdk_base_version"] shouldBe "1.0.0" - attributes["ossdk.app_package_id"] shouldBe "com.test.app" - attributes["ossdk.app_version"] shouldBe "1.0" - attributes["device.manufacturer"] shouldBe "TestManufacturer" - attributes["device.model.identifier"] shouldBe "TestModel" - attributes["os.name"] shouldBe "Android" - attributes["os.version"] shouldBe "10" - attributes["os.build_id"] shouldBe "TEST123" - } - } - - test("getAttributes should include wrapper fields when present") { - setupDefaultMocks(sdkWrapper = "unity", sdkWrapperVersion = "2.0.0") - - runBlocking { - val attributes = fields.getAttributes() - - attributes["ossdk.sdk_wrapper"] shouldBe "unity" - attributes["ossdk.sdk_wrapper_version"] shouldBe "2.0.0" - } - } - - test("getAttributes should exclude null wrapper fields") { - setupDefaultMocks(sdkWrapper = null, sdkWrapperVersion = null) - - runBlocking { - val attributes = fields.getAttributes() - - attributes.keys shouldNotContain "ossdk.sdk_wrapper" - attributes.keys shouldNotContain "ossdk.sdk_wrapper_version" - } - } -}) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/config/OtelConfigTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/config/OtelConfigTest.kt deleted file mode 100644 index f4f8daaf18..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/config/OtelConfigTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.onesignal.otel.config - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.opentelemetry.semconv.ServiceAttributes - -class OtelConfigTest : FunSpec({ - - // ===== OtelConfigShared.ResourceConfig Tests ===== - - test("ResourceConfig should create resource with service name") { - val resource = OtelConfigShared.ResourceConfig.create(emptyMap()) - - resource.attributes.get(ServiceAttributes.SERVICE_NAME) shouldBe "OneSignalDeviceSDK" - } - - test("ResourceConfig should include custom attributes") { - val customAttributes = mapOf( - "custom.key1" to "value1", - "custom.key2" to "value2" - ) - - val resource = OtelConfigShared.ResourceConfig.create(customAttributes) - - resource.attributes.get(ServiceAttributes.SERVICE_NAME) shouldBe "OneSignalDeviceSDK" - resource.attributes.asMap().entries.any { it.key.key == "custom.key1" } shouldBe true - resource.attributes.asMap().entries.any { it.key.key == "custom.key2" } shouldBe true - } - - test("ResourceConfig should handle empty attributes map") { - val resource = OtelConfigShared.ResourceConfig.create(emptyMap()) - - resource shouldNotBe null - resource.attributes.get(ServiceAttributes.SERVICE_NAME) shouldBe "OneSignalDeviceSDK" - } - - // ===== OtelConfigShared.LogLimitsConfig Tests ===== - - test("LogLimitsConfig should create valid log limits") { - val logLimits = OtelConfigShared.LogLimitsConfig.logLimits() - - logLimits shouldNotBe null - logLimits.maxNumberOfAttributes shouldBe 128 - logLimits.maxAttributeValueLength shouldBe 32000 - } - - // ===== OtelConfigShared.LogRecordProcessorConfig Tests ===== - - test("LogRecordProcessorConfig should create batch processor") { - val mockExporter = io.mockk.mockk(relaxed = true) - - val processor = OtelConfigShared.LogRecordProcessorConfig.batchLogRecordProcessor(mockExporter) - - processor shouldNotBe null - } - - // ===== OtelConfigRemoteOneSignal Tests ===== - - test("buildEndpoint should construct correct URL from base and appId") { - val endpoint = OtelConfigRemoteOneSignal.buildEndpoint("https://api.onesignal.com", "my-app") - endpoint shouldBe "https://api.onesignal.com/sdk/otel/v1/logs?app_id=my-app" - } - - test("HttpRecordBatchExporter should create exporter with correct endpoint") { - val headers = mapOf("X-Test-Header" to "test-value") - val appId = "test-app-id" - val apiBaseUrl = "https://api.onesignal.com" - - val exporter = OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create(headers, appId, apiBaseUrl) - - exporter shouldNotBe null - } - - test("LogRecordExporterConfig should create OTLP HTTP exporter") { - val headers = mapOf("Authorization" to "Bearer token") - val endpoint = "https://example.com/v1/logs" - - val exporter = OtelConfigRemoteOneSignal.LogRecordExporterConfig.otlpHttpLogRecordExporter( - headers, - endpoint - ) - - exporter shouldNotBe null - } - - test("SdkLoggerProviderConfig should create logger provider") { - val resource = OtelConfigShared.ResourceConfig.create(emptyMap()) - val headers = mapOf("X-OneSignal-App-Id" to "test-app-id") - - val provider = OtelConfigRemoteOneSignal.SdkLoggerProviderConfig.create( - resource, - headers, - "test-app-id", - "https://api.onesignal.com", - ) - - provider shouldNotBe null - } - - // ===== OtelConfigCrashFile Tests ===== - - test("OtelConfigCrashFile should create file log storage") { - val tempDir = System.getProperty("java.io.tmpdir") + "/otel-test-" + System.currentTimeMillis() - java.io.File(tempDir).mkdirs() - - try { - val storage = OtelConfigCrashFile.SdkLoggerProviderConfig.getFileLogRecordStorage( - tempDir, - 5000L - ) - - storage shouldNotBe null - } finally { - java.io.File(tempDir).deleteRecursively() - } - } - - test("OtelConfigCrashFile should create logger provider") { - val resource = OtelConfigShared.ResourceConfig.create(emptyMap()) - val tempDir = System.getProperty("java.io.tmpdir") + "/otel-test-" + System.currentTimeMillis() - java.io.File(tempDir).mkdirs() - - try { - val provider = OtelConfigCrashFile.SdkLoggerProviderConfig.create( - resource, - tempDir, - 5000L - ) - - provider shouldNotBe null - } finally { - java.io.File(tempDir).deleteRecursively() - } - } -}) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashHandlerTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashHandlerTest.kt deleted file mode 100644 index 2572c2f162..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashHandlerTest.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.onesignal.otel.crash - -import com.onesignal.otel.IOtelCrashReporter -import com.onesignal.otel.IOtelLogger -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import io.mockk.verify - -class OtelCrashHandlerTest : FunSpec({ - val mockCrashReporter = mockk(relaxed = true) - val mockLogger = mockk(relaxed = true) - - fun createFreshHandler() = OtelCrashHandler(mockCrashReporter, mockLogger) - - beforeEach { - clearMocks(mockCrashReporter, mockLogger) - } - - test("initialize should set up uncaught exception handler") { - val originalHandler = Thread.getDefaultUncaughtExceptionHandler() - val crashHandler = createFreshHandler() - - crashHandler.initialize() - - Thread.getDefaultUncaughtExceptionHandler() shouldBe crashHandler - verify { mockLogger.info(match { it.contains("Setting up uncaught exception handler") }) } - verify { mockLogger.info(match { it.contains("Successfully initialized") }) } - - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - } - - test("initialize should not initialize twice") { - val originalHandler = Thread.getDefaultUncaughtExceptionHandler() - val crashHandler = createFreshHandler() - - crashHandler.initialize() - crashHandler.initialize() - - verify(exactly = 1) { mockLogger.warn("OtelCrashHandler already initialized, skipping") } - - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - } - - test("uncaughtException should not process non-OneSignal crashes") { - val originalHandler = Thread.getDefaultUncaughtExceptionHandler() - val mockHandler = mockk(relaxed = true) - Thread.setDefaultUncaughtExceptionHandler(mockHandler) - val crashHandler = createFreshHandler() - crashHandler.initialize() - - val throwable = RuntimeException("Non-OneSignal crash") - val thread = Thread.currentThread() - - crashHandler.uncaughtException(thread, throwable) - - coVerify(exactly = 0) { mockCrashReporter.saveCrash(any(), any()) } - verify { mockHandler.uncaughtException(thread, throwable) } - - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - } - - test("uncaughtException should process OneSignal crashes") { - val originalHandler = Thread.getDefaultUncaughtExceptionHandler() - val mockHandler = mockk(relaxed = true) - Thread.setDefaultUncaughtExceptionHandler(mockHandler) - val crashHandler = createFreshHandler() - crashHandler.initialize() - - val throwable = RuntimeException("OneSignal crash").apply { - stackTrace = arrayOf( - StackTraceElement("com.onesignal.SomeClass", "someMethod", "SomeClass.kt", 10) - ) - } - val thread = Thread.currentThread() - - coEvery { mockCrashReporter.saveCrash(any(), any()) } returns Unit - - crashHandler.uncaughtException(thread, throwable) - - coVerify(exactly = 1) { mockCrashReporter.saveCrash(thread, throwable) } - verify { mockHandler.uncaughtException(thread, throwable) } - - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - } - - test("uncaughtException should not process same throwable twice") { - val originalHandler = Thread.getDefaultUncaughtExceptionHandler() - val crashHandler = createFreshHandler() - crashHandler.initialize() - - val throwable = RuntimeException("OneSignal crash").apply { - stackTrace = arrayOf( - StackTraceElement("com.onesignal.SomeClass", "someMethod", "SomeClass.kt", 10) - ) - } - val thread = Thread.currentThread() - - coEvery { mockCrashReporter.saveCrash(any(), any()) } returns Unit - - crashHandler.uncaughtException(thread, throwable) - crashHandler.uncaughtException(thread, throwable) - - coVerify(exactly = 1) { mockCrashReporter.saveCrash(any(), any()) } - - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - } - - test("uncaughtException should handle crash reporter failures gracefully") { - val originalHandler = Thread.getDefaultUncaughtExceptionHandler() - val mockHandler = mockk(relaxed = true) - Thread.setDefaultUncaughtExceptionHandler(mockHandler) - val crashHandler = createFreshHandler() - crashHandler.initialize() - - val throwable = RuntimeException("OneSignal crash").apply { - stackTrace = arrayOf( - StackTraceElement("com.onesignal.SomeClass", "someMethod", "SomeClass.kt", 10) - ) - } - val thread = Thread.currentThread() - - coEvery { mockCrashReporter.saveCrash(any(), any()) } throws RuntimeException("Reporter failed") - - crashHandler.uncaughtException(thread, throwable) - - verify { mockLogger.error(match { it.contains("Failed to save crash report") }) } - verify { mockHandler.uncaughtException(thread, throwable) } - - Thread.setDefaultUncaughtExceptionHandler(originalHandler) - } - - // ===== isOneSignalAtFault Tests ===== - - test("isOneSignalAtFault should return true for OneSignal stack traces") { - val stackTrace = arrayOf( - StackTraceElement("com.onesignal.core.SomeClass", "method", "File.kt", 10) - ) - - isOneSignalAtFault(stackTrace) shouldBe true - } - - test("isOneSignalAtFault should return false for non-OneSignal stack traces") { - val stackTrace = arrayOf( - StackTraceElement("com.example.app.SomeClass", "method", "File.kt", 10) - ) - - isOneSignalAtFault(stackTrace) shouldBe false - } - - test("isOneSignalAtFault should return false for empty stack traces") { - val stackTrace = emptyArray() - - isOneSignalAtFault(stackTrace) shouldBe false - } - - test("isOneSignalAtFault with throwable should check throwable stack trace") { - val throwable = RuntimeException("test").apply { - stackTrace = arrayOf( - StackTraceElement("com.onesignal.SomeClass", "method", "File.kt", 10) - ) - } - - isOneSignalAtFault(throwable) shouldBe true - } -}) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashReporterTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashReporterTest.kt deleted file mode 100644 index 3b9e27c77c..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashReporterTest.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.onesignal.otel.crash - -import com.onesignal.otel.IOtelCrashReporter -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryCrash -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.types.shouldBeInstanceOf -import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import io.opentelemetry.api.logs.LogRecordBuilder -import io.opentelemetry.api.logs.Severity -import io.opentelemetry.sdk.common.CompletableResultCode -import kotlinx.coroutines.runBlocking - -class OtelCrashReporterTest : FunSpec({ - val mockOpenTelemetry = mockk(relaxed = true) - val mockLogger = mockk(relaxed = true) - val mockLogRecordBuilder = mockk(relaxed = true) - val mockCompletableResult = mockk(relaxed = true) - - fun setupDefaultMocks() { - coEvery { mockOpenTelemetry.getLogger() } returns mockLogRecordBuilder - coEvery { mockOpenTelemetry.forceFlush() } returns mockCompletableResult - every { mockLogRecordBuilder.setSeverity(any()) } returns mockLogRecordBuilder - every { mockLogRecordBuilder.setTimestamp(any()) } returns mockLogRecordBuilder - every { mockLogRecordBuilder.emit() } returns Unit - } - - beforeEach { - clearMocks(mockOpenTelemetry, mockLogger, mockLogRecordBuilder, mockCompletableResult) - setupDefaultMocks() - } - - test("should implement IOtelCrashReporter interface") { - val crashReporter = OtelCrashReporter(mockOpenTelemetry, mockLogger) - - crashReporter.shouldBeInstanceOf() - } - - test("saveCrash should get logger and emit log record") { - val crashReporter = OtelCrashReporter(mockOpenTelemetry, mockLogger) - val throwable = RuntimeException("Test crash") - val thread = Thread.currentThread() - - runBlocking { - crashReporter.saveCrash(thread, throwable) - } - - coVerify(exactly = 1) { mockOpenTelemetry.getLogger() } - coVerify(exactly = 1) { mockOpenTelemetry.forceFlush() } - verify { mockLogRecordBuilder.setSeverity(Severity.FATAL) } - verify { mockLogRecordBuilder.emit() } - } - - test("saveCrash should log info messages") { - val crashReporter = OtelCrashReporter(mockOpenTelemetry, mockLogger) - val throwable = RuntimeException("Test crash") - val thread = Thread.currentThread() - - runBlocking { - crashReporter.saveCrash(thread, throwable) - } - - verify { mockLogger.info(match { it.contains("Starting to save crash report") }) } - verify { mockLogger.info(match { it.contains("Crash report saved and flushed successfully") }) } - } - - test("saveCrash should handle null exception message") { - val crashReporter = OtelCrashReporter(mockOpenTelemetry, mockLogger) - val throwable = RuntimeException() // No message - val thread = Thread.currentThread() - - runBlocking { - crashReporter.saveCrash(thread, throwable) - } - - coVerify { mockOpenTelemetry.getLogger() } - verify { mockLogRecordBuilder.emit() } - } - - test("saveCrash should re-throw RuntimeException on failure") { - coEvery { mockOpenTelemetry.getLogger() } throws RuntimeException("OpenTelemetry failed") - - val crashReporter = OtelCrashReporter(mockOpenTelemetry, mockLogger) - val throwable = RuntimeException("Test crash") - val thread = Thread.currentThread() - - shouldThrow { - runBlocking { - crashReporter.saveCrash(thread, throwable) - } - } - - verify { mockLogger.error(match { it.contains("Failed to save crash report") }) } - } - - test("saveCrash should re-throw IOException on IO failure") { - coEvery { mockOpenTelemetry.getLogger() } throws java.io.IOException("IO failed") - - val crashReporter = OtelCrashReporter(mockOpenTelemetry, mockLogger) - val throwable = RuntimeException("Test crash") - val thread = Thread.currentThread() - - shouldThrow { - runBlocking { - crashReporter.saveCrash(thread, throwable) - } - } - - verify { mockLogger.error(match { it.contains("IO error saving crash report") }) } - } - - test("saveCrash should re-throw IllegalStateException") { - coEvery { mockOpenTelemetry.getLogger() } throws IllegalStateException("Illegal state") - - val crashReporter = OtelCrashReporter(mockOpenTelemetry, mockLogger) - val throwable = RuntimeException("Test crash") - val thread = Thread.currentThread() - - // Note: IllegalStateException extends RuntimeException, so it gets caught by the RuntimeException handler - shouldThrow { - runBlocking { - crashReporter.saveCrash(thread, throwable) - } - } - - verify { mockLogger.error(match { it.contains("Failed to save crash report") }) } - } - - test("saveCrash should set timestamp") { - val crashReporter = OtelCrashReporter(mockOpenTelemetry, mockLogger) - val throwable = RuntimeException("Test crash") - val thread = Thread.currentThread() - - runBlocking { - crashReporter.saveCrash(thread, throwable) - } - - verify { mockLogRecordBuilder.setTimestamp(any()) } - } -}) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashUploaderTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashUploaderTest.kt deleted file mode 100644 index 4f46ef30d9..0000000000 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashUploaderTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.onesignal.otel.crash - -import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryRemote -import com.onesignal.otel.IOtelPlatformProvider -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.mockk.clearMocks -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import io.opentelemetry.sdk.common.CompletableResultCode -import io.opentelemetry.sdk.logs.export.LogRecordExporter -import kotlinx.coroutines.runBlocking -import java.io.File - -class OtelCrashUploaderTest : FunSpec({ - val mockRemoteTelemetry = mockk(relaxed = true) - val mockPlatformProvider = mockk(relaxed = true) - val mockLogger = mockk(relaxed = true) - val mockExporter = mockk(relaxed = true) - - // Use temp directory for tests that need file system access - fun createTempDir(): String { - val tempDir = File(System.getProperty("java.io.tmpdir"), "otel-test-${System.currentTimeMillis()}") - tempDir.mkdirs() - return tempDir.absolutePath - } - - fun setupDefaultMocks( - remoteLogLevel: String? = "ERROR", - crashStoragePath: String? = null, - minFileAgeForReadMillis: Long = 0L // Use 0 to avoid delays in tests - ) { - val path = crashStoragePath ?: createTempDir() - every { mockPlatformProvider.remoteLogLevel } returns remoteLogLevel - every { mockPlatformProvider.crashStoragePath } returns path - every { mockPlatformProvider.minFileAgeForReadMillis } returns minFileAgeForReadMillis - every { mockRemoteTelemetry.logExporter } returns mockExporter - every { mockExporter.export(any()) } returns CompletableResultCode.ofSuccess() - } - - beforeEach { - clearMocks(mockRemoteTelemetry, mockPlatformProvider, mockLogger, mockExporter) - } - - test("should create uploader with dependencies") { - setupDefaultMocks() - - val uploader = OtelCrashUploader(mockRemoteTelemetry, mockPlatformProvider, mockLogger) - - uploader shouldNotBe null - } - - test("start should return immediately when remote logging is disabled (null)") { - setupDefaultMocks(remoteLogLevel = null) - - val uploader = OtelCrashUploader(mockRemoteTelemetry, mockPlatformProvider, mockLogger) - - runBlocking { uploader.start() } - - verify { mockLogger.info("OtelCrashUploader: remote logging disabled (level: null)") } - } - - test("start should return immediately when remote logging is NONE") { - setupDefaultMocks(remoteLogLevel = "NONE") - - val uploader = OtelCrashUploader(mockRemoteTelemetry, mockPlatformProvider, mockLogger) - - runBlocking { uploader.start() } - - verify { mockLogger.info("OtelCrashUploader: remote logging disabled (level: NONE)") } - } - - test("start should proceed when remote logging is enabled") { - setupDefaultMocks(remoteLogLevel = "ERROR") - - val uploader = OtelCrashUploader(mockRemoteTelemetry, mockPlatformProvider, mockLogger) - - runBlocking { uploader.start() } - - verify { mockLogger.info("OtelCrashUploader: starting") } - } - - test("SEND_TIMEOUT_SECONDS should be 30 seconds") { - OtelCrashUploader.SEND_TIMEOUT_SECONDS shouldBe 30L - } -}) diff --git a/OneSignalSDK/settings.gradle b/OneSignalSDK/settings.gradle index 3cdfa40b21..76fb5755e6 100644 --- a/OneSignalSDK/settings.gradle +++ b/OneSignalSDK/settings.gradle @@ -30,4 +30,3 @@ include ':OneSignal:in-app-messages' include ':OneSignal:location' include ':OneSignal:notifications' include ':OneSignal:testhelpers' -include ':OneSignal:otel' diff --git a/examples/demo/app/build.gradle.kts b/examples/demo/app/build.gradle.kts index 9dbda6eca6..8aea101410 100644 --- a/examples/demo/app/build.gradle.kts +++ b/examples/demo/app/build.gradle.kts @@ -1,11 +1,8 @@ plugins { id("com.android.application") id("kotlin-android") - id("org.jetbrains.kotlin.plugin.compose") version "2.2.0" } -val kotlinVersion: String by rootProject.extra - // Apply GMS or Huawei plugin based on build variant // Check at configuration time, not when task graph is ready val taskRequests = gradle.startParameter.taskRequests.toString().lowercase() @@ -36,6 +33,10 @@ android { compose = true } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.14" + } + flavorDimensions += "default" productFlavors { @@ -89,7 +90,7 @@ android { dependencies { // Kotlin - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") // AndroidX diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt index 7df8fec384..2cfe743f3b 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.kt @@ -72,9 +72,8 @@ class MainApplication : MultiDexApplication() { OneSignal.consentGiven = SharedPreferenceUtil.getUserPrivacyConsent(this) // Initialize OneSignal on main thread (required) - // Crash handler + ANR detector are initialized early inside initWithContext OneSignal.initWithContext(this, appId) - LogManager.i(TAG, "OneSignal init completed (crash handler, ANR detector, and logging active)") + LogManager.i(TAG, "OneSignal init completed") // Set up all OneSignal listeners setupOneSignalListeners() diff --git a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt index 0655dc843c..6c3ef7f6cf 100644 --- a/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt +++ b/examples/demo/app/src/main/java/com/onesignal/sdktest/ui/secondary/SecondaryActivity.kt @@ -3,11 +3,8 @@ package com.onesignal.sdktest.ui.secondary import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -22,14 +19,9 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.onesignal.sdktest.ui.components.DestructiveButton import com.onesignal.sdktest.ui.theme.LightBackground import com.onesignal.sdktest.ui.theme.OneSignalRed import com.onesignal.sdktest.ui.theme.OneSignalTheme -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale class SecondaryActivity : ComponentActivity() { @@ -59,45 +51,19 @@ class SecondaryActivity : ComponentActivity() { }, containerColor = LightBackground ) { paddingValues -> - Column( + Box( modifier = Modifier .fillMaxSize() .padding(paddingValues), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + contentAlignment = Alignment.Center ) { Text( text = "Secondary Activity", style = MaterialTheme.typography.headlineMedium ) - - Spacer(modifier = Modifier.height(32.dp)) - - DestructiveButton( - text = "CRASH", - onClick = { triggerCrash() } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - DestructiveButton( - text = "SIMULATE ANR (10s block)", - onClick = { triggerAnr() } - ) } } } } } - - private fun triggerCrash() { - val timestamp = SimpleDateFormat("MMM dd, yyyy HH:mm:ss", Locale.getDefault()) - .format(Date()) - throw RuntimeException("Test crash from OneSignal Demo App - $timestamp") - } - - @Suppress("MagicNumber") - private fun triggerAnr() { - Thread.sleep(10_000) - } } diff --git a/examples/demo/build.gradle.kts b/examples/demo/build.gradle.kts index b21f952b6f..0244a29bc4 100644 --- a/examples/demo/build.gradle.kts +++ b/examples/demo/build.gradle.kts @@ -1,9 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -val kotlinVersion by extra("2.2.0") - buildscript { - val kotlinVersion: String by extra repositories { google() mavenCentral() @@ -13,7 +10,7 @@ buildscript { } dependencies { classpath("com.android.tools.build:gradle:8.8.2") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24") classpath("com.google.gms:google-services:4.3.10") classpath("com.huawei.agconnect:agcp:1.9.1.304") }