forked from dsutanto/bChot-android
First Commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("java-library")
|
||||
id("com.android.lint")
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = Versions.javaVersion
|
||||
targetCompatibility = Versions.javaVersion
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = Versions.javaLanguageVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.coroutines.core)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.bool
|
||||
|
||||
fun Boolean?.orTrue() = this ?: true
|
||||
|
||||
fun Boolean?.orFalse() = this ?: false
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.cache
|
||||
|
||||
/**
|
||||
* A FIFO circular buffer of T.
|
||||
* This class is not thread safe.
|
||||
*/
|
||||
class CircularCache<T : Any>(cacheSize: Int, factory: (Int) -> Array<T?>) {
|
||||
companion object {
|
||||
inline fun <reified T : Any> create(cacheSize: Int) = CircularCache(cacheSize) { Array<T?>(cacheSize) { null } }
|
||||
}
|
||||
|
||||
private val cache = factory(cacheSize)
|
||||
private var writeIndex = 0
|
||||
|
||||
fun contains(value: T): Boolean = cache.contains(value)
|
||||
|
||||
fun put(value: T) {
|
||||
if (writeIndex == cache.size) {
|
||||
writeIndex = 0
|
||||
}
|
||||
cache[writeIndex] = value
|
||||
writeIndex++
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.plus
|
||||
|
||||
/**
|
||||
* Create a child scope of the current scope.
|
||||
* The child scope will be cancelled if the parent scope is cancelled.
|
||||
* The child scope will be cancelled if an exception is thrown in the parent scope.
|
||||
* The parent scope won't be cancelled when an exception is thrown in the child scope.
|
||||
*
|
||||
* @param dispatcher the dispatcher to use for this scope.
|
||||
* @param name the name of the coroutine.
|
||||
*/
|
||||
fun CoroutineScope.childScope(
|
||||
dispatcher: CoroutineDispatcher,
|
||||
name: String,
|
||||
): CoroutineScope = run {
|
||||
val supervisorJob = SupervisorJob(parent = coroutineContext.job)
|
||||
this + dispatcher + supervisorJob + CoroutineName(name)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
data class CoroutineDispatchers(
|
||||
val io: CoroutineDispatcher,
|
||||
val computation: CoroutineDispatcher,
|
||||
val main: CoroutineDispatcher,
|
||||
) {
|
||||
companion object {
|
||||
val Default = CoroutineDispatchers(
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main,
|
||||
)
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
* A [StateFlow] that derives its value from a [Flow].
|
||||
* Useful when you want to apply transformations to a [Flow] and expose it as a [StateFlow].
|
||||
*/
|
||||
@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
|
||||
class DerivedStateFlow<T>(
|
||||
private val getValue: () -> T,
|
||||
private val flow: Flow<T>
|
||||
) : StateFlow<T> {
|
||||
override val replayCache: List<T>
|
||||
get() = listOf(value)
|
||||
|
||||
override val value: T
|
||||
get() = getValue()
|
||||
|
||||
override suspend fun collect(collector: FlowCollector<T>): Nothing {
|
||||
coroutineScope { flow.distinctUntilChanged().stateIn(this).collect(collector) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the value of a [StateFlow] to a new value and returns a new [StateFlow] with the mapped value.
|
||||
*/
|
||||
fun <T1, R> StateFlow<T1>.mapState(transform: (a: T1) -> R): StateFlow<R> {
|
||||
return DerivedStateFlow(
|
||||
getValue = { transform(this.value) },
|
||||
flow = this.map { a -> transform(a) }
|
||||
)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/** Create a Flow emitting a single error event. It should be useful for tests. */
|
||||
fun <T> errorFlow(throwable: Throwable) = flow<T> { throw throwable }
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.runningFold
|
||||
|
||||
/**
|
||||
* Returns the first element of the flow that is an instance of [T], waiting for it if necessary.
|
||||
*/
|
||||
suspend inline fun <reified T> Flow<*>.firstInstanceOf(): T {
|
||||
return first { it is T } as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flow that emits pairs of the previous and current values.
|
||||
* The first emission will be a pair of `null` and the first value emitted by the source flow.
|
||||
*/
|
||||
fun <T> Flow<T>.withPreviousValue(): Flow<Pair<T?, T>> {
|
||||
return runningFold(null) { prev: Pair<T?, T>?, current ->
|
||||
prev?.second to current
|
||||
}
|
||||
.filterNotNull()
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
// https://jivimberg.io/blog/2018/05/04/parallel-map-in-kotlin/
|
||||
suspend fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = coroutineScope {
|
||||
map { async { f(it) } }.awaitAll()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun suspendWithMinimumDuration(
|
||||
minimumDurationMillis: Long = 500,
|
||||
block: suspend () -> Unit
|
||||
) = suspend {
|
||||
val duration = measureTimeMillis {
|
||||
block()
|
||||
}
|
||||
delay(minimumDurationMillis - duration)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
fun <T> suspendLazy(coroutineContext: CoroutineContext = EmptyCoroutineContext, block: suspend () -> T): Lazy<Deferred<T>> {
|
||||
return lazy(LazyThreadSafetyMode.NONE) {
|
||||
val deferred = CompletableDeferred<T>()
|
||||
CoroutineScope(coroutineContext).launch {
|
||||
deferred.complete(block())
|
||||
}
|
||||
deferred
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.data
|
||||
|
||||
enum class ByteUnit(val bitShift: Int) {
|
||||
BYTES(0),
|
||||
KB(10),
|
||||
MB(20),
|
||||
GB(30)
|
||||
}
|
||||
|
||||
class ByteSize internal constructor(val value: Long, val unit: ByteUnit) {
|
||||
fun to(dest: ByteUnit): Long {
|
||||
if (unit == dest) return value
|
||||
return value shl unit.bitShift shr dest.bitShift
|
||||
}
|
||||
}
|
||||
|
||||
val Number.gigaBytes get() = ByteSize(toLong(), ByteUnit.GB)
|
||||
val Number.megaBytes get() = ByteSize(toLong(), ByteUnit.MB)
|
||||
val Number.kiloBytes get() = ByteSize(toLong(), ByteUnit.KB)
|
||||
val Number.bytes get() = ByteSize(toLong(), ByteUnit.BYTES)
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.data
|
||||
|
||||
/**
|
||||
* Returns a list containing first [count] elements matching the given [predicate].
|
||||
* If the list contains less elements matching the [predicate], then all of them are returned.
|
||||
*
|
||||
* @param T the type of elements contained in the list.
|
||||
* @param count the maximum number of elements to take.
|
||||
* @param predicate the predicate used to match elements.
|
||||
* @return a list containing first [count] elements matching the given [predicate].
|
||||
*/
|
||||
inline fun <T> Iterable<T>.filterUpTo(count: Int, predicate: (T) -> Boolean): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
for (element in this) {
|
||||
if (predicate(element)) {
|
||||
result.add(element)
|
||||
if (result.size == count) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.data
|
||||
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
* Can be used to catch [Exception]s in a block of code, returning `null` if an exception occurs.
|
||||
*
|
||||
* If the block throws a [CancellationException], it will be rethrown.
|
||||
*/
|
||||
inline fun <A> tryOrNull(onException: ((Exception) -> Unit) = { }, operation: () -> A): A? {
|
||||
return try {
|
||||
operation()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
onException.invoke(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.extensions
|
||||
|
||||
import java.text.Normalizer
|
||||
import java.util.Locale
|
||||
|
||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||
fun Boolean.to01() = if (this) "1" else "0"
|
||||
|
||||
inline fun <T> T.ooi(block: (T) -> Unit): T = also(block)
|
||||
|
||||
/**
|
||||
* Return empty CharSequence if the CharSequence is null.
|
||||
*/
|
||||
fun CharSequence?.orEmpty() = this ?: ""
|
||||
|
||||
/**
|
||||
* Useful to append a String at the end of a filename but before the extension if any
|
||||
* Ex:
|
||||
* - "file.txt".insertBeforeLast("_foo") will return "file_foo.txt"
|
||||
* - "file".insertBeforeLast("_foo") will return "file_foo"
|
||||
* - "fi.le.txt".insertBeforeLast("_foo") will return "fi.le_foo.txt"
|
||||
* - null.insertBeforeLast("_foo") will return "_foo".
|
||||
*/
|
||||
fun String?.insertBeforeLast(insert: String, delimiter: String = "."): String {
|
||||
if (this == null) return insert
|
||||
val idx = lastIndexOf(delimiter)
|
||||
return if (idx == -1) {
|
||||
this + insert
|
||||
} else {
|
||||
replaceRange(idx, idx, insert)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate and ellipsize text if it exceeds the given length.
|
||||
*
|
||||
* Throws if length is < 1.
|
||||
*/
|
||||
fun String.ellipsize(length: Int): String {
|
||||
require(length >= 1)
|
||||
|
||||
if (this.length <= length) {
|
||||
return this
|
||||
}
|
||||
|
||||
return "${this.take(length)}…"
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the old prefix with the new prefix.
|
||||
* If the string does not start with the old prefix, the string is returned as is.
|
||||
*/
|
||||
fun String.replacePrefix(oldPrefix: String, newPrefix: String): String {
|
||||
return if (startsWith(oldPrefix)) {
|
||||
newPrefix + substring(oldPrefix.length)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Surround with brackets.
|
||||
*/
|
||||
fun String.withBrackets(prefix: String = "(", suffix: String = ")"): String {
|
||||
return "$prefix$this$suffix"
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize the string.
|
||||
*/
|
||||
fun String.safeCapitalize(): String {
|
||||
return replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(Locale.getDefault())
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.withoutAccents(): String {
|
||||
return Normalizer.normalize(this, Normalizer.Form.NFD)
|
||||
.replace("\\p{Mn}+".toRegex(), "")
|
||||
}
|
||||
|
||||
private const val RTL_OVERRIDE_CHAR = '\u202E'
|
||||
private const val LTR_OVERRIDE_CHAR = '\u202D'
|
||||
|
||||
fun String.ensureEndsLeftToRight() = if (containsRtLOverride()) "$this$LTR_OVERRIDE_CHAR" else this
|
||||
|
||||
fun String.containsRtLOverride() = contains(RTL_OVERRIDE_CHAR)
|
||||
|
||||
fun String.filterDirectionOverrides() = filterNot { it == RTL_OVERRIDE_CHAR || it == LTR_OVERRIDE_CHAR }
|
||||
|
||||
/**
|
||||
* This works around https://github.com/element-hq/element-x-android/issues/2105.
|
||||
* @param maxLength Max characters to retrieve. Defaults to `500`.
|
||||
* @param ellipsize Whether to add an ellipsis (`…`) char at the end or not. Defaults to `false`.
|
||||
* @return The string truncated to [maxLength] characters, with an optional ellipsis if larger.
|
||||
*/
|
||||
fun String.toSafeLength(
|
||||
maxLength: Int = 500,
|
||||
ellipsize: Boolean = false,
|
||||
): String {
|
||||
return if (ellipsize) {
|
||||
ellipsize(maxLength)
|
||||
} else if (length > maxLength) {
|
||||
take(maxLength)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.extensions
|
||||
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
|
||||
fun BuildMeta.isElement(): Boolean {
|
||||
return when (buildType) {
|
||||
BuildType.RELEASE -> applicationId == "io.element.android.x"
|
||||
BuildType.NIGHTLY -> applicationId == "io.element.android.x.nightly"
|
||||
BuildType.DEBUG -> applicationId == "io.element.android.x.debug"
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.extensions
|
||||
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
* Can be used to catch exceptions in a block of code and return a [Result].
|
||||
* If the block throws a [CancellationException], it will be rethrown.
|
||||
* If it throws any other exception, it will be wrapped in a [Result.failure].
|
||||
*
|
||||
* [Error]s are not caught by this function, as they are not meant to be caught in normal application flow.
|
||||
*/
|
||||
inline fun <T> runCatchingExceptions(
|
||||
block: () -> T
|
||||
): Result<T> {
|
||||
return try {
|
||||
Result.success(block())
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to catch exceptions in a block of code and return a [Result].
|
||||
* If the block throws a [CancellationException], it will be rethrown.
|
||||
* If it throws any other exception, it will be wrapped in a [Result.failure].
|
||||
*
|
||||
* [Error]s are not caught by this function, as they are not meant to be caught in normal application flow.
|
||||
*/
|
||||
inline fun <T, R> T.runCatchingExceptions(
|
||||
block: T.() -> R
|
||||
): Result<R> {
|
||||
return try {
|
||||
Result.success(block())
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to transform a [Result] into another [Result] by applying a [block] to the value if it is successful.
|
||||
* If the original [Result] is a failure, the exception will be wrapped in a new [Result.failure].
|
||||
*
|
||||
* This is a safer version of [Result.mapCatching].
|
||||
*/
|
||||
inline fun <R, T> Result<T>.mapCatchingExceptions(
|
||||
block: (T) -> R,
|
||||
): Result<R> {
|
||||
return fold(
|
||||
onSuccess = { value -> runCatchingExceptions { block(value) } },
|
||||
onFailure = { exception -> Result.failure(exception) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to transform some Throwable into some other.
|
||||
*/
|
||||
inline fun <R, T : R> Result<T>.mapFailure(transform: (exception: Throwable) -> Throwable): Result<R> {
|
||||
return when (val exception = exceptionOrNull()) {
|
||||
null -> this
|
||||
else -> Result.failure(transform(exception))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to apply a [transform] that returns a [Result] to a base [Result] and get another [Result].
|
||||
* @return The result of the transform as a [Result].
|
||||
*/
|
||||
inline fun <R, T> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R> {
|
||||
return map(transform).fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.failure(it) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to apply a [transform] that returns a [Result] to a base [Result] and get another [Result], catching any exception.
|
||||
* @return The result of the transform or a caught exception wrapped in a [Result].
|
||||
*/
|
||||
inline fun <R, T> Result<T>.flatMapCatching(transform: (T) -> Result<R>): Result<R> {
|
||||
return mapCatchingExceptions(transform).fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { Result.failure(it) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to execute a block of code after the [Result] has been processed, regardless of whether it was successful or not.
|
||||
* The block receives the exception if there was one, or `null` if the result was successful.
|
||||
*/
|
||||
inline fun <T> Result<T>.finally(block: (exception: Throwable?) -> Unit): Result<T> {
|
||||
onSuccess { block(null) }
|
||||
onFailure(block)
|
||||
return this
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.hash
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Compute a Hash of a String, using md5 algorithm.
|
||||
*/
|
||||
fun String.md5() = try {
|
||||
val digest = MessageDigest.getInstance("md5")
|
||||
val locale = Locale.ROOT
|
||||
digest.update(toByteArray())
|
||||
digest.digest()
|
||||
.joinToString("") { String.format(locale, "%02X", it) }
|
||||
.lowercase(locale)
|
||||
} catch (exc: Exception) {
|
||||
// Should not happen, but just in case
|
||||
hashCode().toString()
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.log.logger
|
||||
|
||||
/**
|
||||
* Parent class for custom logger tags. Can be used with Timber :
|
||||
*
|
||||
* val loggerTag = LoggerTag("MyTag", LoggerTag.VOIP)
|
||||
* Timber.tag(loggerTag.value).v("My log message")
|
||||
*/
|
||||
open class LoggerTag(name: String, parentTag: LoggerTag? = null) {
|
||||
object PushLoggerTag : LoggerTag("Push")
|
||||
object NotificationLoggerTag : LoggerTag("Notification", PushLoggerTag)
|
||||
|
||||
val value: String = if (parentTag == null) {
|
||||
name
|
||||
} else {
|
||||
"${parentTag.value}/$name"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.meta
|
||||
|
||||
data class BuildMeta(
|
||||
val buildType: BuildType,
|
||||
val isDebuggable: Boolean,
|
||||
val applicationName: String,
|
||||
val productionApplicationName: String,
|
||||
val desktopApplicationName: String,
|
||||
val applicationId: String,
|
||||
val isEnterpriseBuild: Boolean,
|
||||
val lowPrivacyLoggingEnabled: Boolean,
|
||||
val versionName: String,
|
||||
val versionCode: Long,
|
||||
val gitRevision: String,
|
||||
val gitBranchName: String,
|
||||
val flavorDescription: String,
|
||||
val flavorShortDescription: String,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.meta
|
||||
|
||||
enum class BuildType {
|
||||
RELEASE,
|
||||
NIGHTLY,
|
||||
DEBUG
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.mimetype
|
||||
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
|
||||
// The Android SDK does not provide constant for mime type, add some of them here
|
||||
@Suppress("ktlint:standard:property-naming")
|
||||
object MimeTypes {
|
||||
const val Any: String = "*/*"
|
||||
const val Json = "application/json"
|
||||
const val OctetStream = "application/octet-stream"
|
||||
const val Apk = "application/vnd.android.package-archive"
|
||||
const val Pdf = "application/pdf"
|
||||
|
||||
const val Images = "image/*"
|
||||
|
||||
const val Png = "image/png"
|
||||
const val BadJpg = "image/jpg"
|
||||
const val Jpeg = "image/jpeg"
|
||||
const val Gif = "image/gif"
|
||||
const val WebP = "image/webp"
|
||||
const val Svg = "image/svg+xml"
|
||||
|
||||
const val Videos = "video/*"
|
||||
const val Mp4 = "video/mp4"
|
||||
|
||||
const val Audio = "audio/*"
|
||||
|
||||
const val Ogg = "audio/ogg"
|
||||
const val Mp3 = "audio/mp3"
|
||||
|
||||
const val PlainText = "text/plain"
|
||||
|
||||
fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this
|
||||
|
||||
fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse()
|
||||
fun String?.isMimeTypeAnimatedImage() = this == Gif || this == WebP
|
||||
fun String?.isMimeTypeVideo() = this?.startsWith("video/").orFalse()
|
||||
fun String?.isMimeTypeAudio() = this?.startsWith("audio/").orFalse()
|
||||
fun String?.isMimeTypeApplication() = this?.startsWith("application/").orFalse()
|
||||
fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse()
|
||||
fun String?.isMimeTypeText() = this?.startsWith("text/").orFalse()
|
||||
fun String?.isMimeTypeAny() = this?.startsWith("*/").orFalse()
|
||||
|
||||
fun fromFileExtension(fileExtension: String): String {
|
||||
return when (fileExtension.lowercase()) {
|
||||
"apk" -> Apk
|
||||
"pdf" -> Pdf
|
||||
else -> OctetStream
|
||||
}
|
||||
}
|
||||
|
||||
fun hasSubtype(mimeType: String): Boolean {
|
||||
val components = mimeType.split("/")
|
||||
if (components.size != 2) return false
|
||||
val subType = components.last()
|
||||
return subType.isNotBlank() && subType != "*"
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.preview
|
||||
|
||||
val loremIpsum = """
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut la
|
||||
bore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate v
|
||||
elit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proide
|
||||
nt, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
""".trimIndent()
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.uri
|
||||
|
||||
import java.net.URI
|
||||
|
||||
fun String.isValidUrl(): Boolean {
|
||||
return try {
|
||||
URI(this).toURL()
|
||||
true
|
||||
} catch (t: Throwable) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty
|
||||
*/
|
||||
fun String.ensureProtocol(): String {
|
||||
return when {
|
||||
isEmpty() -> this
|
||||
!startsWith("http") -> "https://$this"
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
fun String.ensureTrailingSlash(): String {
|
||||
return when {
|
||||
isEmpty() -> this
|
||||
!endsWith("/") -> "$this/"
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.cache
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class CircularCacheTest {
|
||||
@Test
|
||||
fun `when putting more than cache size then cache is limited to cache size`() {
|
||||
val (cache, internalData) = createIntCache(cacheSize = 3)
|
||||
|
||||
cache.putInOrder(1, 1, 1, 1, 1, 1)
|
||||
|
||||
assertThat(internalData).isEqualTo(arrayOf(1, 1, 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when putting more than cache then acts as FIFO`() {
|
||||
val (cache, internalData) = createIntCache(cacheSize = 3)
|
||||
|
||||
cache.putInOrder(1, 2, 3, 4)
|
||||
|
||||
assertThat(internalData).isEqualTo(arrayOf(4, 2, 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given empty cache when checking if contains key then is false`() {
|
||||
val (cache, _) = createIntCache(cacheSize = 3)
|
||||
|
||||
val result = cache.contains(1)
|
||||
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given cached key when checking if contains key then is true`() {
|
||||
val (cache, _) = createIntCache(cacheSize = 3)
|
||||
|
||||
cache.put(1)
|
||||
val result = cache.contains(1)
|
||||
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
private fun createIntCache(cacheSize: Int): Pair<CircularCache<Int>, Array<Int?>> {
|
||||
var internalData: Array<Int?>? = null
|
||||
val factory: (Int) -> Array<Int?> = {
|
||||
Array<Int?>(it) { null }.also { array -> internalData = array }
|
||||
}
|
||||
return CircularCache(cacheSize, factory) to internalData!!
|
||||
}
|
||||
|
||||
private fun CircularCache<Int>.putInOrder(vararg values: Int) {
|
||||
values.forEach { put(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.data
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class ByteSizeTest {
|
||||
@Test
|
||||
fun testSizeConversions() {
|
||||
// Check bytes to other units
|
||||
val bytes = 10_000_000.bytes
|
||||
assertThat(bytes.to(ByteUnit.BYTES)).isEqualTo(bytes.value)
|
||||
assertThat(bytes.to(ByteUnit.KB)).isEqualTo(bytes.value / 1024L)
|
||||
assertThat(bytes.to(ByteUnit.MB)).isEqualTo(bytes.value / 1024L / 1024L)
|
||||
assertThat(bytes.to(ByteUnit.GB)).isEqualTo(bytes.value / 1024L / 1024L / 1024L)
|
||||
|
||||
// Now check for values too small to be converted
|
||||
assertThat(100.bytes.to(ByteUnit.KB)).isEqualTo(0)
|
||||
assertThat(100.bytes.to(ByteUnit.MB)).isEqualTo(0)
|
||||
assertThat(100.bytes.to(ByteUnit.GB)).isEqualTo(0)
|
||||
|
||||
// Check for KBs
|
||||
val kiloBytes = 10_000.kiloBytes
|
||||
assertThat(kiloBytes.to(ByteUnit.BYTES)).isEqualTo(kiloBytes.value * 1024L)
|
||||
assertThat(kiloBytes.to(ByteUnit.KB)).isEqualTo(kiloBytes.value)
|
||||
assertThat(kiloBytes.to(ByteUnit.MB)).isEqualTo(kiloBytes.value / 1024L)
|
||||
assertThat(kiloBytes.to(ByteUnit.GB)).isEqualTo(kiloBytes.value / 1024L / 1024L)
|
||||
|
||||
// Check for MBs
|
||||
val megaBytes = 10_000.megaBytes
|
||||
assertThat(megaBytes.to(ByteUnit.BYTES)).isEqualTo(megaBytes.value * 1024L * 1024L)
|
||||
assertThat(megaBytes.to(ByteUnit.KB)).isEqualTo(megaBytes.value * 1024L)
|
||||
assertThat(megaBytes.to(ByteUnit.MB)).isEqualTo(megaBytes.value)
|
||||
assertThat(megaBytes.to(ByteUnit.GB)).isEqualTo(megaBytes.value / 1024L)
|
||||
|
||||
// Check for GBs
|
||||
val gigaBytes = 10.gigaBytes
|
||||
assertThat(gigaBytes.to(ByteUnit.BYTES)).isEqualTo(gigaBytes.value * 1024L * 1024L * 1024L)
|
||||
assertThat(gigaBytes.to(ByteUnit.KB)).isEqualTo(gigaBytes.value * 1024L * 1024L)
|
||||
assertThat(gigaBytes.to(ByteUnit.MB)).isEqualTo(gigaBytes.value * 1024L)
|
||||
assertThat(gigaBytes.to(ByteUnit.GB)).isEqualTo(gigaBytes.value)
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.extensions
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class BasicExtensionsTest {
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `test ellipsize at 0`() {
|
||||
"1234567890".ellipsize(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ellipsize at 1`() {
|
||||
assertEquals(
|
||||
"1…",
|
||||
"1234567890".ellipsize(1)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ellipsize at 5`() {
|
||||
val output = "1234567890".ellipsize(5)
|
||||
assertEquals("12345…", output)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ellipsize noop 1`() {
|
||||
val input = "12345"
|
||||
val output = input.ellipsize(5)
|
||||
assertEquals(input, output)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ellipsize noop 2`() {
|
||||
val input = "123"
|
||||
val output = input.ellipsize(5)
|
||||
assertEquals(input, output)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given text with RtL unicode override, when checking contains RtL Override, then returns true`() {
|
||||
val textWithRtlOverride = "hello\u202Eworld"
|
||||
val result = textWithRtlOverride.containsRtLOverride()
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given text without RtL unicode override, when checking contains RtL Override, then returns false`() {
|
||||
val textWithRtlOverride = "hello world"
|
||||
val result = textWithRtlOverride.containsRtLOverride()
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given text with RtL unicode override, when ensuring ends LtR, then appends a LtR unicode override`() {
|
||||
val textWithRtlOverride = "123\u202E456"
|
||||
val result = textWithRtlOverride.ensureEndsLeftToRight()
|
||||
assertEquals("$textWithRtlOverride\u202D", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given text with unicode direction overrides, when filtering direction overrides, then removes all overrides`() {
|
||||
val textWithDirectionOverrides = "123\u202E456\u202d789"
|
||||
val result = textWithDirectionOverrides.filterDirectionOverrides()
|
||||
assertEquals("123456789", result)
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.extensions
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class ResultTest {
|
||||
@Test
|
||||
fun testFlatMap() {
|
||||
val initial = Result.success("initial")
|
||||
val otherResult = initial.flatMap { Result.success("other") }
|
||||
val errorResult = initial.flatMap { Result.failure<String>(IllegalStateException("error")) }
|
||||
|
||||
assertThat(otherResult.getOrNull()).isEqualTo("other")
|
||||
assertThat(errorResult.exceptionOrNull()?.message).isEqualTo("error")
|
||||
try {
|
||||
initial.flatMap<String, String> { error("caught error") }
|
||||
} catch (e: IllegalStateException) {
|
||||
assertThat(e.message).isEqualTo("caught error")
|
||||
}
|
||||
|
||||
val initialError = Result.failure<String>(IllegalStateException("initial error"))
|
||||
val mapErrorToSuccess = initialError.flatMap { Result.success("other") }
|
||||
val mapErrorToError = initialError.flatMap { Result.failure<String>(IllegalStateException("error")) }
|
||||
val mapErrorAndCatch: Result<String> = initialError.flatMap { error("error") }
|
||||
|
||||
assertThat(mapErrorToSuccess.exceptionOrNull()?.message).isEqualTo("initial error")
|
||||
assertThat(mapErrorToError.exceptionOrNull()?.message).isEqualTo("initial error")
|
||||
assertThat(mapErrorAndCatch.exceptionOrNull()?.message).isEqualTo("initial error")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFlatMapCatching() {
|
||||
val initial = Result.success("initial")
|
||||
val otherResult = initial.flatMapCatching { Result.success("other") }
|
||||
val errorResult = initial.flatMapCatching { Result.failure<String>(IllegalStateException("error")) }
|
||||
val caughtExceptionResult: Result<String> = initial.flatMapCatching { error("caught error") }
|
||||
|
||||
assertThat(otherResult.getOrNull()).isEqualTo("other")
|
||||
assertThat(errorResult.exceptionOrNull()?.message).isEqualTo("error")
|
||||
assertThat(caughtExceptionResult.exceptionOrNull()?.message).isEqualTo("caught error")
|
||||
|
||||
val initialError = Result.failure<String>(IllegalStateException("initial error"))
|
||||
val mapErrorToSuccess = initialError.flatMapCatching { Result.success("other") }
|
||||
val mapErrorToError = initialError.flatMapCatching { Result.failure<String>(IllegalStateException("error")) }
|
||||
val mapErrorAndCatch: Result<String> = initialError.flatMapCatching { error("error") }
|
||||
|
||||
assertThat(mapErrorToSuccess.exceptionOrNull()?.message).isEqualTo("initial error")
|
||||
assertThat(mapErrorToError.exceptionOrNull()?.message).isEqualTo("initial error")
|
||||
assertThat(mapErrorAndCatch.exceptionOrNull()?.message).isEqualTo("initial error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user