Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-11 18:55:37 +03:00
parent 330ae73357
commit e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions

View file

@ -1,20 +1,21 @@
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
kotlin("kapt")
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
dependencies {
/** DI */
implementation(Library.hiltCore)
kapt(Library.hiltKapt)
// region DI
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
// endregion
/** Coroutines */
implementation(Library.coroutine)
}
// region Coroutines
implementation(deps.kotlin.coroutines)
// endregion
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
// region Time dependencies
implementation(deps.jodatime)
// endregion
}

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.utils" />

View file

@ -0,0 +1,14 @@
package com.tangem.utils
/**
* Convert address to brief format. Example, 33BddS...ga2B.
* If [this.length] is less than a sum of [startCharsCount] and [endCharsCount], return [this].
*/
fun String.toBriefAddressFormat(startCharsCount: Int = 6, endCharsCount: Int = 4): String {
return if (startCharsCount + endCharsCount < length) {
substring(startIndex = 0, endIndex = startCharsCount) + "..." +
substring(startIndex = length - endCharsCount, endIndex = length)
} else {
this
}
}

View file

@ -1,9 +1,15 @@
package com.tangem.utils
import java.math.BigDecimal
inline fun <reified T : Enum<T>> safeValueOf(type: String, default: T): T {
return try {
java.lang.Enum.valueOf(T::class.java, type)
} catch (e: IllegalArgumentException) {
default
}
}
fun BigDecimal?.isNullOrZero(): Boolean {
return this == null || this.compareTo(BigDecimal.ZERO) == 0
}

View file

@ -1,91 +0,0 @@
package com.tangem.utils
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
// todo determine where to place this extensions
fun BigDecimal.toFormattedString(
decimals: Int,
roundingMode: RoundingMode = RoundingMode.DOWN,
locale: Locale = Locale.US,
): String {
val symbols = DecimalFormatSymbols(locale)
val df = DecimalFormat().apply {
decimalFormatSymbols = symbols
maximumFractionDigits = decimals
minimumFractionDigits = 0
isGroupingUsed = false
this.roundingMode = roundingMode
}
return df.format(this)
}
@Suppress("MagicNumber")
fun BigDecimal.toFormattedCurrencyString(
decimals: Int,
currency: String,
roundingMode: RoundingMode = RoundingMode.DOWN,
limitNumberOfDecimals: Boolean = true,
): String {
val decimalsForRounding = if (limitNumberOfDecimals) {
if (decimals > 8) 8 else decimals
} else {
decimals
}
val formattedAmount = this.toFormattedString(
decimals = decimalsForRounding,
roundingMode = roundingMode,
)
return "$formattedAmount $currency"
}
fun BigDecimal.toFiatString(
rateValue: BigDecimal,
fiatCurrencyName: String,
formatWithSpaces: Boolean = false,
): String {
val fiatValue = rateValue.multiply(this)
return fiatValue.toFormattedFiatValue(fiatCurrencyName, formatWithSpaces)
}
fun BigDecimal.toFormattedFiatValue(
fiatCurrencyName: String,
formatWithSpaces: Boolean = false,
): String {
val fiatValue = this.setScale(2, RoundingMode.HALF_UP)
.let { if (formatWithSpaces) it.formatWithSpaces() else it }
return "$fiatValue $fiatCurrencyName"
}
@Suppress("MagicNumber")
fun BigDecimal.formatWithSpaces(): String {
val str = this.toString()
var integerStr = str.substringBefore('.')
val reminderStr = str.substringAfter('.')
val packets = arrayListOf<String>()
var index: Int = integerStr.length
while (0 < index) {
if (index <= 3) {
packets.add(integerStr)
break
}
index -= 3
packets.add(integerStr.substring(startIndex = index))
integerStr = integerStr.substring(startIndex = 0, endIndex = index)
}
return buildString {
packets.reversed().forEachIndexed { index, packet ->
append(packet)
if (index != packets.lastIndex) append(' ')
}
if (reminderStr.isNotBlank()) {
append('.')
append(reminderStr)
}
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.utils
/**
* Provider for lazy initialization
*
* @param action initialization action
*
[REDACTED_AUTHOR]
*/
class Provider<T>(action: () -> T) : () -> T by action

View file

@ -0,0 +1,10 @@
package com.tangem.utils
/**
* Provider for suspend lazy initialization
*
* @param action initialization action
*
[REDACTED_AUTHOR]
*/
class ProviderSuspend<T>(action: suspend () -> T) : suspend () -> T by action

View file

@ -0,0 +1,15 @@
package com.tangem.utils
object StringsSigns {
const val DOT = ""
const val PLUS = "+"
const val MINUS = "-"
const val DASH_SIGN = ""
const val LOWER_SIGN = "<"
const val TILDE_SIGN = "~"
const val INFINITY_SIGN = ""
const val NON_BREAKING_SPACE = '\u00A0'
const val PERCENT = "%"
const val THREE_STARS = "\u2217\u2217\u2217"
}

View file

@ -0,0 +1,37 @@
package com.tangem.utils
import java.util.Locale
object SupportedLanguages {
const val ENGLISH = "en"
const val RUSSIAN = "ru"
const val GERMAN = "de"
const val FRANCH = "fr"
const val ITALIAN = "it"
const val JAPANESE = "ja"
const val UKRAINIAN = "uk"
const val CHINESE = "uk"
const val SPANISH = "es"
val supportedLangugeCodes = listOf(
ENGLISH,
RUSSIAN,
GERMAN,
FRANCH,
ITALIAN,
JAPANESE,
UKRAINIAN,
CHINESE,
SPANISH,
)
fun getCurrentSupportedLanguageCode(): String {
val locale = Locale.getDefault()
return if (supportedLangugeCodes.contains(locale.language)) {
locale.language
} else {
ENGLISH
}
}
}

View file

@ -0,0 +1,4 @@
package com.tangem.utils
const val H24_MILLIS = 24L * 60 * 60 * 1000
const val WEEK_MILLIS = 7L * H24_MILLIS

View file

@ -1,8 +1,29 @@
package com.tangem.utils.converter
interface Converter<I, O> {
interface Converter<I : Any, O : Any?> {
fun convert(value: I): O
fun convertList(input: List<I>): List<O> {
return input.map { convert(it) }
fun convertList(input: Collection<I>): List<O> {
return input.map(::convert)
}
fun convertSet(input: Collection<I>): Set<O> {
return input.mapTo(hashSetOf(), ::convert)
}
fun convertListIgnoreErrors(input: Collection<I>, onError: ((Throwable) -> Unit)? = null): List<O> {
return input.mapNotNull {
try {
convert(it)
} catch (throwable: Throwable) {
onError?.invoke(throwable)
null
}
}
}
fun <T> T?.asMandatory(name: String): T {
return this ?: error("$name must not be null")
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.utils.converter
interface TwoWayConverter<I, O> : Converter<I, O> {
interface TwoWayConverter<I : Any, O> : Converter<I, O> {
fun convertBack(value: O): I
fun convertListBack(input: List<O>): List<I> {
fun convertListBack(input: Collection<O>): List<I> {
return input.map { convertBack(it) }
}
}

View file

@ -2,19 +2,31 @@ package com.tangem.utils.coroutines
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
import javax.inject.Inject
interface CoroutineDispatcherProvider {
val main: CoroutineDispatcher
val mainImmediate: CoroutineDispatcher
val io: CoroutineDispatcher
val default: CoroutineDispatcher
val single: CoroutineDispatcher
}
class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider {
override val main: CoroutineDispatcher = Dispatchers.Main
override val mainImmediate: CoroutineDispatcher = Dispatchers.Main.immediate
override val io: CoroutineDispatcher = Dispatchers.IO
override val default: CoroutineDispatcher = Dispatchers.Default
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
}
class TestingCoroutineDispatcherProvider(
override val main: CoroutineDispatcher = Dispatchers.Unconfined,
override val mainImmediate: CoroutineDispatcher = Dispatchers.Unconfined,
override val io: CoroutineDispatcher = Dispatchers.Unconfined,
) : CoroutineDispatcherProvider
override val default: CoroutineDispatcher = Dispatchers.Unconfined,
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
) : CoroutineDispatcherProvider

View file

@ -1,11 +1,8 @@
package com.tangem.utils.coroutines
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
suspend fun <R> runCatching(dispatcher: CoroutineDispatcher, block: suspend () -> R): Result<R> {
return runCatching {
@ -13,19 +10,36 @@ suspend fun <R> runCatching(dispatcher: CoroutineDispatcher, block: suspend () -
}
}
suspend fun <R> waitForDelay(delay: Long, block: suspend CoroutineScope.() -> R): R = coroutineScope {
val minWaitingTime = async { delay(delay) }
val actionInvoke = async { block() }
minWaitingTime.await()
actionInvoke.await()
}
class Debouncer {
private var debounceJob: Job? = null
fun debounce(
waitMs: Long = 300L,
coroutineScope: CoroutineScope,
destinationFunction: () -> Unit,
waitMs: Long = 300L,
context: CoroutineContext = EmptyCoroutineContext,
forceUpdate: Boolean = false,
destinationFunction: suspend () -> Unit,
) {
debounceJob?.cancel()
debounceJob = coroutineScope.launch {
delay(waitMs)
debounceJob = coroutineScope.launch(context) {
if (!forceUpdate) delay(waitMs)
destinationFunction.invoke()
}
}
fun release() {
debounceJob?.cancel()
}
companion object {
const val DEFAULT_WAIT_TIME_MS = 500L
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.utils.coroutines
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DelayedWork

View file

@ -0,0 +1,41 @@
package com.tangem.utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* Job holder. It is automatically finished old job if new one is started
*
[REDACTED_AUTHOR]
*/
class JobHolder {
private var job: Job? = null
/** Update current [JobHolder.job] and return new [job] */
fun update(job: Job): Job {
this.job?.cancel()
this.job = job
return job
}
/** Cancel current [job] */
fun cancel() {
job?.cancel()
}
}
fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this)
suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join()
fun CoroutineScope.withDebounce(jobHolder: JobHolder, timeMillis: Long = 800L, function: () -> Unit) {
launch {
delay(timeMillis = timeMillis)
function()
}
.saveIn(jobHolder)
}

View file

@ -0,0 +1,61 @@
package com.tangem.utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
class PeriodicTask<T>(
private val delay: Long,
private val task: suspend () -> Result<T>,
private val onSuccess: (T) -> Unit,
private val onError: (Throwable) -> Unit,
private val isDelayFirst: Boolean = false,
) {
private var isActive: AtomicBoolean = AtomicBoolean(false)
suspend fun runTaskWithDelay() {
isActive.set(true)
if (isDelayFirst) {
delay(delay)
}
while (isActive.get()) {
task.invoke()
.onSuccess {
if (!isActive.get()) {
return@onSuccess
}
onSuccess.invoke(it)
}
.onFailure {
if (!isActive.get()) {
return@onFailure
}
onError.invoke(it)
}
delay(delay)
}
}
fun cancel() {
isActive.set(false)
}
}
class SingleTaskScheduler<T> {
private var lastTask: PeriodicTask<T>? = null
fun scheduleTask(scope: CoroutineScope, task: PeriodicTask<T>) {
lastTask?.cancel()
lastTask = task
scope.launch {
task.runTaskWithDelay()
}
}
fun cancelTask() {
lastTask?.cancel()
}
}

View file

@ -22,7 +22,7 @@ package com.tangem.utils.detekt
AnnotationTarget.TYPE,
AnnotationTarget.EXPRESSION,
AnnotationTarget.FILE,
AnnotationTarget.TYPEALIAS
AnnotationTarget.TYPEALIAS,
)
@Retention(AnnotationRetention.SOURCE)
annotation class UnusedRequiredComponent(val description: String)

View file

@ -0,0 +1,23 @@
package com.tangem.utils.di
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DelayedWorkCoroutineModule {
@Provides
@Singleton
@DelayedWork
fun provideDelayedWorkCoroutineScope(coroutineDispatcherProvider: CoroutineDispatcherProvider): CoroutineScope {
return CoroutineScope(SupervisorJob() + coroutineDispatcherProvider.io)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.utils.extensions
import java.math.BigDecimal
/** Returns [BigDecimal] or [BigDecimal.ZERO] if [this] is null */
fun BigDecimal?.orZero(): BigDecimal = this ?: BigDecimal.ZERO
/** Checks if [this] is [BigDecimal] */
fun BigDecimal.isZero(): Boolean = this.compareTo(BigDecimal.ZERO) == 0
/** Checks if [this] is positive */
fun BigDecimal.isPositive(): Boolean = this.signum() == 1
/** Removes trailing zeros and returns plain [String] */
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
/** Compares two [BigDecimal] numbers */
infix fun BigDecimal.isEqualTo(other: BigDecimal): Boolean = this.compareTo(other) == 0

View file

@ -0,0 +1,6 @@
package com.tangem.utils.extensions
/**
[REDACTED_AUTHOR]
*/
fun Char.isNotWhitespace(): Boolean = !this.isWhitespace()

View file

@ -0,0 +1,26 @@
package com.tangem.utils.extensions
/**
[REDACTED_AUTHOR]
*/
/**
* Checks if the [Collection] contains a single item.
*
* @return [Boolean] indicating whether the [Collection] contains exactly one element.
*/
fun <T> Collection<T>.isSingleItem(): Boolean = this.size == 1
/**
* Creates a shallow copy of this [Collection].
*
* @return A copy of the [Collection].
*/
fun <T> Collection<T>.copy(): Collection<T> {
return this.map { it }
}
inline fun <T> List<T>.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? {
val index = indexOfFirst(predicate)
return if (index == -1) null else index
}

View file

@ -0,0 +1,8 @@
package com.tangem.utils.extensions
import org.joda.time.DateTime
import org.joda.time.LocalDate
fun DateTime.isToday(): Boolean = LocalDate.now().equals(LocalDate(this))
fun DateTime.isYesterday(): Boolean = LocalDate.now().minusDays(1).equals(LocalDate(this))

View file

@ -0,0 +1,6 @@
package com.tangem.utils.extensions
/**
[REDACTED_AUTHOR]
*/
fun Int.isEven(): Boolean = this % 2 == 0

View file

@ -0,0 +1,75 @@
package com.tangem.utils.extensions
/**
* Removes elements from the collection based on the provided predicate.
*
* !!!This function is not thread-safe!!!
*
* @param predicate The condition to remove an element.
* @return [Boolean] indicating whether an element was removed.
*/
fun <T> MutableList<T>.removeBy(predicate: (T) -> Boolean): Boolean {
val toRemove = this.filter(predicate)
this.removeAll(toRemove)
return toRemove.isNotEmpty()
}
/**
* Replaces an element in the list with the provided item based on the predicate.
*
* @param item The element to replace the existing one.
* @param predicate The condition to replace an existing element.
* @return [Boolean] indicating whether an element was replaced.
*/
inline fun <T> MutableList<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boolean {
val index = indexOfFirst(predicate)
if (index == -1) {
return false
}
this[index] = item
return true
}
/**
* Adds the specified element to the list or replaces an existing element.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to be added or replace the existing one.
* @param predicate The condition to replace an existing element.
* @return The modified [List] after adding or replacing the element.
*/
inline fun <T> List<T>.addOrReplace(item: T, predicate: (T) -> Boolean): List<T> {
val mutableList = this.toMutableList()
mutableList.addOrReplace(item, predicate)
return mutableList
}
/**
* Adds the specified element to the mutable list or replaces an existing element.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to be added or replace the existing one.
* @param predicate The condition to replace an existing element.
*/
inline fun <T> MutableList<T>.addOrReplace(item: T, predicate: (T) -> Boolean) {
val isReplaced = replaceBy(item, predicate)
if (!isReplaced) {
add(item)
}
}
fun <T> List<T>.filterIf(condition: Boolean, predicate: (T) -> Boolean): List<T> {
return if (condition) {
this.filter(predicate)
} else {
this
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.utils.extensions
/**
* Replaces an element in the set with the provided item based on the predicate.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to replace the existing one.
* @param predicate The condition to replace an existing element.
* @return [Boolean] indicating whether an element was replaced.
*/
inline fun <T> MutableSet<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boolean {
val foundItem = firstOrNull(predicate) ?: return false
remove(foundItem)
add(item)
return true
}
/**
* Adds the specified element to the set or replaces an existing element.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to be added or replace the existing one.
* @param predicate The condition to replace an existing element.
* @return The modified [Set] after adding or replacing the element.
*/
inline fun <T> Set<T>.addOrReplace(item: T, predicate: (T) -> Boolean): Set<T> {
val mutableList = this.toMutableSet()
val isReplaced = mutableList.replaceBy(item, predicate)
if (!isReplaced) {
mutableList.add(item)
}
return mutableList
}
inline fun <T> Set<T>.addOrReplace(items: Set<T>, predicate: (T, T) -> Boolean): Set<T> {
val updatedValues = this.toMutableSet()
items.forEach { newValue ->
val isReplaced = updatedValues.replaceBy(item = newValue, predicate = { predicate(it, newValue) })
if (!isReplaced) updatedValues.add(newValue)
}
return updatedValues
}

View file

@ -0,0 +1,7 @@
package com.tangem.utils.extensions
/**
[REDACTED_AUTHOR]
*/
const val DELAY_SDK_DIALOG_CLOSE = 1400L

View file

@ -0,0 +1,8 @@
package com.tangem.utils.transformer
/**
* Transforms state to updated state.
*/
interface Transformer<S> {
fun transform(prevState: S): S
}

View file

@ -0,0 +1,8 @@
package com.tangem.utils.version
interface AppVersionProvider {
val versionName: String
val versionCode: Int
}