Coverage Summary for Class: GatewayManager (cloud.mindbox.mobile_sdk.managers)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| GatewayManager |
25%
(7/28)
|
32.4%
(22/68)
|
29.1%
(51/175)
|
27.3%
(254/930)
|
| GatewayManager$checkCustomerSegmentations$2$1 |
0%
(0/1)
|
|
0%
(0/2)
|
0%
(0/15)
|
| GatewayManager$checkCustomerSegmentations$2$1$onResponse$$inlined$resumeFromJson$1 |
0%
(0/1)
|
|
| GatewayManager$checkCustomerSegmentations$2$1$onResponse$$inlined$resumeFromJson$1$1 |
0%
(0/1)
|
|
| GatewayManager$checkCustomerSegmentations$2$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/14)
|
| GatewayManager$checkGeoTargeting$2$1 |
0%
(0/1)
|
|
0%
(0/2)
|
0%
(0/15)
|
| GatewayManager$checkGeoTargeting$2$1$onResponse$$inlined$resumeFromJson$1 |
0%
(0/1)
|
|
| GatewayManager$checkGeoTargeting$2$1$onResponse$$inlined$resumeFromJson$1$1 |
0%
(0/1)
|
|
| GatewayManager$checkGeoTargeting$2$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/14)
|
| GatewayManager$checkProductSegmentation$2$1 |
0%
(0/1)
|
|
0%
(0/2)
|
0%
(0/15)
|
| GatewayManager$checkProductSegmentation$2$1$onResponse$$inlined$resumeFromJson$1 |
0%
(0/1)
|
|
| GatewayManager$checkProductSegmentation$2$1$onResponse$$inlined$resumeFromJson$1$1 |
0%
(0/1)
|
|
| GatewayManager$checkProductSegmentation$2$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/14)
|
| GatewayManager$Companion |
|
| GatewayManager$convertJsonToBody$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/13)
|
| GatewayManager$fetchMobileConfig$2$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/8)
|
| GatewayManager$fetchMobileConfig$2$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/11)
|
| GatewayManager$fetchWebViewContent$2$request$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/7)
|
| GatewayManager$fetchWebViewContent$2$request$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/11)
|
| GatewayManager$gatewayScope$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/14)
|
| GatewayManager$gson$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/3)
|
| GatewayManager$handleError$1 |
0%
(0/1)
|
0%
(0/15)
|
0%
(0/38)
|
0%
(0/194)
|
| GatewayManager$handleSuccessResponse$1 |
0%
(0/1)
|
0%
(0/4)
|
0%
(0/6)
|
0%
(0/76)
|
| GatewayManager$resumeFromJson$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/9)
|
| GatewayManager$resumeFromJson$1$invoke$$inlined$fromJsonTyped$1 |
0%
(0/1)
|
|
| GatewayManager$sendAsyncEvent$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/6)
|
| GatewayManager$sendAsyncEvent$2 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/10)
|
| GatewayManager$sendEvent$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/11)
|
| Total |
13%
(7/54)
|
25.3%
(22/87)
|
21.3%
(51/239)
|
18.3%
(254/1390)
|
package cloud.mindbox.mobile_sdk.managers
import android.util.Log
import androidx.annotation.VisibleForTesting
import cloud.mindbox.mobile_sdk.SdkValidation
import cloud.mindbox.mobile_sdk.fromJsonTyped
import cloud.mindbox.mobile_sdk.inapp.data.dto.GeoTargetingDto
import cloud.mindbox.mobile_sdk.inapp.domain.models.*
import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl
import cloud.mindbox.mobile_sdk.logger.mindboxLogE
import cloud.mindbox.mobile_sdk.models.*
import cloud.mindbox.mobile_sdk.models.operation.OperationResponseBaseInternal
import cloud.mindbox.mobile_sdk.models.operation.request.LogResponseDto
import cloud.mindbox.mobile_sdk.models.operation.request.SegmentationCheckRequest
import cloud.mindbox.mobile_sdk.models.operation.response.SegmentationCheckResponse
import cloud.mindbox.mobile_sdk.network.MindboxServiceGenerator
import cloud.mindbox.mobile_sdk.repository.MindboxPreferences
import cloud.mindbox.mobile_sdk.toUrlQueryString
import cloud.mindbox.mobile_sdk.utils.loggingRunCatching
import com.android.volley.DefaultRetryPolicy
import com.android.volley.DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
import com.android.volley.ParseError
import com.android.volley.Request
import com.android.volley.VolleyError
import com.android.volley.toolbox.StringRequest
import com.google.gson.Gson
import kotlinx.coroutines.*
import org.json.JSONException
import org.json.JSONObject
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
internal class GatewayManager(private val mindboxServiceGenerator: MindboxServiceGenerator) {
companion object {
private const val TIMEOUT_DELAY = 60000
private const val MAX_RETRIES = 0
private const val MONITORING_DELAY = 5000
private const val OPERATION_MOBILE_SDK_LOGS = "MobileSdk.Logs"
private const val OPERATION_CHECK_PRODUCT_SEGMENTS = "Tracker.CheckProductSegments"
private const val OPERATION_CHECK_CUSTOMER_SEGMENTS = "Tracker.CheckCustomerSegments"
}
private val gson by lazy { Gson() }
private val gatewayScope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Main + Job()) }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getCustomerSegmentationsUrl(configuration: Configuration): String {
return buildEventUrl(
configuration = configuration,
deviceUuid = MindboxPreferences.deviceUuid,
shouldCountOffset = false,
event = Event(
eventType = EventType.SyncOperation(OPERATION_CHECK_CUSTOMER_SEGMENTS)
)
)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getProductSegmentationUrl(configuration: Configuration): String {
return buildEventUrl(
configuration = configuration,
deviceUuid = MindboxPreferences.deviceUuid,
shouldCountOffset = false,
event = Event(
eventType = EventType.SyncOperation(OPERATION_CHECK_PRODUCT_SEGMENTS)
)
)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getLogsUrl(configuration: Configuration): String {
return buildEventUrl(
configuration = configuration,
deviceUuid = MindboxPreferences.deviceUuid,
shouldCountOffset = false,
event = Event(
eventType = EventType.AsyncOperation(OPERATION_MOBILE_SDK_LOGS)
)
)
}
private fun getConfigUrl(configuration: Configuration): String {
return "${SdkValidation.toBaseUrl(configuration.domain)}/mobile/byendpoint/${configuration.endpointId}.json"
}
/**
* Resolves the host to use for operations endpoints using the priority:
* 1. operationsDomainFromConfig — settings.baseAddresses.operations from the remote mobile config
* 2. operationsDomain from Mindbox.init configuration
* 3. domain from Mindbox.init configuration (fallback, preserves backward compatibility)
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun resolveOperationsDomain(
configuration: Configuration,
operationsDomainFromConfig: String?,
): String {
operationsDomainFromConfig
?.takeIf { it.isNotBlank() }
?.let { return it }
configuration.operationsDomain
?.takeIf { it.isNotBlank() }
?.let { return it }
return configuration.domain
}
private fun buildEventUrl(
configuration: Configuration,
deviceUuid: String,
shouldCountOffset: Boolean,
event: Event,
): String {
val urlQueries: LinkedHashMap<String, String> = linkedMapOf(
UrlQuery.DEVICE_UUID.value to deviceUuid,
)
when (event.eventType) {
is EventType.AppInstalled,
is EventType.AppInstalledWithoutCustomer,
is EventType.AppInfoUpdated,
is EventType.AppKeepalive,
is EventType.PushClicked,
is EventType.AsyncOperation,
-> {
urlQueries[UrlQuery.ENDPOINT_ID.value] = configuration.endpointId
urlQueries[UrlQuery.OPERATION.value] = event.eventType.operation
urlQueries[UrlQuery.TRANSACTION_ID.value] = event.transactionId
urlQueries[UrlQuery.DATE_TIME_OFFSET.value] =
getTimeOffset(event.enqueueTimestamp, shouldCountOffset)
}
is EventType.TrackVisit -> {
urlQueries[UrlQuery.TRANSACTION_ID.value] = event.transactionId
urlQueries[UrlQuery.DATE_TIME_OFFSET.value] =
getTimeOffset(event.enqueueTimestamp, shouldCountOffset)
}
is EventType.SyncOperation -> {
urlQueries[UrlQuery.ENDPOINT_ID.value] = configuration.endpointId
urlQueries[UrlQuery.OPERATION.value] = event.eventType.operation
}
}
val domain = resolveOperationsDomain(configuration, MindboxPreferences.operationsDomainFromConfig)
val baseUrl = SdkValidation.toBaseUrl(domain)
return "$baseUrl${event.eventType.endpoint}${urlQueries.toUrlQueryString()}"
}
fun sendAsyncEvent(
configuration: Configuration,
deviceUuid: String,
event: Event,
shouldCountOffset: Boolean,
isSentListener: (Boolean) -> Unit,
) = sendEvent(
configuration = configuration,
deviceUuid = deviceUuid,
event = event,
shouldCountOffset = shouldCountOffset,
onSuccess = { isSentListener.invoke(true) },
onError = { error -> isSentListener.invoke(isAsyncSent(error.statusCode)) },
)
fun <T : OperationResponseBaseInternal> sendEvent(
configuration: Configuration,
deviceUuid: String,
event: Event,
classOfT: Class<T>,
shouldCountOffset: Boolean,
onSuccess: (T) -> Unit,
onError: (MindboxError) -> Unit,
) = sendEvent(
configuration = configuration,
deviceUuid = deviceUuid,
event = event,
shouldCountOffset = shouldCountOffset,
onSuccess = { body -> handleSuccessResponse(body, onSuccess, onError, classOfT) },
onError = onError,
)
fun sendEvent(
configuration: Configuration,
deviceUuid: String,
event: Event,
shouldCountOffset: Boolean,
onSuccess: (String) -> Unit,
onError: (MindboxError) -> Unit,
) {
try {
val requestType: Int = getRequestType(event.eventType)
val url: String = buildEventUrl(configuration, deviceUuid, shouldCountOffset, event)
val jsonRequest: JSONObject? = convertBodyToJson(event.body)
val request = MindboxRequest(
methodType = requestType,
fullUrl = url,
configuration = configuration,
jsonRequest = jsonRequest,
listener = {
MindboxLoggerImpl.d(this, "Event from background successful sent")
onSuccess.invoke(it.toString())
},
errorsListener = { volleyError ->
handleError(volleyError, onSuccess, onError)
},
).apply {
setShouldCache(false)
retryPolicy = DefaultRetryPolicy(TIMEOUT_DELAY, MAX_RETRIES, DEFAULT_BACKOFF_MULT)
}
mindboxServiceGenerator.addToRequestQueue(request)
} catch (e: Exception) {
MindboxLoggerImpl.e(this, "Sending event was failure with exception", e)
onError.invoke(MindboxError.Unknown(e))
}
}
private fun getRequestType(eventType: EventType): Int = when (eventType) {
is EventType.AppInstalled,
is EventType.AppInstalledWithoutCustomer,
is EventType.AppInfoUpdated,
is EventType.AppKeepalive,
is EventType.PushClicked,
is EventType.TrackVisit,
is EventType.AsyncOperation,
is EventType.SyncOperation,
-> Request.Method.POST
}
private fun getTimeOffset(
timeMls: Long,
shouldCountOffset: Boolean,
): String = if (shouldCountOffset) {
(System.currentTimeMillis() - timeMls).toString()
} else {
"0"
}
private fun <T : OperationResponseBaseInternal> handleSuccessResponse(
data: String,
onSuccess: (T) -> Unit,
onError: (MindboxError) -> Unit,
classOfT: Class<T>,
) = gatewayScope.launch {
try {
val body = convertJsonToBody(data, MindboxResponse::class.java)
if (body.status != MindboxResponse.STATUS_VALIDATION_ERROR) {
onSuccess.invoke(convertJsonToBody(data, classOfT))
} else {
val validationMessages = body.validationMessages ?: emptyList()
onError.invoke(MindboxError.Validation(200, body.status, validationMessages))
}
} catch (e: Exception) {
onError.invoke(MindboxError.Unknown(e))
}
}
private fun handleError(
volleyError: VolleyError,
onSuccess: (String) -> Unit,
onError: (MindboxError) -> Unit,
) = gatewayScope.launch {
try {
val error = volleyError.networkResponse
if (error == null) {
onError.invoke(MindboxError.UnknownServer())
return@launch
}
val code = error.statusCode
val errorData = error.data
val errorBody: MindboxResponse? = errorData?.let { data ->
convertJsonToBody(String(data), MindboxResponse::class.java)
}
when (val status = errorBody?.status) {
null -> onError.invoke(MindboxError.UnknownServer())
MindboxResponse.STATUS_SUCCESS,
MindboxResponse.STATUS_TRANSACTION_ALREADY_PROCESSED,
-> {
onSuccess.invoke(String(errorData))
}
MindboxResponse.STATUS_VALIDATION_ERROR -> onError.invoke(
MindboxError.Validation(
statusCode = code,
status = status,
validationMessages = errorBody.validationMessages ?: emptyList(),
)
)
MindboxResponse.STATUS_PROTOCOL_ERROR -> onError.invoke(
MindboxError.Protocol(
statusCode = code,
status = status,
errorMessage = errorBody.errorMessage,
errorId = errorBody.errorId,
httpStatusCode = errorBody.httpStatusCode,
)
)
MindboxResponse.STATUS_INTERNAL_SERVER_ERROR -> onError.invoke(
MindboxError.InternalServer(
statusCode = code,
status = status,
errorMessage = errorBody.errorMessage,
errorId = errorBody.errorId,
httpStatusCode = errorBody.httpStatusCode,
)
)
else -> onError.invoke(
MindboxError.UnknownServer(
statusCode = code,
status = status,
errorMessage = errorBody.errorMessage,
errorId = errorBody.errorId,
httpStatusCode = errorBody.httpStatusCode,
)
)
}
} catch (e: Exception) {
MindboxLoggerImpl.e(this, "Parsing server response was failure", e)
onError.invoke(MindboxError.Unknown(e))
}
}
private fun convertBodyToJson(body: String?): JSONObject? =
if (body == null) {
null
} else {
try {
JSONObject(body)
} catch (_: JSONException) {
null
}
}
private suspend fun <T> convertJsonToBody(
data: String,
classOfT: Class<T>,
) = withContext(Dispatchers.Default) { gson.fromJson(data, classOfT) }
private fun isAsyncSent(statusCode: Int?) = statusCode?.let { code ->
code < 300 || code in 400..499
} ?: false
suspend fun checkGeoTargeting(configuration: Configuration): GeoTargetingDto {
return suspendCoroutine { continuation ->
mindboxServiceGenerator.addToRequestQueue(
MindboxRequest(
Request.Method.GET,
"${SdkValidation.toBaseUrl(configuration.domain)}/geo",
configuration,
null,
{ response ->
continuation.resumeFromJson<GeoTargetingDto>(
json = response.toString()
)
},
{ error ->
continuation.resumeWithException(GeoError(error))
}
)
)
}
}
suspend fun checkProductSegmentation(
configuration: Configuration,
segmentation: ProductSegmentationRequestDto,
): ProductSegmentationResponseDto {
return suspendCoroutine { continuation ->
mindboxServiceGenerator.addToRequestQueue(
MindboxRequest(
Request.Method.POST,
getProductSegmentationUrl(configuration),
configuration,
convertBodyToJson(
gson.toJson(
segmentation,
ProductSegmentationRequestDto::class.java
)
)!!,
{ response ->
continuation.resumeFromJson<ProductSegmentationResponseDto>(
json = response.toString()
)
},
{ error ->
continuation.resumeWithException(ProductSegmentationError(error))
}
)
)
}
}
suspend fun checkCustomerSegmentations(
configuration: Configuration,
segmentationCheckRequest: SegmentationCheckRequest,
): SegmentationCheckResponse {
return suspendCoroutine { continuation ->
mindboxServiceGenerator.addToRequestQueue(
MindboxRequest(
Request.Method.POST,
getCustomerSegmentationsUrl(configuration),
configuration,
convertBodyToJson(
gson.toJson(
segmentationCheckRequest,
SegmentationCheckRequest::class.java
)
)!!,
{ response ->
continuation.resumeFromJson<SegmentationCheckResponse>(
json = response.toString()
)
},
{ error ->
continuation.resumeWithException(CustomerSegmentationError(error))
}
)
)
}
}
fun sendLogEvent(logs: LogResponseDto, configuration: Configuration) {
try {
val url = getLogsUrl(configuration)
val jsonRequest: JSONObject? = convertBodyToJson(gson.toJson(logs))
val request = MindboxRequest(
methodType = Request.Method.POST,
fullUrl = url,
configuration = configuration,
jsonRequest = jsonRequest,
listener = {
Log.d("Success", "Sending logs success")
},
errorsListener = { volleyError ->
Log.e("Error", "Sending logs was failure with exception", volleyError)
}
).apply {
setShouldCache(false)
retryPolicy =
DefaultRetryPolicy(MONITORING_DELAY, MAX_RETRIES, DEFAULT_BACKOFF_MULT)
}
mindboxServiceGenerator.addToRequestQueue(request)
} catch (e: Exception) {
Log.e("Error", "Sending event was failure with exception", e)
}
}
suspend fun fetchMobileConfig(configuration: Configuration): String {
return suspendCoroutine { continuation ->
mindboxServiceGenerator.addToRequestQueue(
MindboxRequest(
methodType = Request.Method.GET,
fullUrl = getConfigUrl(configuration),
configuration = configuration,
jsonRequest = null,
listener = { response ->
continuation.resume(response.toString())
},
errorsListener = { error ->
continuation.resumeWithException(error)
},
)
)
}
}
suspend fun fetchWebViewContent(contentUrl: String): String {
return suspendCoroutine { continuation ->
try {
val request: StringRequest = StringRequest(
Request.Method.GET,
contentUrl,
{ response -> continuation.resume(response) },
{ error -> continuation.resumeWithException(error) }
).apply {
setShouldCache(false)
}
mindboxServiceGenerator.addToRequestQueue(request)
} catch (e: Exception) {
mindboxLogE("Failed to fetch WebView content", e)
continuation.resumeWithException(e)
}
}
}
private inline fun <reified T> Continuation<T>.resumeFromJson(json: String) {
loggingRunCatching(null) {
gson.fromJsonTyped<T>(json)
}?.let { dto ->
resume(dto)
} ?: run {
resumeWithException(
ParseError(JSONException("Could not parse JSON: $json"))
)
}
}
}