Coverage Summary for Class: ExtensionsKt (cloud.mindbox.mobile_sdk)

Class Method, % Branch, % Line, % Instruction, %
ExtensionsKt 61% (25/41) 35.6% (32/90) 61.8% (76/123) 66.8% (626/937)
ExtensionsKt$addUnique$1 100% (1/1) 100% (1/1) 100% (5/5)
ExtensionsKt$fromJson$1$1 0% (0/1) 0% (0/1) 0% (0/2)
ExtensionsKt$fromJson$2$1 0% (0/1) 0% (0/1) 0% (0/2)
ExtensionsKt$fromJsonTyped$1 0% (0/1) 0% (0/1) 0% (0/2)
ExtensionsKt$isUuid$1 100% (1/1) 100% (2/2) 100% (6/6)
ExtensionsKt$setOnAnimationEnd$1 0% (0/4) 0% (0/4) 0% (0/7)
ExtensionsKt$sortByPriority$$inlined$sortedByDescending$1 0% (0/1)
ExtensionsKt$toJsonTyped$1 0% (0/1) 0% (0/1) 0% (0/2)
ExtensionsKt$toUrlQueryString$1 100% (1/1) 100% (2/2) 100% (32/32)
Total 52.8% (28/53) 35.6% (32/90) 59.6% (81/136) 67.2% (669/995)


 package cloud.mindbox.mobile_sdk
 
 import android.app.Activity
 import android.app.ActivityManager
 import android.app.Application
 import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageInfo
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.PackageInfoFlags
 import android.content.res.Resources
 import android.os.Build
 import android.os.Looper
 import android.os.Process
 import android.view.View
 import android.view.ViewGroup
 import android.view.animation.Animation
 import android.view.animation.Animation.AnimationListener
 import androidx.annotation.IdRes
 import androidx.core.app.NotificationCompat
 import cloud.mindbox.mobile_sdk.Mindbox.logE
 import cloud.mindbox.mobile_sdk.Mindbox.logW
 import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp
 import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType
 import cloud.mindbox.mobile_sdk.inapp.domain.models.Layer
 import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl
 import cloud.mindbox.mobile_sdk.pushes.PushNotificationManager.EXTRA_UNIQ_PUSH_BUTTON_KEY
 import cloud.mindbox.mobile_sdk.pushes.PushNotificationManager.EXTRA_UNIQ_PUSH_KEY
 import cloud.mindbox.mobile_sdk.pushes.PushNotificationManager.IS_OPENED_FROM_PUSH_BUNDLE_KEY
 import cloud.mindbox.mobile_sdk.utils.loggingRunCatching
 import com.android.volley.VolleyError
 import com.android.volley.toolbox.HttpHeaderParser
 import com.google.gson.Gson
 import com.google.gson.JsonElement
 import com.google.gson.JsonObject
 import com.google.gson.reflect.TypeToken
 import org.threeten.bp.Instant
 import org.threeten.bp.LocalDateTime
 import org.threeten.bp.ZoneOffset
 import org.threeten.bp.ZonedDateTime
 import org.threeten.bp.format.DateTimeFormatter
 import java.net.URLEncoder
 import java.nio.charset.Charset
 import java.util.Queue
 import java.util.UUID
 import kotlin.math.roundToInt
 
 internal fun LinkedHashMap<String, String>.toUrlQueryString(): String = loggingRunCatching(
     defaultValue = ""
 ) {
     this.map { (k, v) -> "${k.encode()}=${v.encode()}" }
         .joinToString(prefix = "?", separator = "&")
 }
 
 internal fun String.encode(): String = URLEncoder.encode(this, "UTF-8")
 
 internal fun ZonedDateTime.convertToString() = runCatching {
     this.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"))
 }.getOrElse {
     MindboxLoggerImpl.e("Mindbox", "Error converting date", it)
     ""
 }
 
 internal fun Instant.convertToZonedDateTimeAtUTC(): ZonedDateTime {
     return ZonedDateTime.ofInstant(this, ZoneOffset.UTC)
 }
 
 internal fun String.convertToZonedDateTime(): ZonedDateTime = runCatching {
     return LocalDateTime.parse(this, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")).atZone(
         ZoneOffset.UTC
     )
 }.getOrElse {
     MindboxLoggerImpl.e("Mindbox", "Error converting date", it)
     LocalDateTime.parse("1970-01-01T00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))
         .atZone(
             ZoneOffset.UTC
         )
 }
 
 internal fun String.convertToZonedDateTimeWithZ(): ZonedDateTime = runCatching {
     return LocalDateTime.parse(this, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"))
         .atZone(
             ZoneOffset.UTC
         )
 }.getOrElse {
     MindboxLoggerImpl.e("Mindbox", "Error converting date", it)
     LocalDateTime.parse("1970-01-01T00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))
         .atZone(
             ZoneOffset.UTC
         )
 }
 
 internal fun String?.equalsAny(vararg values: String, ignoreCase: Boolean = false): Boolean = values.any { this?.equals(it, ignoreCase) == true }
 
 internal inline fun <reified T : Enum<T>> String?.enumValue(default: T? = null): T = this?.let {
     enumValues<T>().firstOrNull { value ->
         value.name
             .replace("_", "")
             .equals(
                 this.replace("_", "").trim(),
                 ignoreCase = true
             )
     }
 } ?: default ?: throw IllegalArgumentException("Value for $this could not be found")
 
 internal fun Double?.isInRange(start: Double, end: Double): Boolean {
     if (this == null) return false
     return this in start..end
 }
 
 internal fun Context.isMainProcess(processName: String?): Boolean {
     val mainProcessName = getString(R.string.mindbox_android_process).ifBlank { packageName }
     return processName?.equalsAny(
         mainProcessName,
         "$packageName:$mainProcessName",
         "$packageName$mainProcessName",
     ) ?: false
 }
 
 internal fun Context.getCurrentProcessName(): String? {
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
         return Application.getProcessName()
     }
 
     val mypid = Process.myPid()
     val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
     val processes = manager.runningAppProcesses
 
     return processes.firstOrNull { info -> info.pid == mypid }?.processName
 }
 
 internal fun View.setSingleClickListener(listener: View.OnClickListener) {
     setOnClickListener {
         setOnClickListener(null)
         listener.onClick(it)
     }
 }
 internal typealias SnackbarPosition = InAppType.Snackbar.Position.Gravity.VerticalGravity
 
 internal fun InAppType.Snackbar.isTop(): Boolean {
     return position.gravity.vertical == SnackbarPosition.TOP
 }
 
 internal val Double.px: Double
     get() = (this * Resources.getSystem().displayMetrics.density)
 
 internal val Int.dp: Int
     get() = (this / Resources.getSystem().displayMetrics.density).toInt()
 internal val Int.px: Int
     get() = (this * Resources.getSystem().displayMetrics.density).roundToInt()
 
 internal fun Context.maxScreenDimension(): Int {
     val displayMetrics = applicationContext.resources.displayMetrics
     return maxOf(displayMetrics.widthPixels, displayMetrics.heightPixels)
 }
 
 internal fun Animation.setOnAnimationEnd(runnable: Runnable) {
     setAnimationListener(object : AnimationListener {
         override fun onAnimationStart(animation: Animation?) {
         }
 
         override fun onAnimationEnd(animation: Animation?) {
             runnable.run()
         }
 
         override fun onAnimationRepeat(animation: Animation?) {
         }
     })
 }
 
 internal fun ViewGroup.removeChildById(
     @IdRes viewId: Int
 ) = removeView(findViewById(viewId))
 
 internal val Activity?.root: ViewGroup?
     get() = this?.window?.decorView?.rootView as ViewGroup?
 
 internal fun Activity.postDelayedAnimation(action: Runnable) {
     val duration = try {
         resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
     } catch (_: Exception) {
         window.transitionBackgroundFadeDuration
     }
     this.root?.postDelayed(action, duration)
 }
 
 internal inline fun <T> Queue<T>.addUnique(
     item: T,
     predicate: (T) -> Boolean = { it == item }
 ): Boolean {
     if (any(predicate)) return false
     add(item)
     return true
 }
 
 internal fun PackageManager.getPackageInfoCompat(context: Context, flags: Int): PackageInfo {
     return if (Build.VERSION_CODES.TIRAMISU <= Build.VERSION.SDK_INT) {
         getPackageInfo(context.packageName, PackageInfoFlags.of(flags.toLong()))
     } else {
         getPackageInfo(context.packageName, flags)
     }
 }
 
 internal fun VolleyError.getErrorResponseBodyData(): String {
     return this.networkResponse?.data
         ?.takeIf { it.isNotEmpty() }
         ?.toString(
             Charset.forName(
                 HttpHeaderParser.parseCharset(
                     this.networkResponse?.headers ?: emptyMap()
                 )
             )
         )
         ?: ""
 }
 
 internal fun verifyThreadExecution(methodName: String, shouldBeMainThread: Boolean = true) {
     val isMainThread = Looper.myLooper() == Looper.getMainLooper()
     when {
         shouldBeMainThread && !isMainThread -> logE("Method $methodName must be called on the main thread")
         !shouldBeMainThread && isMainThread -> logW("Method $methodName should not be called on the main thread")
     }
 }
 
 internal fun String.isUuid(): Boolean {
     return loggingRunCatching(false) {
         UUID.fromString(this)
         true
     }
 }
 
 internal inline fun <reified T> Gson.fromJson(json: JsonElement?): Result<T> = runCatching {
     fromJson(json, object : TypeToken<T>() {}.type)
 }
 
 internal inline fun <reified T> Gson.fromJson(json: String?): Result<T> = runCatching {
     fromJson(json, object : TypeToken<T>() {}.type)
 }
 
 internal fun JsonObject.getOrNull(memberName: String?): JsonElement? = runCatching {
     this.get(memberName)
 }.getOrNull()
 
 internal inline fun <T> Result<T>.getOrNull(runIfNull: (Throwable) -> Unit): T? = getOrElse {
     runIfNull(it)
     null
 }
 
 internal inline fun <reified T> Any?.safeAs(): T? {
     return this as? T
 }
 
 /**
  * Adds Mindbox push button unique keys extras to the [Intent].
  *
  * This extension function adds both the unique push key and the unique push button key
  * to the Intent extras. These extras are used by the Mindbox SDK to properly identify
  * and handle push notification button clicks.
  *
  * Note: If your push notification contains multiple buttons, you must call this method
  * for each button with its corresponding unique button key and use intent into [NotificationCompat.Builder.addAction]
  *
  * @param pushUniqKey the unique identifier for the push notification.
  * @param pushButtonKey the unique identifier for the push button.
  */
 public fun Intent.putMindboxPushButtonExtras(pushUniqKey: String, pushButtonKey: String?) {
     this.apply {
         putExtra(EXTRA_UNIQ_PUSH_KEY, pushUniqKey)
         putExtra(EXTRA_UNIQ_PUSH_BUTTON_KEY, pushButtonKey)
         putExtra(IS_OPENED_FROM_PUSH_BUNDLE_KEY, true)
     }
 }
 
 /**
  * Adds Mindbox push extras to the [Intent].
  *
  * This extension function adds the unique push key to the Intent extras. This extra is
  * used by the Mindbox SDK to properly identify and handle a push notification clicks that does not
  * include any push buttons.
  *
  * Note: Use this method before calling [NotificationCompat.Builder.setContentIntent]
  * @param pushUniqKey the unique identifier for the push notification.
  */
 public fun Intent.putMindboxPushExtras(pushUniqKey: String) {
     this.apply {
         putExtra(EXTRA_UNIQ_PUSH_KEY, pushUniqKey)
         putExtra(IS_OPENED_FROM_PUSH_BUNDLE_KEY, true)
     }
 }
 
 public fun Intent.getMindboxUniqKeyFromPushIntent(): String? = this.getStringExtra(EXTRA_UNIQ_PUSH_KEY)
 
 public fun Intent.getMindboxUniqPushButtonKeyFromPushIntent(): String? = this.getStringExtra(EXTRA_UNIQ_PUSH_BUTTON_KEY)
 
 internal inline fun <reified T> Gson.toJsonTyped(src: T): String =
     toJson(src, object : TypeToken<T>() {}.type)
 
 internal inline fun <reified T> Gson.fromJsonTyped(json: String): T? =
     fromJson(json, object : TypeToken<T>() {}.type)
 
 internal fun List<InApp>.sortByPriority(): List<InApp> {
     return this.sortedByDescending { it.isPriority }
 }
 
 internal inline fun <T> Queue<T>.pollIf(predicate: (T) -> Boolean): T? {
     return peek()?.takeIf(predicate)?.let { poll() }
 }
 
 internal fun InAppType.getImageUrl(): String? {
     return when (this) {
         is InAppType.WebView -> this.layers
         is InAppType.ModalWindow -> this.layers
         is InAppType.Snackbar -> this.layers
     }
         .filterIsInstance<Layer.ImageLayer>()
         .firstOrNull()
         ?.source
         ?.let { source ->
             when (source) {
                 is Layer.ImageLayer.Source.UrlSource -> source.url
             }
         }
 }