Merge branch 'develop' into gradlew-update-9.6.1

This commit is contained in:
ganfra
2026-07-21 15:34:15 +02:00
committed by GitHub
41 changed files with 229 additions and 43 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1 -2
View File
@@ -16,9 +16,8 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
# https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }}
persist-credentials: false
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
name: Use JDK 21
with:
distribution: 'temurin' # See 'Supported distributions' for available options
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+2 -2
View File
@@ -37,7 +37,7 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -80,7 +80,7 @@ jobs:
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+6 -6
View File
@@ -69,7 +69,7 @@ jobs:
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -108,7 +108,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -149,7 +149,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -183,7 +183,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -228,7 +228,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -269,7 +269,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
with:
persist-credentials: false
- name: ☕️ Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+3 -3
View File
@@ -38,7 +38,7 @@ jobs:
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -82,7 +82,7 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -126,7 +126,7 @@ jobs:
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: ☕️ Use JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+1
View File
@@ -17,4 +17,5 @@ dependencies {
implementation(projects.libraries.compound)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.wellknown.api)
}
@@ -11,6 +11,7 @@ package io.element.android.features.enterprise.api
import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
interface EnterpriseService {
@@ -41,6 +42,11 @@ interface EnterpriseService {
*/
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
/**
* Gets the overridden Element Well-Known data if it has been set, or null if not set.
*/
fun overriddenElementWellKnown(): ElementWellKnown?
companion object {
const val ANY_ACCOUNT_PROVIDER = "*"
}
@@ -23,6 +23,7 @@ dependencies {
api(projects.features.enterprise.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.wellknown.api)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
@@ -15,6 +15,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@@ -45,4 +46,6 @@ class DefaultEnterpriseService : EnterpriseService {
}
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
override fun overriddenElementWellKnown(): ElementWellKnown? = null
}
@@ -18,5 +18,6 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.compound)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.wellknown.api)
implementation(projects.tests.testutils)
}
@@ -13,6 +13,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
@@ -31,6 +32,7 @@ class FakeEnterpriseService(
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
private val tweakMasUrlResult: (String, String) -> String = { _, _ -> lambdaError() },
private val overrideWellKnownResult: () -> ElementWellKnown? = { lambdaError() },
) : EnterpriseService {
private val brandColorState = MutableStateFlow(initialBrandColor)
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
@@ -79,4 +81,8 @@ class FakeEnterpriseService(
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
return getNoisyNotificationChannelIdResult(sessionId)
}
override fun overriddenElementWellKnown(): ElementWellKnown? {
return overrideWellKnownResult()
}
}
@@ -26,9 +26,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -37,6 +39,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
private val CORNER_RADIUS = 8.dp
@@ -52,6 +55,8 @@ fun GroupHeaderView(
val backgroundColor = Color.Transparent
val shape = RoundedCornerShape(CORNER_RADIUS)
val expandedStateDescription = stringResource(CommonStrings.a11y_state_expanded)
val collapsedStateDescription = stringResource(CommonStrings.a11y_state_collapsed)
Box(
modifier = modifier
.fillMaxWidth()
@@ -62,6 +67,7 @@ fun GroupHeaderView(
)
.clearAndSetSemantics {
contentDescription = text
stateDescription = if (isExpanded) expandedStateDescription else collapsedStateDescription
},
contentAlignment = Alignment.Center
) {
+1 -1
View File
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2026 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.features.preferences.api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* Component for rendering extra developer options in the preferences screen.
*/
interface ExtraDeveloperOptionsRenderer {
@Composable
fun Render(modifier: Modifier)
}
@@ -18,6 +18,7 @@ import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.preferences.api.ExtraDeveloperOptionsRenderer
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.designsystem.showkase.getBrowserIntent
import io.element.android.libraries.di.SessionScope
@@ -28,6 +29,7 @@ class DeveloperSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: DeveloperSettingsPresenter,
private val extraDeveloperOptionsRenderer: ExtraDeveloperOptionsRenderer,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToPushHistory()
@@ -51,6 +53,7 @@ class DeveloperSettingsNode(
onOpenShowkase = ::openShowkase,
onPushHistoryClick = callback::navigateToPushHistory,
onBackClick = callback::onDone,
extraOptions = { extraDeveloperOptionsRenderer.Render(Modifier) },
)
}
}
@@ -43,6 +43,7 @@ fun DeveloperSettingsView(
onPushHistoryClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
extraOptions: @Composable () -> Unit = {},
) {
if (state.showLoader) {
ProgressDialog()
@@ -97,6 +98,9 @@ fun DeveloperSettingsView(
)
}
}
extraOptions()
val cache = state.cacheSize
PreferenceCategory(title = "Cache") {
ListItem(
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2026 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.features.preferences.impl.developer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.preferences.api.ExtraDeveloperOptionsRenderer
@ContributesBinding(AppScope::class)
class NoopExtraDeveloperOptionsRenderer : ExtraDeveloperOptionsRenderer {
@Composable
override fun Render(modifier: Modifier) {
// No-op
}
}
@@ -18,6 +18,7 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.preferences.api.ExtraDeveloperOptionsRenderer
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.designsystem.showkase.getBrowserIntent
@@ -28,6 +29,7 @@ class AppDeveloperSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AppDeveloperSettingsPresenter,
private val extraDeveloperOptionsRenderer: ExtraDeveloperOptionsRenderer,
) : Node(buildContext, plugins = plugins) {
private val callback: PreferencesEntryPoint.DeveloperSettingsCallback = callback()
@@ -45,6 +47,7 @@ class AppDeveloperSettingsNode(
modifier = modifier,
onOpenShowkase = ::openShowkase,
onBackClick = callback::onDone,
extraOptions = { extraDeveloperOptionsRenderer.Render(Modifier) },
)
}
}
@@ -24,6 +24,7 @@ fun AppDeveloperSettingsPage(
onOpenShowkase: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
extraOptions: @Composable () -> Unit = {},
) {
BackHandler(
onBack = onBackClick,
@@ -40,6 +41,7 @@ fun AppDeveloperSettingsPage(
onOpenShowkase = onOpenShowkase,
modifier = Modifier.padding(top = 8.dp)
)
extraOptions()
}
}
+6 -6
View File
@@ -5,7 +5,7 @@
# Project
android_gradle_plugin = "9.2.1"
# When updating this, please also update the version in the file ./idea/kotlinc.xml
kotlin = "2.4.0"
kotlin = "2.4.10"
kotlinpoet = "2.3.0"
ksp = "2.3.10"
firebaseAppDistribution = "5.3.0"
@@ -32,7 +32,7 @@ accompanist = "0.37.3"
# Test
test_core = "1.7.0"
roborazzi = "1.67.0"
roborazzi = "1.68.0"
# Jetbrain
datetime = "0.8.0"
@@ -51,10 +51,10 @@ telephoto = "0.19.0"
haze = "1.7.2"
# Dependency analysis
dependencyAnalysis = "3.16.0"
dependencyAnalysis = "3.16.1"
# DI
metro = "1.3.0"
metro = "1.3.2"
# Auto service
autoservice = "1.1.1"
@@ -84,7 +84,7 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
# https://firebase.google.com/docs/android/setup#available-libraries
google_firebase_bom = "com.google.firebase:firebase-bom:34.15.0"
google_firebase_bom = "com.google.firebase:firebase-bom:34.16.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
@@ -242,7 +242,7 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0"
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
# Element Call
element_call_embedded = "io.element.android:element-call-embedded:0.20.3"
element_call_embedded = "io.element.android:element-call-embedded:0.22.0"
# Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
@@ -8,6 +8,8 @@
<resources>
<string name="action_mark_as_read">"Mark as read"</string>
<string name="a11y_jump_to_unread_messages">"Jump to first unread message"</string>
<string name="a11y_state_expanded">"Expanded"</string>
<string name="a11y_state_collapsed">"Collapsed"</string>
<string name="screen_local_network_opt_in_title">Allow access to local network</string>
<string name="screen_local_network_opt_in_subtitle">Your homeserver is on your local network. To connect, Element needs permission to reach other devices on this network.</string>
<string name="a11y_active">Active</string>
@@ -0,0 +1,12 @@
/*
* Copyright (c) 2026 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.wellknown.api
fun interface ElementWellKnownParser {
fun parse(json: String): Result<ElementWellKnown>
}
@@ -34,6 +34,7 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.network)
implementation(projects.libraries.cachestore.api)
implementation(projects.features.enterprise.api)
implementation(projects.services.toolbox.api)
testCommonDependencies(libs)
@@ -41,5 +42,6 @@ dependencies {
testImplementation(projects.libraries.cachestore.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.wellknown.test)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.services.toolbox.test)
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2026 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.wellknown.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
@ContributesBinding(AppScope::class)
class DefaultElementWellKnownParser(
private val jsonProvider: JsonProvider,
) : ElementWellKnownParser {
override fun parse(json: String): Result<ElementWellKnown> {
return runCatchingExceptions {
val decoder = jsonProvider()
decoder.decodeFromString<InternalElementWellKnown>(json).map()
}
}
}
@@ -9,13 +9,14 @@
package io.element.android.libraries.wellknown.impl
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.core.extensions.mapCatchingExceptions
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.ElementWellknownStore
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
@@ -26,14 +27,20 @@ import timber.log.Timber
@ContributesBinding(SessionScope::class)
class DefaultSessionWellknownRetriever(
private val matrixClient: MatrixClient,
private val json: JsonProvider,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val elementWellknownStore: ElementWellknownStore,
private val elementWellKnownParser: ElementWellKnownParser,
private val enterpriseService: EnterpriseService,
) : SessionWellknownRetriever {
private val domain by lazy { matrixClient.userIdServerName() }
override suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown> {
val overriddenElementWellKnown = enterpriseService.overriddenElementWellKnown()
if (overriddenElementWellKnown != null) {
return WellknownRetrieverResult.Success(overriddenElementWellKnown)
}
val cacheData = elementWellknownStore.get(domain)
return when (cacheData) {
is WellknownRetrieverResult.Success -> {
@@ -69,7 +76,7 @@ class DefaultSessionWellknownRetriever(
.getUrl(url)
.mapCatchingExceptions {
val data = String(it)
val parsed = json().decodeFromString<InternalElementWellKnown>(data).map()
val parsed = elementWellKnownParser.parse(data).getOrThrow()
// Also store in cache, if valid
elementWellknownStore.update(domain, data)
.onFailure { exception ->
@@ -9,11 +9,11 @@ package io.element.android.libraries.wellknown.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.cachestore.api.CacheData
import io.element.android.libraries.cachestore.api.CacheStore
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.ElementWellknownStore
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.api.systemclock.SystemClock
@@ -21,14 +21,14 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock
@ContributesBinding(AppScope::class)
class DefaultWellKnownStore(
private val cacheStore: CacheStore,
private val json: JsonProvider,
private val elementWellKnownParser: ElementWellKnownParser,
private val systemClock: SystemClock,
) : ElementWellknownStore {
override suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown> {
return runCatchingExceptions {
val cachedData = cacheStore.getData(key(domain))
if (cachedData != null) {
val data = json().decodeFromString<InternalElementWellKnown>(cachedData.value).map()
val data = elementWellKnownParser.parse(cachedData.value).getOrThrow()
if (systemClock.epochMillis() > cachedData.updatedAt + CACHE_VALIDITY_MILLIS) {
WellknownRetrieverResult.Outdated(data)
} else {
@@ -10,6 +10,7 @@ package io.element.android.libraries.wellknown.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.uri.ensureProtocol
@@ -28,8 +29,14 @@ class DefaultWellknownRetriever(
private val retrofitFactory: RetrofitFactory,
private val elementWellknownStore: ElementWellknownStore,
private val jsonProvider: JsonProvider,
private val enterpriseService: EnterpriseService,
) : WellknownRetriever {
override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown> {
val overriddenElementWellKnown = enterpriseService.overriddenElementWellKnown()
if (overriddenElementWellKnown != null) {
return WellknownRetrieverResult.Success(overriddenElementWellKnown)
}
val domain = URL(baseUrl).host
return buildWellknownApi(baseUrl)
.map { wellknownApi ->
@@ -11,15 +11,16 @@
package io.element.android.libraries.wellknown.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.wellknown.test.FakeElementWellknownStore
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -276,7 +277,7 @@ class DefaultSessionWellknownRetrieverTest {
Result.success("{}".toByteArray())
},
cacheStore = cacheStore,
jsonProvider = JsonProvider { error("Failed to parse JSON") }
elementWellKnownParser = { Result.failure(IllegalStateException("Failed to parse JSON")) }
)
assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
// Ensure that the cache is deleted after the failure to parse it
@@ -320,20 +321,43 @@ class DefaultSessionWellknownRetrieverTest {
)
}
@Test
fun `get element wellknown was overridden`() = runTest {
val getLambda = lambdaRecorder<String, Result<ByteArray>> { Result.failure(IllegalStateException("BOOM")) }
val wellKnown = anElementWellKnown()
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = getLambda,
enterpriseService = FakeEnterpriseService(
overrideWellKnownResult = { wellKnown }
)
)
// The overridden value is returned
assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success(wellKnown)
)
// And the endpoint is never hit
getLambda.assertions().isNeverCalled()
}
private fun parsedWellKnownContent() = DefaultJsonProvider().invoke().decodeFromString<InternalElementWellKnown>(WELLKNOWN_CONTENT).map()
private fun TestScope.createDefaultSessionWellknownRetriever(
cacheStore: FakeElementWellknownStore = FakeElementWellknownStore(),
getUrlLambda: (String) -> Result<ByteArray>,
jsonProvider: JsonProvider = DefaultJsonProvider(),
elementWellKnownParser: ElementWellKnownParser = DefaultElementWellKnownParser(DefaultJsonProvider()),
enterpriseService: FakeEnterpriseService = FakeEnterpriseService(overrideWellKnownResult = { null }),
) = DefaultSessionWellknownRetriever(
matrixClient = FakeMatrixClient(
userIdServerNameLambda = { "user.domain.org" },
getUrlLambda = getUrlLambda,
),
json = jsonProvider,
sessionCoroutineScope = backgroundScope,
elementWellknownStore = cacheStore,
elementWellKnownParser = elementWellKnownParser,
enterpriseService = enterpriseService,
)
companion object {
@@ -13,6 +13,7 @@ import io.element.android.libraries.androidutils.json.DefaultJsonProvider
import io.element.android.libraries.cachestore.api.CacheData
import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
@@ -150,9 +151,10 @@ class DefaultWellKnownStoreTest {
private fun createDefaultWellKnownStore(
cacheStore: InMemoryCacheStore = InMemoryCacheStore(),
systemClock: FakeSystemClock = FakeSystemClock(),
elementWellKnownParser: ElementWellKnownParser = DefaultElementWellKnownParser(jsonProvider),
) = DefaultWellKnownStore(
cacheStore = cacheStore,
json = jsonProvider,
systemClock = systemClock,
elementWellKnownParser = elementWellKnownParser,
)
}
@@ -11,6 +11,7 @@
package io.element.android.libraries.wellknown.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.wellknown.test.FakeElementWellknownStore
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
@@ -19,6 +20,7 @@ import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
@@ -236,6 +238,27 @@ class DefaultWellknownRetrieverTest {
assertThat(cacheStore.get(WELLKNOWN_URL)).isEqualTo(WellknownRetrieverResult.NotFound)
}
@Test
fun `get element wellknown was overridden`() = runTest {
val getLambda = lambdaRecorder<Request, Call> { mockCall() }
val wellKnown = anElementWellKnown()
val sut = createDefaultWellknownRetriever(
callFactory = getLambda,
enterpriseService = FakeEnterpriseService(
overrideWellKnownResult = { wellKnown }
)
)
// The overridden value is returned
assertThat(sut.getElementWellKnown(WELLKNOWN_URL)).isEqualTo(
WellknownRetrieverResult.Success(wellKnown)
)
// And the endpoint is never hit
getLambda.assertions().isNeverCalled()
}
private fun defaultResponse(
body: String = WELLKNOWN_CONTENT,
status: Int = 200,
@@ -251,6 +274,7 @@ class DefaultWellknownRetrieverTest {
callFactory: Call.Factory = Call.Factory { _: Request -> mockCall(defaultResponse()) },
cacheStore: FakeElementWellknownStore = FakeElementWellknownStore(),
jsonProvider: JsonProvider = DefaultJsonProvider(),
enterpriseService: FakeEnterpriseService = FakeEnterpriseService(overrideWellKnownResult = { null }),
) = DefaultWellknownRetriever(
retrofitFactory = RetrofitFactory(
callFactory = { callFactory },
@@ -258,6 +282,7 @@ class DefaultWellknownRetrieverTest {
),
jsonProvider = jsonProvider,
elementWellknownStore = cacheStore,
enterpriseService = enterpriseService,
)
companion object {
@@ -88,6 +88,8 @@ class KonsistPreviewTest {
"BackgroundVerticalGradientPreview",
"ColorAliasesPreview",
"EmojiItemWithPopupPreview",
"EnterpriseExtraDeveloperOptionsRendererBottomSheetPreview",
"EnterpriseExtraDeveloperOptionsRendererListItemPreview",
"FocusedEventPreview",
"GradientFloatingActionButtonCircleShapePreview",
"HeaderFooterPageScrollablePreview",