Skip to content
5 changes: 5 additions & 0 deletions app/src/main/kotlin/com/wire/android/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl
import com.wire.android.mapper.MessageResourceProvider
import com.wire.android.ui.analytics.AnalyticsConfiguration
import com.wire.android.ui.debug.securityproviders.AppPathsProvider
import com.wire.android.ui.debug.securityproviders.NetworkDiagnosticsProvider
import com.wire.android.ui.home.conversations.MessageSharedState
import com.wire.android.ui.home.messagecomposer.location.LocationPickerParameters
import com.wire.android.util.CurrentTimeProvider
Expand Down Expand Up @@ -142,4 +143,8 @@ object AppModule {
context = context,
currentAccount = currentAccount
)

@Provides
fun provideNetworkDiagnosticsProvider(@ApplicationContext context: Context): NetworkDiagnosticsProvider =
NetworkDiagnosticsProvider(context = context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.debug.securityproviders

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.core.net.toUri
import com.wire.android.appLogger
import java.io.IOException
import java.net.Inet6Address
import java.net.InetAddress

/**
* Reports how the device is currently reaching the backend: whether the active network is a VPN and which
* addresses the backend host resolves to through that very network, which is what a split tunnel or a
* misbehaving DNS would show up in.
*
* Resolution hits the network, so this must be called off the main thread.
*/
class NetworkDiagnosticsProvider(
private val context: Context,

Check warning on line 37 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L36-L37

Added lines #L36 - L37 were not covered by tests
) {
operator fun invoke(apiUrl: String): NetworkDiagnostics {
val host = apiUrl.toUri().host.orEmpty()
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
val activeNetwork = connectivityManager?.activeNetwork
?: return NetworkDiagnostics(
isVpn = false,
networkTypes = emptyList(),
backendHost = host,
addresses = AddressResolution.NoActiveNetwork,

Check warning on line 47 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L43-L47

Added lines #L43 - L47 were not covered by tests
)

val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork)

Check warning on line 50 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L50

Added line #L50 was not covered by tests
val networkTypes = NETWORK_TYPE_NAMES.filter { (transport, _) -> capabilities?.hasTransport(transport) == true }
.values
.toList()

Check warning on line 53 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L52-L53

Added lines #L52 - L53 were not covered by tests
val isVpn = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true

val addresses = when {

Check warning on line 56 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L56

Added line #L56 was not covered by tests
host.isEmpty() -> AddressResolution.Failed
else -> runCatching { activeNetwork.getAllByName(host) }

Check warning on line 58 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L58

Added line #L58 was not covered by tests
.fold(
onSuccess = { resolved -> AddressResolution.Resolved(resolved.mapNotNull { it.toResolvedAddress() }) },

Check warning on line 60 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L60

Added line #L60 was not covered by tests
onFailure = { error ->
if (error is IOException) {
appLogger.w("Could not resolve backend host through the active network", error)
AddressResolution.Failed

Check warning on line 64 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L63-L64

Added lines #L63 - L64 were not covered by tests
} else {
throw error

Check warning on line 66 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L66

Added line #L66 was not covered by tests
}
}
)
}

return NetworkDiagnostics(
isVpn = isVpn,
networkTypes = networkTypes,
backendHost = host,
addresses = addresses,

Check warning on line 76 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L72-L76

Added lines #L72 - L76 were not covered by tests
)
}

private companion object {
val NETWORK_TYPE_NAMES = linkedMapOf(
NetworkCapabilities.TRANSPORT_WIFI to "WIFI",
NetworkCapabilities.TRANSPORT_CELLULAR to "CELLULAR",
NetworkCapabilities.TRANSPORT_ETHERNET to "ETHERNET",
NetworkCapabilities.TRANSPORT_BLUETOOTH to "BLUETOOTH",
NetworkCapabilities.TRANSPORT_WIFI_AWARE to "WIFI_AWARE",

Check warning on line 86 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L81-L86

Added lines #L81 - L86 were not covered by tests
)
}
}

data class NetworkDiagnostics(
val isVpn: Boolean,
val networkTypes: List<String>,
val backendHost: String,
val addresses: AddressResolution,

Check warning on line 95 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L91-L95

Added lines #L91 - L95 were not covered by tests
)

sealed interface AddressResolution {
data class Resolved(val addresses: List<ResolvedAddress>) : AddressResolution
data object NoActiveNetwork : AddressResolution
data object Failed : AddressResolution

Check warning on line 101 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L99-L101

Added lines #L99 - L101 were not covered by tests
}

data class ResolvedAddress(
val address: String,
val version: IpVersion,

Check warning on line 106 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L104-L106

Added lines #L104 - L106 were not covered by tests
)

enum class IpVersion { V4, V6 }

Check warning on line 109 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L109

Added line #L109 was not covered by tests

private fun InetAddress.toResolvedAddress(): ResolvedAddress? = hostAddress?.let { address ->
ResolvedAddress(
address = address,

Check warning on line 113 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt#L112-L113

Added lines #L112 - L113 were not covered by tests
version = if (this is Inet6Address) IpVersion.V6 else IpVersion.V4,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package com.wire.android.ui.debug.securityproviders

import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
Expand Down Expand Up @@ -81,7 +82,65 @@ fun SecurityProvidersScreen(
state.appPaths.forEach { entry ->
SettingsItem(title = stringResource(entry.labelRes), text = entry.path)
}

state.network?.let { network ->
SectionHeader(stringResource(R.string.debug_settings_network))
NetworkSection(network)
}
}
}
)
}

@Composable
private fun NetworkSection(network: NetworkDiagnostics) {
val unknown = stringResource(R.string.debug_settings_network_unknown)

SettingsItem(
title = stringResource(R.string.debug_settings_network_vpn),
text = stringResource(
if (network.isVpn) R.string.debug_settings_network_vpn_active else R.string.debug_settings_network_vpn_inactive
),
)
SettingsItem(
title = stringResource(R.string.debug_settings_network_type),
text = network.networkTypes.joinToString().ifEmpty { unknown },
)
SettingsItem(
title = stringResource(R.string.debug_settings_network_backend_host),
text = network.backendHost.ifEmpty { unknown },
)

val addressesLabel = stringResource(R.string.debug_settings_network_resolved_addresses)
when (val addresses = network.addresses) {
is AddressResolution.Resolved -> if (addresses.addresses.isEmpty()) {
SettingsItem(title = addressesLabel, text = unknown)
} else {
addresses.addresses.forEach { resolved ->
SettingsItem(
title = stringResource(
R.string.debug_settings_network_resolved_address,
stringResource(resolved.version.labelRes())
),
text = resolved.address,
)
}
}

AddressResolution.NoActiveNetwork -> SettingsItem(
title = addressesLabel,
text = stringResource(R.string.debug_settings_network_no_active_network),
)

AddressResolution.Failed -> SettingsItem(
title = addressesLabel,
text = stringResource(R.string.debug_settings_network_resolution_failed),
)
}
}

@StringRes
private fun IpVersion.labelRes(): Int = when (this) {
IpVersion.V4 -> R.string.debug_settings_network_ip_v4
IpVersion.V6 -> R.string.debug_settings_network_ip_v6
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,24 @@

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.appLogger
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase
import com.wire.kalium.network.NetworkStateObserver
import dev.zacsweers.metro.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.security.Provider

class SecurityProvidersViewModel @Inject constructor(
private val appPathsProvider: AppPathsProvider,
private val networkDiagnosticsProvider: NetworkDiagnosticsProvider,
private val networkStateObserver: NetworkStateObserver,
private val selfServerConfig: SelfServerConfigUseCase,
private val dispatchers: DispatcherProvider,

Check warning on line 39 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt#L36-L39

Added lines #L36 - L39 were not covered by tests
) : ViewModel() {

private val _state = MutableStateFlow(SecurityProvidersViewState())
Expand All @@ -37,18 +46,30 @@
viewModelScope.launch {
_state.update { current -> current.copy(appPaths = appPathsProvider()) }
}
viewModelScope.launch {
observeNetworkDiagnostics()

Check warning on line 50 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt#L49-L50

Added lines #L49 - L50 were not covered by tests
}
}
}

/**
* `Provider.getVersionStr()` needs API 28 and `Provider.getVersion()` is deprecated, so read the version
* straight out of the provider's own property map, where it is registered under this key.
*/
private const val PROVIDER_VERSION_PROPERTY = "Provider.id version"
private suspend fun observeNetworkDiagnostics() {
val apiUrl = apiUrl() ?: return
networkStateObserver.observeCurrentNetwork()
.map { networkDiagnosticsProvider(apiUrl) }
.flowOn(dispatchers.io())
.collect { diagnostics -> _state.update { current -> current.copy(network = diagnostics) } }

Check warning on line 59 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt#L56-L59

Added lines #L56 - L59 were not covered by tests
}

private fun Provider.versionString(): String = getProperty(PROVIDER_VERSION_PROPERTY).orEmpty()
private suspend fun apiUrl(): String? = when (val result = selfServerConfig()) {

Check warning on line 62 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt#L62

Added line #L62 was not covered by tests
is SelfServerConfigUseCase.Result.Success -> result.serverLinks.links.api
is SelfServerConfigUseCase.Result.Failure -> {
appLogger.w("Could not read the server config, skipping network diagnostics")
null

Check warning on line 66 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt#L64-L66

Added lines #L64 - L66 were not covered by tests
}
}
}

data class SecurityProvidersViewState(
val appPaths: List<AppPathEntry> = emptyList(),
val network: NetworkDiagnostics? = null,

Check warning on line 73 in app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt#L73

Added line #L73 was not covered by tests
val providers: List<SecurityProvider>? = null,
)
13 changes: 13 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,19 @@ In group conversations, the group admin can overwrite this setting.</string>
<string name="debug_settings_app_path_no_backup_dir" translatable="false">No backup dir</string>
<string name="debug_settings_app_path_external_files_dir" translatable="false">External files dir</string>
<string name="debug_settings_security_providers_entry_count" translatable="false">%1$d entries</string>
<string name="debug_settings_network" translatable="false">Network</string>
<string name="debug_settings_network_vpn" translatable="false">VPN</string>
<string name="debug_settings_network_vpn_active" translatable="false">Active</string>
<string name="debug_settings_network_vpn_inactive" translatable="false">Not active</string>
<string name="debug_settings_network_type" translatable="false">Network type</string>
<string name="debug_settings_network_backend_host" translatable="false">Backend host</string>
<string name="debug_settings_network_resolved_addresses" translatable="false">Resolved addresses</string>
<string name="debug_settings_network_resolved_address" translatable="false">Resolved address (%1$s)</string>
<string name="debug_settings_network_ip_v4" translatable="false">IPv4</string>
<string name="debug_settings_network_ip_v6" translatable="false">IPv6</string>
<string name="debug_settings_network_no_active_network" translatable="false">No active network</string>
<string name="debug_settings_network_resolution_failed" translatable="false">Could not be resolved</string>
<string name="debug_settings_network_unknown" translatable="false">Unknown</string>

<!-- Personal to team migration screen-->
<string name="personal_to_team_migration_step_label">Step %1$d of 4</string>
Expand Down
10 changes: 9 additions & 1 deletion app/stability/app-devDebug.stability
Original file line number Diff line number Diff line change
Expand Up @@ -3941,6 +3941,13 @@ public fun com.wire.android.ui.debug.securityProvidersViewModel(): com.wire.andr
restartable: true
params:

@Composable
private fun com.wire.android.ui.debug.securityproviders.NetworkSection(network: com.wire.android.ui.debug.securityproviders.NetworkDiagnostics): kotlin.Unit
skippable: false
restartable: true
params:
- network: RUNTIME (requires runtime check)

@Composable
public fun com.wire.android.ui.debug.securityproviders.SecurityProvidersScreen(navigator: com.wire.android.navigation.Navigator, modifier: androidx.compose.ui.Modifier, viewModel: com.wire.android.ui.debug.securityproviders.SecurityProvidersViewModel): kotlin.Unit
skippable: false
Expand Down Expand Up @@ -6007,7 +6014,7 @@ public fun com.wire.android.ui.home.conversations.mediaGalleryViewModel(): com.w
params:

@Composable
public fun com.wire.android.ui.home.conversations.mention.MemberItemToMention(avatarData: com.wire.android.model.UserAvatarData, name: kotlin.String, label: kotlin.String, membership: com.wire.android.ui.home.conversationslist.model.Membership, searchQuery: kotlin.String, clickable: com.wire.android.model.Clickable, modifier: androidx.compose.ui.Modifier): kotlin.Unit
public fun com.wire.android.ui.home.conversations.mention.MemberItemToMention(avatarData: com.wire.android.model.UserAvatarData, name: kotlin.String, label: kotlin.String, membership: com.wire.android.ui.home.conversationslist.model.Membership, searchQuery: kotlin.String, clickable: com.wire.android.model.Clickable, modifier: androidx.compose.ui.Modifier, backgroundColor: androidx.compose.ui.graphics.Color): kotlin.Unit
skippable: true
restartable: true
params:
Expand All @@ -6018,6 +6025,7 @@ public fun com.wire.android.ui.home.conversations.mention.MemberItemToMention(av
- searchQuery: STABLE (String is immutable)
- clickable: STABLE (class with no mutable properties)
- modifier: STABLE (marked @Stable or @Immutable)
- backgroundColor: STABLE (marked @Stable or @Immutable)

@Composable
public fun com.wire.android.ui.home.conversations.messageAttachmentsViewModel(): com.wire.android.ui.home.conversations.attachment.MessageAttachmentsViewModel
Expand Down
Loading