Merge branch 'develop' into feat/variable-playback-speed

This commit is contained in:
Florian 2025-12-30 21:29:18 +01:00 committed by GitHub
commit 0c004d933c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8020 changed files with 56825 additions and 29988 deletions

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -14,15 +15,14 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.SessionId
interface AccountSelectEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: Callback,
): Node
interface Callback : Plugin {
fun onSelectAccount(sessionId: SessionId)
fun onAccountSelected(sessionId: SessionId)
fun onCancel()
}
}

View file

@ -2,9 +2,10 @@ import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -17,7 +18,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.architecture.callback
@ContributesNode(AppScope::class)
@AssistedInject
@ -26,23 +27,15 @@ class AccountSelectNode(
@Assisted plugins: List<Plugin>,
private val presenter: AccountSelectPresenter,
) : Node(buildContext, plugins = plugins) {
private val callbacks = plugins.filterIsInstance<AccountSelectEntryPoint.Callback>()
private fun onDismiss() {
callbacks.forEach { it.onCancel() }
}
private fun onSelectAccount(sessionId: SessionId) {
callbacks.forEach { it.onSelectAccount(sessionId) }
}
private val callback: AccountSelectEntryPoint.Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
AccountSelectView(
state = state,
onDismiss = ::onDismiss,
onSelectAccount = ::onSelectAccount,
onDismiss = callback::onCancel,
onSelectAccount = callback::onAccountSelected,
modifier = modifier,
)
}

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -9,28 +10,18 @@ package io.element.android.libraries.accountselect.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
@Inject
class DefaultAccountSelectEntryPoint : AccountSelectEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): AccountSelectEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
return object : AccountSelectEntryPoint.NodeBuilder {
override fun callback(callback: AccountSelectEntryPoint.Callback): AccountSelectEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return parentNode.createNode<AccountSelectNode>(buildContext, plugins)
}
}
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: AccountSelectEntryPoint.Callback,
): Node {
return parentNode.createNode<AccountSelectNode>(buildContext, listOf(callback))
}
}

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -32,12 +33,14 @@ class DefaultAccountSelectEntryPointTest {
)
}
val callback = object : AccountSelectEntryPoint.Callback {
override fun onSelectAccount(sessionId: SessionId) = lambdaError()
override fun onAccountSelected(sessionId: SessionId) = lambdaError()
override fun onCancel() = lambdaError()
}
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
.callback(callback)
.build()
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
callback = callback,
)
assertThat(result).isInstanceOf(AccountSelectNode::class.java)
assertThat(result.plugins).contains(callback)
}

View file

@ -2,9 +2,10 @@ import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
@ -32,6 +33,7 @@ dependencies {
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.exifinterface)
implementation(libs.androidx.datastore.preferences)
implementation(libs.serialization.json)
api(libs.androidx.browser)
testCommonDependencies(libs)

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

View file

@ -0,0 +1,46 @@
/*
* 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.androidutils.assets
import android.content.Context
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.annotations.ApplicationContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
/**
* Read asset files.
*/
@Inject
class AssetReader(
@ApplicationContext private val context: Context,
) {
private val cache = ConcurrentHashMap<String, String?>()
/**
* Read an asset from resource and return a String or null in case of error.
*
* @param assetFilename Asset filename
* @return the content of the asset file, or null in case of error
*/
fun readAssetFile(assetFilename: String): String? {
return cache.getOrPut(assetFilename, {
return try {
context.assets.open(assetFilename).use { it.bufferedReader().readText() }
} catch (e: Exception) {
Timber.e(e, "## readAssetFile() failed")
null
}
})
}
fun clearCache() {
cache.clear()
}
}

View file

@ -1,7 +1,8 @@
/*
* Copyright 2022-2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -59,7 +60,7 @@ fun Activity.openUrlInChromeCustomTab(
})
}
.launchUrl(this, url.toUri())
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
openUrlInExternalApp(url)
}
}

View file

@ -0,0 +1,60 @@
/*
* 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.androidutils.browser
import android.util.Log
import android.webkit.ConsoleMessage
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import timber.log.Timber
interface ConsoleMessageLogger {
fun log(
tag: String,
consoleMessage: ConsoleMessage,
)
}
@ContributesBinding(AppScope::class)
class DefaultConsoleMessageLogger : ConsoleMessageLogger {
override fun log(
tag: String,
consoleMessage: ConsoleMessage,
) {
val priority = when (consoleMessage.messageLevel()) {
ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
ConsoleMessage.MessageLevel.WARNING -> Log.WARN
else -> Log.DEBUG
}
val message = buildString {
append(consoleMessage.sourceId())
append(":")
append(consoleMessage.lineNumber())
append(" ")
append(consoleMessage.message())
}
// Avoid logging any messages that contain "password" to prevent leaking sensitive information
if (message.contains("password=")) {
return
}
Timber.tag(tag).log(
priority = priority,
message = buildString {
append(consoleMessage.sourceId())
append(":")
append(consoleMessage.lineNumber())
append(" ")
append(consoleMessage.message())
},
)
}
}

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -13,13 +14,11 @@ import android.content.Context
import androidx.core.content.getSystemService
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.annotations.ApplicationContext
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
@Inject
class AndroidClipboardHelper(
@ApplicationContext private val context: Context,
) : ClipboardHelper {

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -27,7 +28,7 @@ class DiffCacheUpdater<ListItem, CachedItem>(
private val cacheInvalidator: DiffCacheInvalidator<CachedItem> = DefaultDiffCacheInvalidator(),
private val areItemsTheSame: (oldItem: ListItem?, newItem: ListItem?) -> Boolean,
) {
private val lock = Object()
private val lock = Any()
private var prevOriginalList: List<ListItem> = emptyList()
private val listUpdateCallback = object : ListUpdateCallback {

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -38,8 +39,5 @@ fun compressFile(file: File): File? {
} catch (e: Exception) {
Timber.e(e, "## compressFile() failed")
null
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## compressFile() failed")
null
}
}

View file

@ -1,7 +1,8 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -11,7 +12,6 @@ import android.content.Context
import android.net.Uri
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.annotations.ApplicationContext
import timber.log.Timber
@ -23,7 +23,6 @@ interface TemporaryUriDeleter {
}
@ContributesBinding(AppScope::class)
@Inject
class DefaultTemporaryUriDeleter(
@ApplicationContext private val context: Context,
) : TemporaryUriDeleter {

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -12,12 +13,10 @@ import android.os.Build
import android.text.format.Formatter
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ContributesBinding(AppScope::class)
@Inject
class AndroidFileSizeFormatter(
@ApplicationContext private val context: Context,
private val sdkIntProvider: BuildVersionSdkIntProvider,
@ -25,12 +24,15 @@ class AndroidFileSizeFormatter(
override fun format(fileSize: Long, useShortFormat: Boolean): String {
// Since Android O, the system considers that 1kB = 1000 bytes instead of 1024 bytes.
// We want to avoid that.
// Sadly we do not have access to the flags values Formatter.FLAG_IEC_UNITS and Formatter.FLAG_SHORTER
// nor the method Formatter.formatFileSize with the flags parameter.
// So for Android 0 and more, first convert the fileSize to MB/GB/TB ourselves
val normalizedSize = if (sdkIntProvider.get() <= Build.VERSION_CODES.N) {
fileSize
} else {
// First convert the size
when {
fileSize < 1024 -> fileSize
fileSize <= 1 -> fileSize
fileSize < 1024 * 1024 -> fileSize * 1000 / 1024
fileSize < 1024 * 1024 * 1024 -> fileSize * 1000 / 1024 * 1000 / 1024
else -> fileSize * 1000 / 1024 * 1000 / 1024 * 1000 / 1024
@ -41,6 +43,6 @@ class AndroidFileSizeFormatter(
Formatter.formatShortFileSize(context, normalizedSize)
} else {
Formatter.formatFileSize(context, normalizedSize)
}
}.replace("kB", "KB")
}
}

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2022-2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -19,7 +20,7 @@ fun String.hash() = try {
digest.digest()
.joinToString("") { String.format(Locale.ROOT, "%02X", it) }
.lowercase(Locale.ROOT)
} catch (exc: Exception) {
} catch (_: Exception) {
// Should not happen, but just in case
hashCode().toString()
}

View file

@ -0,0 +1,28 @@
/*
* 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.androidutils.json
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import kotlinx.serialization.json.Json
/**
* Provides a Json instance configured to ignore unknown keys.
*/
fun interface JsonProvider {
operator fun invoke(): Json
}
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultJsonProvider : JsonProvider {
private val json: Json by lazy { Json { ignoreUnknownKeys = true } }
override fun invoke() = json
}

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -0,0 +1,20 @@
/*
* 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.androidutils.system
import android.content.Context
import android.provider.Settings
fun Context.getAnimationScale(): Float {
return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f)
}
fun Context.areAnimationsEnabled(): Boolean {
return getAnimationScale() > 0f
}

View file

@ -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.androidutils.system
import android.app.Activity
import android.view.WindowManager
/**
* Set the screen brightness for the given activity.
*
* @receiver current Activity.
* @param full If true, override brightness to full; otherwise, set to none (default).
*/
fun Activity.setFullBrightness(full: Boolean) {
window.attributes = window.attributes.apply {
screenBrightness = if (full) {
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
} else {
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
}
}

View file

@ -1,7 +1,8 @@
/*
* Copyright 2022-2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -13,7 +14,6 @@ import android.content.Intent
import android.content.IntentFilter
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.androidutils.system.DateTimeObserver.Event
import io.element.android.libraries.di.annotations.ApplicationContext
@ -32,7 +32,6 @@ interface DateTimeObserver {
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
@Inject
class DefaultDateTimeObserver(
@ApplicationContext context: Context
) : DateTimeObserver {

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -31,7 +32,7 @@ fun Context.getApplicationLabel(packageName: String): String {
return try {
val ai = packageManager.getApplicationInfoCompat(packageName, 0)
packageManager.getApplicationLabel(ai).toString()
} catch (e: PackageManager.NameNotFoundException) {
} catch (_: PackageManager.NameNotFoundException) {
packageName
}
}
@ -95,7 +96,7 @@ fun Context.startNotificationSettingsIntent(
} else {
startActivity(intent)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}
@ -111,7 +112,7 @@ fun Context.openAppSettingsPage(
data = Uri.fromParts("package", packageName, null)
}
)
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}
@ -125,7 +126,7 @@ fun Context.startInstallFromSourceIntent(
.setData("package:$packageName".toUri())
try {
activityResultLauncher.launch(intent)
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}
@ -156,7 +157,7 @@ fun Context.startSharePlainTextIntent(
} else {
startActivity(intent)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -29,6 +30,7 @@ object LinkifyHelper {
@LinkifyCompat.LinkifyMask linkifyMask: Int = Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES,
): CharSequence {
// Convert the text to a Spannable to be able to add URL spans, return the original text if it's not possible (in tests, i.e.)
@Suppress("USELESS_ELVIS")
val spannable = text.toSpannable() ?: return text
// Get all URL spans, as they will be removed by LinkifyCompat.addLinks

View file

@ -0,0 +1,15 @@
/*
* 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.androidutils.text
import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.charset.Charset
fun String.urlEncoded(charset: Charset = Charsets.UTF_8): String = URLEncoder.encode(this, charset.name())
fun String.urlDecoded(charset: Charset = Charsets.UTF_8): String = URLDecoder.decode(this, charset.name())

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* 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.androidutils.throttler

View file

@ -1,7 +1,8 @@
/*
* Copyright 2022-2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2021-2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2021-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"Nije pronađena kompatibilna aplikacija za izvršavanje ove radnje."</string>
</resources>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2022 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2025 Element Creations Ltd.
~ Copyright 2022 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources xmlns:tools="http://schemas.android.com/tools">

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -14,45 +15,59 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
class AndroidFileSizeFormatterTest {
@Config(sdk = [Build.VERSION_CODES.N])
@Test
fun `test api 24 long format`() {
val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.N)
assertThat(sut.format(1, useShortFormat = false)).isEqualTo("1.00B")
assertThat(sut.format(1000, useShortFormat = false)).isEqualTo("0.98KB")
assertThat(sut.format(1024, useShortFormat = false)).isEqualTo("1.00KB")
assertThat(sut.format(1024 * 1024, useShortFormat = false)).isEqualTo("1.00MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = false)).isEqualTo("1.00GB")
assertThat(sut.format(1, useShortFormat = false)).isEqualTo("1 B")
assertThat(sut.format(1000, useShortFormat = false)).isEqualTo("0.98 KB")
assertThat(sut.format(1024, useShortFormat = false)).isEqualTo("1.00 KB")
assertThat(sut.format(1024 * 500, useShortFormat = false)).isEqualTo("500 KB")
assertThat(sut.format(1024 * 1024, useShortFormat = false)).isEqualTo("1.00 MB")
assertThat(sut.format(1024 * 1024 * 500, useShortFormat = false)).isEqualTo("500 MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = false)).isEqualTo("1.00 GB")
}
@Config(sdk = [Build.VERSION_CODES.O])
@Test
fun `test api 26 long format`() {
val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.O)
assertThat(sut.format(1, useShortFormat = false)).isEqualTo("1.00B")
assertThat(sut.format(1000, useShortFormat = false)).isEqualTo("0.98KB")
assertThat(sut.format(1024 * 1024, useShortFormat = false)).isEqualTo("0.95MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = false)).isEqualTo("0.93GB")
assertThat(sut.format(1, useShortFormat = false)).isEqualTo("1 B")
assertThat(sut.format(1000, useShortFormat = false)).isEqualTo("0.98 KB")
assertThat(sut.format(1024, useShortFormat = false)).isEqualTo("1.00 KB")
assertThat(sut.format(1024 * 500, useShortFormat = false)).isEqualTo("500 KB")
assertThat(sut.format(1024 * 1024, useShortFormat = false)).isEqualTo("1.00 MB")
assertThat(sut.format(1024 * 1024 * 500, useShortFormat = false)).isEqualTo("500 MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = false)).isEqualTo("1.00 GB")
}
@Config(sdk = [Build.VERSION_CODES.N])
@Test
fun `test api 24 short format`() {
val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.N)
assertThat(sut.format(1, useShortFormat = true)).isEqualTo("1.0B")
assertThat(sut.format(1000, useShortFormat = true)).isEqualTo("0.98KB")
assertThat(sut.format(1024, useShortFormat = true)).isEqualTo("1.0KB")
assertThat(sut.format(1024 * 1024, useShortFormat = true)).isEqualTo("1.0MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = true)).isEqualTo("1.0GB")
assertThat(sut.format(1, useShortFormat = true)).isEqualTo("1 B")
assertThat(sut.format(1000, useShortFormat = true)).isEqualTo("0.98 KB")
assertThat(sut.format(1024, useShortFormat = true)).isEqualTo("1.0 KB")
assertThat(sut.format(1024 * 500, useShortFormat = true)).isEqualTo("500 KB")
assertThat(sut.format(1024 * 1024, useShortFormat = true)).isEqualTo("1.0 MB")
assertThat(sut.format(1024 * 1024 * 500, useShortFormat = true)).isEqualTo("500 MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = true)).isEqualTo("1.0 GB")
}
@Config(sdk = [Build.VERSION_CODES.O])
@Test
fun `test api 26 short format`() {
val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.O)
assertThat(sut.format(1, useShortFormat = true)).isEqualTo("1.0B")
assertThat(sut.format(1000, useShortFormat = true)).isEqualTo("0.98KB")
assertThat(sut.format(1024 * 1024, useShortFormat = true)).isEqualTo("0.95MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = true)).isEqualTo("0.93GB")
assertThat(sut.format(1, useShortFormat = true)).isEqualTo("1 B")
assertThat(sut.format(1000, useShortFormat = true)).isEqualTo("0.98 KB")
assertThat(sut.format(1024, useShortFormat = true)).isEqualTo("1.0 KB")
assertThat(sut.format(1024 * 500, useShortFormat = true)).isEqualTo("500 KB")
assertThat(sut.format(1024 * 1024, useShortFormat = true)).isEqualTo("1.0 MB")
assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = true)).isEqualTo("1.0 GB")
}
private fun createAndroidFileSizeFormatter(sdkLevel: Int) = AndroidFileSizeFormatter(

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -2,9 +2,10 @@ import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -32,6 +33,11 @@ sealed interface AsyncAction<out T> {
data object ConfirmingNoParams : Confirming
/**
* User cancels the action, use this object to ask for confirmation.
*/
data object ConfirmingCancellation : Confirming
/**
* Represents an operation that is currently ongoing.
*/
@ -149,16 +155,16 @@ inline fun <T> MutableState<AsyncAction<T>>.runUpdatingStateNoSuccess(
* @param resultBlock a suspending function that returns a [Result].
* @return the [Result] returned by [resultBlock].
*/
@OptIn(ExperimentalContracts::class)
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
suspend inline fun <T> runUpdatingState(
state: MutableState<AsyncAction<T>>,
errorTransform: (Throwable) -> Throwable = { it },
resultBlock: suspend () -> Result<T>,
): Result<T> {
contract {
callsInPlace(resultBlock, InvocationKind.EXACTLY_ONCE)
}
// Restore when the issue with contracts and AGP 8.13.x is fixed
// contract {
// callsInPlace(resultBlock, InvocationKind.EXACTLY_ONCE)
// }
state.value = AsyncAction.Loading
return try {
resultBlock()

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -10,9 +11,6 @@ package io.element.android.libraries.architecture
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Sealed type that allows to model an asynchronous operation.
@ -133,16 +131,16 @@ suspend inline fun <T> MutableState<AsyncData<T>>.runUpdatingState(
* @param resultBlock a suspending function that returns a [Result].
* @return the [Result] returned by [resultBlock].
*/
@OptIn(ExperimentalContracts::class)
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
suspend inline fun <T> runUpdatingState(
state: MutableState<AsyncData<T>>,
errorTransform: (Throwable) -> Throwable = { it },
resultBlock: suspend () -> Result<T>,
): Result<T> {
contract {
callsInPlace(resultBlock, InvocationKind.EXACTLY_ONCE)
}
// Restore when the issue with contracts and AGP 8.13.x is fixed
// contract {
// callsInPlace(resultBlock, InvocationKind.EXACTLY_ONCE)
// }
val prevData = state.value.dataOrNull()
state.value = AsyncData.Loading(prevData = prevData)
return resultBlock().fold(
@ -162,14 +160,14 @@ suspend inline fun <T> runUpdatingState(
}
inline fun <T, R> AsyncData<T>.map(
transform: (T?) -> R,
transform: (T) -> R,
): AsyncData<R> {
return when (this) {
is AsyncData.Failure -> AsyncData.Failure(
error = error,
prevData = transform(prevData)
prevData = prevData?.let { transform(prevData) }
)
is AsyncData.Loading -> AsyncData.Loading(transform(prevData))
is AsyncData.Loading -> AsyncData.Loading(prevData?.let { transform(prevData) })
is AsyncData.Success -> AsyncData.Success(transform(data))
AsyncData.Uninitialized -> AsyncData.Uninitialized
}

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -0,0 +1,20 @@
/*
* 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.architecture
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
inline fun <reified I : Plugin> Node.callback(): I {
return plugins.callback()
}
inline fun <reified I : Plugin> List<Plugin>.callback(): I {
return requireNotNull(filterIsInstance<I>().singleOrNull()) { "Make sure to actually pass a Callback plugin to your node" }
}

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -75,22 +76,21 @@ class AsyncDataKtTest {
private class TestableMutableState<T>(
value: T
) : MutableState<T> {
@Suppress("ktlint:standard:property-naming")
private val _deque = ArrayDeque<T>(listOf(value))
private val deque = ArrayDeque(listOf(value))
override var value: T
get() = _deque.last()
get() = deque.last()
set(value) {
_deque.addLast(value)
deque.addLast(value)
}
/**
* Returns the states that were set in the order they were set.
*/
fun popFirst(): T = _deque.removeFirst()
fun popFirst(): T = deque.removeFirst()
fun assertNoMoreValues() {
assertThat(_deque).isEmpty()
assertThat(deque).isEmpty()
}
override operator fun component1(): T = value

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,9 +1,10 @@
import extension.setupDependencyInjection
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -15,13 +16,11 @@ import android.os.Build
import androidx.core.content.getSystemService
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.audio.api.AudioFocusRequester
import io.element.android.libraries.di.annotations.ApplicationContext
@ContributesBinding(AppScope::class)
@Inject
class DefaultAudioFocus(
@ApplicationContext private val context: Context,
) : AudioFocus {

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,7 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/

View file

@ -1,9 +1,10 @@
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@ -18,12 +19,12 @@ android {
testOptions {
unitTests.isIncludeAndroidResources = true
}
dependencies {
implementation(libs.showkase)
testCommonDependencies(libs)
testImplementation(libs.test.roborazzi)
testImplementation(libs.test.roborazzi.compose)
testImplementation(libs.test.roborazzi.junit)
}
}
dependencies {
implementation(libs.showkase)
testCommonDependencies(libs)
testImplementation(libs.test.roborazzi)
testImplementation(libs.test.roborazzi.compose)
testImplementation(libs.test.roborazzi.junit)
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6bc48b9f792da838f9fc2c2a630cbbbb906de851a138cc1ac8b7bf67b801ad84
size 211300
oid sha256:fa16f659aa3e7d05fa03a51d52faddc0c40c3ab52231687f8c6c8a4ba81ff6f0
size 219813

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e85fd2c67ff42829b8580ab5c0c2af1fee40973f5c0b34ef7de00e3663cee8e4
size 223041
oid sha256:72fb457dc50bf1a2261502fc1da15c01ab415344e9070354d38dc7b74234d790
size 232095

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:406b62991171a146e891f95bcad0321ebc60cf1fe2cabc9caedbb17fb062af13
size 224320
oid sha256:24cfe760717881ee71f36fae1fb201e74b2c32a2f9a5aef71ef21dab69ea5366
size 233212

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:578e9b5a38791e2686a7b9ba5c461eb1d1fb29dfbe950bf46c113ad75ceac175
size 327758
oid sha256:c846cd10b83361c368bdbb31ed6220cc22693c3cbf52791fb369841af1e9ea48
size 327701

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4cab40fc0506c8f2a2efafb1199e85f1da3ebacb49b176e9105e3f95175f85ee
size 325565
oid sha256:05b35fedbd53dec2cc5c4c211a8db1a56055963de69425ddae2cab5aff7e3e75
size 325750

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:174f9d4ee70a29c0c8c2a01a15daeb14281530678ff7d7fb19a208bfd789533a
size 309210
oid sha256:8d98e64eda5d6333067ccc599e99636f618331397207bb7534595e2756edb75e
size 309312

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7598b98462c015f2bf74b3ea3ad95fc0220b2efb9bb81ac56025cf6a158e3f8a
size 308976
oid sha256:5ccbf1234065b182939f001eb65eca0a62adae41a2d91ef0307d27b059407178
size 309084

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac84a7175c4a4897aa28eddcf722b7997c6576f612eb38fa09ffabcf7be11e00
size 119496
oid sha256:32b12d0b26cd016a632a4cb87b71d5efcb2c0d816bf565bc90aee9963ce2d5df
size 134117

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ff6dfdfab51332cad3bdfa351a4d0e305de5f899853575a8514858cc871e904
size 83609
oid sha256:a699170cabca6fb912d034a588b45961485afe6ef6d2c24f0ab79f10ae00c168
size 85629

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca38f5f23c282a6dc4c01a54705f71bf8c927aeac9c1df0a1f3abe50c10b1b85
size 89336
oid sha256:dd4b2a40fcf02d6db29cb0bc371d93236b4a0be6d4446bab86358692cddb53f5
size 91692

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more