diff --git a/.github/workflows/build-unified.yml b/.github/workflows/build-unified.yml index 0ee42dee49d..56629465d2c 100644 --- a/.github/workflows/build-unified.yml +++ b/.github/workflows/build-unified.yml @@ -224,6 +224,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' @@ -233,6 +234,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 0b48a3b4519..f59704cf9d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,6 +80,28 @@ 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("") { "put(\"${it.key}\", \"${it.value}\");" } + "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 @@ -211,7 +233,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/accountScoped/DebugModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt index 2e49b0df345..e9562206da1 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 @@ -77,4 +78,9 @@ class DebugModule { @ViewModelScoped @Provides fun provideFeatureConfigUseCase(debugScope: DebugScope): GetFeatureConfigUseCase = debugScope.getFeatureConfig + + @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..836a06a0adb 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() + ?: 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, + faultyKey = 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("Repair finalized")) + appLogger.i("Repair performed: $result") + } + } + 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 00c8b8b2e6a..7899a9f530c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1354,6 +1354,7 @@ In group conversations, the group admin can overwrite this setting. Proteus ID Key-packages count MLS Client ID + Repair faulty removal keys 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! @@ -1794,6 +1795,7 @@ In group conversations, the group admin can overwrite this setting. Force API versioning update ⚠️ Break Session Update + Repair 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/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt index 8f06b6d4062..6a3118aa43b 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt @@ -26,7 +26,7 @@ object AndroidSdk { object AndroidApp { const val id = "com.wire.android" - const val versionName = "4.17.2" + const val versionName = "4.17.3" val versionCode by lazy { Versionizer(_rootDir).versionCode } diff --git a/kalium b/kalium index fcce24a7fd2..2d069db408f 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit fcce24a7fd2a78956e49fc4b3c456bdc15716468 +Subproject commit 2d069db408fc5ec78a29f3c0506847c7d1ce94b4