diff --git a/.github/workflows/build-unified.yml b/.github/workflows/build-unified.yml index 607afc993fc..2f30f28c22d 100644 --- a/.github/workflows/build-unified.yml +++ b/.github/workflows/build-unified.yml @@ -222,6 +222,7 @@ jobs: DATADOG_APP_ID: ${{ secrets.DATADOG_APP_ID }} DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} ENABLE_SIGNING: ${{ secrets.ENABLE_SIGNING }} + DOMAIN_REMOVAL_KEYS_FOR_REPAIR: ${{ secrets.DOMAIN_REMOVAL_KEYS_FOR_REPAIR }} - name: Build AAB if: matrix.build-type == 'bundle' || matrix.build-type == 'both' @@ -231,6 +232,7 @@ jobs: DATADOG_APP_ID: ${{ secrets.DATADOG_APP_ID }} DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} ENABLE_SIGNING: ${{ secrets.ENABLE_SIGNING }} + DOMAIN_REMOVAL_KEYS_FOR_REPAIR: ${{ secrets.DOMAIN_REMOVAL_KEYS_FOR_REPAIR }} - name: Move version file for consistent artifact structure if: matrix.generate-version-file == true diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 095225e2b9c..85e1aa34826 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,6 +80,30 @@ android { val datadogAppIdKey = "DATADOG_APP_ID" val appId: String? = System.getenv(datadogAppIdKey) ?: project.getLocalProperty(datadogAppIdKey, null) buildConfigField("String", datadogAppIdKey, appId?.let { "\"$it\"" } ?: "null") + + // DOMAIN_REMOVAL_KEYS_FOR_REPAIR json format {"domain": ["some hex string key"]} + val domainRemovalKeysForRepair = "DOMAIN_REMOVAL_KEYS_FOR_REPAIR" + val domainKeysJson: String? = System.getenv(domainRemovalKeysForRepair) ?: project.getLocalProperty(domainRemovalKeysForRepair, null) + val domainKeysHashMap = if (domainKeysJson != null) { + try { + val jsonMap = groovy.json.JsonSlurper().parseText(domainKeysJson) as Map> + val javaMapEntries = jsonMap.entries.joinToString("; ") { (domain, keys) -> + val keysList = keys.joinToString("\", \"", "\"", "\"") + "put(\"$domain\", java.util.Arrays.asList($keysList))" + } + "new java.util.HashMap>(){{$javaMapEntries;}}" + } catch (e: Exception) { + println("Error parsing domain removal keys: ${e.message}") + "new java.util.HashMap>()" + } + } else { + "new java.util.HashMap>()" + } + buildConfigField( + "java.util.Map>", + domainRemovalKeysForRepair, + domainKeysHashMap + ) } // Most of the configuration is done in the build-logic // through the Wire Application convention plugin @@ -214,7 +238,7 @@ dependencies { implementation(libs.compose.activity) implementation(libs.compose.constraintLayout) implementation(libs.compose.runtime.liveData) - + implementation(libs.androidx.paging3) implementation(libs.androidx.paging3Compose) diff --git a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt index e0228f957ca..fd15acb73f3 100644 --- a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt @@ -63,7 +63,8 @@ class KaliumConfigsModule { limitTeamMembersFetchDuringSlowSync = BuildConfig.LIMIT_TEAM_MEMBERS_FETCH_DURING_SLOW_SYNC, isMlsResetEnabled = BuildConfig.IS_MLS_RESET_ENABLED, collaboraIntegration = BuildConfig.COLLABORA_INTEGRATION_ENABLED, - dbInvalidationControlEnabled = BuildConfig.DB_INVALIDATION_CONTROL_ENABLED + dbInvalidationControlEnabled = BuildConfig.DB_INVALIDATION_CONTROL_ENABLED, + domainWithFaultyKeysMap = BuildConfig.DOMAIN_REMOVAL_KEYS_FOR_REPAIR ) } } diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt index 87e4e41d736..3e115c11e9c 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.debug.BreakSessionUseCase import com.wire.kalium.logic.feature.debug.DebugScope import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase +import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -82,4 +83,9 @@ class DebugModule { @Provides fun provideDebugFeedConversationUseCase(debugScope: DebugScope) = debugScope.debugFeedConversationUseCase + + @ViewModelScoped + @Provides + fun provideRepairFaultyRemovalKeysUseCase(debugScope: DebugScope): RepairFaultyRemovalKeysUseCase = + debugScope.repairFaultyRemovalKeysUseCase } 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 63aee239677..1839e893458 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 @@ -32,17 +32,17 @@ import com.wire.android.R import com.wire.android.di.hiltViewModelScoped import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.model.Clickable -import com.wire.android.ui.common.rowitem.RowItemTemplate import com.wire.android.ui.common.WireDialog import com.wire.android.ui.common.WireDialogButtonProperties import com.wire.android.ui.common.WireDialogButtonType import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.button.WireSwitch import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.rowitem.RowItemTemplate +import com.wire.android.ui.common.rowitem.SectionHeader import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.snackbar.collectAndShowSnackbar import com.wire.android.ui.e2eiEnrollment.GetE2EICertificateUI -import com.wire.android.ui.common.rowitem.SectionHeader import com.wire.android.ui.home.settings.SettingsItem import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme @@ -78,6 +78,7 @@ fun DebugDataOptions( onResendFCMToken = viewModel::forceSendFCMToken, onEnableAsyncNotificationsChange = viewModel::enableAsyncNotifications, onShowFeatureFlags = onShowFeatureFlags, + onRepairFaultyRemovalKeys = viewModel::repairFaultRemovalKeys ) } @@ -98,6 +99,7 @@ fun DebugDataOptionsContent( checkCrlRevocationList: () -> Unit, onResendFCMToken: () -> Unit, onShowFeatureFlags: () -> Unit, + onRepairFaultyRemovalKeys: () -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -209,10 +211,9 @@ fun DebugDataOptionsContent( if (BuildConfig.PRIVATE_BUILD) { MLSOptions( - keyPackagesCount = state.keyPackagesCount, - mlsClientId = state.mslClientId, - mlsErrorMessage = state.mlsErrorMessage, - onCopyText = onCopyText + mlsInfoState = state.mlsInfoState, + onCopyText = onCopyText, + onRepairFaultyRemovalKeys = onRepairFaultyRemovalKeys ) } @@ -269,36 +270,58 @@ private fun GetE2EICertificateSwitch( //region MLS Options @Composable private fun MLSOptions( - keyPackagesCount: Int, - mlsClientId: String, - mlsErrorMessage: String, + mlsInfoState: MLSInfoState, onCopyText: (String) -> Unit, + onRepairFaultyRemovalKeys: () -> Unit, ) { SectionHeader(stringResource(R.string.label_mls_option_title)) - Column { - SettingsItem( - title = "Error Message", - text = mlsErrorMessage, - trailingIcon = null - ) - SettingsItem( - title = stringResource(R.string.label_key_packages_count), - text = keyPackagesCount.toString(), - trailingIcon = R.drawable.ic_copy, - onIconPressed = Clickable( - enabled = true, - onClick = { onCopyText(keyPackagesCount.toString()) } + with(mlsInfoState) { + Column { + SettingsItem( + title = "Error Message", + text = mlsErrorMessage, + trailingIcon = null ) - ) - SettingsItem( - title = stringResource(R.string.label_mls_client_id), - text = mlsClientId, - trailingIcon = R.drawable.ic_copy, - onIconPressed = Clickable( - enabled = true, - onClick = { onCopyText(mlsClientId) } + SettingsItem( + title = stringResource(R.string.label_key_packages_count), + text = keyPackagesCount.toString(), + trailingIcon = R.drawable.ic_copy, + onIconPressed = Clickable( + enabled = true, + onClick = { onCopyText(keyPackagesCount.toString()) } + ) ) - ) + SettingsItem( + title = stringResource(R.string.label_mls_client_id), + text = mlsClientId, + trailingIcon = R.drawable.ic_copy, + onIconPressed = Clickable( + enabled = true, + onClick = { onCopyText(mlsClientId) } + ) + ) + RowItemTemplate( + modifier = Modifier.wrapContentWidth(), + title = { + Text( + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.onBackground, + text = stringResource(R.string.label_mls_repair_faulty_keys), + modifier = Modifier.padding(start = dimensions().spacing8x) + ) + }, + actions = { + WirePrimaryButton( + minSize = MaterialTheme.wireDimensions.buttonMediumMinSize, + minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, + onClick = onRepairFaultyRemovalKeys, + text = stringResource(R.string.debug_settings_force_repair_faulty_keys), + fillMaxWidth = false, + loading = isLoadingRepair + ) + } + ) + } } } //endregion @@ -457,7 +480,9 @@ private fun DebugToolsOptions( ) } ) - EnableAsyncNotifications(isAsyncNotificationsEnabled, onEnableAsyncNotificationsChange) + if (BuildConfig.DEBUG) { + EnableAsyncNotifications(isAsyncNotificationsEnabled, onEnableAsyncNotificationsChange) + } } } } @@ -530,9 +555,12 @@ fun PreviewOtherDebugOptions() = WireTheme { buildVariant = "debug", onCopyText = {}, state = DebugDataOptionsState( - keyPackagesCount = 10, - mslClientId = "clientId", - mlsErrorMessage = "error", + mlsInfoState = MLSInfoState( + mlsClientId = "mlsClientId", + mlsErrorMessage = "-", + keyPackagesCount = 42, + isLoadingRepair = false + ), debugId = "debugId", commitish = "commitish" ), @@ -546,5 +574,6 @@ fun PreviewOtherDebugOptions() = WireTheme { onResendFCMToken = {}, onEnableAsyncNotificationsChange = {}, onShowFeatureFlags = {}, + onRepairFaultyRemovalKeys = {} ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt index adf4ca59c9b..86057cd198f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt @@ -20,9 +20,6 @@ package com.wire.android.ui.debug data class DebugDataOptionsState( val isEventProcessingDisabled: Boolean = false, val isAsyncNotificationsEnabled: Boolean = false, - val keyPackagesCount: Int = 0, - val mslClientId: String = "null", - val mlsErrorMessage: String = "null", val debugId: String = "null", val commitish: String = "null", val certificate: String = "null", @@ -32,4 +29,12 @@ data class DebugDataOptionsState( val isFederationEnabled: Boolean = false, val currentApiVersion: String = "null", val defaultProtocol: String = "null", + val mlsInfoState: MLSInfoState = MLSInfoState() +) + +data class MLSInfoState( + val mlsClientId: String = "null", + val mlsErrorMessage: String = "-", + val keyPackagesCount: Int = 0, + val isLoadingRepair: Boolean = false, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt index d566ba8c281..8d2f5da6472 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt @@ -23,6 +23,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wire.android.BuildConfig.DOMAIN_REMOVAL_KEYS_FOR_REPAIR +import com.wire.android.appLogger import com.wire.android.di.CurrentAccount import com.wire.android.di.ScopedArgs import com.wire.android.di.ViewModelScopedPreview @@ -40,8 +42,11 @@ import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.analytics.GetCurrentAnalyticsTrackingIdentifierUseCase import com.wire.kalium.logic.feature.debug.ObserveIsConsumableNotificationsEnabledUseCase +import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase +import com.wire.kalium.logic.feature.debug.RepairResult import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsResult import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase +import com.wire.kalium.logic.feature.debug.TargetedRepairParam import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountResult @@ -76,6 +81,8 @@ interface DebugDataOptionsViewModel { fun disableEventProcessing(disabled: Boolean) {} fun forceSendFCMToken() {} fun enableAsyncNotifications(enabled: Boolean) {} + + fun repairFaultRemovalKeys() {} } @Suppress("LongParameterList", "TooManyFunctions") @@ -95,6 +102,7 @@ class DebugDataOptionsViewModelImpl private val getDefaultProtocolUseCase: GetDefaultProtocolUseCase, private val observeAsyncNotificationsEnabled: ObserveIsConsumableNotificationsEnabledUseCase, private val startUsingAsyncNotifications: StartUsingAsyncNotificationsUseCase, + private val repairFaultyRemovalKeys: RepairFaultyRemovalKeysUseCase, ) : ViewModel(), DebugDataOptionsViewModel { override var state by mutableStateOf( @@ -242,6 +250,36 @@ class DebugDataOptionsViewModelImpl } } + override fun repairFaultRemovalKeys() { + viewModelScope.launch { + state = state.copy(mlsInfoState = state.mlsInfoState.copy(isLoadingRepair = true)) + val (domain, faultyKey) = DOMAIN_REMOVAL_KEYS_FOR_REPAIR.entries.firstOrNull { it.key == currentAccount.domain } + ?: run { + appLogger.w("No faulty removal keys configured for repair") + _infoMessage.emit(UIText.DynamicString("No faulty removal keys configured for repair")) + state = state.copy(mlsInfoState = state.mlsInfoState.copy(isLoadingRepair = false)) + return@launch + } + + val result = repairFaultyRemovalKeys( + param = TargetedRepairParam( + domain = domain, + faultyKeys = faultyKey + ) + ) + when (result) { + RepairResult.Error -> appLogger.e("Error occurred during repair of faulty removal keys") + RepairResult.NoConversationsToRepair -> appLogger.i("No conversations to repair") + RepairResult.RepairNotNeeded -> appLogger.i("Repair not needed") + is RepairResult.RepairPerformed -> { + _infoMessage.emit(UIText.DynamicString("Reset finalized")) + appLogger.i("Repair performed: ${result.toLogString()}") + } + } + state = state.copy(mlsInfoState = state.mlsInfoState.copy(isLoadingRepair = false)) + } + } + override fun forceSendFCMToken() { viewModelScope.launch { withContext(dispatcherProvider.io()) { @@ -276,22 +314,24 @@ class DebugDataOptionsViewModelImpl when (it) { is MLSKeyPackageCountResult.Success -> { state = state.copy( - keyPackagesCount = it.count, - mslClientId = it.clientId.value + mlsInfoState = state.mlsInfoState.copy( + keyPackagesCount = it.count, + mlsClientId = it.clientId.value + ) ) } is MLSKeyPackageCountResult.Failure.NetworkCallFailure -> { - state = state.copy(mlsErrorMessage = "Network Error!") + state = state.copy(mlsInfoState = state.mlsInfoState.copy(mlsErrorMessage = "Network Error!")) } is MLSKeyPackageCountResult.Failure.FetchClientIdFailure -> { - state = state.copy(mlsErrorMessage = "ClientId Fetch Error!") + state = state.copy(mlsInfoState = state.mlsInfoState.copy(mlsErrorMessage = "ClientId Fetch Error!")) } is MLSKeyPackageCountResult.Failure.Generic -> {} MLSKeyPackageCountResult.Failure.NotEnabled -> { - state = state.copy(mlsErrorMessage = "Not Enabled!") + state = state.copy(mlsInfoState = state.mlsInfoState.copy(mlsErrorMessage = "Not Enabled!")) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5cdada488b6..8bc21a091f5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1351,6 +1351,7 @@ In group conversations, the group admin can overwrite this setting. Proteus ID Key-packages count MLS Client ID + Initiate reset of affected MLS groups Connect with others or create a new group to start collaborating! Select your favorite conversations, and you’ll find them here You are not part of any group conversation yet.\nStart a new conversation! @@ -1790,6 +1791,7 @@ In group conversations, the group admin can overwrite this setting. Force API versioning update ⚠️ Break Session Update + Reset Feature Flags diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt index 7b38eec3c1b..de88e98f82a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt @@ -38,6 +38,7 @@ import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.analytics.GetCurrentAnalyticsTrackingIdentifierUseCase import com.wire.kalium.logic.feature.debug.ObserveIsConsumableNotificationsEnabledUseCase +import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsResult import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase @@ -280,6 +281,9 @@ internal class DebugDataOptionsHiltArrangement { @MockK lateinit var startUsingAsyncNotifications: StartUsingAsyncNotificationsUseCase + @MockK + lateinit var repairFaultyRemovalKeysUseCase: RepairFaultyRemovalKeysUseCase + private val viewModel by lazy { DebugDataOptionsViewModelImpl( context = context, @@ -294,7 +298,8 @@ internal class DebugDataOptionsHiltArrangement { selfServerConfigUseCase = selfServerConfigUseCase, getDefaultProtocolUseCase = getDefaultProtocolUseCase, startUsingAsyncNotifications = startUsingAsyncNotifications, - observeAsyncNotificationsEnabled = observeIsConsumableNotificationsEnabled + observeAsyncNotificationsEnabled = observeIsConsumableNotificationsEnabled, + repairFaultyRemovalKeys = repairFaultyRemovalKeysUseCase ) } diff --git a/kalium b/kalium index cd57cddc8b0..ced16438c79 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit cd57cddc8b0a64f5d029d564bf23cf5bc50bf90b +Subproject commit ced16438c7988cbdd9e4d51b1aee22a02cacb260