From 08f3bd7dd2418056a8b822f624377f512615f769 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 11 Aug 2026 15:07:01 +0200 Subject: [PATCH 1/8] feat: show security providers info screen --- .../di/metro/WireMetroViewModelBindings.kt | 7 + .../wire/android/ui/debug/DebugDataOptions.kt | 13 ++ .../ui/debug/DebugInfoViewModelFactory.kt | 7 + .../ui/debug/DebugInfoViewModelGraph.kt | 5 + .../com/wire/android/ui/debug/DebugScreen.kt | 5 + .../securityproviders/AppPathsProvider.kt | 51 +++++++ .../securityproviders/SecurityProvider.kt | 30 ++++ .../SecurityProviderListItem.kt | 128 ++++++++++++++++++ .../SecurityProvidersScreen.kt | 92 +++++++++++++ .../SecurityProvidersViewModel.kt | 69 ++++++++++ app/src/main/res/values/strings.xml | 10 ++ 11 files changed, 417 insertions(+) create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/AppPathsProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProviderListItem.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt diff --git a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt index bc4c84b5bd7..4f64fa35e47 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt @@ -31,6 +31,7 @@ import com.wire.android.ui.debug.UserDebugViewModel import com.wire.android.ui.debug.conversation.DebugConversationViewModel import com.wire.android.ui.debug.cryptostats.ConversationCryptoStatsViewModel import com.wire.android.ui.debug.featureflags.DebugFeatureFlagsViewModel +import com.wire.android.ui.debug.securityproviders.SecurityProvidersViewModel import com.wire.android.ui.MiscViewModelFactory import com.wire.android.ui.analytics.AnalyticsUsageViewModel import com.wire.android.ui.authentication.AuthenticationManualViewModelFactory @@ -365,6 +366,12 @@ object WireMetroViewModelBindings { fun debugFeatureFlagsViewModel(factory: DebugInfoViewModelFactory): ViewModel = factory.debugFeatureFlagsViewModel() + @Provides + @IntoMap + @ViewModelKey(SecurityProvidersViewModel::class) + fun securityProvidersViewModel(factory: DebugInfoViewModelFactory): ViewModel = + factory.securityProvidersViewModel() + @Provides @IntoMap @ViewModelKey(WhatsNewViewModel::class) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt index bd768c96a3f..9346d3d7520 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt @@ -69,6 +69,7 @@ fun DebugDataOptions( onCopyText: (String) -> Unit, onShowFeatureFlags: () -> Unit, onShowCryptoStats: () -> Unit, + onShowSecurityProviders: () -> Unit, viewModel: DebugDataOptionsViewModel, ) { LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = viewModel.infoMessage) @@ -90,6 +91,7 @@ fun DebugDataOptions( onResendFCMToken = viewModel::forceSendFCMToken, onShowFeatureFlags = onShowFeatureFlags, onShowCryptoStats = onShowCryptoStats, + onShowSecurityProviders = onShowSecurityProviders, onRepairFaultyRemovalKeys = viewModel::repairFaultRemovalKeys, ) } @@ -114,6 +116,7 @@ fun DebugDataOptionsContent( onResendFCMToken: () -> Unit, onShowFeatureFlags: () -> Unit, onShowCryptoStats: () -> Unit, + onShowSecurityProviders: () -> Unit, onRepairFaultyRemovalKeys: () -> Unit, modifier: Modifier = Modifier, ) { @@ -210,6 +213,15 @@ fun DebugDataOptionsContent( trailingIcon = commonR.drawable.ic_arrow_right, ) + SettingsItem( + text = stringResource(R.string.debug_settings_security_providers), + onRowPressed = Clickable( + enabled = true, + onClick = onShowSecurityProviders + ), + trailingIcon = commonR.drawable.ic_arrow_right, + ) + if (BuildConfig.PRIVATE_BUILD && BuildConfig.DEBUG_SCREEN_ENABLED) { E2EICertificateEnrollmentSection( expirationInputState = e2eiCertificateExpirationInputState, @@ -414,6 +426,7 @@ fun PreviewOtherDebugOptions() = WireTheme { onResendFCMToken = {}, onShowFeatureFlags = {}, onShowCryptoStats = {}, + onShowSecurityProviders = {}, onRepairFaultyRemovalKeys = {} ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugInfoViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugInfoViewModelFactory.kt index 8f32c033419..40e3cf0e9d9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugInfoViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugInfoViewModelFactory.kt @@ -25,6 +25,8 @@ import com.wire.android.di.CurrentAccount import com.wire.android.ui.debug.conversation.DebugConversationViewModel import com.wire.android.ui.debug.cryptostats.ConversationCryptoStatsViewModel import com.wire.android.ui.debug.featureflags.DebugFeatureFlagsViewModel +import com.wire.android.ui.debug.securityproviders.AppPathsProvider +import com.wire.android.ui.debug.securityproviders.SecurityProvidersViewModel import com.wire.android.ui.home.settings.about.dependencies.DependenciesViewModel import com.wire.android.ui.home.settings.about.licenses.LicensesViewModel import com.wire.android.ui.home.whatsnew.WhatsNewViewModel @@ -151,6 +153,11 @@ class DebugInfoViewModelFactory @Inject constructor( getFeatureConfig = getFeatureConfig, ) + fun securityProvidersViewModel() = SecurityProvidersViewModel( + appPathsProvider = AppPathsProvider(context = context, currentAccount = currentAccount), + dispatcherProvider = dispatcherProvider, + ) + fun whatsNewViewModel() = WhatsNewViewModel(context = context) fun aboutThisAppViewModel() = AboutThisAppViewModel(context = context) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugInfoViewModelGraph.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugInfoViewModelGraph.kt index 315422bdc17..9be43bccc14 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugInfoViewModelGraph.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugInfoViewModelGraph.kt @@ -26,6 +26,7 @@ import com.wire.android.di.metro.sessionKeyedMetroViewModel import com.wire.android.ui.debug.conversation.DebugConversationViewModel import com.wire.android.ui.debug.cryptostats.ConversationCryptoStatsViewModel import com.wire.android.ui.debug.featureflags.DebugFeatureFlagsViewModel +import com.wire.android.ui.debug.securityproviders.SecurityProvidersViewModel import com.wire.android.ui.home.settings.about.dependencies.DependenciesViewModel import com.wire.android.ui.home.settings.about.licenses.LicensesViewModel import com.wire.android.ui.home.whatsnew.WhatsNewViewModel @@ -75,6 +76,10 @@ fun conversationCryptoStatsViewModel(): ConversationCryptoStatsViewModel = fun debugFeatureFlagsViewModel(): DebugFeatureFlagsViewModel = debugInfoViewModel() +@Composable +fun securityProvidersViewModel(): SecurityProvidersViewModel = + debugInfoViewModel() + @Composable fun whatsNewViewModel(): WhatsNewViewModel = debugInfoViewModel() diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt index 987027dd6e4..16d139590f3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt @@ -53,6 +53,7 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.ramcosta.composedestinations.generated.app.destinations.ConversationCryptoStatsScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.DebugFeatureFlagsScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.ImportMediaScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.SecurityProvidersScreenDestination import com.wire.android.ui.common.rowitem.SectionHeader import com.wire.android.ui.home.settings.SettingsItem import com.wire.android.ui.home.settings.backup.BackupAndRestoreDialog @@ -92,6 +93,9 @@ fun DebugScreen( onShowCryptoStats = { navigator.navigate(NavigationCommand(ConversationCryptoStatsScreenDestination)) }, + onShowSecurityProviders = { + navigator.navigate(NavigationCommand(SecurityProvidersScreenDestination)) + }, viewModel = debugDataOptionsViewModel, ) }, @@ -293,6 +297,7 @@ internal fun PreviewUserDebugContent() = WireTheme { onCopyText = it::copyToClipboard, onShowFeatureFlags = {}, onShowCryptoStats = {}, + onShowSecurityProviders = {}, viewModel = object : DebugDataOptionsViewModel {}, ) }, diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/AppPathsProvider.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/AppPathsProvider.kt new file mode 100644 index 00000000000..06061075724 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/AppPathsProvider.kt @@ -0,0 +1,51 @@ +/* + * 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 androidx.annotation.StringRes +import com.wire.android.R +import com.wire.kalium.logic.data.user.UserId + +class AppPathsProvider( + private val context: Context, + private val currentAccount: UserId, +) { + operator fun invoke(): List = with(context) { + val accountSuffix = "${currentAccount.domain}/${currentAccount.value}" + listOf( + AppPathEntry(R.string.debug_settings_app_path_assets, "$filesDir/$accountSuffix"), + AppPathEntry(R.string.debug_settings_app_path_cache, "$cacheDir/$accountSuffix"), + AppPathEntry(R.string.debug_settings_app_path_files_dir, filesDir.absolutePath), + AppPathEntry(R.string.debug_settings_app_path_cache_dir, cacheDir.absolutePath), + AppPathEntry(R.string.debug_settings_app_path_databases_dir, getDatabasePath(DATABASE_NAME_PROBE).parent.orEmpty()), + AppPathEntry(R.string.debug_settings_app_path_no_backup_dir, noBackupFilesDir.absolutePath), + AppPathEntry(R.string.debug_settings_app_path_external_files_dir, getExternalFilesDir(null)?.absolutePath.orEmpty()), + ) + } + + private companion object { + /** Only used to resolve the databases directory, the file itself never has to exist. */ + const val DATABASE_NAME_PROBE = "probe" + } +} + +data class AppPathEntry( + @StringRes val labelRes: Int, + val path: String, +) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvider.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvider.kt new file mode 100644 index 00000000000..d695c13128a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvider.kt @@ -0,0 +1,30 @@ +/* + * 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 + +data class SecurityProvider( + val name: String, + val version: String, + val info: String, + val entries: List, +) + +data class KeyValueEntry( + val key: String, + val value: String, +) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProviderListItem.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProviderListItem.kt new file mode 100644 index 00000000000..65937f4808d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProviderListItem.kt @@ -0,0 +1,128 @@ +/* + * 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 androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.model.Clickable +import com.wire.android.ui.common.R as commonR +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.rowitem.RowItem +import com.wire.android.ui.common.typography +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +fun SecurityProviderListItem( + provider: SecurityProvider, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + + Column(modifier = modifier.fillMaxWidth()) { + RowItem( + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = dimensions().spacing48x) + .padding(dimensions().spacing16x), + clickable = Clickable { expanded = !expanded } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Text( + text = listOf(provider.name, provider.version).filter(String::isNotBlank).joinToString(" "), + style = typography().body02, + color = colorsScheme().onBackground, + ) + Text( + text = provider.info, + style = typography().label01, + color = colorsScheme().secondaryText, + ) + Text( + text = stringResource(R.string.debug_settings_security_providers_entry_count, provider.entries.size), + style = typography().label01, + color = colorsScheme().secondaryText, + ) + } + + Icon( + painter = painterResource( + if (expanded) commonR.drawable.ic_collapse else commonR.drawable.ic_expand_more + ), + contentDescription = null, + tint = colorsScheme().onSurfaceVariant, + ) + } + + AnimatedVisibility(expanded) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = dimensions().spacing16x, + end = dimensions().spacing16x, + bottom = dimensions().spacing16x, + ) + ) { + provider.entries.forEach { entry -> + Text( + text = "${entry.key}: ${entry.value}", + modifier = Modifier.fillMaxWidth(), + style = typography().label01, + color = colorsScheme().secondaryText, + ) + } + } + } + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewSecurityProviderListItem() = WireTheme { + SecurityProviderListItem( + provider = SecurityProvider( + name = "AndroidKeyStore", + version = "1.0", + info = "Android KeyStore security provider", + entries = listOf( + KeyValueEntry("KeyStore.AndroidKeyStore", "android.security.keystore2.AndroidKeyStoreProvider"), + KeyValueEntry("Signature.SHA256withECDSA", "android.security.keystore2.AndroidKeyStoreSignatureSpi\$ECDSA"), + ) + ) + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt new file mode 100644 index 00000000000..2c561a7f68a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt @@ -0,0 +1,92 @@ +/* + * 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 androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.annotation.app.WireRootDestination +import com.wire.android.ui.common.rememberTopBarElevationState +import com.wire.android.ui.common.rowitem.SectionHeader +import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.topappbar.NavigationIconType +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar +import com.wire.android.ui.common.topappbar.WireTopAppBarTitle +import com.wire.android.ui.common.typography +import com.wire.android.ui.debug.securityProvidersViewModel +import com.wire.android.ui.home.settings.SettingsItem + +@WireRootDestination +@Composable +fun SecurityProvidersScreen( + navigator: Navigator, + modifier: Modifier = Modifier, + viewModel: SecurityProvidersViewModel = securityProvidersViewModel(), +) { + val scrollState = rememberScrollState() + + WireScaffold( + modifier = modifier, + topBar = { + WireCenterAlignedTopAppBar( + elevation = scrollState.rememberTopBarElevationState().value, + titleContent = { + WireTopAppBarTitle( + title = stringResource(R.string.debug_settings_security_providers), + style = typography().title01, + maxLines = 2 + ) + }, + navigationIconType = NavigationIconType.Close(R.string.content_description_conversation_details_close_btn), + onNavigationPressed = { + navigator.navigateBack() + } + ) + }, + content = { paddingValues -> + + val state by viewModel.state.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(scrollState) + ) { + SectionHeader(stringResource(R.string.debug_settings_app_paths)) + state.appPaths.forEach { entry -> + SettingsItem(title = stringResource(entry.labelRes), text = entry.path) + } + + SectionHeader(stringResource(R.string.debug_settings_security_providers)) + state.providers?.forEach { provider -> + SecurityProviderListItem(provider) + } + } + } + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt new file mode 100644 index 00000000000..7534cd35a18 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt @@ -0,0 +1,69 @@ +/* + * 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 androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.util.dispatchers.DispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.security.Provider +import java.security.Security + +class SecurityProvidersViewModel( + private val appPathsProvider: AppPathsProvider, + private val dispatcherProvider: DispatcherProvider, +) : ViewModel() { + + private val _state = MutableStateFlow(SecurityProvidersViewState()) + val state = _state.asStateFlow() + + init { + viewModelScope.launch { + val providers = withContext(dispatcherProvider.default()) { + Security.getProviders().map { provider -> + SecurityProvider( + name = provider.name, + version = provider.versionString(), + info = provider.info, + entries = provider.entries + .map { (key, value) -> KeyValueEntry(key.toString(), value.toString()) } + .sortedBy(KeyValueEntry::key) + ) + } + } + _state.update { current -> current.copy(appPaths = appPathsProvider(), providers = providers) } + } + } +} + +/** + * `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 fun Provider.versionString(): String = getProperty(PROVIDER_VERSION_PROPERTY).orEmpty() + +data class SecurityProvidersViewState( + val appPaths: List = emptyList(), + val providers: List? = null, +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dd67d3aeae8..b476f936541 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1785,6 +1785,16 @@ In group conversations, the group admin can overwrite this setting. Reset Feature Flags Conversation Crypto Stats + Security Providers + App Paths + Assets path + Cache path + Files dir + Cache dir + Databases dir + No backup dir + External files dir + %1$d entries Step %1$d of 4 From ab16e8fbc3b58769e159721672d240322aaa8012 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 11 Aug 2026 15:35:23 +0200 Subject: [PATCH 2/8] chore: conflicts --- app/src/main/kotlin/com/wire/android/di/AppModule.kt | 9 +++++++++ .../wire/android/di/metro/WireMetroViewModelBindings.kt | 3 +-- .../securityproviders/SecurityProvidersViewModel.kt | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/AppModule.kt b/app/src/main/kotlin/com/wire/android/di/AppModule.kt index 4e0d583b11e..35515e12218 100644 --- a/app/src/main/kotlin/com/wire/android/di/AppModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/AppModule.kt @@ -30,6 +30,7 @@ import com.wire.android.feature.analytics.AnonymousAnalyticsManager 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.home.conversations.MessageSharedState import com.wire.android.ui.home.messagecomposer.location.LocationPickerParameters import com.wire.android.util.CurrentTimeProvider @@ -39,6 +40,7 @@ import com.wire.android.util.dispatchers.DefaultDispatcherProvider import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.AndroidUiTextResolver import com.wire.android.util.ui.UiTextResolver +import com.wire.kalium.logic.data.user.UserId import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.Provides import dev.zacsweers.metro.Named @@ -133,4 +135,11 @@ object AppModule { @Provides fun provideGetMediaMetadataUseCase(): GetMediaMetadataUseCase = GetMediaMetadataUseCaseImpl() + + @Provides + fun provideAppPathsProvider(@ApplicationContext context: Context, @CurrentAccount currentAccount: UserId): AppPathsProvider = + AppPathsProvider( + context = context, + currentAccount = currentAccount + ) } diff --git a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt index 1e0b4dd63a0..3496c60be76 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroViewModelBindings.kt @@ -364,8 +364,7 @@ object WireMetroViewModelBindings { @Provides @IntoMap @ViewModelKey(SecurityProvidersViewModel::class) - fun securityProvidersViewModel(viewModel: DebugFeatureFlagsViewModel): ViewModel = - factory.securityProvidersViewModel() + fun securityProvidersViewModel(viewModel: SecurityProvidersViewModel): ViewModel = viewModel @Provides @IntoMap diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt index 7534cd35a18..b8bd59b0d39 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.debug.securityproviders import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.util.dispatchers.DispatcherProvider +import dev.zacsweers.metro.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -28,7 +29,7 @@ import kotlinx.coroutines.withContext import java.security.Provider import java.security.Security -class SecurityProvidersViewModel( +class SecurityProvidersViewModel @Inject constructor( private val appPathsProvider: AppPathsProvider, private val dispatcherProvider: DispatcherProvider, ) : ViewModel() { From 98f2255812dad8aa6850eee5afe2836b2c038890 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 13 Aug 2026 12:06:53 +0200 Subject: [PATCH 3/8] feat: remove security providers section --- .../SecurityProviderListItem.kt | 128 ------------------ .../SecurityProvidersScreen.kt | 5 - .../SecurityProvidersViewModel.kt | 18 +-- 3 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProviderListItem.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProviderListItem.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProviderListItem.kt deleted file mode 100644 index 65937f4808d..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProviderListItem.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.wire.android.R -import com.wire.android.model.Clickable -import com.wire.android.ui.common.R as commonR -import com.wire.android.ui.common.colorsScheme -import com.wire.android.ui.common.dimensions -import com.wire.android.ui.common.rowitem.RowItem -import com.wire.android.ui.common.typography -import com.wire.android.ui.theme.WireTheme -import com.wire.android.util.ui.PreviewMultipleThemes - -@Composable -fun SecurityProviderListItem( - provider: SecurityProvider, - modifier: Modifier = Modifier, -) { - var expanded by remember { mutableStateOf(false) } - - Column(modifier = modifier.fillMaxWidth()) { - RowItem( - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = dimensions().spacing48x) - .padding(dimensions().spacing16x), - clickable = Clickable { expanded = !expanded } - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - Text( - text = listOf(provider.name, provider.version).filter(String::isNotBlank).joinToString(" "), - style = typography().body02, - color = colorsScheme().onBackground, - ) - Text( - text = provider.info, - style = typography().label01, - color = colorsScheme().secondaryText, - ) - Text( - text = stringResource(R.string.debug_settings_security_providers_entry_count, provider.entries.size), - style = typography().label01, - color = colorsScheme().secondaryText, - ) - } - - Icon( - painter = painterResource( - if (expanded) commonR.drawable.ic_collapse else commonR.drawable.ic_expand_more - ), - contentDescription = null, - tint = colorsScheme().onSurfaceVariant, - ) - } - - AnimatedVisibility(expanded) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - start = dimensions().spacing16x, - end = dimensions().spacing16x, - bottom = dimensions().spacing16x, - ) - ) { - provider.entries.forEach { entry -> - Text( - text = "${entry.key}: ${entry.value}", - modifier = Modifier.fillMaxWidth(), - style = typography().label01, - color = colorsScheme().secondaryText, - ) - } - } - } - } -} - -@PreviewMultipleThemes -@Composable -fun PreviewSecurityProviderListItem() = WireTheme { - SecurityProviderListItem( - provider = SecurityProvider( - name = "AndroidKeyStore", - version = "1.0", - info = "Android KeyStore security provider", - entries = listOf( - KeyValueEntry("KeyStore.AndroidKeyStore", "android.security.keystore2.AndroidKeyStoreProvider"), - KeyValueEntry("Signature.SHA256withECDSA", "android.security.keystore2.AndroidKeyStoreSignatureSpi\$ECDSA"), - ) - ) - ) -} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt index 2c561a7f68a..672f3a89aa8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt @@ -81,11 +81,6 @@ fun SecurityProvidersScreen( state.appPaths.forEach { entry -> SettingsItem(title = stringResource(entry.labelRes), text = entry.path) } - - SectionHeader(stringResource(R.string.debug_settings_security_providers)) - state.providers?.forEach { provider -> - SecurityProviderListItem(provider) - } } } ) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt index b8bd59b0d39..7a3d5011ac3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt @@ -19,19 +19,15 @@ package com.wire.android.ui.debug.securityproviders import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.util.dispatchers.DispatcherProvider import dev.zacsweers.metro.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.security.Provider -import java.security.Security class SecurityProvidersViewModel @Inject constructor( private val appPathsProvider: AppPathsProvider, - private val dispatcherProvider: DispatcherProvider, ) : ViewModel() { private val _state = MutableStateFlow(SecurityProvidersViewState()) @@ -39,19 +35,7 @@ class SecurityProvidersViewModel @Inject constructor( init { viewModelScope.launch { - val providers = withContext(dispatcherProvider.default()) { - Security.getProviders().map { provider -> - SecurityProvider( - name = provider.name, - version = provider.versionString(), - info = provider.info, - entries = provider.entries - .map { (key, value) -> KeyValueEntry(key.toString(), value.toString()) } - .sortedBy(KeyValueEntry::key) - ) - } - } - _state.update { current -> current.copy(appPaths = appPathsProvider(), providers = providers) } + _state.update { current -> current.copy(appPaths = appPathsProvider()) } } } } From 44cfbdb2af87601410a88f8a7f5c1c474343f533 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 13 Aug 2026 14:54:00 +0200 Subject: [PATCH 4/8] feat: update strings.xml --- .../main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt | 2 +- .../ui/debug/securityproviders/SecurityProvidersScreen.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt index 9346d3d7520..39478c47a2e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt @@ -214,7 +214,7 @@ fun DebugDataOptionsContent( ) SettingsItem( - text = stringResource(R.string.debug_settings_security_providers), + text = stringResource(R.string.debug_settings_security_diagnostics), onRowPressed = Clickable( enabled = true, onClick = onShowSecurityProviders diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt index 672f3a89aa8..8a490c2520f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt @@ -56,7 +56,7 @@ fun SecurityProvidersScreen( elevation = scrollState.rememberTopBarElevationState().value, titleContent = { WireTopAppBarTitle( - title = stringResource(R.string.debug_settings_security_providers), + title = stringResource(R.string.debug_settings_security_diagnostics), style = typography().title01, maxLines = 2 ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b476f936541..4b97530408d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1785,7 +1785,7 @@ In group conversations, the group admin can overwrite this setting. Reset Feature Flags Conversation Crypto Stats - Security Providers + Security Diagnostics App Paths Assets path Cache path From 5eab2049985a42ae9289174f2857e919c0adab8d Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 14 Aug 2026 12:32:55 +0200 Subject: [PATCH 5/8] feat: show network details --- .../kotlin/com/wire/android/di/AppModule.kt | 5 + .../NetworkDiagnosticsProvider.kt | 116 ++++++++++++++++++ .../SecurityProvidersScreen.kt | 59 +++++++++ .../SecurityProvidersViewModel.kt | 37 ++++-- app/src/main/res/values/strings.xml | 13 ++ 5 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt diff --git a/app/src/main/kotlin/com/wire/android/di/AppModule.kt b/app/src/main/kotlin/com/wire/android/di/AppModule.kt index 35515e12218..0059fee6eda 100644 --- a/app/src/main/kotlin/com/wire/android/di/AppModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/AppModule.kt @@ -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 @@ -142,4 +143,8 @@ object AppModule { context = context, currentAccount = currentAccount ) + + @Provides + fun provideNetworkDiagnosticsProvider(@ApplicationContext context: Context): NetworkDiagnosticsProvider = + NetworkDiagnosticsProvider(context = context) } diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt new file mode 100644 index 00000000000..44d06c16643 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/NetworkDiagnosticsProvider.kt @@ -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, +) { + 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, + ) + + val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) + val networkTypes = NETWORK_TYPE_NAMES.filter { (transport, _) -> capabilities?.hasTransport(transport) == true } + .values + .toList() + val isVpn = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true + + val addresses = when { + host.isEmpty() -> AddressResolution.Failed + else -> runCatching { activeNetwork.getAllByName(host) } + .fold( + onSuccess = { resolved -> AddressResolution.Resolved(resolved.mapNotNull { it.toResolvedAddress() }) }, + onFailure = { error -> + if (error is IOException) { + appLogger.w("Could not resolve backend host through the active network", error) + AddressResolution.Failed + } else { + throw error + } + } + ) + } + + return NetworkDiagnostics( + isVpn = isVpn, + networkTypes = networkTypes, + backendHost = host, + addresses = addresses, + ) + } + + 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", + ) + } +} + +data class NetworkDiagnostics( + val isVpn: Boolean, + val networkTypes: List, + val backendHost: String, + val addresses: AddressResolution, +) + +sealed interface AddressResolution { + data class Resolved(val addresses: List) : AddressResolution + data object NoActiveNetwork : AddressResolution + data object Failed : AddressResolution +} + +data class ResolvedAddress( + val address: String, + val version: IpVersion, +) + +enum class IpVersion { V4, V6 } + +private fun InetAddress.toResolvedAddress(): ResolvedAddress? = hostAddress?.let { address -> + ResolvedAddress( + address = address, + version = if (this is Inet6Address) IpVersion.V6 else IpVersion.V4, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt index 8a490c2520f..8c0039d207d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersScreen.kt @@ -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 @@ -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 +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt index 7a3d5011ac3..4eb8057e385 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt @@ -19,15 +19,24 @@ package com.wire.android.ui.debug.securityproviders 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, ) : ViewModel() { private val _state = MutableStateFlow(SecurityProvidersViewState()) @@ -37,18 +46,30 @@ class SecurityProvidersViewModel @Inject constructor( viewModelScope.launch { _state.update { current -> current.copy(appPaths = appPathsProvider()) } } + viewModelScope.launch { + observeNetworkDiagnostics() + } } -} -/** - * `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) } } + } -private fun Provider.versionString(): String = getProperty(PROVIDER_VERSION_PROPERTY).orEmpty() + private suspend fun apiUrl(): String? = when (val result = selfServerConfig()) { + is SelfServerConfigUseCase.Result.Success -> result.serverLinks.links.api + is SelfServerConfigUseCase.Result.Failure -> { + appLogger.w("Could not read the server config, skipping network diagnostics") + null + } + } +} data class SecurityProvidersViewState( val appPaths: List = emptyList(), + val network: NetworkDiagnostics? = null, val providers: List? = null, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4b97530408d..402aa082d63 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1795,6 +1795,19 @@ In group conversations, the group admin can overwrite this setting. No backup dir External files dir %1$d entries + Network + VPN + Active + Not active + Network type + Backend host + Resolved addresses + Resolved address (%1$s) + IPv4 + IPv6 + No active network + Could not be resolved + Unknown Step %1$d of 4 From 831d3a6400f8e21815940b50721d83f6a79a7b0e Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 18 Aug 2026 12:18:52 +0200 Subject: [PATCH 6/8] chore: cleanup --- .../securityproviders/SecurityProvidersViewModel.kt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt index 61fb7aafacc..a895cb373be 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt @@ -30,7 +30,6 @@ 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, @@ -51,14 +50,7 @@ class SecurityProvidersViewModel @Inject constructor( observeNetworkDiagnostics() } } - /** - * `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 fun Provider.versionString(): String = getProperty(PROVIDER_VERSION_PROPERTY).orEmpty() - + private suspend fun observeNetworkDiagnostics() { val apiUrl = apiUrl() ?: return networkStateObserver.observeCurrentNetwork() From 61e32b988b1ce49e6b3e1c7fe1309210b4376277 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 18 Aug 2026 12:44:16 +0200 Subject: [PATCH 7/8] chore: cleanup --- .../ui/debug/securityproviders/SecurityProvidersViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt index a895cb373be..4eb8057e385 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/securityproviders/SecurityProvidersViewModel.kt @@ -50,7 +50,7 @@ class SecurityProvidersViewModel @Inject constructor( observeNetworkDiagnostics() } } - + private suspend fun observeNetworkDiagnostics() { val apiUrl = apiUrl() ?: return networkStateObserver.observeCurrentNetwork() From a7b24e22aa924a58b25e913d7b028ee8ad80a141 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 18 Aug 2026 13:47:47 +0200 Subject: [PATCH 8/8] chore: cleanup --- app/stability/app-devDebug.stability | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/stability/app-devDebug.stability b/app/stability/app-devDebug.stability index 876954f9789..ae577be8ab8 100644 --- a/app/stability/app-devDebug.stability +++ b/app/stability/app-devDebug.stability @@ -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 @@ -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: @@ -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