From 7496480a4bb72af229b84421e306f1a5c32d7ad1 Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Sun, 21 Jan 2024 11:56:40 +0100 Subject: [PATCH 001/134] fix: add correct colors for dark mode when recording audio (WPB-4534) (#2599) --- .../main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt index 10702cfb0ba..67293aee148 100644 --- a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt +++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt @@ -340,8 +340,8 @@ private val DarkWireColorScheme = WireColorScheme( classifiedBannerForegroundColor = WireColorPalette.DarkGreen500, unclassifiedBannerBackgroundColor = WireColorPalette.DarkRed500, unclassifiedBannerForegroundColor = Color.Black, - recordAudioStartColor = WireColorPalette.LightBlue500, - recordAudioStopColor = WireColorPalette.LightRed500, + recordAudioStartColor = WireColorPalette.DarkBlue500, + recordAudioStopColor = WireColorPalette.DarkRed500, scrollToBottomButtonColor = WireColorPalette.Gray60, onScrollToBottomButtonColor = Color.Black, validE2eiStatusColor = WireColorPalette.DarkGreen500, From 381f99b54a5550f2b637e88c5dfd19e00226ee42 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Mon, 22 Jan 2024 11:24:43 +0100 Subject: [PATCH 002/134] chore: update source location strings english (#2602) --- app/src/main/res/values/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b256620c5f..55464d682d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -624,8 +624,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -727,8 +727,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group From 5b1a77fba873bd2b42fe7ba5b6b2a43ed8f0c0e0 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Mon, 22 Jan 2024 12:02:59 +0100 Subject: [PATCH 003/134] fix: sharing location crash when device location off (WPB-6182) (#2601) --- .../location/LocationPickerComponent.kt | 97 +++++++++++++++---- .../location/LocationPickerState.kt | 1 + .../location/LocationPickerViewModel.kt | 61 +++++++++--- app/src/main/res/values/strings.xml | 1 + 4 files changed, 127 insertions(+), 33 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt index 46c7ae976d9..37f5badfc80 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt @@ -17,6 +17,7 @@ */ package com.wire.android.ui.home.messagecomposer.location +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -30,9 +31,14 @@ import androidx.compose.material.icons.filled.Send import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -40,6 +46,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R import com.wire.android.ui.common.Icon @@ -58,6 +65,7 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.orDefault import com.wire.android.util.permission.PermissionsDeniedRequestDialog import com.wire.android.util.permission.rememberCurrentLocationFlow +import kotlinx.coroutines.launch /** * Component to pick the current location to send. @@ -104,42 +112,91 @@ fun LocationPickerComponent( } } add { - Column( + Box( modifier = Modifier - .align(alignment = Alignment.Start) - .padding(horizontal = dimensions().spacing16x) .wrapContentHeight() .fillMaxWidth() ) { - WirePrimaryButton( - onClick = { - onLocationPicked(geoLocatedAddress!!) - onLocationClosed() - }, - leadingIcon = Icons.Filled.Send.Icon(Modifier.padding(end = dimensions().spacing8x)), - text = stringResource(id = R.string.content_description_send_button), - state = if (isLocationLoading || geoLocatedAddress == null) { - WireButtonState.Disabled - } else { - WireButtonState.Default + if (showLocationSharingError) { + LocationErrorMessage { + coroutineScope.launch { + sheetState.hide() + viewModel.onLocationSharingErrorDialogDiscarded() + onLocationClosed() + } } + } + SendLocationButton( + isLocationLoading = isLocationLoading, + geoLocatedAddress = geoLocatedAddress, + onLocationPicked = onLocationPicked, + onLocationClosed = onLocationClosed ) - VerticalSpace.x16() } } } ) + + if (showPermissionDeniedDialog) { + PermissionsDeniedRequestDialog( + body = R.string.location_app_permission_dialog_body, + onDismiss = { + viewModel.onPermissionsDialogDiscarded() + onLocationClosed() + } + ) + } } } +} - if (viewModel.state.showPermissionDeniedDialog) { - PermissionsDeniedRequestDialog( - body = R.string.location_app_permission_dialog_body, - onDismiss = { - viewModel.onPermissionsDialogDiscarded() +@Composable +private fun SendLocationButton( + isLocationLoading: Boolean, + geoLocatedAddress: GeoLocatedAddress?, + onLocationPicked: (GeoLocatedAddress) -> Unit, + onLocationClosed: () -> Unit +) { + Column( + modifier = Modifier + .padding(horizontal = dimensions().spacing16x) + .wrapContentHeight() + .fillMaxWidth() + ) { + WirePrimaryButton( + onClick = { + onLocationPicked(geoLocatedAddress!!) onLocationClosed() + }, + leadingIcon = Icons.Filled.Send.Icon(Modifier.padding(end = dimensions().spacing8x)), + text = stringResource(id = R.string.content_description_send_button), + state = if (isLocationLoading || geoLocatedAddress == null) { + WireButtonState.Disabled + } else { + WireButtonState.Default } ) + VerticalSpace.x16() + } +} + +@Composable +private fun LocationErrorMessage( + message: String = stringResource(id = R.string.location_could_not_be_shared), + onLocationClosed: () -> Unit +) { + Box(Modifier.zIndex(Float.MAX_VALUE), contentAlignment = Alignment.BottomCenter) { + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(snackbarHostState) { + val result = snackbarHostState.showSnackbar(message = message, duration = SnackbarDuration.Short) + when (result) { + SnackbarResult.Dismissed -> onLocationClosed() + SnackbarResult.ActionPerformed -> { + /* do nothing */ + } + } + } + SnackbarHost(hostState = snackbarHostState) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerState.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerState.kt index e8d10c30848..d0f2d55d703 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerState.kt @@ -27,5 +27,6 @@ data class LocationPickerState( val isLocationLoading: Boolean = false, val isPermissionDiscarded: Boolean = false, val showPermissionDeniedDialog: Boolean = false, + val showLocationSharingError: Boolean = false, val wireModalSheetState: WireModalSheetState = WireModalSheetState(SheetValue.Hidden) ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt index fbde6c867d7..23c685054e1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt @@ -26,6 +26,7 @@ import android.location.LocationManager import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.core.location.LocationManagerCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.gms.location.LocationServices @@ -40,6 +41,7 @@ import javax.inject.Inject @HiltViewModel class LocationPickerViewModel @Inject constructor() : ViewModel() { + var state: LocationPickerState by mutableStateOf(LocationPickerState()) private set @@ -47,16 +49,36 @@ class LocationPickerViewModel @Inject constructor() : ViewModel() { state = state.copy(showPermissionDeniedDialog = false) } + fun onLocationSharingErrorDialogDiscarded() { + state = state.copy(showLocationSharingError = false) + } + fun onPermissionsDenied() { state = state.copy(showPermissionDeniedDialog = true) } private fun toStartLoadingLocationState() { - state = state.copy(isLocationLoading = true, geoLocatedAddress = null) + state = state.copy( + showLocationSharingError = false, + isLocationLoading = true, + geoLocatedAddress = null + ) } private fun toLocationLoadedState(geoLocatedAddress: GeoLocatedAddress) { - state = state.copy(isLocationLoading = false, geoLocatedAddress = geoLocatedAddress) + state = state.copy( + showLocationSharingError = false, + isLocationLoading = false, + geoLocatedAddress = geoLocatedAddress + ) + } + + private fun toLocationError() { + state = state.copy( + showLocationSharingError = true, + isLocationLoading = false, + geoLocatedAddress = null, + ) } fun getCurrentLocation(context: Context) { @@ -74,23 +96,36 @@ class LocationPickerViewModel @Inject constructor() : ViewModel() { @SuppressLint("MissingPermission") private fun getLocationWithGms(context: Context) = viewModelScope.launch { appLogger.d("Getting location with GMS") - val locationProvider = LocationServices.getFusedLocationProviderClient(context) - val currentLocation = locationProvider.getCurrentLocation(PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token).await() - val address = Geocoder(context).getFromLocation(currentLocation.latitude, currentLocation.longitude, 1).orEmpty() - toLocationLoadedState(GeoLocatedAddress(address.firstOrNull(), currentLocation)) + if (isLocationServicesEnabled(context)) { + val locationProvider = LocationServices.getFusedLocationProviderClient(context) + val currentLocation = locationProvider.getCurrentLocation(PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token).await() + val address = Geocoder(context).getFromLocation(currentLocation.latitude, currentLocation.longitude, 1).orEmpty() + toLocationLoadedState(GeoLocatedAddress(address.firstOrNull(), currentLocation)) + } else { + toLocationError() + } } @SuppressLint("MissingPermission") private fun getLocationWithoutGms(context: Context) = viewModelScope.launch { appLogger.d("Getting location without GMS") - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - val networkLocationListener: LocationListener = object : LocationListener { - override fun onLocationChanged(location: Location) { - val address = Geocoder(context).getFromLocation(location.latitude, location.longitude, 1).orEmpty() - toLocationLoadedState(GeoLocatedAddress(address.firstOrNull(), location)) - locationManager.removeUpdates(this) // important step, otherwise it will keep listening for location changes + if (isLocationServicesEnabled(context)) { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val networkLocationListener: LocationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val address = Geocoder(context).getFromLocation(location.latitude, location.longitude, 1).orEmpty() + toLocationLoadedState(GeoLocatedAddress(address.firstOrNull(), location)) + locationManager.removeUpdates(this) // important step, otherwise it will keep listening for location changes + } } + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, networkLocationListener) + } else { + toLocationError() } - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, networkLocationListener) + } + + private fun isLocationServicesEnabled(context: Context): Boolean { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return LocationManagerCompat.isLocationEnabled(locationManager) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55464d682d5..5d0860c87be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1360,4 +1360,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared From e455480b7cb6136f79f401d12494faf639870f50 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 22 Jan 2024 13:25:05 +0100 Subject: [PATCH 004/134] fix: original image path was used images when sharing from share extension (#2604) --- .../com/wire/android/model/ImageAsset.kt | 4 ++-- .../ImportMediaAuthenticatedViewModel.kt | 2 -- .../android/ui/sharing/ImportedMediaTypes.kt | 10 +++------ .../wire/android/util/ui/AssetImageFetcher.kt | 17 +-------------- .../android/util/ui/WireSessionImageLoader.kt | 1 - .../com/wire/android/model/ImageAssetTest.kt | 21 ++++++++++--------- .../android/util/ui/AssetImageFetcherTest.kt | 3 +-- 7 files changed, 18 insertions(+), 40 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt b/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt index 02658abf206..b5c78f40e74 100644 --- a/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt +++ b/app/src/main/kotlin/com/wire/android/model/ImageAsset.kt @@ -18,13 +18,13 @@ package com.wire.android.model -import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserAssetId +import okio.Path @Stable sealed class ImageAsset(private val imageLoader: WireSessionImageLoader) { @@ -50,7 +50,7 @@ sealed class ImageAsset(private val imageLoader: WireSessionImageLoader) { @Stable data class LocalImageAsset( private val imageLoader: WireSessionImageLoader, - val dataUri: Uri, + val dataPath: Path, val idKey: String ) : ImageAsset(imageLoader) { diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt index 3d6b03389fc..0df4866b6be 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt @@ -445,7 +445,6 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( size = fileMetadata.sizeInBytes, mimeType = mimeType, dataPath = tempAssetPath, - dataUri = uri, key = assetKey, width = imgWidth, height = imgHeight, @@ -460,7 +459,6 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( size = fileMetadata.sizeInBytes, mimeType = mimeType, dataPath = tempAssetPath, - dataUri = uri, key = assetKey ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaTypes.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaTypes.kt index 5d1fd1880dd..1bf55d4e388 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaTypes.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.sharing -import android.net.Uri import androidx.compose.runtime.Composable import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset @@ -70,7 +69,6 @@ sealed class ImportedMediaAsset( open val size: Long, open val mimeType: String, open val dataPath: Path, - open val dataUri: Uri, open val key: String ) { class GenericAsset( @@ -78,9 +76,8 @@ sealed class ImportedMediaAsset( override val size: Long, override val mimeType: String, override val dataPath: Path, - override val dataUri: Uri, override val key: String - ) : ImportedMediaAsset(name, size, mimeType, dataPath, dataUri, key) + ) : ImportedMediaAsset(name, size, mimeType, dataPath, key) class Image( val width: Int, @@ -89,10 +86,9 @@ sealed class ImportedMediaAsset( override val size: Long, override val mimeType: String, override val dataPath: Path, - override val dataUri: Uri, override val key: String, val wireSessionImageLoader: WireSessionImageLoader - ) : ImportedMediaAsset(name, size, mimeType, dataPath, dataUri, key) { - val localImageAsset = ImageAsset.LocalImageAsset(wireSessionImageLoader, dataUri, key) + ) : ImportedMediaAsset(name, size, mimeType, dataPath, key) { + val localImageAsset = ImageAsset.LocalImageAsset(wireSessionImageLoader, dataPath, key) } } diff --git a/app/src/main/kotlin/com/wire/android/util/ui/AssetImageFetcher.kt b/app/src/main/kotlin/com/wire/android/util/ui/AssetImageFetcher.kt index faf1f250e67..505f614073b 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/AssetImageFetcher.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/AssetImageFetcher.kt @@ -18,15 +18,11 @@ package com.wire.android.util.ui -import android.content.Context import coil.ImageLoader -import coil.decode.DataSource -import coil.fetch.DrawableResult import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.request.Options import com.wire.android.model.ImageAsset -import com.wire.android.util.toDrawable import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.feature.asset.DeleteAssetUseCase @@ -36,7 +32,6 @@ import com.wire.kalium.logic.feature.asset.MessageAssetResult import com.wire.kalium.logic.feature.asset.PublicAssetResult internal class AssetImageFetcher( - private val context: Context, private val assetFetcherParameters: AssetFetcherParameters, private val getPublicAsset: GetAvatarAssetUseCase, private val getPrivateAsset: GetMessageAssetUseCase, @@ -79,15 +74,7 @@ internal class AssetImageFetcher( } } - is ImageAsset.LocalImageAsset -> { - data.dataUri.toDrawable(context)?.let { - DrawableResult( - drawable = it, - isSampled = true, - dataSource = DataSource.DISK - ) - } - } + is ImageAsset.LocalImageAsset -> drawableResultWrapper.toFetchResult(data.dataPath) } } } @@ -103,7 +90,6 @@ internal class AssetImageFetcher( private val getPrivateAssetUseCase: GetMessageAssetUseCase, private val deleteAssetUseCase: DeleteAssetUseCase, private val drawableResultWrapper: DrawableResultWrapper, - private val context: Context ) : Fetcher.Factory { override fun create( data: ImageAsset, @@ -115,7 +101,6 @@ internal class AssetImageFetcher( getPrivateAsset = getPrivateAssetUseCase, deleteAsset = deleteAssetUseCase, drawableResultWrapper = drawableResultWrapper, - context = context ) } } diff --git a/app/src/main/kotlin/com/wire/android/util/ui/WireSessionImageLoader.kt b/app/src/main/kotlin/com/wire/android/util/ui/WireSessionImageLoader.kt index 2c66526d8ea..a78af9835f3 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/WireSessionImageLoader.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/WireSessionImageLoader.kt @@ -136,7 +136,6 @@ class WireSessionImageLoader( getPrivateAssetUseCase = getPrivateAsset, deleteAssetUseCase = deleteAsset, drawableResultWrapper = DrawableResultWrapper(resources), - context = context ) ) if (SDK_INT >= 28) { diff --git a/app/src/test/kotlin/com/wire/android/model/ImageAssetTest.kt b/app/src/test/kotlin/com/wire/android/model/ImageAssetTest.kt index 76dd4baef92..a7cf6427b9f 100644 --- a/app/src/test/kotlin/com/wire/android/model/ImageAssetTest.kt +++ b/app/src/test/kotlin/com/wire/android/model/ImageAssetTest.kt @@ -19,7 +19,6 @@ package com.wire.android.model import android.net.Uri -import androidx.core.net.toUri import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserAssetId @@ -28,6 +27,8 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.mockkStatic +import okio.Path +import okio.Path.Companion.toPath import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldNotBeEqualTo import org.junit.jupiter.api.BeforeEach @@ -46,21 +47,21 @@ class ImageAssetTest { every { Uri.parse(any()) } returns mockUri } - fun createUserAvatarAsset(userAssetId: UserAssetId) = ImageAsset.UserAvatarAsset( + private fun createUserAvatarAsset(userAssetId: UserAssetId) = ImageAsset.UserAvatarAsset( imageLoader, userAssetId ) - fun createPrivateAsset( + private fun createPrivateAsset( conversationId: ConversationId, messageId: String, isSelfAsset: Boolean ) = ImageAsset.PrivateAsset(imageLoader, conversationId, messageId, isSelfAsset) - fun createLocalAsset( - dataUri: Uri, + private fun createLocalAsset( + dataPath: Path, imageKey: String ): ImageAsset.LocalImageAsset { - return ImageAsset.LocalImageAsset(imageLoader, dataUri, imageKey) + return ImageAsset.LocalImageAsset(imageLoader, dataPath, imageKey) } @Test @@ -138,7 +139,7 @@ class ImageAssetTest { @Test fun givenEqualUriAndKeyLocalAssets_whenGettingUniqueKey_thenResultsShouldBeEqual() { val assetKey = "assetKey" - val localAssetUri = "local-uri".toUri() + val localAssetUri = "local-uri".toPath() val subject1 = createLocalAsset( localAssetUri, @@ -154,7 +155,7 @@ class ImageAssetTest { @Test fun givenSameUriButDifferentKeyLocalAssets_whenGettingUniqueKey_thenResultsShouldBeDifferent() { - val assetUri = "assetUri".toUri() + val assetUri = "assetUri".toPath() val assetKey = "assetKey" val baseSubject = createLocalAsset( @@ -171,8 +172,8 @@ class ImageAssetTest { @Test fun givenSameKeyButDifferentUriLocalAssets_whenGettingUniqueKey_thenResultsShouldBeTheSame() { - val assetUri1 = "assetUri1".toUri() - val assetUri2 = "assetUri2".toUri() + val assetUri1 = "assetUri1".toPath() + val assetUri2 = "assetUri2".toPath() val assetKey = "assetKey" val baseSubject = createLocalAsset( diff --git a/app/src/test/kotlin/com/wire/android/util/ui/AssetImageFetcherTest.kt b/app/src/test/kotlin/com/wire/android/util/ui/AssetImageFetcherTest.kt index b16ba1b5c15..b5d1e57edf7 100644 --- a/app/src/test/kotlin/com/wire/android/util/ui/AssetImageFetcherTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/ui/AssetImageFetcherTest.kt @@ -372,8 +372,7 @@ internal class AssetImageFetcherTest { getPublicAsset = getPublicAsset, getPrivateAsset = getPrivateAsset, deleteAsset = deleteAsset, - drawableResultWrapper = drawableResultWrapper, - context = mockContext + drawableResultWrapper = drawableResultWrapper ) } From d16ce74e10fe6c886b53494b44c79331af3ad207 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 22 Jan 2024 18:50:41 +0100 Subject: [PATCH 005/134] ci: cherry pick gh action result in bot as auther (#2608) --- .github/workflows/cherry-pick-rc-to-develop.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cherry-pick-rc-to-develop.yml b/.github/workflows/cherry-pick-rc-to-develop.yml index 4f24fbf8eeb..48598b3d69a 100644 --- a/.github/workflows/cherry-pick-rc-to-develop.yml +++ b/.github/workflows/cherry-pick-rc-to-develop.yml @@ -108,6 +108,14 @@ jobs: CHERRY_PICK_COMMIT=$(git rev-parse HEAD) echo "cherryPickCommit=$CHERRY_PICK_COMMIT" >> $GITHUB_ENV + - name: Get Original Author + id: get-author + if: env.shouldCherryPick == 'true' + run: | + ORIGINAL_AUTHOR=$(git log -1 --pretty=format:'%an <%ae>' ${{ github.event.pull_request.merge_commit_sha }}) + echo "Original author: $ORIGINAL_AUTHOR" + echo "originalAuthor=$ORIGINAL_AUTHOR" >> $GITHUB_ENV + - name: Cherry-pick commits id: commit-cherry-pick if: env.shouldCherryPick == 'true' @@ -122,7 +130,10 @@ jobs: echo "Captured conflicted files: $CONFLICTED_FILES" if [[ "$OUTPUT" == *"CONFLICT"* ]]; then # Commit the remaining conflicts - git commit -am "Commit with unresolved merge conflicts outside of ${{ env.SUBMODULE_NAME }}" + git add . + git commit --author "${{ env.originalAuthor }}" -am "Commit with unresolved merge conflicts outside of ${{ env.SUBMODULE_NAME }}" + else + git commit --author "${{ env.originalAuthor }}" --amend --no-edit fi # Push branch and remove temp From 5ddbbe0e22ebf4ddb9ee797a0a45d64539dc947c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:38:20 +0100 Subject: [PATCH 006/134] fix: changes to hopefully improve startup and ANRs [WPB-6048] (#2607) Co-authored-by: Yamil Medina --- app/build.gradle.kts | 1 + .../com/wire/android/util/DataDogLogger.kt | 2 +- .../com/wire/android/util/DataDogLogger.kt | 2 +- app/src/main/AndroidManifest.xml | 39 ++++----- .../com/wire/android/WireApplication.kt | 85 +++++++++---------- .../initializer/FirebaseInitializer.kt | 40 +++++++++ .../initializer/InitializerEntryPoint.kt | 37 ++++++++ .../com/wire/android/ui/WireActivity.kt | 45 ++++++---- .../ui/calling/ProximitySensorManager.kt | 11 +-- .../res/drawable/ic_launcher_wire_logo.xml | 39 +++++++++ .../com/wire/android/util/DataDogLogger.kt | 2 +- gradle/libs.versions.toml | 2 + 12 files changed, 214 insertions(+), 91 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/initializer/FirebaseInitializer.kt create mode 100644 app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt create mode 100644 app/src/main/res/drawable/ic_launcher_wire_logo.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd0d0540d80..e146779449b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(libs.androidx.splashscreen) implementation(libs.androidx.exifInterface) implementation(libs.androidx.biometric) + implementation(libs.androidx.startup) implementation(libs.ktx.dateTime) implementation(libs.material) diff --git a/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt index 8eaf3265325..fd84a0bfccd 100644 --- a/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt @@ -29,8 +29,8 @@ object DataDogLogger : LogWriter() { private val logger = Logger.Builder() .setNetworkInfoEnabled(true) - .setLogcatLogsEnabled(true) .setLogcatLogsEnabled(false) // we already use platformLogWriter() along with DataDogLogger, don't need duplicates in LogCat + .setDatadogLogsEnabled(true) .setBundleWithTraceEnabled(true) .setLoggerName("DATADOG") .build() diff --git a/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt index 8eaf3265325..fd84a0bfccd 100644 --- a/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt @@ -29,8 +29,8 @@ object DataDogLogger : LogWriter() { private val logger = Logger.Builder() .setNetworkInfoEnabled(true) - .setLogcatLogsEnabled(true) .setLogcatLogsEnabled(false) // we already use platformLogWriter() along with DataDogLogger, don't need duplicates in LogCat + .setDatadogLogsEnabled(true) .setBundleWithTraceEnabled(true) .setLoggerName("DATADOG") .build() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f60f99354dc..f76f5432ac8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -227,7 +227,16 @@ android:host="e2ei" android:scheme="wire" /> + + + + + + - - - - - - - - - diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index 7924cbf3153..5554a2e5614 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -24,14 +24,11 @@ import android.os.Build import android.os.StrictMode import androidx.work.Configuration import co.touchlab.kermit.platformLogWriter -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.ApplicationScope import com.wire.android.di.KaliumCoreLogic import com.wire.android.util.DataDogLogger import com.wire.android.util.LogFileWriter -import com.wire.android.util.extension.isGoogleServicesAvailable import com.wire.android.util.getGitBuildId import com.wire.android.util.lifecycle.ConnectionPolicyManager import com.wire.android.workmanager.WireWorkerFactory @@ -39,6 +36,7 @@ import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logic.CoreLogger import com.wire.kalium.logic.CoreLogic +import dagger.Lazy import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first @@ -50,29 +48,29 @@ class WireApplication : Application(), Configuration.Provider { @Inject @KaliumCoreLogic - lateinit var coreLogic: CoreLogic + lateinit var coreLogic: Lazy @Inject - lateinit var logFileWriter: LogFileWriter + lateinit var logFileWriter: Lazy @Inject - lateinit var connectionPolicyManager: ConnectionPolicyManager + lateinit var connectionPolicyManager: Lazy @Inject - lateinit var wireWorkerFactory: WireWorkerFactory + lateinit var wireWorkerFactory: Lazy @Inject - lateinit var globalObserversManager: GlobalObserversManager + lateinit var globalObserversManager: Lazy @Inject - lateinit var globalDataStore: GlobalDataStore + lateinit var globalDataStore: Lazy @Inject @ApplicationScope lateinit var globalAppScope: CoroutineScope override fun getWorkManagerConfiguration(): Configuration { return Configuration.Builder() - .setWorkerFactory(wireWorkerFactory) + .setWorkerFactory(wireWorkerFactory.get()) .build() } @@ -81,23 +79,19 @@ class WireApplication : Application(), Configuration.Provider { enableStrictMode() - if (this.isGoogleServicesAvailable()) { - val firebaseOptions = FirebaseOptions.Builder() - .setApplicationId(BuildConfig.FIREBASE_APP_ID) - .setGcmSenderId(BuildConfig.FIREBASE_PUSH_SENDER_ID) - .setApiKey(BuildConfig.GOOGLE_API_KEY) - .setProjectId(BuildConfig.FCM_PROJECT_ID) - .build() - FirebaseApp.initializeApp(this, firebaseOptions) - } + globalAppScope.launch { + initializeApplicationLoggingFrameworks() - initializeApplicationLoggingFrameworks() - connectionPolicyManager.startObservingAppLifecycle() + appLogger.i("$TAG app lifecycle") + connectionPolicyManager.get().startObservingAppLifecycle() - // TODO: Can be handled in one of Sync steps - coreLogic.updateApiVersionsScheduler.schedulePeriodicApiVersionUpdate() + appLogger.i("$TAG api version update") + // TODO: Can be handled in one of Sync steps + coreLogic.get().updateApiVersionsScheduler.schedulePeriodicApiVersionUpdate() - globalObserversManager.observe() + appLogger.i("$TAG global observers") + globalObserversManager.get().observe() + } } private fun enableStrictMode() { @@ -121,29 +115,27 @@ class WireApplication : Application(), Configuration.Provider { } } - private fun initializeApplicationLoggingFrameworks() { - globalAppScope.launch { - // 1. Datadog should be initialized first - ExternalLoggerManager.initDatadogLogger(applicationContext, globalDataStore) - // 2. Initialize our internal logging framework - val isLoggingEnabled = globalDataStore.isLoggingEnabled().first() - val config = if (isLoggingEnabled) { - KaliumLogger.Config.DEFAULT.apply { - setLogLevel(KaliumLogLevel.VERBOSE) - setLogWriterList(listOf(DataDogLogger, platformLogWriter())) - } - } else { - KaliumLogger.Config.disabled() + private suspend fun initializeApplicationLoggingFrameworks() { + // 1. Datadog should be initialized first + ExternalLoggerManager.initDatadogLogger(applicationContext, globalDataStore.get()) + // 2. Initialize our internal logging framework + val isLoggingEnabled = globalDataStore.get().isLoggingEnabled().first() + val config = if (isLoggingEnabled) { + KaliumLogger.Config.DEFAULT.apply { + setLogLevel(KaliumLogLevel.VERBOSE) + setLogWriterList(listOf(DataDogLogger, platformLogWriter())) } - // 2. Initialize our internal logging framework - AppLogger.init(config) - CoreLogger.init(config) - // 3. Initialize our internal FILE logging framework - logFileWriter.start() - // 4. Everything ready, now we can log device info - appLogger.i("Logger enabled") - logDeviceInformation() + } else { + KaliumLogger.Config.disabled() } + // 2. Initialize our internal logging framework + AppLogger.init(config) + CoreLogger.init(config) + // 3. Initialize our internal FILE logging framework + logFileWriter.get().start() + // 4. Everything ready, now we can log device info + appLogger.i("Logger enabled") + logDeviceInformation() } private fun logDeviceInformation() { @@ -169,7 +161,7 @@ class WireApplication : Application(), Configuration.Provider { override fun onLowMemory() { super.onLowMemory() appLogger.w("onLowMemory called - Stopping logging, buckling the seatbelt and hoping for the best!") - logFileWriter.stop() + logFileWriter.get().stop() } private companion object { @@ -190,5 +182,6 @@ class WireApplication : Application(), Configuration.Provider { values().firstOrNull { it.level == value } ?: TRIM_MEMORY_UNKNOWN } } + private const val TAG = "WireApplication" } } diff --git a/app/src/main/kotlin/com/wire/android/initializer/FirebaseInitializer.kt b/app/src/main/kotlin/com/wire/android/initializer/FirebaseInitializer.kt new file mode 100644 index 00000000000..2a5f62a33ad --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/initializer/FirebaseInitializer.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2024 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.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.wire.android.BuildConfig +import com.wire.android.util.extension.isGoogleServicesAvailable + +class FirebaseInitializer : Initializer { + override fun create(context: Context) { + if (context.isGoogleServicesAvailable()) { + val firebaseOptions = FirebaseOptions.Builder() + .setApplicationId(BuildConfig.FIREBASE_APP_ID) + .setGcmSenderId(BuildConfig.FIREBASE_PUSH_SENDER_ID) + .setApiKey(BuildConfig.GOOGLE_API_KEY) + .setProjectId(BuildConfig.FCM_PROJECT_ID) + .build() + FirebaseApp.initializeApp(context, firebaseOptions) + } + } + override fun dependencies(): List>> = emptyList() // no dependencies on other libraries +} diff --git a/app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt b/app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt new file mode 100644 index 00000000000..365ce3eac6b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt @@ -0,0 +1,37 @@ +/* + * Wire + * Copyright (C) 2024 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.initializer + +import android.content.Context +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface InitializerEntryPoint { + + companion object { + // a helper method to resolve the InitializerEntryPoint from the context + fun resolve(context: Context): InitializerEntryPoint { + val appContext = context.applicationContext ?: throw IllegalStateException() + return EntryPointAccessors.fromApplication(appContext, InitializerEntryPoint::class.java) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 5e7381dae30..e1b4e05ae21 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -97,6 +97,7 @@ import com.wire.android.util.debug.FeatureVisibilityFlags import com.wire.android.util.debug.LocalFeatureVisibilityFlags import com.wire.android.util.deeplink.DeepLinkResult import com.wire.android.util.ui.updateScreenSettings +import dagger.Lazy import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest @@ -117,7 +118,7 @@ class WireActivity : AppCompatActivity() { lateinit var proximitySensorManager: ProximitySensorManager @Inject - lateinit var lockCodeTimeManager: LockCodeTimeManager + lateinit var lockCodeTimeManager: Lazy private val viewModel: WireActivityViewModel by viewModels() @@ -133,26 +134,39 @@ class WireActivity : AppCompatActivity() { private var shouldKeepSplashOpen = true override fun onCreate(savedInstanceState: Bundle?) { + + appLogger.i("$TAG splash install") // We need to keep the splash screen open until the first screen is drawn. // Otherwise a white screen is displayed. // It's an API limitation, at some point we may need to remove it - installSplashScreen().setKeepOnScreenCondition { - shouldKeepSplashOpen - } + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) - proximitySensorManager.initialize() + splashScreen.setKeepOnScreenCondition { shouldKeepSplashOpen } + lifecycle.addObserver(currentScreenManager) WindowCompat.setDecorFitsSystemWindows(window, false) - viewModel.observePersistentConnectionStatus() - val startDestination = when (viewModel.initialAppState) { - InitialAppState.NOT_MIGRATED -> MigrationScreenDestination - InitialAppState.NOT_LOGGED_IN -> WelcomeScreenDestination - InitialAppState.LOGGED_IN -> HomeScreenDestination - } - setComposableContent(startDestination) { - shouldKeepSplashOpen = false - handleDeepLink(intent, savedInstanceState) + appLogger.i("$TAG proximity sensor") + proximitySensorManager.initialize() + + lifecycleScope.launch { + + appLogger.i("$TAG persistent connection status") + viewModel.observePersistentConnectionStatus() + + appLogger.i("$TAG start destination") + val startDestination = when (viewModel.initialAppState) { + InitialAppState.NOT_MIGRATED -> MigrationScreenDestination + InitialAppState.NOT_LOGGED_IN -> WelcomeScreenDestination + InitialAppState.LOGGED_IN -> HomeScreenDestination + } + + appLogger.i("$TAG composable content") + setComposableContent(startDestination) { + appLogger.i("$TAG splash hide") + shouldKeepSplashOpen = false + handleDeepLink(intent, savedInstanceState) + } } } @@ -412,7 +426,7 @@ class WireActivity : AppCompatActivity() { super.onResume() lifecycleScope.launch { - lockCodeTimeManager.observeAppLock() + lockCodeTimeManager.get().observeAppLock() // Listen to one flow in a lifecycle-aware manner using flowWithLifecycle .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .first().let { @@ -506,6 +520,7 @@ class WireActivity : AppCompatActivity() { companion object { private const val HANDLED_DEEPLINK_FLAG = "deeplink_handled_flag_key" + private const val TAG = "WireActivity" } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ProximitySensorManager.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ProximitySensorManager.kt index cd688f23720..d72a972fb9b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ProximitySensorManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ProximitySensorManager.kt @@ -31,6 +31,7 @@ import com.wire.android.di.KaliumCoreLogic import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -39,8 +40,8 @@ import javax.inject.Singleton @Singleton class ProximitySensorManager @Inject constructor( private val context: Context, - private val currentSession: CurrentSessionUseCase, - @KaliumCoreLogic private val coreLogic: CoreLogic, + private val currentSession: Lazy, + @KaliumCoreLogic private val coreLogic: Lazy, @ApplicationScope private val appCoroutineScope: CoroutineScope ) { @@ -69,11 +70,11 @@ class ProximitySensorManager @Inject constructor( override fun onSensorChanged(event: SensorEvent) { appCoroutineScope.launch { - coreLogic.globalScope { - when (val currentSession = currentSession()) { + coreLogic.get().globalScope { + when (val currentSession = currentSession.get().invoke()) { is CurrentSessionResult.Success -> { val userId = currentSession.accountInfo.userId - val isCallRunning = coreLogic.getSessionScope(userId).calls.isCallRunning() + val isCallRunning = coreLogic.get().getSessionScope(userId).calls.isCallRunning() val distance = event.values.first() val shouldTurnOffScreen = distance == NEAR_DISTANCE && isCallRunning appLogger.i( diff --git a/app/src/main/res/drawable/ic_launcher_wire_logo.xml b/app/src/main/res/drawable/ic_launcher_wire_logo.xml new file mode 100644 index 00000000000..7b6912bb1b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_wire_logo.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt index 8eaf3265325..fd84a0bfccd 100644 --- a/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt @@ -29,8 +29,8 @@ object DataDogLogger : LogWriter() { private val logger = Logger.Builder() .setNetworkInfoEnabled(true) - .setLogcatLogsEnabled(true) .setLogcatLogsEnabled(false) // we already use platformLogWriter() along with DataDogLogger, don't need duplicates in LogCat + .setDatadogLogsEnabled(true) .setBundleWithTraceEnabled(true) .setLoggerName("DATADOG") .build() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 90f4c21fb1b..6969257997c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ androidx-splashscreen = "1.0.1" androidx-workManager = "2.8.1" androidx-browser = "1.5.0" androidx-biometric = "1.1.0" +androidx-startup = "1.1.1" # Compose composeBom = "2023.10.01" # TODO check if in new version [anchoredDraggable] is available @@ -158,6 +159,7 @@ androidx-exifInterface = { module = "androidx.exifinterface:exifinterface", vers androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" } androidx-profile-installer = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidx-biometric" } +androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidx-startup" } # Dependency Injection hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } From 97eacae7a41b000179df7714e91097c1a7d655e1 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Tue, 23 Jan 2024 18:13:06 +0100 Subject: [PATCH 007/134] feat: improve enrollment dialog (WPB-4372) (#2610) Co-authored-by: AndroidBob --- .../wire/android/ui/WireActivityDialogs.kt | 3 +- .../android/ui/authentication/ServerTitle.kt | 59 ++++++--- .../login/sso/LoginSSOScreen.kt | 3 +- .../com/wire/android/ui/common/WireDialog.kt | 118 +++++++++-------- .../ui/common/dialogs/CustomServerDialog.kt | 119 +++++++++++++++--- app/src/main/res/values/strings.xml | 9 +- 6 files changed, 225 insertions(+), 86 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt index 180c054480e..e00b00b3754 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt @@ -219,8 +219,7 @@ fun CustomBackendDialog( ) { if (globalAppState.customBackendDialog != null) { CustomServerDialog( - serverLinksTitle = globalAppState.customBackendDialog.serverLinks.title, - serverLinksApi = globalAppState.customBackendDialog.serverLinks.api, + serverLinks = globalAppState.customBackendDialog.serverLinks, onDismiss = onDismiss, onConfirm = onConfirm ) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/ServerTitle.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/ServerTitle.kt index da422d39476..d8ce0da998f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/ServerTitle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/ServerTitle.kt @@ -44,9 +44,11 @@ import com.wire.android.ui.common.WireDialogButtonType import com.wire.android.ui.common.clickable import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.stringWithStyledArgs import com.wire.kalium.logic.configuration.server.ServerConfig import java.net.URL @@ -93,24 +95,47 @@ fun ServerTitle( ) if (serverFullDetailsDialogState) { - WireDialog( - title = stringResource(id = R.string.server_details_dialog_title), - text = LocalContext.current.resources.stringWithStyledArgs( - R.string.server_details_dialog_body, - MaterialTheme.wireTypography.body02, - MaterialTheme.wireTypography.body02, - normalColor = colorsScheme().secondaryText, - argsColor = colorsScheme().onBackground, - serverLinks.title, - serverLinks.api - ), - onDismiss = { serverFullDetailsDialogState = false }, - optionButton1Properties = WireDialogButtonProperties( - stringResource(id = R.string.label_ok), - onClick = { serverFullDetailsDialogState = false }, - type = WireDialogButtonType.Primary - ) + ServerEnrollmentDialogContent( + serverLinks = serverLinks, + onClick = { serverFullDetailsDialogState = false }, + onDismiss = { serverFullDetailsDialogState = false } ) } } } + +@Composable +private fun ServerEnrollmentDialogContent( + serverLinks: ServerConfig.Links, + onDismiss: () -> Unit, + onClick: () -> Unit, +) { + WireDialog( + title = stringResource(id = R.string.server_details_dialog_title), + text = LocalContext.current.resources.stringWithStyledArgs( + R.string.server_details_dialog_body, + MaterialTheme.wireTypography.body02, + MaterialTheme.wireTypography.body02, + normalColor = colorsScheme().secondaryText, + argsColor = colorsScheme().onBackground, + serverLinks.title, + serverLinks.api + ), + onDismiss = onDismiss, + optionButton1Properties = WireDialogButtonProperties( + stringResource(id = R.string.label_ok), + onClick = onClick, + type = WireDialogButtonType.Primary + ) + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewServerEnrollmentDialog() = WireTheme { + ServerEnrollmentDialogContent( + serverLinks = ServerConfig.DEFAULT, + onClick = { }, + onDismiss = { } + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt index d60f10de2d0..211e26273cb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt @@ -142,8 +142,7 @@ private fun LoginSSOContent( if (loginState.customServerDialogState != null) { CustomServerDialog( - serverLinksTitle = loginState.customServerDialogState.serverLinks.title, - serverLinksApi = loginState.customServerDialogState.serverLinks.api, + serverLinks = loginState.customServerDialogState.serverLinks, onDismiss = onCustomServerDialogDismiss, onConfirm = onCustomServerDialogConfirm ) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt index b3375ee30e1..14f24856b50 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt @@ -188,29 +188,25 @@ private fun WireDialogContent( .padding(contentPadding), horizontalAlignment = if (centerContent) Alignment.CenterHorizontally else Alignment.Start ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = title, - style = MaterialTheme.wireTypography.title02, - ) - if (titleLoading) { - WireCircularProgressIndicator(progressColor = MaterialTheme.wireColorScheme.onBackground) - } - } - text?.let { - LazyColumn( - modifier = Modifier - .weight(1f, fill = false) - .fillMaxWidth() - ) { + // Title + TitleDialogSection(title, titleLoading) + + // Dynamic sized body content + LazyColumn( + modifier = Modifier + .weight(1f, fill = false) + .padding( + top = MaterialTheme.wireDimensions.dialogTextsSpacing, + bottom = MaterialTheme.wireDimensions.dialogTextsSpacing + ) + .fillMaxWidth() + ) { + text?.let { item { ClickableText( text = text, style = MaterialTheme.wireTypography.body01, - modifier = Modifier.padding( - top = MaterialTheme.wireDimensions.dialogTextsSpacing, - bottom = MaterialTheme.wireDimensions.dialogTextsSpacing, - ), + modifier = Modifier.padding(bottom = MaterialTheme.wireDimensions.dialogTextsSpacing), onClick = { offset -> text.getStringAnnotations( tag = MarkdownConstants.TAG_URL, @@ -221,42 +217,66 @@ private fun WireDialogContent( ) } } - } - content?.let { - Box { - it.invoke() - } - } - val containsAnyButton = dismissButtonProperties != null || optionButton1Properties != null || optionButton2Properties != null - val dialogButtonsSpacing = if (containsAnyButton) dimensions().dialogButtonsSpacing else dimensions().spacing0x - if (buttonsHorizontalAlignment) { - Row(Modifier.padding(top = dialogButtonsSpacing)) { - dismissButtonProperties.getButton(Modifier.weight(1f)) - if (dismissButtonProperties != null) { - Spacer(Modifier.width(dialogButtonsSpacing)) - } - optionButton1Properties.getButton(Modifier.weight(1f)) - if (optionButton2Properties != null) { - Spacer(Modifier.width(dialogButtonsSpacing)) + content?.let { + item { + Box { + it.invoke() + } } - optionButton2Properties.getButton(Modifier.weight(1f)) } - } else { - Column(Modifier.padding(top = dialogButtonsSpacing)) { - optionButton1Properties.getButton() + } - if (optionButton2Properties != null) { - Spacer(Modifier.height(dialogButtonsSpacing)) - } - optionButton2Properties.getButton() + // Buttons actions + DialogButtonsSection(dismissButtonProperties, optionButton1Properties, optionButton2Properties, buttonsHorizontalAlignment) + } + } +} - if (dismissButtonProperties != null) { - Spacer(Modifier.height(dialogButtonsSpacing)) - } - dismissButtonProperties.getButton() - } +@Composable +private fun TitleDialogSection(title: String, titleLoading: Boolean) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = title, style = MaterialTheme.wireTypography.title02) + if (titleLoading) { + WireCircularProgressIndicator(progressColor = MaterialTheme.wireColorScheme.onBackground) + } + } +} + +@Composable +private fun DialogButtonsSection( + dismissButtonProperties: WireDialogButtonProperties?, + optionButton1Properties: WireDialogButtonProperties?, + optionButton2Properties: WireDialogButtonProperties?, + buttonsHorizontalAlignment: Boolean +) { + val containsAnyButton = dismissButtonProperties != null || optionButton1Properties != null || optionButton2Properties != null + val dialogButtonsSpacing = if (containsAnyButton) dimensions().dialogButtonsSpacing else dimensions().spacing0x + if (buttonsHorizontalAlignment) { + Row(Modifier.padding(top = dialogButtonsSpacing)) { + dismissButtonProperties.getButton(Modifier.weight(1f)) + if (dismissButtonProperties != null) { + Spacer(Modifier.width(dialogButtonsSpacing)) + } + optionButton1Properties.getButton(Modifier.weight(1f)) + if (optionButton2Properties != null) { + Spacer(Modifier.width(dialogButtonsSpacing)) + } + optionButton2Properties.getButton(Modifier.weight(1f)) + } + } else { + Column(Modifier.padding(top = dialogButtonsSpacing)) { + optionButton1Properties.getButton() + + if (optionButton2Properties != null) { + Spacer(Modifier.height(dialogButtonsSpacing)) + } + optionButton2Properties.getButton() + + if (dismissButtonProperties != null) { + Spacer(Modifier.height(dialogButtonsSpacing)) } + dismissButtonProperties.getButton() } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt index 3e2c80feaf6..a884f7266c8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt @@ -18,40 +18,46 @@ package com.wire.android.ui.common.dialogs +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration import com.wire.android.R 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.WireButtonState import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.common.wireDialogPropertiesBuilder +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography -import com.wire.android.util.ui.stringWithStyledArgs +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.configuration.server.ServerConfig @Composable internal fun CustomServerDialog( - serverLinksTitle: String, - serverLinksApi: String, + serverLinks: ServerConfig.Links, onDismiss: () -> Unit, onConfirm: () -> Unit ) { + var showDetails by remember { mutableStateOf(false) } WireDialog( title = stringResource(R.string.custom_backend_dialog_title), - text = LocalContext.current.resources.stringWithStyledArgs( - R.string.custom_backend_dialog_body, - MaterialTheme.wireTypography.body01, - MaterialTheme.wireTypography.body02, - colorsScheme().onBackground, - colorsScheme().onBackground, - serverLinksTitle, - serverLinksApi - ), - + text = stringResource(R.string.custom_backend_dialog_body), buttonsHorizontalAlignment = true, properties = wireDialogPropertiesBuilder( dismissOnBackPress = false, @@ -69,8 +75,91 @@ internal fun CustomServerDialog( type = WireDialogButtonType.Primary, state = WireButtonState.Default - ) + ), + content = { + Column { + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_name), + value = serverLinks.title + ) + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_api), + value = serverLinks.api + ) + if (showDetails) { + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_websocket), + value = serverLinks.webSocket + ) + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_blacklist), + value = serverLinks.blackList + ) + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_teams), + value = serverLinks.teams + ) + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_accounts), + value = serverLinks.accounts + ) + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_website), + value = serverLinks.website + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = dimensions().spacing8x) + ) { + Text( + text = stringResource(id = if (showDetails) R.string.label_hide_details else R.string.label_show_details), + style = MaterialTheme.wireTypography.body02.copy( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colorScheme.primary + ), + modifier = Modifier + .align(Alignment.Start) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { showDetails = !showDetails } + ) + ) + } + } + } ) } +@Composable +private fun CustomServerPropertyInfo( + title: String, + value: String +) { + Text( + text = title, + style = MaterialTheme.wireTypography.body01, + color = colorsScheme().onBackground, + ) + VerticalSpace.x4() + Text( + text = value, + style = MaterialTheme.wireTypography.body02, + color = colorsScheme().onBackground, + ) + VerticalSpace.x16() +} + data class CustomServerDialogState(val serverLinks: ServerConfig.Links) + +@PreviewMultipleThemes +@Composable +fun PreviewCustomServerDialog() = WireTheme { + CustomServerDialog( + serverLinks = ServerConfig.DEFAULT, + onConfirm = { }, + onDismiss = { } + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d0860c87be..2c08559428b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1009,7 +1009,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + blackListURL: + teamsURL: + accountsURL: + websiteURL: + backendWSURL: Receiving new messages Text copied to clipboard Logs From c669dce7279789562ad0f680912a9f2bb41c21ed Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Thu, 25 Jan 2024 10:49:06 +0100 Subject: [PATCH 008/134] test: add sharing location coverage for viewmodel (#2620) --- .../location/LocationPickerComponent.kt | 8 +- .../location/LocationPickerHelper.kt | 91 ++++++++++++++++ .../location/LocationPickerViewModel.kt | 73 ++----------- .../location/LocationPickerViewModelTest.kt | 101 ++++++++++++++++++ gradle/libs.versions.toml | 2 +- 5 files changed, 207 insertions(+), 68 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelTest.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt index 37f5badfc80..75f6fc587dc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt @@ -42,8 +42,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.zIndex @@ -61,6 +59,7 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.common.spacers.HorizontalSpace import com.wire.android.ui.common.spacers.VerticalSpace +import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography import com.wire.android.util.orDefault import com.wire.android.util.permission.PermissionsDeniedRequestDialog @@ -78,12 +77,11 @@ fun LocationPickerComponent( onLocationClosed: () -> Unit ) { val viewModel = hiltViewModel() - val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val sheetState = rememberDismissibleWireModalSheetState(initialValue = SheetValue.Expanded, onLocationClosed) val locationFlow = LocationFlow( - onCurrentLocationPicked = { viewModel.getCurrentLocation(context) }, + onCurrentLocationPicked = viewModel::getCurrentLocation, onLocationDenied = viewModel::onPermissionsDenied ) LaunchedEffect(Unit) { @@ -219,7 +217,7 @@ private fun LocationInformation(geoLocatedAddress: GeoLocatedAddress?) { @Composable private fun RowScope.LoadingLocation() { WireCircularProgressIndicator( - progressColor = Color.Black, + progressColor = MaterialTheme.wireColorScheme.primary, modifier = Modifier.align(alignment = Alignment.CenterVertically) ) HorizontalSpace.x8() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt new file mode 100644 index 00000000000..22c11a61c86 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt @@ -0,0 +1,91 @@ +/* + * Wire + * Copyright (C) 2024 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.home.messagecomposer.location + +import android.annotation.SuppressLint +import android.content.Context +import android.location.Geocoder +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import androidx.core.location.LocationManagerCompat +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import com.google.android.gms.tasks.CancellationTokenSource +import com.wire.android.util.extension.isGoogleServicesAvailable +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.tasks.await +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocationPickerHelper @Inject constructor(@ApplicationContext val context: Context) { + + suspend fun getLocation(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + if (context.isGoogleServicesAvailable()) { + getLocationWithGms( + onSuccess = onSuccess, + onError = onError + ) + } else { + getLocationWithoutGms( + onSuccess = onSuccess, + onError = onError + ) + } + } + + /** + * Choosing the best location estimate by docs. + * https://developer.android.com/develop/sensors-and-location/location/retrieve-current#BestEstimate + */ + @SuppressLint("MissingPermission") + private suspend fun getLocationWithGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + if (isLocationServicesEnabled()) { + val locationProvider = LocationServices.getFusedLocationProviderClient(context) + val currentLocation = + locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token).await() + val address = Geocoder(context).getFromLocation(currentLocation.latitude, currentLocation.longitude, 1).orEmpty() + onSuccess(GeoLocatedAddress(address.firstOrNull(), currentLocation)) + } else { + onError() + } + } + + @SuppressLint("MissingPermission") + private fun getLocationWithoutGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + if (isLocationServicesEnabled()) { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val networkLocationListener: LocationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val address = Geocoder(context).getFromLocation(location.latitude, location.longitude, 1).orEmpty() + onSuccess(GeoLocatedAddress(address.firstOrNull(), location)) + locationManager.removeUpdates(this) // important step, otherwise it will keep listening for location changes + } + } + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, networkLocationListener) + } else { + onError() + } + } + + private fun isLocationServicesEnabled(): Boolean { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return LocationManagerCompat.isLocationEnabled(locationManager) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt index 23c685054e1..353929d758f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt @@ -17,30 +17,17 @@ */ package com.wire.android.ui.home.messagecomposer.location -import android.annotation.SuppressLint -import android.content.Context -import android.location.Geocoder -import android.location.Location -import android.location.LocationListener -import android.location.LocationManager import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.location.LocationManagerCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY -import com.google.android.gms.tasks.CancellationTokenSource -import com.wire.android.appLogger -import com.wire.android.util.extension.isGoogleServicesAvailable import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import kotlinx.coroutines.tasks.await import javax.inject.Inject @HiltViewModel -class LocationPickerViewModel @Inject constructor() : ViewModel() { +class LocationPickerViewModel @Inject constructor(private val locationPickerHelper: LocationPickerHelper) : ViewModel() { var state: LocationPickerState by mutableStateOf(LocationPickerState()) private set @@ -57,6 +44,16 @@ class LocationPickerViewModel @Inject constructor() : ViewModel() { state = state.copy(showPermissionDeniedDialog = true) } + fun getCurrentLocation() { + viewModelScope.launch { + toStartLoadingLocationState() + locationPickerHelper.getLocation( + onSuccess = { toLocationLoadedState(it) }, + onError = ::toLocationError + ) + } + } + private fun toStartLoadingLocationState() { state = state.copy( showLocationSharingError = false, @@ -80,52 +77,4 @@ class LocationPickerViewModel @Inject constructor() : ViewModel() { geoLocatedAddress = null, ) } - - fun getCurrentLocation(context: Context) { - toStartLoadingLocationState() - when (context.isGoogleServicesAvailable()) { - true -> getLocationWithGms(context) - false -> getLocationWithoutGms(context) - } - } - - /** - * Choosing the best location estimate by docs. - * https://developer.android.com/develop/sensors-and-location/location/retrieve-current#BestEstimate - */ - @SuppressLint("MissingPermission") - private fun getLocationWithGms(context: Context) = viewModelScope.launch { - appLogger.d("Getting location with GMS") - if (isLocationServicesEnabled(context)) { - val locationProvider = LocationServices.getFusedLocationProviderClient(context) - val currentLocation = locationProvider.getCurrentLocation(PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token).await() - val address = Geocoder(context).getFromLocation(currentLocation.latitude, currentLocation.longitude, 1).orEmpty() - toLocationLoadedState(GeoLocatedAddress(address.firstOrNull(), currentLocation)) - } else { - toLocationError() - } - } - - @SuppressLint("MissingPermission") - private fun getLocationWithoutGms(context: Context) = viewModelScope.launch { - appLogger.d("Getting location without GMS") - if (isLocationServicesEnabled(context)) { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - val networkLocationListener: LocationListener = object : LocationListener { - override fun onLocationChanged(location: Location) { - val address = Geocoder(context).getFromLocation(location.latitude, location.longitude, 1).orEmpty() - toLocationLoadedState(GeoLocatedAddress(address.firstOrNull(), location)) - locationManager.removeUpdates(this) // important step, otherwise it will keep listening for location changes - } - } - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, networkLocationListener) - } else { - toLocationError() - } - } - - private fun isLocationServicesEnabled(context: Context): Boolean { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - return LocationManagerCompat.isLocationEnabled(locationManager) - } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelTest.kt new file mode 100644 index 00000000000..899370151bc --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelTest.kt @@ -0,0 +1,101 @@ +/* + * Wire + * Copyright (C) 2024 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.home.messagecomposer.location + +import android.location.Location +import com.wire.android.config.CoroutineTestExtension +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class LocationPickerViewModelTest { + + @Test + fun `given user has device location disabled, when sharing location, then an error message will be shown`() = runTest { + // given + val (_, viewModel) = Arrangement() + .withGetGeoLocationError() + .arrange() + + // when + viewModel.getCurrentLocation() + + // then + assertEquals(true, viewModel.state.showLocationSharingError) + assertEquals(true, viewModel.state.geoLocatedAddress == null) + } + + @Test + fun `given user has device location enabled, when sharing location, then should load the location`() = runTest { + // given + val (arrangement, viewModel) = Arrangement() + .withGetGeoLocationSuccess() + .arrange() + + // when + viewModel.getCurrentLocation() + + // then + assertEquals(false, viewModel.state.showLocationSharingError) + assertEquals(true, viewModel.state.geoLocatedAddress != null) + coVerify(exactly = 1) { arrangement.locationPickerHelper.getLocation(any(), any()) } + } + + private class Arrangement { + + val locationPickerHelper = mockk() + + fun withGetGeoLocationSuccess() = apply { + coEvery { + locationPickerHelper.getLocation( + capture(onEngineStartSuccess), + capture(onEngineStartFailure) + ) + } coAnswers { + firstArg().invoke(successResponse) + } + } + + fun withGetGeoLocationError() = apply { + coEvery { + locationPickerHelper.getLocation( + capture(onEngineStartSuccess), + capture(onEngineStartFailure) + ) + } coAnswers { + secondArg<() -> Unit>().invoke() + } + } + + fun arrange() = this to LocationPickerViewModel(locationPickerHelper) + } + + private companion object { + val onEngineStartSuccess = slot() + val onEngineStartFailure = slot<() -> Unit>() + val successResponse = GeoLocatedAddress(null, Location("dummy-location")) + } +} + +private typealias PickedGeoLocation = (GeoLocatedAddress) -> Unit diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6969257997c..85a672045d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,7 +87,7 @@ androidx-text-archCore = "2.1.0" junit4 = "4.13.2" junit5 = "5.10.0" kluent = "1.73" -mockk = "1.13.5" +mockk = "1.13.9" okio = "3.6.0" turbine = "1.0.0" From de6b9602efb9833395e762cf3477b1c60a246d60 Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 25 Jan 2024 12:15:58 +0100 Subject: [PATCH 009/134] chore: source base strings new for custom dialog deeplink --- app/src/main/res/values/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2c08559428b..1ef36c52232 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1012,11 +1012,11 @@ If you proceed, your client will be redirected to the following on-premises backend: Backend name: Backend URL: - blackListURL: - teamsURL: - accountsURL: - websiteURL: - backendWSURL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs From da8b5edc3836ab3da5e7591c61efa063386df85d Mon Sep 17 00:00:00 2001 From: AndroidBob Date: Thu, 25 Jan 2024 15:46:15 +0100 Subject: [PATCH 010/134] chore: update localization strings via Crowdin (rc) (#2628) Co-authored-by: yamilmedina --- app/src/main/res/values-af/strings.xml | 20 ++++++++++++------ app/src/main/res/values-ar/strings.xml | 20 ++++++++++++------ app/src/main/res/values-bn/strings.xml | 20 ++++++++++++------ app/src/main/res/values-ca/strings.xml | 20 ++++++++++++------ app/src/main/res/values-cs/strings.xml | 20 ++++++++++++------ app/src/main/res/values-da/strings.xml | 20 ++++++++++++------ app/src/main/res/values-de/strings.xml | 18 ++++++++++++----- app/src/main/res/values-el/strings.xml | 20 ++++++++++++------ app/src/main/res/values-es/strings.xml | 28 +++++++++++++++----------- app/src/main/res/values-et/strings.xml | 20 ++++++++++++------ app/src/main/res/values-fa/strings.xml | 20 ++++++++++++------ app/src/main/res/values-fi/strings.xml | 20 ++++++++++++------ app/src/main/res/values-fr/strings.xml | 20 ++++++++++++------ app/src/main/res/values-he/strings.xml | 20 ++++++++++++------ app/src/main/res/values-hi/strings.xml | 20 ++++++++++++------ app/src/main/res/values-hr/strings.xml | 23 ++++++++++++++------- app/src/main/res/values-hu/strings.xml | 22 +++++++++++++------- app/src/main/res/values-id/strings.xml | 20 ++++++++++++------ app/src/main/res/values-it/strings.xml | 28 +++++++++++++++----------- app/src/main/res/values-ja/strings.xml | 20 ++++++++++++------ app/src/main/res/values-ko/strings.xml | 20 ++++++++++++------ app/src/main/res/values-lt/strings.xml | 20 ++++++++++++------ app/src/main/res/values-mk/strings.xml | 20 ++++++++++++------ app/src/main/res/values-nl/strings.xml | 20 ++++++++++++------ app/src/main/res/values-no/strings.xml | 20 ++++++++++++------ app/src/main/res/values-pa/strings.xml | 20 ++++++++++++------ app/src/main/res/values-pl/strings.xml | 28 +++++++++++++++----------- app/src/main/res/values-pt/strings.xml | 28 +++++++++++++++----------- app/src/main/res/values-ro/strings.xml | 20 ++++++++++++------ app/src/main/res/values-ru/strings.xml | 18 ++++++++++++----- app/src/main/res/values-si/strings.xml | 22 ++++++++++++++------ app/src/main/res/values-sk/strings.xml | 20 ++++++++++++------ app/src/main/res/values-sl/strings.xml | 20 ++++++++++++------ app/src/main/res/values-sr/strings.xml | 20 ++++++++++++------ app/src/main/res/values-sv/strings.xml | 24 ++++++++++++++-------- app/src/main/res/values-tr/strings.xml | 20 ++++++++++++------ app/src/main/res/values-uk/strings.xml | 20 ++++++++++++------ app/src/main/res/values-vi/strings.xml | 20 ++++++++++++------ app/src/main/res/values-zh/strings.xml | 20 ++++++++++++------ 39 files changed, 559 insertions(+), 260 deletions(-) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 007cff28cb8..1d068fdcfd7 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 999f34415fa..25b40377762 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -407,7 +407,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -621,8 +621,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -774,8 +774,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1061,7 +1061,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1446,4 +1453,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 5f0812e9f09..55c1f71fffc 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 007cff28cb8..1d068fdcfd7 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8837a226354..6ebdc3e238b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -405,7 +405,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -615,8 +615,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -742,8 +742,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1025,7 +1025,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1390,4 +1397,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 007cff28cb8..1d068fdcfd7 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e438e7f8959..dfe49a4f5ac 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -610,8 +610,8 @@ %1$s **Teilnehmer** konnten der Gruppe nicht hinzugefΓΌgt werden. %1$s konnten der Gruppe nicht hinzugefΓΌgt werden. %1$s konnte der Gruppe nicht hinzugefΓΌgt werden. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + Diese Unterhaltung ist nicht mehr ΓΌberprΓΌft, da mindestens ein Teilnehmer ein neues GerΓ€t verwendet oder ein ungΓΌltiges Zertifikat hat. + Diese Unterhaltung ist nicht mehr ΓΌberprΓΌft, da mindestens ein Teilnehmer ein neues GerΓ€t verwendet oder ein ungΓΌltiges Zertifikat hat. Alle GerΓ€te sind ΓΌberprΓΌft (Ende-zu-Ende-IdentitΓ€t) Alle FingerabdrΓΌcke sind ΓΌberprΓΌft (Proteus) Kommunikation in Wire ist immer Ende-zu-Ende verschlΓΌsselt. Alles, was Sie in dieser Unterhaltung senden und empfangen, ist nur fΓΌr Sie und andere Gruppenteilnehmer zugΓ€nglich.\n**Bitte seien Sie dennoch vorsichtig, mit wem Sie vertrauliche Informationen teilen.** @@ -711,8 +711,8 @@ Medien Bilder Dateien - Es wurden noch keine Bilder in dieser Unterhaltung geteilt πŸ™€ - Es wurden noch keine Dateien in dieser Unterhaltung geteilt πŸ™€ + Bislang hat niemand Bilder in dieser Unterhaltung geteilt πŸ₯² + Bislang hat niemand Dateien in dieser Unterhaltung geteilt πŸ™€ KONTAKTE Neue Gruppe @@ -990,7 +990,14 @@ Profil ΓΆffnen Sie kΓΆnnen wΓ€hrend eines Anrufs nicht das Konto wechseln Umleitung auf ein lokales Backend? - Wenn Sie fortfahren, wird Ihr Client auf das folgende Backend weitergeleitet:\n\nBackend-Name:\n%1$s\n\nBackend-URL:\n%2$s + Wenn Sie fortfahren, wird Ihr Client an das folgende lokale Backend weitergeleitet: + Backend-Name: + Backend-URL: + Blacklist-URL: + Teams-URL: + Accounts-URL: + Website-URL: + Backend-WSURL: Empfang von neuen Nachrichten Text in Zwischenablage kopiert Protokolle @@ -1336,4 +1343,5 @@ registriert. Bitte versuchen Sie es mit einer anderen. Standort teilen Erlauben Sie Wire den Zugriff auf Ihren GerΓ€testandort, um Ihren Standort zu senden. Bitte warten… + Standort konnte nicht geteilt werden diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 007cff28cb8..1d068fdcfd7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 34bbd9453a6..d67e0bf4f0d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -401,7 +401,9 @@ Un mensaje eliminado no puede ser restaurado. Guardado Archivo no disponible Error al cargar archivo - ¿Desea abrir el archivo o guardarlo en la carpeta de descargas de su dispositivo? + Do you want to open the file, or save it to your + device\'s download folder? + Abrir Guardar Cuenta eliminada @@ -607,8 +609,8 @@ Hasta 500 personas pueden unirse a una conversación en grupo. %1$s **participants** could not be added to the group. %1$s no pudieron ser aΓ±adidos a la conversaciΓ³n. %1$s no pudo ser aΓ±adido a la conversaciΓ³n. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -708,8 +710,8 @@ Hasta 500 personas pueden unirse a una conversación en grupo. Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTOS Nuevo Grupo @@ -984,13 +986,14 @@ Hasta 500 personas pueden unirse a una conversación en grupo. Abrir perfil No puedes cambiar de cuenta mientras estás en una llamada ¿Redirigir a un backend local? - Si continúas, tu cliente será redirigido al siguiente backend local: - -Nombre del backend: -%1$s - -URL del backend: -%2$s + Si continΓΊa, su cliente serΓ‘ redirigido al siguiente servidor local: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Recibiendo nuevos mensajes Texto copiado al portapapeles Registros @@ -1337,4 +1340,5 @@ URL del backend: Compartir ubicaciΓ³n Permite a Wire acceder a la ubicaciΓ³n del dispositivo para enviar tu ubicaciΓ³n. Un momento... + No se pudo compartir la ubicaciΓ³n diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 05ba6c92fde..1328a3fd24f 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 5f0812e9f09..55c1f71fffc 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 007cff28cb8..1d068fdcfd7 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ed415f1bcb0..639dd951c50 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -392,7 +392,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -598,8 +598,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -699,8 +699,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -978,7 +978,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1323,4 +1330,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index d5cf344936c..d11a263a31a 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -405,7 +405,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -615,8 +615,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -742,8 +742,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1025,7 +1025,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1390,4 +1397,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 5f0812e9f09..55c1f71fffc 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 290291c7304..a7df1d6587f 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -398,8 +398,9 @@ Spremljeno Datoteka nije dostupna Prijenos datoteke nije uspio - Ε½elite li otvoriti datoteku ili je spremiti na svoju - mapu za preuzimanje na ureΔ‘aju? + Do you want to open the file, or save it to your + device\'s download folder? + Otvori Spremi Obrisan kontakt @@ -605,8 +606,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -719,8 +720,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ KONTAKTI Nova Grupa @@ -1000,7 +1001,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1355,4 +1363,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ea2076fae84..8a1f437c504 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -403,8 +403,8 @@ Mentve A fΓ‘jl nem elΓ©rhetΕ‘ A fΓ‘jl feltΓΆltΓ©se nem sikerΓΌlt - Megnyitja a fΓ‘jlt, vagy menti - eszkΓΆze letΓΆltΓ©si mappΓ‘jΓ‘ba? + Do you want to open the file, or save it to your + device\'s download folder? MegnyitΓ‘s MentΓ©s @@ -609,8 +609,8 @@ %1$s **rΓ©sztvevΕ‘ket** nem sikerΓΌlt hozzΓ‘adni a csoporthoz. %1$s nem sikerΓΌlt hozzΓ‘adni a csoporthoz. %1$s nem sikerΓΌlt hozzΓ‘adni a csoporthoz. - Ez a beszΓ©lgetΓ©s tΓΆbbΓ© nem ellenΕ‘rzΓΆtt, mivel valaki legalΓ‘bb egy eszkΓΆzt Γ©rvΓ©nyes vΓ©gpontok kΓΆzΓΆtti azonosΓ­tΓ³ tanΓΊsΓ­tvΓ‘ny nΓ©lkΓΌl hasznΓ‘l. - Ez a beszΓ©lgetΓ©s tΓΆbbΓ© nem ellenΕ‘rzΓΆtt, mivel valaki legalΓ‘bb egy eszkΓΆzt Γ©rvΓ©nyes vΓ©gpontok kΓΆzΓΆtti azonosΓ­tΓ³ tanΓΊsΓ­tvΓ‘ny nΓ©lkΓΌl hasznΓ‘l. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. Minden eszkΓΆz ellenΕ‘rzΓΆtt (vΓ©gpontok kΓΆzΓΆtti azonosΓ­tΓ‘s) Minden ujjlenyomat ellenΕ‘rizve (Proteus) A Wire-ben a kommunikΓ‘ciΓ³ mindig titkos a vΓ©gpontok kΓΆzΓΆtt. Minden, amit kΓΌld Γ©s fogad ebben a beszΓ©lgetΓ©sben, csak az Γ–n Γ©s a csoport rΓ©sztvevΕ‘i szΓ‘mΓ‘ra hozzΓ‘fΓ©rhetΕ‘.\n**TovΓ‘bbra is legyen kΓΆrΓΌltekintΕ‘, kivel oszt meg Γ©rzΓ©keny informΓ‘ciΓ³kat.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ NΓ‰VJEGYEK Új csoport @@ -989,7 +989,14 @@ Profil megnyitΓ‘sa Nem vΓ‘lthat fiΓ³kot hΓ­vΓ‘s kΓΆzben ÁtirΓ‘nyΓ­tja egy helyi kiszolgΓ‘lΓ³ra? - Ha tovΓ‘bblΓ©p, a kliense Γ‘tirΓ‘nyΓ­tΓ‘sra kerΓΌl a kΓΆvetkezΕ‘ helyi kiszolgΓ‘lΓ³ra:\n\nKiszolgΓ‘lΓ³ neve:\n%1$s\n\nKiszolgΓ‘lΓ³ URL-je:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Új ΓΌzenetek lekΓ©rdezΓ©se SzΓΆveg a vΓ‘gΓ³lapra mΓ‘solva NaplΓ³k @@ -1335,4 +1342,5 @@ KΓ©rjΓΌk, prΓ³bΓ‘lja meg ΓΊjra. Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index c4628b3f439..6c7340fea2e 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -402,7 +402,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -606,8 +606,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -694,8 +694,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -971,7 +971,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1306,4 +1313,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9ddd78a7713..cea78b18b54 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -401,7 +401,9 @@ Un messaggio eliminato non può essere ripristinato. Salvato File non disponibile Caricamento del file non riuscito - Vuoi aprire il file o salvarlo nella cartella dei download del tuo dispositivo? + Do you want to open the file, or save it to your + device\'s download folder? + Apri Salva Account eliminato @@ -607,8 +609,8 @@ Fino a 500 persone possono unirsi a una conversazione di gruppo. Impossibile aggiungere %1$s **partecipanti** al gruppo. Impossibile aggiungere %1$s al gruppo. Impossibile aggiungere %1$s al gruppo. - Questa conversazione non Γ¨ piΓΉ verificata, poichΓ© alcuni utenti utilizzano almeno un dispositivo privo del certificato di identitΓ  end-to-end. - Questa conversazione non Γ¨ piΓΉ verificata, poichΓ© alcuni utenti utilizzano almeno un dispositivo privo del certificato di identitΓ  end-to-end. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. Tutti i dispositivi sono verificati (identitΓ  end-to-end) Tutte le impronte digitali sono verificate (Proteus) La comunicazione su Wire Γ¨ sempre crittografata end-to-end. Tutto ciΓ² che invii e ricevi in questa conversazione Γ¨ accessibile soltanto a te e agli altri partecipanti del gruppo.\n**Ti preghiamo di prestare comunque attenzione a con chi condividi le informazioni sensibili.** @@ -708,8 +710,8 @@ Fino a 500 persone possono unirsi a una conversazione di gruppo. Multimedia Immagini File - Nessuna immagine Γ¨ stata ancora condivisa in questa conversazione πŸ₯² - Ancora nessun file Γ¨ stato condiviso in questa conversazione πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTATTI Nuovo Gruppo @@ -984,13 +986,14 @@ Rispondendo qui, verrà riagganciata l\'altra chiamata. Apri il profilo Non puoi cambiare account durante una chiamata Redirect verso un backend on-premises? - Se procedi, il tuo client verrà reindirizzato al seguente backend on-premises: - -Nome del backend: - %1$s - -URL del backend: - %2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Ricezione nuovi messaggi in corso Testo copiato negli appunti Registri @@ -1336,4 +1339,5 @@ registrato. Sei pregato di riprovare. Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e62210675a6..a2b8b772cb3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -402,7 +402,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -606,8 +606,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -694,8 +694,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -971,7 +971,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1306,4 +1313,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3ede90b32a2..a2340e3f407 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -402,7 +402,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -606,8 +606,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -694,8 +694,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -971,7 +971,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1306,4 +1313,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index b0bcd6cc8f9..c90a1bc7897 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -405,7 +405,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -615,8 +615,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -742,8 +742,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1025,7 +1025,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1390,4 +1397,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 5f0812e9f09..55c1f71fffc 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 007cff28cb8..1d068fdcfd7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 007cff28cb8..1d068fdcfd7 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 5f0812e9f09..55c1f71fffc 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b5d9e457376..eaed3a43ce9 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -405,7 +405,9 @@ Usunięta wiadomość nie może zostać przywrócona.Zapisane Plik jest niedostępny Ładowanie pliku nie powiodło się - Czy chcesz otworzyć plik czy zapisać go w folderze pobierania na Twoim urządzeniu? + Do you want to open the file, or save it to your + device\'s download folder? + Otwórz Zapisz Konto usunięte @@ -615,8 +617,8 @@ Do grupy może dołączyć maksymalnie 500 osób. %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -742,8 +744,8 @@ Do grupy może dołączyć maksymalnie 500 osób. Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ KONTAKTY Nowa grupa @@ -1024,13 +1026,14 @@ Dołączenie do tego połączenia spowoduje zakończenie tam Otwórz profil Nie możesz przełączać kont podczas rozmowy Przekierować do lokalnego serwera? - Jeśli kontynuujesz, twojemu klientowi zostanie przekierowany do następującego lokalnego serwera: - -Nazwa serwera: -%1$s - -URL serwera: -%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Pobieranie nowych wiadomości Tekst skopiowany do schowka Dzienniki @@ -1396,4 +1399,5 @@ Prosimy użyć zarządzania zespołami (%1$s) na tym środow Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 0a81771612f..ae03f0480f1 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -403,7 +403,9 @@ Uma mensagem excluΓ­da nΓ£o pode ser restaurada. Salvo Arquivo nΓ£o disponΓ­vel Falha ao enviar o arquivo - VocΓͺ quer abrir o arquivo ou salvΓ‘-lo na pasta de download do dispositivo? + Do you want to open the file, or save it to your + device\'s download folder? + Abrir Salvar Conta deletada @@ -608,8 +610,8 @@ AtΓ© 500 pessoas podem participar de uma conversa em grupo. %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -709,8 +711,8 @@ AtΓ© 500 pessoas podem participar de uma conversa em grupo. Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTATOS Novo Grupo @@ -984,13 +986,14 @@ AtΓ© 500 pessoas podem participar de uma conversa em grupo. Abrir Perfil VocΓͺ nΓ£o pode mudar de conta enquanto estiver em uma chamada Redirecionar para um backend local? - Se vocΓͺ prosseguir, o seu cliente serΓ‘ redirecionado para o seguinte backend local: - -Nome do backend: -%1$s - -URL do backend: -%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Recebendo novas mensagens Texto copiado para a Γ‘rea de transferΓͺncia Registros @@ -1338,4 +1341,5 @@ Por favor, use o gerenciamento de equipe (%1$s) neste backend. Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index f88480ae315..188c89b24bb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -404,7 +404,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -612,8 +612,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -726,8 +726,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1007,7 +1007,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1362,4 +1369,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 443d277a6d6..20c055d04aa 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -614,8 +614,8 @@ НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π² Π³Ρ€ΡƒΠΏΠΏΡƒ %1$s **участников**. НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ %1$s Π² Π³Ρ€ΡƒΠΏΠΏΡƒ. НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ %1$s Π² Π³Ρ€ΡƒΠΏΠΏΡƒ. - Π­Ρ‚Π° бСсСда большС Π½Π΅ являСтся Π²Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ, ΠΏΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ ΠΊΡ‚ΠΎ-Ρ‚ΠΎ ΠΈΠ· ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ ΠΏΠΎ ΠΊΡ€Π°ΠΉΠ½Π΅ΠΉ ΠΌΠ΅Ρ€Π΅ ΠΎΠ΄Π½ΠΎ устройство Π±Π΅Π· Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ сСртификата сквозной ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ. - Π­Ρ‚Π° бСсСда большС Π½Π΅ являСтся Π²Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ, ΠΏΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ ΠΊΡ‚ΠΎ-Ρ‚ΠΎ ΠΈΠ· ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ ΠΏΠΎ ΠΊΡ€Π°ΠΉΠ½Π΅ΠΉ ΠΌΠ΅Ρ€Π΅ ΠΎΠ΄Π½ΠΎ устройство Π±Π΅Π· Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ сСртификата сквозной ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ. + Π­Ρ‚Π° бСсСда большС являСтся Π²Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ, ΠΏΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ ΠΏΠΎ ΠΊΡ€Π°ΠΉΠ½Π΅ΠΉ ΠΌΠ΅Ρ€Π΅ ΠΎΠ΄ΠΈΠ½ ΠΈΠ· участников Π½Π°Ρ‡Π°Π» ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π½ΠΎΠ²ΠΎΠ΅ устройство ΠΈΠ»ΠΈ ΠΈΠΌΠ΅Π΅Ρ‚ Π½Π΅Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ сСртификат. + Π­Ρ‚Π° бСсСда большС являСтся Π²Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ, ΠΏΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ ΠΏΠΎ ΠΊΡ€Π°ΠΉΠ½Π΅ΠΉ ΠΌΠ΅Ρ€Π΅ ΠΎΠ΄ΠΈΠ½ ΠΈΠ· участников Π½Π°Ρ‡Π°Π» ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π½ΠΎΠ²ΠΎΠ΅ устройство ΠΈΠ»ΠΈ ΠΈΠΌΠ΅Π΅Ρ‚ Π½Π΅Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ сСртификат. ВсС устройства Π²Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Ρ‹ (сквозная идСнтификация) ВсС ΠΎΡ‚ΠΏΠ΅Ρ‡Π°Ρ‚ΠΊΠΈ Π²Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Ρ‹ (Proteus) ΠžΠ±Ρ‰Π΅Π½ΠΈΠ΅ Π² Wire всСгда вСдСтся с использованиСм сквозного ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½ΠΈΡ. ВсС, Ρ‡Ρ‚ΠΎ Π²Ρ‹ отправляСтС ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚Π΅ Π² этой бСсСдС, доступно Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π²Π°ΠΌ ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠΌ участникам Π³Ρ€ΡƒΠΏΠΏΡ‹.\n**ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π±ΡƒΠ΄ΡŒΡ‚Π΅ остороТны с Ρ‚Π΅ΠΌΠΈ, ΠΊΠΎΠΌΡƒ Π²Ρ‹ сообщаСтС ΠΊΠΎΠ½Ρ„ΠΈΠ΄Π΅Π½Ρ†ΠΈΠ°Π»ΡŒΠ½ΡƒΡŽ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ.** @@ -741,8 +741,8 @@ МСдиа Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΡ Π€Π°ΠΉΠ»Ρ‹ - Π’ этой бСсСдС ΠΏΠΎΠΊΠ° Π½Π΅Ρ‚ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ πŸ₯² - Π’ этой бСсСдС ΠΏΠΎΠΊΠ° Π½Π΅Ρ‚ Ρ„Π°ΠΉΠ»ΠΎΠ² πŸ₯² + Никто Π½Π΅ дСлился фотографиями Π² этой бСсСдС πŸ₯² + Никто Π½Π΅ дСлился Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ Π² этой бСсСдС πŸ™€ КОНВАКВЫ Новая Π³Ρ€ΡƒΠΏΠΏΠ° @@ -1024,7 +1024,14 @@ ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΡŒ НСвозмоТно ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π°Ρ‚ΡŒ Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚Ρ‹ Π²ΠΎ врСмя Π·Π²ΠΎΠ½ΠΊΠ° ΠŸΠ΅Ρ€Π΅Π½Π°ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π° Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ бэкэнд? - Π’ случаС продолТСния ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠ΅Ρ€Π΅Π½Π°ΠΏΡ€Π°Π²Π»Π΅Π½ Π½Π° ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠΉ Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ бэкэнд:\n\nНазваниС бэкэнда:\n%1$s\n\nURL бэкэнда:\n%2$s + ΠŸΡ€ΠΈ ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ΅Π½ΠΈΠΈ Π²Ρ‹ Π±ΡƒΠ΄Π΅Ρ‚Π΅ ΠΏΠ΅Ρ€Π΅Π½Π°ΠΏΡ€Π°Π²Π»Π΅Π½Ρ‹ Π½Π° ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠΉ Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ бэкэнд: + НазваниС бэкэнда: + URL бэкэнда: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π½ΠΎΠ²Ρ‹Ρ… сообщСний ВСкст скопирован Π² Π±ΡƒΡ„Π΅Ρ€ ΠΎΠ±ΠΌΠ΅Π½Π° Π–ΡƒΡ€Π½Π°Π»Ρ‹ @@ -1390,4 +1397,5 @@ ΠŸΠΎΠ΄Π΅Π»ΠΈΡ‚ΡŒΡΡ мСстополоТСниСм Π Π°Π·Ρ€Π΅ΡˆΠΈΡ‚Π΅ Wire ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ доступ ΠΊ ΠΌΠ΅ΡΡ‚ΠΎΠΏΠΎΠ»ΠΎΠΆΠ΅Π½ΠΈΡŽ вашСго устройства, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΠΌΠ΅Ρ‚ΡŒ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ ΠΈΠΌ ΠΏΠΎΠ΄Π΅Π»ΠΈΡ‚ΡŒΡΡ. ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅... + НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΏΠΎΠ΄Π΅Π»ΠΈΡ‚ΡŒΡΡ мСстополоТСниСм diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 908c75d236f..0c64c14578b 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -395,7 +395,9 @@ ΰ·ƒΰ·”ΰΆ»ΰ·ΰΆšΰ·’ΰΆ«ΰ·’ ΰΆœΰ·œΰΆ±ΰ·”ΰ·€ ΰΆ±ΰ·œΰΆ­ΰ·’ΰΆΆΰ·š ΰΆœΰ·œΰΆ±ΰ·”ΰ·€ ΰΆ‹ΰΆ©ΰ·”ΰΆœΰΆ­ ΰΆ±ΰ·œΰ·€ΰ·’ΰΆ«ΰ·’ - ΰΆ”ΰΆΆΰΆ§ ΰΆœΰ·œΰΆ±ΰ·”ΰ·€ ΰΆ‡ΰΆ»ΰ·“ΰΆΈΰΆ§ හෝ ΰΆ‘ΰΆΊ ΰΆ”ΰΆΆΰΆœΰ·š ΰΆ‹ΰΆ΄ΰ·ΰΆ‚ΰΆœΰΆΊΰ·š ΰΆΆΰ·ΰΆœΰ·ΰΆ±ΰ·“ΰΆΈΰ·Š ࢢහාࢽුࢸࢧ ΰ·ƒΰ·”ΰΆ»ΰ·ΰΆšΰ·“ΰΆΈΰΆ§ වුවࢸࢱා ΰΆ―? + Do you want to open the file, or save it to your + device\'s download folder? + ΰΆ…ΰΆ»ΰ·’ΰΆ±ΰ·ŠΰΆ± ΰ·ƒΰ·”ΰΆ»ΰΆšΰ·’ΰΆ±ΰ·ŠΰΆ± ΰΆΈΰΆšΰ·ΰΆ―ΰ·ΰΆΈΰ·– ΰΆœΰ·’ΰΆ«ΰ·”ΰΆΈΰΆšΰ·’ @@ -597,8 +599,8 @@ **ΰ·ƒΰ·„ΰΆ·ΰ·ΰΆœΰ·“ΰΆ±ΰ·Š** %1$s ්࢚ ΰ·ƒΰΆΈΰ·–ΰ·„ΰΆΊΰΆ§ ΰΆ‘ΰΆšΰ·Š ΰΆšΰ·’ΰΆ»ΰ·“ΰΆΈΰΆ§ ΰΆ±ΰ·œΰ·„ΰ·ΰΆšΰ·’ ΰ·€ΰ·’ΰΆΊ. %1$s ΰΆ―ΰ·™ΰΆ±ΰ·™ΰΆšΰ·Š ΰ·ƒΰΆΈΰ·–ΰ·„ΰΆΊΰΆ§ ΰΆ‘ΰΆšΰ·Š ΰΆšΰ·’ΰΆ»ΰ·“ΰΆΈΰΆ§ ΰΆ±ΰ·œΰ·„ΰ·ΰΆšΰ·’ ΰ·€ΰ·’ΰΆΊ. %1$s ΰΆ―ΰ·™ΰΆ±ΰ·™ΰΆšΰ·Š ΰ·ƒΰΆΈΰ·–ΰ·„ΰΆΊΰΆ§ ΰΆ‘ΰΆšΰ·Š ΰΆšΰ·’ΰΆ»ΰ·“ΰΆΈΰΆ§ ΰΆ±ΰ·œΰ·„ΰ·ΰΆšΰ·’ ΰ·€ΰ·’ΰΆΊ. - ΰΆ‡ΰΆ­ΰ·ΰΆΈΰ·Š ΰΆ΄ΰΆ»ΰ·’ΰ·ΰ·Šβ€ΰΆ»ΰ·“ΰΆ½ΰΆšΰΆΊΰ·’ΰΆ±ΰ·Š ΰ·€ΰΆ½ΰΆ‚ΰΆœΰ·” ΰΆ…ΰΆ±ΰ·ŠΰΆ­ ΰΆ…ΰΆ±ΰΆ±ΰ·Šβ€ΰΆΊΰΆ­ΰ· ΰ·ƒΰ·„ΰΆ­ΰ·’ΰΆšΰΆΊΰΆšΰ·Š ࢱැࢭිව ΰΆ…ΰ·€ΰΆΈ ΰ·€ΰ·ΰΆΊΰ·™ΰΆ±ΰ·Š ΰΆ‘ΰΆšΰ·Š ΰΆ‹ΰΆ΄ΰ·ΰΆ‚ΰΆœΰΆΊΰΆšΰ·Š ࢷාවිࢭා ࢚ࢻࢱ ΰΆΆΰ·ΰ·€ΰ·’ΰΆ±ΰ·Š ΰΆΈΰ·™ΰΆΈ සࢂවාࢯࢺ ΰΆ­ΰ·€ΰΆ―ΰ·”ΰΆ»ΰΆ§ΰΆ­ΰ·Š ΰ·ƒΰΆ­ΰ·Šβ€ΰΆΊΰ·ΰΆ΄ΰ·’ΰΆ­ ΰΆ±ΰ·œΰ·€ΰ·š. - ΰΆ‡ΰΆ­ΰ·ΰΆΈΰ·Š ΰΆ΄ΰΆ»ΰ·’ΰ·ΰ·Šβ€ΰΆ»ΰ·“ΰΆ½ΰΆšΰΆΊΰ·’ΰΆ±ΰ·Š ΰ·€ΰΆ½ΰΆ‚ΰΆœΰ·” ΰΆ…ΰΆ±ΰ·ŠΰΆ­ ΰΆ…ΰΆ±ΰΆ±ΰ·Šβ€ΰΆΊΰΆ­ΰ· ΰ·ƒΰ·„ΰΆ­ΰ·’ΰΆšΰΆΊΰΆšΰ·Š ࢱැࢭිව ΰΆ…ΰ·€ΰΆΈ ΰ·€ΰ·ΰΆΊΰ·™ΰΆ±ΰ·Š ΰΆ‘ΰΆšΰ·Š ΰΆ‹ΰΆ΄ΰ·ΰΆ‚ΰΆœΰΆΊΰΆšΰ·Š ࢷාවිࢭා ࢚ࢻࢱ ΰΆΆΰ·ΰ·€ΰ·’ΰΆ±ΰ·Š ΰΆΈΰ·™ΰΆΈ සࢂවාࢯࢺ ΰΆ­ΰ·€ΰΆ―ΰ·”ΰΆ»ΰΆ§ΰΆ­ΰ·Š ΰ·ƒΰΆ­ΰ·Šβ€ΰΆΊΰ·ΰΆ΄ΰ·’ΰΆ­ ΰΆ±ΰ·œΰ·€ΰ·š. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. ΰ·ƒΰ·’ΰΆΊΰΆ½ΰ·” ΰΆ‹ΰΆ΄ΰ·ΰΆ‚ΰΆœ ΰ·ƒΰΆ­ΰ·Šβ€ΰΆΊΰ·ΰΆ΄ΰ·’ΰΆ­ΰΆΊΰ·’ (ΰΆ…ΰΆ±ΰ·ŠΰΆ­ ΰΆ…ΰΆ±ΰΆ±ΰ·Šβ€ΰΆΊΰΆ­ΰ·ΰ·€) ΰ·ƒΰ·’ΰΆΊΰΆ½ΰ·” ΰΆ‡ΰΆŸΰ·’ΰΆ½ΰ·’ ΰ·ƒΰΆ§ΰ·„ΰΆ±ΰ·Š ΰ·ƒΰΆ­ΰ·Šβ€ΰΆΊΰ·ΰΆ΄ΰ·’ΰΆ­ΰΆΊΰ·’ (ΰΆ΄ΰ·Šβ€ΰΆ»ΰ·ΰΆ§ΰ·’ΰΆΊΰ·ƒΰ·Š) ΰ·€ΰΆΊΰΆ»ΰ·Š ΰ·ƒΰΆ±ΰ·ŠΰΆ±ΰ·’ΰ·€ΰ·šΰΆ―ΰΆ±ΰΆΊ ΰ·ƒΰ·‘ΰΆΈ ΰ·€ΰ·’ΰΆ§ΰΆΈ ΰΆ…ΰΆ±ΰ·ŠΰΆ­ ΰ·ƒΰΆ‚ΰΆšΰ·šΰΆ­ΰ·’ΰΆ­ΰΆΊΰ·’. ΰΆΈΰ·™ΰΆΈ ΰ·ƒΰΆ‚ΰ·€ΰ·ΰΆ―ΰΆΊΰ·š ΰΆ”ΰΆΆ ΰΆΊΰ·€ΰΆ± ΰ·ƒΰ·„ ࢽැࢢෙࢱ ΰ·ƒΰ·‘ΰΆΈ ΰΆ―ΰ·™ΰΆΊΰΆšΰ·ŠΰΆΈ ΰΆ”ΰΆΆΰΆ§ ΰ·ƒΰ·„ ΰ·€ΰ·™ΰΆ±ΰΆ­ΰ·Š ΰ·ƒΰΆΈΰ·–ΰ·„ ΰ·ƒΰ·„ΰΆ·ΰ·ΰΆœΰ·“ΰΆ±ΰ·ŠΰΆ§ ࢴࢸࢫ්࢚ ΰΆ΄ΰ·Šβ€ΰΆ»ΰ·€ΰ·šΰ· ΰ·€ΰ·“ΰΆΈΰΆ§ ΰ·„ΰ·ΰΆšΰ·’ΰΆΊ.\n**ΰΆ”ΰΆΆ ΰ·ƒΰΆ‚ΰ·€ΰ·šΰΆ―ΰ·“ ΰΆ­ΰ·œΰΆ»ΰΆ­ΰ·”ΰΆ»ΰ·” ΰΆΆΰ·™ΰΆ―ΰ·ΰΆœΰΆ±ΰ·ŠΰΆ±ΰ·š ΰΆšΰ·€ΰ·”ΰΆ»ΰ·”ΰΆ±ΰ·Š ΰ·ƒΰΆΈΰΆŸ ࢯැࢺි ΰΆ­ΰ·€ΰΆ―ΰ·”ΰΆ»ΰΆ§ΰΆ­ΰ·Š ΰ·ƒΰ·ΰΆ½ΰΆšΰ·’ΰΆ½ΰ·’ΰΆΈΰΆ­ΰ·Š ΰ·€ΰΆ±ΰ·ŠΰΆ±.** @@ -698,8 +700,8 @@ ΰΆΈΰ·ΰΆ°ΰ·Šβ€ΰΆΊ ࢑ාࢺාࢻූࢴ ΰΆœΰ·œΰΆ±ΰ·” - ΰΆΈΰ·™ΰΆΈ ΰ·ƒΰΆ‚ΰ·€ΰ·ΰΆ―ΰΆΊΰ·š ࢑ාࢺාࢻූࢴ ΰΆšΰ·’ΰ·ƒΰ·’ΰ·€ΰΆšΰ·Š ΰΆΆΰ·™ΰΆ―ΰ·ΰΆœΰ·™ΰΆ± ࢱැࢭ πŸ₯² - ΰΆΈΰ·™ΰΆΈ ΰ·ƒΰΆ‚ΰ·€ΰ·ΰΆ―ΰΆΊΰ·š ΰΆœΰ·œΰΆ±ΰ·” ΰΆšΰ·’ΰ·ƒΰ·’ΰ·€ΰΆšΰ·Š ΰΆΆΰ·™ΰΆ―ΰ·ΰΆœΰ·™ΰΆ± ࢱැࢭ πŸ₯² + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ සࢢࢳࢭා ΰΆ±ΰ·€ ΰ·ƒΰΆΈΰ·–ΰ·„ΰΆΊΰΆšΰ·Š @@ -975,7 +977,14 @@ ΰΆ΄ΰ·ΰΆ­ΰ·’ΰΆšΰΆ© ΰΆ…ΰΆ»ΰ·’ΰΆ±ΰ·ŠΰΆ± ΰΆ‡ΰΆΈΰΆ­ΰ·”ΰΆΈΰΆšΰ·Š ΰΆ‡ΰΆ­ΰ·ŠΰΆ±ΰΆΈΰ·Š ΰΆœΰ·’ΰΆ«ΰ·”ΰΆΈΰ·Š ΰΆ…ΰΆ­ΰΆ» ࢸාࢻු ΰ·€ΰ·“ΰΆΈΰΆ§ ΰΆ±ΰ·œΰ·„ΰ·ΰΆšΰ·’ΰΆΊ ΰΆ΄ΰΆ»ΰ·’ΰ·ΰ·Šβ€ΰΆ»ΰΆΊΰΆš ΰ·ƒΰ·šΰ·€ΰ·ΰΆ―ΰ·ΰΆΊΰΆšΰΆΊΰΆšΰΆ§ හࢻවා ΰΆΊΰ·€ΰΆ±ΰ·ŠΰΆ±ΰΆ―? - ΰΆ”ΰΆΆ ΰΆ‰ΰΆ―ΰ·’ΰΆ»ΰ·’ΰΆΊΰΆ§ ΰΆœΰ·’ΰΆΊΰ·„ΰ·œΰΆ­ΰ·Š, ΰΆ”ΰΆΆΰΆœΰ·š ΰΆ…ΰΆ±ΰ·”ΰΆœΰ·Šβ€ΰΆ»ΰ·ΰ·„ΰΆšΰΆΊ ΰΆ΄ΰ·„ΰΆ­ ΰΆ΄ΰΆ»ΰ·’ΰ·ΰ·Šβ€ΰΆ»ΰΆΊΰ·š ΰ·ƒΰ·šΰ·€ΰ·ΰΆ―ΰ·ΰΆΊΰΆšΰΆΊΰΆ§ හࢻවා ΰΆΊΰ·€ΰΆ±ΰ·” ࢽැࢢේ:\n\nΰ·ƒΰ·šΰ·€ΰ·ΰΆ―ΰ·ΰΆΊΰΆšΰΆΊΰ·š ΰΆ±ΰΆΈ:\n%1$s\n\nΰΆ’.ΰ·ƒ.ΰΆ±ΰ·’.:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: ΰΆ±ΰ·€ ΰΆ΄ΰΆ«ΰ·’ΰ·€ΰ·’ΰΆ© ΰΆ½ΰ·ΰΆΆΰ·™ΰΆΈΰ·’ΰΆ±ΰ·Š ΰΆ΄ΰ·™ΰ·… ΰΆ΄ΰ·ƒΰ·”ΰΆ»ΰ·” ΰΆ΄ΰ·”ΰ·€ΰΆ»ΰ·”ΰ·€ΰΆ§ ΰΆ΄ΰ·’ΰΆ§ΰΆ΄ΰΆ­ΰ·Š ΰ·€ΰ·’ΰΆΊ ΰ·ƒΰΆ§ΰ·„ΰΆ±ΰ·Š @@ -1320,4 +1329,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index db649f6dd7f..fe993069714 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -405,7 +405,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -615,8 +615,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -742,8 +742,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1025,7 +1025,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1390,4 +1397,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 900a6041f27..d8ce8a38247 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -405,7 +405,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -615,8 +615,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -742,8 +742,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1025,7 +1025,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1390,4 +1397,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index f88480ae315..188c89b24bb 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -404,7 +404,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -612,8 +612,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -726,8 +726,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1007,7 +1007,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1362,4 +1369,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index d29c49d5014..54cc4d5c16d 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Bilder Filer - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ KONTAKTER Ny grupp @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Loggar @@ -1328,10 +1335,11 @@ Call Anyway App permissions - Settings - Not Now + InstΓ€llningar + Inte nu Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f20a2211db5..a127da00fc1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -403,7 +403,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -609,8 +609,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -710,8 +710,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -989,7 +989,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1334,4 +1341,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0c223940d19..0fb22659d3f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -405,7 +405,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -615,8 +615,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -742,8 +742,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -1025,7 +1025,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1390,4 +1397,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 3ede90b32a2..a2340e3f407 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -402,7 +402,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -606,8 +606,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -694,8 +694,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -971,7 +971,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1306,4 +1313,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index c4628b3f439..6c7340fea2e 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -402,7 +402,7 @@ Saved File not available File upload failed - Do you want to open the file or save it to your + Do you want to open the file, or save it to your device\'s download folder? Open @@ -606,8 +606,8 @@ %1$s **participants** could not be added to the group. %1$s could not be added to the group. %1$s could not be added to the group. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. - This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. + This conversation is no longer verified, as at least one participant started using a new device or has an invalid certificate. All devices are verified (end-to-end identity) All fingerprints are verified (Proteus) Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.** @@ -694,8 +694,8 @@ Media Pictures Files - No pictures have been shared in this conversation yet πŸ₯² - No files have been shared in this conversation yet πŸ™€ + Nobody shared pictures in this conversation yet πŸ₯² + Nobody shared files in this conversation yet πŸ™€ CONTACTS New Group @@ -971,7 +971,14 @@ Open Profile You can\'t switch accounts while in a call Redirect to an on-premises backend? - If you proceed, your client will be redirected to the following on-premises backend:\n\nBackend name:\n%1$s\n\nBackend URL:\n%2$s + If you proceed, your client will be redirected to the following on-premises backend: + Backend name: + Backend URL: + Blacklist URL: + Teams URL: + Accounts URL: + Website URL: + Backend WSURL: Receiving new messages Text copied to clipboard Logs @@ -1306,4 +1313,5 @@ Share Location Allow Wire to access your device location to send your location. Please wait... + Location could not be shared From 4a50f6502694683c18a13752640ecd87fda3222c Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Fri, 26 Jan 2024 14:17:18 +0100 Subject: [PATCH 011/134] feat(e2ei): respect e2ei during login and mls client creation (WPB-5851) (#2621) --- app/src/main/AndroidManifest.xml | 9 + ...fE2EIRequiredDuringLoginUseCaseProvider.kt | 37 +++ .../android/di/accountScoped/UserModule.kt | 6 + .../feature/e2ei/GetE2EICertificateUseCase.kt | 10 +- .../wire/android/feature/e2ei/OAuthUseCase.kt | 11 +- .../feature/MigrateClientsDataUseCase.kt | 15 +- .../com/wire/android/ui/WireActivity.kt | 7 +- .../wire/android/ui/WireActivityViewModel.kt | 27 +- .../create/code/CreateAccountCodeViewModel.kt | 5 + .../ui/authentication/devices/model/Device.kt | 14 ++ .../devices/register/RegisterDeviceScreen.kt | 7 +- .../devices/register/RegisterDeviceState.kt | 11 +- .../register/RegisterDeviceViewModel.kt | 19 +- .../devices/remove/RemoveDeviceScreen.kt | 11 +- .../devices/remove/RemoveDeviceViewModel.kt | 15 +- .../ui/authentication/login/LoginScreen.kt | 20 +- .../login/email/LoginEmailScreen.kt | 2 +- .../email/LoginEmailVerificationCodeScreen.kt | 2 +- .../login/email/LoginEmailViewModel.kt | 11 +- .../login/sso/LoginSSOScreen.kt | 2 +- .../login/sso/LoginSSOViewModel.kt | 31 ++- .../wire/android/ui/debug/DebugDataOptions.kt | 2 +- .../ui/e2eiEnrollment/E2EIEnrollmentScreen.kt | 231 ++++++++++++++++++ .../e2eiEnrollment/E2EIEnrollmentViewModel.kt | 123 ++++++++++ .../sync/FeatureFlagNotificationViewModel.kt | 5 +- .../devices/DeviceDetailsViewModel.kt | 7 +- .../settings/devices/SelfDevicesViewModel.kt | 3 +- .../e2ei/E2eiCertificateDetailsScreen.kt | 2 +- .../wire/android/util/CurrentScreenManager.kt | 4 + .../kotlin/com/wire/android/util/UriUtil.kt | 5 + .../android/ui/WireActivityViewModelTest.kt | 13 +- .../login/email/LoginEmailViewModelTest.kt | 8 +- .../login/sso/LoginSSOViewModelTest.kt | 14 +- .../search/SearchUserViewModelTest.kt | 7 +- .../devices/DeviceDetailsViewModelTest.kt | 2 +- .../src/main/kotlin/AndroidCoordinates.kt | 2 +- kalium | 2 +- 37 files changed, 627 insertions(+), 75 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/di/ObserveIfE2EIRequiredDuringLoginUseCaseProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f76f5432ac8..f3f51dcd5fa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -238,6 +238,15 @@ android:authorities="${applicationId}.firebaseinitprovider" tools:node="remove" /> + + + ) -> Unit - operator fun invoke(context: Context, enrollmentResultHandler: (Either) -> Unit) { + operator fun invoke( + context: Context, + isNewClient: Boolean, + enrollmentResultHandler: (Either) -> Unit + ) { this.enrollmentResultHandler = enrollmentResultHandler scope.launch { - enrollE2EI.initialEnrollment().fold({ + enrollE2EI.initialEnrollment(isNewClientRegistration = isNewClient).fold({ enrollmentResultHandler(Either.Left(it)) }, { if (it is E2EIEnrollmentResult.Initialized) { initialEnrollmentResult = it - OAuthUseCase(context, it.target, it.oAuthState).launch( + OAuthUseCase(context, it.target, it.oAuthClaims, it.oAuthState).launch( context.getActivity()!!.activityResultRegistry, ::oAuthResultHandler ) diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt index 6e0476116e9..12c2885903a 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt @@ -28,6 +28,8 @@ import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContracts import com.wire.android.appLogger import com.wire.android.util.deeplink.DeepLinkProcessor +import com.wire.android.util.removeQueryParams +import kotlinx.serialization.json.JsonObject import net.openid.appauth.AppAuthConfiguration import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationException @@ -41,7 +43,9 @@ import net.openid.appauth.ResponseTypeValues import net.openid.appauth.browser.BrowserAllowList import net.openid.appauth.browser.VersionedBrowserMatcher import net.openid.appauth.connectivity.ConnectionBuilder +import org.json.JSONObject import java.net.HttpURLConnection +import java.net.URI import java.net.URL import java.security.MessageDigest import java.security.SecureRandom @@ -52,7 +56,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager -class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: String?) { +class OAuthUseCase(context: Context, private val authUrl: String, private val claims: JsonObject, oAuthState: String?) { private var authState: AuthState = oAuthState?.let { AuthState.jsonDeserialize(it) } ?: AuthState() @@ -117,7 +121,7 @@ class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: St handleActivityResult(result, resultHandler) } AuthorizationServiceConfiguration.fetchFromUrl( - Uri.parse(authUrl.plus(IDP_CONFIGURATION_PATH)), + Uri.parse(URI(authUrl).removeQueryParams().toString().plus(IDP_CONFIGURATION_PATH)), { configuration, ex -> if (ex == null) { authServiceConfig = configuration!! @@ -177,7 +181,8 @@ class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: St AuthorizationRequest.Scope.EMAIL, AuthorizationRequest.Scope.PROFILE, AuthorizationRequest.Scope.OFFLINE_ACCESS - ).build() + ).setClaims(JSONObject(claims.toString())) + .build() private fun AuthorizationRequest.Builder.setCodeVerifier(): AuthorizationRequest.Builder { val codeVerifier = getCodeVerifier() diff --git a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt index b4f2378bf82..eb2a9cd3cbc 100644 --- a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt @@ -47,7 +47,7 @@ class MigrateClientsDataUseCase @Inject constructor( private val scalaUserDBProvider: ScalaUserDatabaseProvider, private val userDataStoreProvider: UserDataStoreProvider ) { - @Suppress("ReturnCount") + @Suppress("ReturnCount", "ComplexMethod") suspend operator fun invoke(userId: UserId, isFederated: Boolean): Either = scalaUserDBProvider.clientDAO(userId.value).flatMap { clientDAO -> val clientId = clientDAO.clientInfo()?.clientId?.let { ClientId(it) } @@ -103,6 +103,19 @@ class MigrateClientsDataUseCase @Inject constructor( userDataStoreProvider.getOrCreate(userId).setInitialSyncCompleted() } } + + is RegisterClientResult.E2EICertificateRequired -> + withTimeoutOrNull(SYNC_START_TIMEOUT) { + syncManager.waitUntilStartedOrFailure() + }.let { + it ?: Either.Left(NetworkFailure.NoNetworkConnection(null)) + }.flatMap { + syncManager.waitUntilLiveOrFailure() + .onSuccess { + userDataStoreProvider.getOrCreate(userId).setInitialSyncCompleted() + TODO() // TODO: ask question about this! + } + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index e1b4e05ae21..b3bf8103ea4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -66,6 +66,7 @@ import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.CommonTopAppBar import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel import com.wire.android.ui.destinations.ConversationScreenDestination +import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination @@ -158,9 +159,9 @@ class WireActivity : AppCompatActivity() { val startDestination = when (viewModel.initialAppState) { InitialAppState.NOT_MIGRATED -> MigrationScreenDestination InitialAppState.NOT_LOGGED_IN -> WelcomeScreenDestination - InitialAppState.LOGGED_IN -> HomeScreenDestination - } - + InitialAppState.ENROLL_E2EI -> E2EIEnrollmentScreenDestination + InitialAppState.LOGGED_IN -> HomeScreenDestination + } appLogger.i("$TAG composable content") setComposableContent(startDestination) { appLogger.i("$TAG splash hide") diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 5d934eb6b93..84574392d9e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -29,6 +29,7 @@ import com.wire.android.appLogger import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.AuthServerConfigProvider import com.wire.android.di.KaliumCoreLogic +import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider import com.wire.android.di.ObserveSyncStateUseCaseProvider import com.wire.android.feature.AccountSwitchUseCase @@ -110,6 +111,7 @@ class WireActivityViewModel @Inject constructor( private val currentScreenManager: CurrentScreenManager, private val observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory, private val globalDataStore: GlobalDataStore, + private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory ) : ViewModel() { var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState()) @@ -142,12 +144,16 @@ class WireActivityViewModel @Inject constructor( private val _observeSyncFlowState: MutableStateFlow = MutableStateFlow(null) val observeSyncFlowState: StateFlow = _observeSyncFlowState + private val _observeE2EIState: MutableStateFlow = MutableStateFlow(null) + private val observeE2EIState: StateFlow = _observeE2EIState + init { observeSyncState() observeUpdateAppState() observeNewClientState() observeScreenshotCensoringConfigState() observeAppThemeState() + observerE2EIState() } private fun observeAppThemeState() { @@ -160,6 +166,18 @@ class WireActivityViewModel @Inject constructor( } } + fun observerE2EIState() { + viewModelScope.launch(dispatchers.io()) { + observeUserId + .flatMapLatest { + it?.let { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(it).observeIfE2EIIsRequiredDuringLogin() } + ?: flowOf(null) + } + .distinctUntilChanged() + .collect { _observeE2EIState.emit(it) } + } + } + private fun observeSyncState() { viewModelScope.launch(dispatchers.io()) { observeUserId @@ -233,6 +251,7 @@ class WireActivityViewModel @Inject constructor( get() = when { shouldMigrate() -> InitialAppState.NOT_MIGRATED shouldLogIn() -> InitialAppState.NOT_LOGGED_IN + blockedByE2EI() -> InitialAppState.ENROLL_E2EI else -> InitialAppState.LOGGED_IN } @@ -263,8 +282,10 @@ class WireActivityViewModel @Inject constructor( // to handle the deepLinks above user needs to be Logged in // do nothing, already handled by initialAppState } + result is DeepLinkResult.JoinConversation -> onConversationInviteDeepLink(result.code, result.key, result.domain, onOpenConversation) + result != null -> onResult(result) result is DeepLinkResult.Unknown -> appLogger.e("unknown deeplink result $result") } @@ -391,6 +412,10 @@ class WireActivityViewModel @Inject constructor( fun shouldLogIn(): Boolean = !hasValidCurrentSession() + fun blockedByE2EI(): Boolean { + return observeE2EIState.value == true + } + private fun hasValidCurrentSession(): Boolean = runBlocking { // TODO: the usage of currentSessionFlow is a temporary solution, it should be replaced with a proper solution currentSessionFlow().first().let { @@ -510,5 +535,5 @@ data class GlobalAppState( ) enum class InitialAppState { - NOT_MIGRATED, NOT_LOGGED_IN, LOGGED_IN + NOT_MIGRATED, NOT_LOGGED_IN, LOGGED_IN, ENROLL_E2EI } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt index d8b8fda010c..c7a075c102c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt @@ -187,6 +187,11 @@ class CreateAccountCodeViewModel @Inject constructor( is RegisterClientResult.Success -> { onSuccess() } + + is RegisterClientResult.E2EICertificateRequired -> { + // TODO + onSuccess() + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt index 1e8e7c3122e..4f5e3911eb0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt @@ -51,6 +51,20 @@ data class Device( mlsPublicKeys = client.mlsPublicKeys, e2eiCertificateStatus = e2eiCertificateStatus ) + + fun updateFromClient(client: Client): Device = copy( + name = client.displayName(), + clientId = client.id, + registrationTime = client.registrationTime?.toIsoDateTimeString(), + lastActiveInWholeWeeks = client.lastActiveInWholeWeeks(), + isValid = client.isValid, + isVerifiedProteus = client.isVerified, + mlsPublicKeys = client.mlsPublicKeys, + ) + + fun updateE2EICertificateStatus(e2eiCertificateStatus: CertificateStatus): Device = copy( + e2eiCertificateStatus = e2eiCertificateStatus + ) } /** diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt index 72c89dbeaf3..ba16c4bb82c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt @@ -61,6 +61,7 @@ import com.wire.android.ui.common.textfield.clearAutofillTree import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState +import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination import com.wire.android.ui.destinations.RemoveDeviceScreenDestination @@ -81,11 +82,14 @@ fun RegisterDeviceScreen(navigator: Navigator) { is RegisterDeviceFlowState.Success -> { navigator.navigate( NavigationCommand( - destination = if (flowState.initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination, + destination = if (flowState.isE2EIRequired) E2EIEnrollmentScreenDestination + else if (flowState.initialSyncCompleted) HomeScreenDestination + else InitialSyncScreenDestination, backStackMode = BackStackMode.CLEAR_WHOLE ) ) } + is RegisterDeviceFlowState.TooManyDevices -> navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination)) else -> RegisterDeviceContent( @@ -189,6 +193,7 @@ private fun PasswordTextField(state: RegisterDeviceState, onPasswordChange: (Tex state = when (state.flowState) { is RegisterDeviceFlowState.Error.InvalidCredentialsError -> WireTextFieldState.Error(stringResource(id = R.string.remove_device_invalid_password)) + else -> WireTextFieldState.Default }, imeAction = ImeAction.Done, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt index c0169ccdc8c..766959596da 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt @@ -20,17 +20,26 @@ package com.wire.android.ui.authentication.devices.register import androidx.compose.ui.text.input.TextFieldValue import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.user.UserId data class RegisterDeviceState( val password: TextFieldValue = TextFieldValue(""), val continueEnabled: Boolean = false, val flowState: RegisterDeviceFlowState = RegisterDeviceFlowState.Default ) + sealed class RegisterDeviceFlowState { object Default : RegisterDeviceFlowState() object Loading : RegisterDeviceFlowState() object TooManyDevices : RegisterDeviceFlowState() - data class Success(val initialSyncCompleted: Boolean) : RegisterDeviceFlowState() + data class Success( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + val clientId: ClientId, + val userId: UserId? = null + ) : RegisterDeviceFlowState() + sealed class Error : RegisterDeviceFlowState() { object InvalidCredentialsError : Error() data class GenericError(val coreFailure: CoreFailure) : Error() diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt index bc9ac87c013..b7d0354d7f3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt @@ -83,8 +83,25 @@ class RegisterDeviceViewModel @Inject constructor( )) { is RegisterClientResult.Failure.TooManyClients -> updateFlowState(RegisterDeviceFlowState.TooManyDevices) + is RegisterClientResult.Success -> - updateFlowState(RegisterDeviceFlowState.Success(userDataStore.initialSyncCompleted.first())) + updateFlowState( + RegisterDeviceFlowState.Success( + userDataStore.initialSyncCompleted.first(), + false, + registerDeviceResult.client.id + ) + ) + + is RegisterClientResult.E2EICertificateRequired -> + updateFlowState( + RegisterDeviceFlowState.Success( + userDataStore.initialSyncCompleted.first(), + true, + registerDeviceResult.client.id, + registerDeviceResult.userId + ) + ) is RegisterClientResult.Failure.Generic -> state = state.copy( continueEnabled = true, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt index 1e8f40bdbfe..a0c36cdc40d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt @@ -58,6 +58,7 @@ import com.wire.android.ui.common.divider.WireDivider import com.wire.android.ui.common.rememberTopBarElevationState import com.wire.android.ui.common.textfield.clearAutofillTree import com.wire.android.ui.common.visbility.rememberVisibilityState +import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination import com.wire.android.util.dialogErrorStrings @@ -73,9 +74,11 @@ fun RemoveDeviceScreen(navigator: Navigator) { val state: RemoveDeviceState = viewModel.state val clearSessionState: ClearSessionState = clearSessionViewModel.state - fun navigateAfterSuccess(initialSyncCompleted: Boolean) = navigator.navigate( + fun navigateAfterSuccess(initialSyncCompleted: Boolean, isE2EIRequired: Boolean) = navigator.navigate( NavigationCommand( - destination = if (initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination, + destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination + else if (initialSyncCompleted) HomeScreenDestination + else InitialSyncScreenDestination, backStackMode = BackStackMode.CLEAR_WHOLE ) ) @@ -84,9 +87,9 @@ fun RemoveDeviceScreen(navigator: Navigator) { RemoveDeviceContent( state = state, clearSessionState = clearSessionState, - onItemClicked = { viewModel.onItemClicked(it) { navigateAfterSuccess(it) } }, + onItemClicked = { viewModel.onItemClicked(it, ::navigateAfterSuccess) }, onPasswordChange = viewModel::onPasswordChange, - onRemoveConfirm = { viewModel.onRemoveConfirmed { navigateAfterSuccess(it) } }, + onRemoveConfirm = { viewModel.onRemoveConfirmed(::navigateAfterSuccess) }, onDialogDismiss = viewModel::onDialogDismissed, onErrorDialogDismiss = viewModel::clearDeleteClientError, onBackButtonClicked = clearSessionViewModel::onBackButtonClicked, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt index 44422ce4613..ceb0f3b653f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt @@ -92,7 +92,7 @@ class RemoveDeviceViewModel @Inject constructor( updateStateIfDialogVisible { state.copy(error = RemoveDeviceError.None) } } - fun onItemClicked(device: Device, onCompleted: (initialSyncCompleted: Boolean) -> Unit) { + fun onItemClicked(device: Device, onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { viewModelScope.launch { val isPasswordRequired: Boolean = when (val passwordRequiredResult = isPasswordRequired()) { is IsPasswordRequiredUseCase.Result.Failure -> { @@ -113,7 +113,7 @@ class RemoveDeviceViewModel @Inject constructor( } } - private suspend fun registerClient(password: String?, onCompleted: (initialSyncCompleted: Boolean) -> Unit) { + private suspend fun registerClient(password: String?, onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { registerClientUseCase( RegisterClientUseCase.RegisterClientParam(password, null) ).also { result -> @@ -125,12 +125,17 @@ class RemoveDeviceViewModel @Inject constructor( is RegisterClientResult.Failure.Generic -> state = state.copy(error = RemoveDeviceError.GenericError(result.genericFailure)) is RegisterClientResult.Failure.InvalidCredentials -> state = state.copy(error = RemoveDeviceError.InvalidCredentialsError) is RegisterClientResult.Failure.TooManyClients -> loadClientsList() - is RegisterClientResult.Success -> onCompleted(userDataStore.initialSyncCompleted.first()) + is RegisterClientResult.Success -> onCompleted(userDataStore.initialSyncCompleted.first(), false) + is RegisterClientResult.E2EICertificateRequired -> onCompleted(userDataStore.initialSyncCompleted.first(), true) } } } - private suspend fun deleteClient(password: String?, device: Device, onCompleted: (initialSyncCompleted: Boolean) -> Unit) { + private suspend fun deleteClient( + password: String?, + device: Device, + onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit + ) { when (val deleteResult = deleteClientUseCase(DeleteClientParam(password, device.clientId))) { is DeleteClientResult.Failure.Generic -> { state = state.copy(error = RemoveDeviceError.GenericError(deleteResult.genericFailure)) @@ -147,7 +152,7 @@ class RemoveDeviceViewModel @Inject constructor( } } - fun onRemoveConfirmed(onCompleted: (initialSyncCompleted: Boolean) -> Unit) { + fun onRemoveConfirmed(onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { (state.removeDeviceDialogState as? RemoveDeviceDialogState.Visible)?.let { dialogStateVisible -> updateStateIfDialogVisible { state.copy(removeDeviceDialogState = it.copy(loading = true, removeEnabled = false)) } viewModelScope.launch { diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt index 55a656be33c..acad24526f9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt @@ -74,6 +74,7 @@ import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogState import com.wire.android.ui.common.rememberTopBarElevationState import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState +import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination import com.wire.android.ui.destinations.RemoveDeviceScreenDestination @@ -98,13 +99,12 @@ fun LoginScreen( LoginContent( navigator::navigateBack, - { initialSyncCompleted -> - navigator.navigate( - NavigationCommand( - if (initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination, - BackStackMode.CLEAR_WHOLE - ) - ) + { initialSyncCompleted, isE2EIRequired -> + val destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination + else if (initialSyncCompleted) HomeScreenDestination + else InitialSyncScreenDestination + + navigator.navigate(NavigationCommand(destination, BackStackMode.CLEAR_WHOLE)) }, { navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination, BackStackMode.CLEAR_WHOLE)) }, loginViewModel, @@ -117,7 +117,7 @@ fun LoginScreen( @Composable private fun LoginContent( onBackPressed: () -> Unit, - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, viewModel: LoginViewModel, loginEmailViewModel: LoginEmailViewModel, @@ -146,7 +146,7 @@ private fun LoginContent( @Composable private fun MainLoginContent( onBackPressed: () -> Unit, - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, viewModel: LoginViewModel, loginEmailViewModel: LoginEmailViewModel, @@ -353,6 +353,6 @@ enum class LoginTabItem(@StringRes override val titleResId: Int) : TabItem { @Composable private fun PreviewLoginScreen() { WireTheme { - MainLoginContent({}, {}, {}, hiltViewModel(), hiltViewModel(), ssoLoginResult = null) + MainLoginContent({}, { _, _ -> }, {}, hiltViewModel(), hiltViewModel(), ssoLoginResult = null) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt index a3a326cd82d..f03624ae095 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt @@ -77,7 +77,7 @@ import kotlinx.coroutines.launch @Composable fun LoginEmailScreen( - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, loginEmailViewModel: LoginEmailViewModel, scrollState: ScrollState = rememberScrollState() diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt index f2dcd0f71de..43fb16021ca 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt @@ -46,7 +46,7 @@ import com.wire.android.util.ui.UIText @Composable fun LoginEmailVerificationCodeScreen( - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, viewModel: LoginEmailViewModel = hiltViewModel() ) = LoginEmailVerificationCodeContent( viewModel.secondFactorVerificationCodeState, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt index 52f2e6a9e3e..85361e510b0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt @@ -72,7 +72,7 @@ class LoginEmailViewModel @Inject constructor( ) @Suppress("LongMethod") - fun login(onSuccess: (initialSyncCompleted: Boolean) -> Unit) { + fun login(onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { loginState = loginState.copy(emailLoginLoading = true, loginError = LoginError.None).updateEmailLoginEnabled() viewModelScope.launch { val authScope = withContext(dispatchers.io()) { resolveCurrentAuthScope() } ?: return@launch @@ -123,7 +123,12 @@ class LoginEmailViewModel @Inject constructor( } is RegisterClientResult.Success -> { - onSuccess(isInitialSyncCompleted(storedUserId)) + onSuccess(isInitialSyncCompleted(storedUserId), false) + } + + is RegisterClientResult.E2EICertificateRequired -> { + onSuccess(isInitialSyncCompleted(storedUserId), true) + return@launch } } } @@ -215,7 +220,7 @@ class LoginEmailViewModel @Inject constructor( loginState = loginState.copy(proxyPassword = newText).updateEmailLoginEnabled() } - fun onCodeChange(newValue: CodeFieldValue, onSuccess: (initialSyncCompleted: Boolean) -> Unit) { + fun onCodeChange(newValue: CodeFieldValue, onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { secondFactorVerificationCodeState = secondFactorVerificationCodeState.copy(codeInput = newValue, isCurrentCodeInvalid = false) if (newValue.isFullyFilled) { login(onSuccess) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt index 211e26273cb..a7ceb4f44f8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt @@ -61,7 +61,7 @@ import kotlinx.coroutines.flow.onEach @Composable fun LoginSSOScreen( - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, ssoLoginResult: DeepLinkResult.SSOLogin?, scrollState: ScrollState = rememberScrollState() diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt index 263f1f25b9a..af0e111012c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt @@ -197,9 +197,13 @@ class LoginSSOViewModel @Inject constructor( } } - @Suppress("ComplexMethod") + @Suppress("ComplexMethod", "LongMethod") @VisibleForTesting - fun establishSSOSession(cookie: String, serverConfigId: String, onSuccess: (initialSyncCompleted: Boolean) -> Unit) { + fun establishSSOSession( + cookie: String, + serverConfigId: String, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit + ) { loginState = loginState.copy(ssoLoginLoading = true, loginError = LoginError.None).updateSSOLoginEnabled() viewModelScope.launch { val authScope = @@ -251,13 +255,17 @@ class LoginSSOViewModel @Inject constructor( registerClient(storedUserId, null).let { when (it) { is RegisterClientResult.Success -> { - onSuccess(isInitialSyncCompleted(storedUserId)) + onSuccess(isInitialSyncCompleted(storedUserId), false) } is RegisterClientResult.Failure -> { updateSSOLoginError(it.toLoginError()) return@launch } + + is RegisterClientResult.E2EICertificateRequired -> { + onSuccess(isInitialSyncCompleted(storedUserId), true) + } } } } @@ -272,15 +280,18 @@ class LoginSSOViewModel @Inject constructor( savedStateHandle.set(SSO_CODE_SAVED_STATE_KEY, newText.text) } - fun handleSSOResult(ssoLoginResult: DeepLinkResult.SSOLogin?, onSuccess: (initialSyncCompleted: Boolean) -> Unit) = + fun handleSSOResult( + ssoLoginResult: DeepLinkResult.SSOLogin?, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit + ) = when (ssoLoginResult) { - is DeepLinkResult.SSOLogin.Success -> { - establishSSOSession(ssoLoginResult.cookie, ssoLoginResult.serverConfigId, onSuccess) - } + is DeepLinkResult.SSOLogin.Success -> { + establishSSOSession(ssoLoginResult.cookie, ssoLoginResult.serverConfigId, onSuccess) + } - is DeepLinkResult.SSOLogin.Failure -> updateSSOLoginError(LoginError.DialogError.SSOResultError(ssoLoginResult.ssoError)) - null -> {} - } + is DeepLinkResult.SSOLogin.Failure -> updateSSOLoginError(LoginError.DialogError.SSOResultError(ssoLoginResult.ssoError)) + null -> {} + } private fun openWebUrl(url: String) { viewModelScope.launch { 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 e492e64a7d7..8978e3ed043 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 @@ -128,7 +128,7 @@ class DebugDataOptionsViewModel } fun enrollE2EICertificate(context: Context) { - e2eiCertificateUseCase(context) { result -> + e2eiCertificateUseCase(context, false) { result -> result.fold({ state = state.copy( certificate = (it as E2EIFailure.FailedOAuth).reason, showCertificate = true diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt new file mode 100644 index 00000000000..e341e844c95 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt @@ -0,0 +1,231 @@ +/* + * Wire + * Copyright (C) 2024 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.e2eiEnrollment + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.R +import com.wire.android.feature.NavigationSwitchAccountActions +import com.wire.android.navigation.BackStackMode +import com.wire.android.navigation.NavigationCommand +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.ClickableText +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dialogs.CancelLoginDialogContent +import com.wire.android.ui.common.dialogs.CancelLoginDialogState +import com.wire.android.ui.common.dimensions +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.visbility.rememberVisibilityState +import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination +import com.wire.android.ui.destinations.InitialSyncScreenDestination +import com.wire.android.ui.home.E2EIErrorWithDismissDialog +import com.wire.android.ui.home.E2EISuccessDialog +import com.wire.android.ui.markdown.MarkdownConstants +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireDimensions +import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes + +@RootNavGraph +@Destination( + style = PopUpNavigationAnimation::class +) +@Composable +fun E2EIEnrollmentScreen( + navigator: Navigator, + viewModel: E2EIEnrollmentViewModel = hiltViewModel(), +) { + val state = viewModel.state + val context = LocalContext.current + + E2EIEnrollmentScreenContent( + state = state, + dismissSuccess = { + navigator.navigate(NavigationCommand(InitialSyncScreenDestination, BackStackMode.CLEAR_WHOLE)) + viewModel.finalizeMLSClient() + }, + dismissErrorDialog = viewModel::dismissErrorDialog, + enrollE2EICertificate = { viewModel.enrollE2EICertificate(context) }, + openCertificateDetails = { + navigator.navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(state.certificate))) + }, + onBackButtonClicked = viewModel::onBackButtonClicked, + onCancelEnrollmentClicked = { viewModel.onCancelEnrollmentClicked(NavigationSwitchAccountActions(navigator::navigate)) }, + onProceedEnrollmentClicked = viewModel::onProceedEnrollmentClicked + ) +} + +@Composable +private fun E2EIEnrollmentScreenContent( + state: E2EIEnrollmentState, + dismissSuccess: () -> Unit, + dismissErrorDialog: () -> Unit, + enrollE2EICertificate: () -> Unit, + openCertificateDetails: () -> Unit, + onBackButtonClicked: () -> Unit, + onCancelEnrollmentClicked: () -> Unit, + onProceedEnrollmentClicked: () -> Unit +) { + val uriHandler = LocalUriHandler.current + BackHandler { + onBackButtonClicked() + } + val cancelLoginDialogState = rememberVisibilityState() + CancelLoginDialogContent( + dialogState = cancelLoginDialogState, + onActionButtonClicked = { + onCancelEnrollmentClicked() + }, + onProceedButtonClicked = { + onProceedEnrollmentClicked() + } + ) + if (state.showCancelLoginDialog) { + cancelLoginDialogState.show( + cancelLoginDialogState.savedState ?: CancelLoginDialogState + ) + } else { + cancelLoginDialogState.dismiss() + } + WireScaffold( + topBar = { + WireCenterAlignedTopAppBar( + elevation = 0.dp, + title = stringResource(id = R.string.end_to_end_identity_required_dialog_title), + navigationIconType = NavigationIconType.Close, + onNavigationPressed = onBackButtonClicked + ) + }, + bottomBar = { + Column( + Modifier + .wrapContentWidth(Alignment.CenterHorizontally) + ) { + WirePrimaryButton( + onClick = enrollE2EICertificate, + text = stringResource(id = R.string.end_to_end_identity_required_dialog_positive_button), + state = WireButtonState.Default, + loading = state.isLoading, + modifier = Modifier.padding( + top = dimensions().spacing16x, + start = dimensions().spacing16x, + end = dimensions().spacing16x, + bottom = dimensions().spacing16x + ) + ) + } + } + ) { internalPadding -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(PaddingValues(MaterialTheme.wireDimensions.dialogContentPadding)) + ) { + Spacer(modifier = Modifier.height(internalPadding.calculateTopPadding())) + val text = buildAnnotatedString { + val style = SpanStyle( + color = colorsScheme().onBackground, + fontWeight = MaterialTheme.wireTypography.body01.fontWeight, + fontSize = MaterialTheme.wireTypography.body01.fontSize, + fontFamily = MaterialTheme.wireTypography.body01.fontFamily, + fontStyle = MaterialTheme.wireTypography.body01.fontStyle + ) + withStyle(style) { append(stringResource(id = R.string.end_to_end_identity_required_dialog_text_no_snooze)) } + } + ClickableText( + text = text, + style = MaterialTheme.wireTypography.body01, + modifier = Modifier.padding( + top = MaterialTheme.wireDimensions.dialogTextsSpacing, + bottom = MaterialTheme.wireDimensions.dialogTextsSpacing, + ), + onClick = { offset -> + text.getStringAnnotations( + tag = MarkdownConstants.TAG_URL, + start = offset, + end = offset, + ).firstOrNull()?.let { result -> uriHandler.openUri(result.item) } + } + ) + } + + if (state.isCertificateEnrollError) { + E2EIErrorWithDismissDialog( + isE2EILoading = state.isLoading, + updateCertificate = enrollE2EICertificate, + onDismiss = dismissErrorDialog + ) + } + + if (state.isCertificateEnrollSuccess) { + E2EISuccessDialog( + openCertificateDetails = openCertificateDetails, + dismissDialog = dismissSuccess + ) + } + } +} + +@PreviewMultipleThemes +@Composable +fun previewE2EIEnrollmentScreenContent() { + WireTheme { + E2EIEnrollmentScreenContent(E2EIEnrollmentState(), {}, {}, {}, {}, {}, {}) { } + } +} + +@PreviewMultipleThemes +@Composable +fun previewE2EIEnrollmentScreenContentWithSuccess() { + WireTheme { + E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollSuccess = true), {}, {}, {}, {}, {}, {}) { } + } +} + +@PreviewMultipleThemes +@Composable +fun previewE2EIEnrollmentScreenContentWithError() { + WireTheme { + E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollError = true), {}, {}, {}, {}, {}, {}) { } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt new file mode 100644 index 00000000000..514ca785d31 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt @@ -0,0 +1,123 @@ +/* + * Wire + * Copyright (C) 2024 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.e2eiEnrollment + +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.appLogger +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.SwitchAccountActions +import com.wire.android.feature.SwitchAccountParam +import com.wire.android.feature.e2ei.GetE2EICertificateUseCase +import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment +import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult +import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import com.wire.kalium.logic.feature.session.DeleteSessionUseCase +import com.wire.kalium.logic.functional.fold +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class E2EIEnrollmentState( + val certificate: String = "null", + val showCertificate: Boolean = false, + val isLoading: Boolean = false, + val isCertificateEnrollError: Boolean = false, + val isCertificateEnrollSuccess: Boolean = false, + val showCancelLoginDialog: Boolean = false +) + +@HiltViewModel +class E2EIEnrollmentViewModel @Inject constructor( + private val e2eiCertificateUseCase: GetE2EICertificateUseCase, + private val finalizeMLSClientAfterE2EIEnrollment: FinalizeMLSClientAfterE2EIEnrollment, + private val currentSession: CurrentSessionUseCase, + private val deleteSession: DeleteSessionUseCase, + private val switchAccount: AccountSwitchUseCase +) : ViewModel() { + var state by mutableStateOf(E2EIEnrollmentState()) + + fun finalizeMLSClient() { + viewModelScope.launch { + finalizeMLSClientAfterE2EIEnrollment.invoke() + } + } + + fun onBackButtonClicked() { + state = state.copy(showCancelLoginDialog = true) + } + + fun onProceedEnrollmentClicked() { + state = state.copy(showCancelLoginDialog = false) + } + + fun onCancelEnrollmentClicked(switchAccountActions: SwitchAccountActions) { + state = state.copy(showCancelLoginDialog = false) + viewModelScope.launch { + currentSession().let { + when (it) { + is CurrentSessionResult.Success -> { + deleteSession(it.accountInfo.userId) + } + is CurrentSessionResult.Failure.Generic -> { + appLogger.e("failed to delete session") + } + CurrentSessionResult.Failure.SessionNotFound -> { + appLogger.e("session not found") + } + } + } + }.invokeOnCompletion { + viewModelScope.launch { + switchAccount(SwitchAccountParam.TryToSwitchToNextAccount) + .callAction(switchAccountActions) + } + } + } + fun enrollE2EICertificate(context: Context) { + state = state.copy(isLoading = true) + e2eiCertificateUseCase(context, true) { result -> + result.fold({ + state = state.copy( + isLoading = false, + isCertificateEnrollError = true + ) + }, { + if (it is E2EIEnrollmentResult.Finalized) { + state = state.copy( + certificate = it.certificate, + isCertificateEnrollSuccess = true, + isCertificateEnrollError = false, + isLoading = false + ) + } + }) + } + } + + fun dismissErrorDialog() { + state = state.copy( + isCertificateEnrollError = false, + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 39f0e77dc0a..70007e52c2a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -275,7 +275,10 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun getE2EICertificate(e2eiRequired: FeatureFlagState.E2EIRequired, context: Context) { featureFlagState = featureFlagState.copy(isE2EILoading = true) currentUserId?.let { userId -> - GetE2EICertificateUseCase(coreLogic.getSessionScope(userId).enrollE2EI, dispatcherProvider).invoke(context) { result -> + GetE2EICertificateUseCase(coreLogic.getSessionScope(userId).enrollE2EI, dispatcherProvider).invoke( + context, + isNewClient = false + ) { result -> result.fold({ featureFlagState = featureFlagState.copy( isE2EILoading = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index c9366037c48..ae7a9ec28d3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -112,7 +112,8 @@ class DeviceDetailsViewModel @Inject constructor( state.copy( isE2eiCertificateActivated = true, e2eiCertificate = certificate.certificate, - isLoadingCertificate = false + isLoadingCertificate = false, + device = state.device.updateE2EICertificateStatus(certificate.certificate.status) ) } else { state.copy(isE2eiCertificateActivated = false, isLoadingCertificate = false) @@ -122,7 +123,7 @@ class DeviceDetailsViewModel @Inject constructor( fun enrollE2eiCertificate(context: Context) { state = state.copy(isLoadingCertificate = true) - enrolE2EICertificateUseCase(context) { result -> + enrolE2EICertificateUseCase(context, false) { result -> result.fold({ state = state.copy( isLoadingCertificate = false, @@ -162,7 +163,7 @@ class DeviceDetailsViewModel @Inject constructor( is GetClientDetailsResult.Success -> { state.copy( - device = Device(result.client), + device = state.device.updateFromClient(result.client), isCurrentDevice = result.isCurrentClient, removeDeviceDialogState = RemoveDeviceDialogState.Hidden, canBeRemoved = !result.isCurrentClient && isSelfClient && result.client.type == ClientType.Permanent, diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt index 18a0acf72c8..dd9af69f756 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt @@ -67,7 +67,8 @@ class SelfDevicesViewModel @Inject constructor( state.copy( isLoadingClientsList = false, currentDevice = result.clients - .firstOrNull { it.id == currentClientId }?.let { Device(it, e2eiCertificates[it.id.value]?.status) }, + .firstOrNull { it.id == currentClientId } + ?.let { Device(it, e2eiCertificates[it.id.value]?.status) }, deviceList = result.clients .filter { it.id != currentClientId } .map { Device(it, e2eiCertificates[it.id.value]?.status) } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt index 70f3093c9fa..b883aff5c73 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt @@ -154,4 +154,4 @@ fun E2eiCertificateDetailsContent( ) } -const val CERTIFICATE_FILE_NAME = "certificate.pem" +const val CERTIFICATE_FILE_NAME = "certificate.txt" diff --git a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt index 542e38e0fcd..ecc3afd2965 100644 --- a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt +++ b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt @@ -34,6 +34,7 @@ import com.wire.android.ui.destinations.CreateAccountSummaryScreenDestination import com.wire.android.ui.destinations.CreatePersonalAccountOverviewScreenDestination import com.wire.android.ui.destinations.CreateTeamAccountOverviewScreenDestination import com.wire.android.ui.destinations.Destination +import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination import com.wire.android.ui.destinations.IncomingCallScreenDestination @@ -43,6 +44,7 @@ import com.wire.android.ui.destinations.LoginScreenDestination import com.wire.android.ui.destinations.MigrationScreenDestination import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination +import com.wire.android.ui.destinations.RegisterDeviceScreenDestination import com.wire.android.ui.destinations.RemoveDeviceScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination @@ -213,6 +215,8 @@ sealed class CurrentScreen { is CreateAccountSummaryScreenDestination, is MigrationScreenDestination, is InitialSyncScreenDestination, + is E2EIEnrollmentScreenDestination, + is RegisterDeviceScreenDestination, is RemoveDeviceScreenDestination -> AuthRelated else -> SomeOther diff --git a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt index ca24a259712..92795d61b34 100644 --- a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt @@ -61,3 +61,8 @@ fun sanitizeUrl(url: String): String { return url // Return the original URL if any errors occur } } + +fun URI.removeQueryParams(): URI { + val regex = Regex("[?&][^=]+=[^&]*") + return URI(this.toString().replace(regex, "")) +} diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 7c163531275..7b61cf67699 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -26,6 +26,7 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.AuthServerConfigProvider +import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider import com.wire.android.di.ObserveSyncStateUseCaseProvider import com.wire.android.feature.AccountSwitchUseCase @@ -588,6 +589,10 @@ class WireActivityViewModelTest { } private class Arrangement { + + // TODO add tests for cases when observeIfE2EIIsRequiredDuringLogin emits semothing + private val observeIfE2EIIsRequiredDuringLogin = MutableSharedFlow() + init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) @@ -608,6 +613,8 @@ class WireActivityViewModelTest { coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(ObserveScreenshotCensoringConfigResult.Disabled) coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) coEvery { globalDataStore.selectedThemeOptionFlow() } returns flowOf(ThemeOption.LIGHT) + coEvery { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(any()).observeIfE2EIIsRequiredDuringLogin() } returns + observeIfE2EIIsRequiredDuringLogin } @MockK @@ -663,6 +670,9 @@ class WireActivityViewModelTest { @MockK private lateinit var observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory + @MockK + private lateinit var observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory + @MockK lateinit var globalDataStore: GlobalDataStore @@ -691,7 +701,8 @@ class WireActivityViewModelTest { clearNewClientsForUser = clearNewClientsForUser, currentScreenManager = currentScreenManager, observeScreenshotCensoringConfigUseCaseProviderFactory = observeScreenshotCensoringConfigUseCaseProviderFactory, - globalDataStore = globalDataStore + globalDataStore = globalDataStore, + observeIfE2EIRequiredDuringLoginUseCaseProviderFactory = observeIfE2EIRequiredDuringLoginUseCaseProviderFactory ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt index 71d5c8d9dbf..c4c079e269f 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt @@ -116,7 +116,7 @@ class LoginEmailViewModelTest { private lateinit var authenticationScope: AuthenticationScope @MockK(relaxed = true) - private lateinit var onSuccess: (Boolean) -> Unit + private lateinit var onSuccess: (Boolean, Boolean) -> Unit private lateinit var loginViewModel: LoginEmailViewModel @@ -210,7 +210,7 @@ class LoginEmailViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { loginUseCase(any(), any(), any(), any(), any()) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } - coVerify(exactly = 1) { onSuccess(true) } + coVerify(exactly = 1) { onSuccess(true, false) } } @Test @@ -233,7 +233,7 @@ class LoginEmailViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { loginUseCase(any(), any(), any(), any(), any()) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } - coVerify(exactly = 1) { onSuccess(false) } + coVerify(exactly = 1) { onSuccess(false, false) } } @Test @@ -440,7 +440,7 @@ class LoginEmailViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { loginUseCase(email, any(), any(), any(), code) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } - coVerify(exactly = 1) { onSuccess(any()) } + coVerify(exactly = 1) { onSuccess(any(), any()) } } @Test diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt index f99282e6a33..e6e33468d51 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt @@ -121,7 +121,7 @@ class LoginSSOViewModelTest { private lateinit var fetchSSOSettings: FetchSSOSettingsUseCase @MockK(relaxed = true) - private lateinit var onSuccess: (Boolean) -> Unit + private lateinit var onSuccess: (Boolean, Boolean) -> Unit private lateinit var loginViewModel: LoginSSOViewModel @@ -262,7 +262,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - coVerify(exactly = 1) { onSuccess(false) } + coVerify(exactly = 1) { onSuccess(false, false) } } @Test @@ -288,7 +288,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - coVerify(exactly = 1) { onSuccess(true) } + coVerify(exactly = 1) { onSuccess(true, false) } } @Test @@ -312,7 +312,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 0) { loginViewModel.registerClient(any(), null) } coVerify(exactly = 0) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - verify(exactly = 0) { onSuccess(any()) } + verify(exactly = 0) { onSuccess(any(), any()) } } @Test @@ -353,7 +353,7 @@ class LoginSSOViewModelTest { loginViewModel.handleSSOResult(DeepLinkResult.SSOLogin.Success("", ""), onSuccess) advanceUntilIdle() - verify(exactly = 1) { onSuccess(any()) } + verify(exactly = 1) { onSuccess(any(), any()) } } @Test @@ -376,7 +376,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 0) { loginViewModel.registerClient(any(), null) } coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - verify(exactly = 0) { onSuccess(any()) } + verify(exactly = 0) { onSuccess(any(), any()) } } @Test @@ -405,7 +405,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - verify(exactly = 0) { onSuccess(any()) } + verify(exactly = 0) { onSuccess(any(), any()) } } @Test diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt index 9029b72f584..845c3a2f721 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt @@ -38,7 +38,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.verify import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test @@ -77,7 +76,7 @@ class SearchUserViewModelTest { ) } - verify(exactly = 1) { + coVerify(exactly = 1) { arrangement.federatedSearchParser(any()) } } @@ -114,7 +113,7 @@ class SearchUserViewModelTest { ) } - verify(exactly = 1) { + coVerify(exactly = 1) { arrangement.federatedSearchParser(any()) } } @@ -170,7 +169,7 @@ class SearchUserViewModelTest { ) } - verify(exactly = 1) { + coVerify(exactly = 1) { arrangement.federatedSearchParser(any()) } diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt index 428f868d683..12af6ab551e 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt @@ -279,7 +279,7 @@ class DeviceDetailsViewModelTest { viewModel.enrollE2eiCertificate(arrangement.context) coVerify { - arrangement.enrolE2EICertificateUseCase(any(), any()) + arrangement.enrolE2EICertificateUseCase(any(), any(), any()) } assertTrue(viewModel.state.isLoadingCertificate) } diff --git a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt index 8786a24986d..0088a7ea5a2 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt @@ -25,6 +25,6 @@ object AndroidSdk { object AndroidApp { const val id = "com.wire.android" - const val versionName = "4.6.0" + const val versionName = "4.7.0" val versionCode = Versionizer().versionCode } diff --git a/kalium b/kalium index aa8d9077dcf..07735575323 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit aa8d9077dcf4be11b174de6f78f3ed5869765edf +Subproject commit 07735575323400ca9c43e6259ec2f264e24669ad From 847af47f8ee7edc074f7039d12f67c18917c35d3 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Fri, 26 Jan 2024 14:50:29 +0100 Subject: [PATCH 012/134] chore: remove un-needed changes (#2634) --- app/src/main/AndroidManifest.xml | 9 --------- .../plugins/src/main/kotlin/AndroidCoordinates.kt | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f3f51dcd5fa..f76f5432ac8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -238,15 +238,6 @@ android:authorities="${applicationId}.firebaseinitprovider" tools:node="remove" /> - - - Date: Fri, 26 Jan 2024 17:08:19 +0100 Subject: [PATCH 013/134] feat: Show a dialog when current client's certificate is revoked (WPB-6145) - cherrypick (#2635) --- .../com/wire/android/ui/WireActivity.kt | 25 ++++++++++ .../wire/android/ui/WireActivityViewModel.kt | 22 ++++++++ .../android/ui/common/WireLabelledCheckbox.kt | 8 ++- .../com/wire/android/ui/home/E2EIDialogs.kt | 50 +++++++++++++++---- .../wire/android/ui/home/FeatureFlagState.kt | 1 + .../sync/FeatureFlagNotificationViewModel.kt | 16 ++++++ .../self/dialog/LogoutOptionsDialog.kt | 12 +++-- app/src/main/res/values/strings.xml | 4 ++ .../FeatureFlagNotificationViewModelTest.kt | 20 ++++++++ 9 files changed, 142 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index b3bf8103ea4..2c9fd2c909f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -55,6 +55,7 @@ import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.config.CustomUiConfigurationProvider import com.wire.android.config.LocalCustomUiConfigurationProvider +import com.wire.android.datastore.UserDataStore import com.wire.android.feature.NavigationSwitchAccountActions import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -65,6 +66,7 @@ import com.wire.android.ui.calling.ProximitySensorManager import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.CommonTopAppBar import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel +import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination @@ -78,6 +80,7 @@ import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination +import com.wire.android.ui.home.E2EICertificateRevokedDialog import com.wire.android.ui.home.E2EIRequiredDialog import com.wire.android.ui.home.E2EIResultDialog import com.wire.android.ui.home.E2EISnoozeDialog @@ -91,6 +94,8 @@ import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedState import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedViewModel import com.wire.android.ui.theme.ThemeOption import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialog +import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialogState import com.wire.android.util.CurrentScreenManager import com.wire.android.util.LocalSyncStateObserver import com.wire.android.util.SyncStateObserver @@ -340,6 +345,26 @@ class WireActivity : AppCompatActivity() { hideDialogStatus = featureFlagNotificationViewModel::dismissSelfDeletingMessagesDialog ) } + val logoutOptionsDialogState = rememberVisibilityState() + + LogoutOptionsDialog( + dialogState = logoutOptionsDialogState, + checkboxEnabled = false, + logout = { + viewModel.doHardLogout( + { UserDataStore(context, it) }, + NavigationSwitchAccountActions(navigate) + ) + logoutOptionsDialogState.dismiss() + } + ) + + if (shouldShowE2eiCertificateRevokedDialog) { + E2EICertificateRevokedDialog( + onLogout = { logoutOptionsDialogState.show(LogoutOptionsDialogState(shouldWipeData = true)) }, + onContinue = featureFlagNotificationViewModel::dismissE2EICertificateRevokedDialog, + ) + } e2EIRequired?.let { E2EIRequiredDialog( diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 84574392d9e..8d21d615c60 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -35,6 +35,7 @@ import com.wire.android.di.ObserveSyncStateUseCaseProvider import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountActions import com.wire.android.feature.SwitchAccountParam +import com.wire.android.feature.SwitchAccountResult import com.wire.android.migration.MigrationManager import com.wire.android.services.ServicesManager import com.wire.android.ui.authentication.devices.model.displayName @@ -310,6 +311,27 @@ class WireActivityViewModel @Inject constructor( } } + // TODO: needs to be covered with test once hard logout is validated to be used + fun doHardLogout( + clearUserData: (userId: UserId) -> Unit, + switchAccountActions: SwitchAccountActions + ) { + viewModelScope.launch { + coreLogic.getGlobalScope().session.currentSession().takeIf { + it is CurrentSessionResult.Success + }?.let { + val currentUserId = (it as CurrentSessionResult.Success).accountInfo.userId + coreLogic.getSessionScope(currentUserId).logout(LogoutReason.SELF_HARD_LOGOUT) + clearUserData(currentUserId) + } + accountSwitch(SwitchAccountParam.TryToSwitchToNextAccount).also { + if (it == SwitchAccountResult.NoOtherAccountToSwitch) { + globalDataStore.clearAppLockPasscode() + } + }.callAction(switchAccountActions) + } + } + fun dismissNewClientsDialog(userId: UserId) { globalAppState = globalAppState.copy(newClientDialog = null) viewModelScope.launch { diff --git a/app/src/main/kotlin/com/wire/android/ui/common/WireLabelledCheckbox.kt b/app/src/main/kotlin/com/wire/android/ui/common/WireLabelledCheckbox.kt index f5f740cfc80..390d779fcde 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/WireLabelledCheckbox.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/WireLabelledCheckbox.kt @@ -46,6 +46,7 @@ fun WireLabelledCheckbox( overflow: TextOverflow = TextOverflow.Visible, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, contentPadding: PaddingValues = PaddingValues(dimensions().spacing0x), + checkboxEnabled: Boolean = true, modifier: Modifier = Modifier ) { Row( @@ -55,12 +56,17 @@ fun WireLabelledCheckbox( .toggleable( value = checked, role = Role.Checkbox, - onValueChange = { onCheckClicked(!checked) } + onValueChange = { + if (checkboxEnabled) { + onCheckClicked(!checked) + } + } ) .padding(contentPadding) ) { Checkbox( checked = checked, + enabled = checkboxEnabled, onCheckedChange = null // null since we are handling the click on parent ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt index 993805ad25b..98cc6c4acf5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt @@ -363,9 +363,39 @@ private fun E2EIRenewNoSnoozeDialog(isLoading: Boolean, updateCertificate: () -> ) } +@Composable +fun E2EICertificateRevokedDialog( + onLogout: () -> Unit, + onContinue: () -> Unit +) { + WireDialog( + title = stringResource(id = R.string.end_to_end_identity_certificate_revoked_dialog_title), + text = stringResource(id = R.string.end_to_end_identity_certificate_revoked_dialog_description), + onDismiss = onContinue, + optionButton1Properties = WireDialogButtonProperties( + onClick = onLogout, + text = stringResource(id = R.string.end_to_end_identity_certificate_revoked_dialog_button_logout), + type = WireDialogButtonType.Primary + ), + optionButton2Properties = WireDialogButtonProperties( + onClick = onContinue, + text = stringResource(id = R.string.end_to_end_identity_certificate_revoked_dialog_button_continue), + type = WireDialogButtonType.Secondary, + ), + buttonsHorizontalAlignment = false, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewE2EICertificateRevokedDialog() { + E2EICertificateRevokedDialog({}, {}) +} + @PreviewMultipleThemes @Composable -fun previewE2EIdRequiredWithSnoozeDialog() { +fun PreviewE2EIdRequiredWithSnoozeDialog() { WireTheme { E2EIRequiredWithSnoozeDialog(false, {}) {} } @@ -373,7 +403,7 @@ fun previewE2EIdRequiredWithSnoozeDialog() { @PreviewMultipleThemes @Composable -fun previewE2EIdRequiredNoSnoozeDialog() { +fun PreviewE2EIdRequiredNoSnoozeDialog() { WireTheme { E2EIRequiredNoSnoozeDialog(false) {} } @@ -381,7 +411,7 @@ fun previewE2EIdRequiredNoSnoozeDialog() { @PreviewMultipleThemes @Composable -fun previewE2EIdRenewRequiredWithSnoozeDialog() { +fun PreviewE2EIdRenewRequiredWithSnoozeDialog() { WireTheme { E2EIRenewWithSnoozeDialog(false, {}) {} } @@ -389,7 +419,7 @@ fun previewE2EIdRenewRequiredWithSnoozeDialog() { @PreviewMultipleThemes @Composable -fun previewE2EIdRenewRequiredNoSnoozeDialog() { +fun PreviewE2EIdRenewRequiredNoSnoozeDialog() { WireTheme { E2EIRenewNoSnoozeDialog(false) {} } @@ -397,7 +427,7 @@ fun previewE2EIdRenewRequiredNoSnoozeDialog() { @PreviewMultipleThemes @Composable -fun previewE2EIdSnoozeDialog() { +fun PreviewE2EIdSnoozeDialog() { WireTheme { E2EISnoozeDialog(2.seconds) {} } @@ -405,7 +435,7 @@ fun previewE2EIdSnoozeDialog() { @PreviewMultipleThemes @Composable -fun previewE2EIRenewErrorDialogNoGracePeriod() { +fun PreviewE2EIRenewErrorDialogNoGracePeriod() { WireTheme { E2EIRenewErrorDialog(FeatureFlagState.E2EIRequired.NoGracePeriod.Renew, false, { }) {} } @@ -413,7 +443,7 @@ fun previewE2EIRenewErrorDialogNoGracePeriod() { @PreviewMultipleThemes @Composable -fun previewE2EIRenewErrorDialogWithGracePeriod() { +fun PreviewE2EIRenewErrorDialogWithGracePeriod() { WireTheme { E2EIRenewErrorDialog(FeatureFlagState.E2EIRequired.WithGracePeriod.Renew(2.days), false, { }) {} } @@ -421,7 +451,7 @@ fun previewE2EIRenewErrorDialogWithGracePeriod() { @PreviewMultipleThemes @Composable -fun previewE2EISuccessDialog() { +fun PreviewE2EISuccessDialog() { WireTheme { E2EISuccessDialog({ }) {} } @@ -429,7 +459,7 @@ fun previewE2EISuccessDialog() { @PreviewMultipleThemes @Composable -fun previewE2EIRenewErrorNoSnoozeDialog() { +fun PreviewE2EIRenewErrorNoSnoozeDialog() { WireTheme { E2EIErrorNoSnoozeDialog(false) { } } @@ -437,7 +467,7 @@ fun previewE2EIRenewErrorNoSnoozeDialog() { @PreviewMultipleThemes @Composable -fun previewE2EIRenewErrorWithSnoozeDialog() { +fun PreviewE2EIRenewErrorWithSnoozeDialog() { WireTheme { E2EIErrorWithSnoozeDialog(isE2EILoading = false, updateCertificate = {}) { } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt b/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt index 6282528519c..33921138561 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt @@ -26,6 +26,7 @@ data class FeatureFlagState( val isFileSharingEnabledState: Boolean = true, val fileSharingRestrictedState: SharingRestrictedState? = null, val shouldShowGuestRoomLinkDialog: Boolean = false, + val shouldShowE2eiCertificateRevokedDialog: Boolean = false, val shouldShowTeamAppLockDialog: Boolean = false, val isTeamAppLockEnabled: Boolean = false, val isGuestRoomLinkEnabled: Boolean = true, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 70007e52c2a..45026da72dc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -120,6 +120,13 @@ class FeatureFlagNotificationViewModel @Inject constructor( launch { setE2EIRequiredState(userId) } launch { setTeamAppLockFeatureFlag(userId) } launch { observeCallEndedBecauseOfConversationDegraded(userId) } + launch { observeShouldNotifyForRevokedCertificate(userId) } + } + } + + private suspend fun observeShouldNotifyForRevokedCertificate(userId: UserId) { + coreLogic.getSessionScope(userId).observeShouldNotifyForRevokedCertificate().collect { + featureFlagState = featureFlagState.copy(shouldShowE2eiCertificateRevokedDialog = it) } } @@ -232,6 +239,15 @@ class FeatureFlagNotificationViewModel @Inject constructor( } } + fun dismissE2EICertificateRevokedDialog() { + featureFlagState = featureFlagState.copy(shouldShowE2eiCertificateRevokedDialog = false) + currentUserId?.let { + viewModelScope.launch { + coreLogic.getSessionScope(it).markNotifyForRevokedCertificateAsNotified() + } + } + } + fun dismissFileSharingDialog() { featureFlagState = featureFlagState.copy(showFileSharingDialog = false) viewModelScope.launch { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/dialog/LogoutOptionsDialog.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/dialog/LogoutOptionsDialog.kt index f1f9611887a..221b012bcb6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/dialog/LogoutOptionsDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/dialog/LogoutOptionsDialog.kt @@ -41,7 +41,8 @@ import com.wire.android.ui.common.visbility.VisibilityState @Composable fun LogoutOptionsDialog( dialogState: VisibilityState, - logout: (Boolean) -> Unit + logout: (Boolean) -> Unit, + checkboxEnabled: Boolean = true ) { VisibilityState(dialogState) { state -> WireDialog( @@ -61,15 +62,16 @@ fun LogoutOptionsDialog( ) ) { WireLabelledCheckbox( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = dimensions().spacing16x) + .clip(RoundedCornerShape(size = dimensions().spacing4x)), label = stringResource(R.string.dialog_logout_wipe_data_checkbox), checked = state.shouldWipeData, onCheckClicked = remember { { dialogState.show(state.copy(shouldWipeData = it)) } }, horizontalArrangement = Arrangement.Center, contentPadding = PaddingValues(vertical = dimensions().spacing4x), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = dimensions().spacing16x) - .clip(RoundedCornerShape(size = dimensions().spacing4x)) + checkboxEnabled = checkboxEnabled ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ef36c52232..497e9d0b39c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1274,6 +1274,10 @@ The certificate is updated and your device is verified. Certificate Details Certificate Details + End-to-end certificate revoked + Log out to reduce security risks. Then log in again, get a new certificate, and reset your password.\n\nIf you keep using this device, your conversations are no longer verified. + Log out + Continue Using This Device Start Recording Recording Audio… diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index fa8363628a2..6c93b0a7b31 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -37,6 +37,7 @@ import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.E2EIRequiredResult import com.wire.kalium.logic.feature.user.MarkEnablingE2EIAsNotifiedUseCase import com.wire.kalium.logic.feature.user.MarkSelfDeletionStatusAsNotifiedUseCase +import com.wire.kalium.logic.feature.user.e2ei.MarkNotifyForRevokedCertificateAsNotifiedUseCase import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlagAsNotChangedUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -284,6 +285,19 @@ class FeatureFlagNotificationViewModelTest { assertEquals(null, viewModel.featureFlagState.e2EIRequired) } + @Test + fun givenADisplayedDialog_whenDismissingIt_thenInvokeMarkFileSharingStatusAsNotifiedUseCaseOnce() = runTest { + val (arrangement, viewModel) = Arrangement() + .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(UserId("value", "domain"))))) + .arrange() + coEvery { arrangement.markNotifyForRevokedCertificateAsNotified() } returns Unit + + viewModel.dismissE2EICertificateRevokedDialog() + + assertEquals(false, viewModel.featureFlagState.shouldShowE2eiCertificateRevokedDialog) + coVerify(exactly = 1) { arrangement.markNotifyForRevokedCertificateAsNotified() } + } + private inner class Arrangement { @MockK @@ -310,6 +324,9 @@ class FeatureFlagNotificationViewModelTest { @MockK lateinit var globalDataStore: GlobalDataStore + @MockK + lateinit var markNotifyForRevokedCertificateAsNotified: MarkNotifyForRevokedCertificateAsNotifiedUseCase + val viewModel: FeatureFlagNotificationViewModel by lazy { FeatureFlagNotificationViewModel( coreLogic = coreLogic, @@ -332,6 +349,9 @@ class FeatureFlagNotificationViewModelTest { coEvery { coreLogic.getSessionScope(any()).observeGuestRoomLinkFeatureFlag.invoke() } returns flowOf() coEvery { coreLogic.getSessionScope(any()).observeE2EIRequired.invoke() } returns flowOf() coEvery { coreLogic.getSessionScope(any()).calls.observeEndCallDialog() } returns flowOf() + coEvery { coreLogic.getSessionScope(any()).observeShouldNotifyForRevokedCertificate() } returns flowOf() + every { coreLogic.getSessionScope(any()).markNotifyForRevokedCertificateAsNotified } returns + markNotifyForRevokedCertificateAsNotified coEvery { ppLockTeamFeatureConfigObserver() } returns flowOf(null) } From ea851f340df320a13bbde7baf748623a4ac9fa9d Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 26 Jan 2024 18:16:22 +0100 Subject: [PATCH 014/134] chore: Update kalium reference (RC) (#2639) --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 07735575323..cf92a574445 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 07735575323400ca9c43e6259ec2f264e24669ad +Subproject commit cf92a57444563b281c2278ba6b6dbe13841bc3d6 From 42c583019dd27983159d1ea2d82a4a087360f8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:03:06 +0100 Subject: [PATCH 015/134] fix: setting items clickable area [WPB-6225] (#2643) --- .../ui/authentication/devices/DeviceItem.kt | 87 +++++++++++-------- .../devices/remove/RemoveDeviceScreen.kt | 4 +- .../android/ui/home/settings/SettingsItem.kt | 2 +- .../ui/settings/devices/SelfDevicesScreen.kt | 5 +- .../other/OtherUserDevicesScreen.kt | 6 +- 5 files changed, 56 insertions(+), 48 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt index d0e598ef275..112d9300e18 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt @@ -20,17 +20,19 @@ package com.wire.android.ui.authentication.devices import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -40,22 +42,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.ui.authentication.devices.model.Device import com.wire.android.ui.authentication.devices.model.lastActiveDescription +import com.wire.android.ui.common.Icon import com.wire.android.ui.common.MLSVerificationIcon import com.wire.android.ui.common.ProteusVerifiedIcon import com.wire.android.ui.common.button.WireSecondaryButton -import com.wire.android.ui.common.button.getMinTouchMargins import com.wire.android.ui.common.button.wireSecondaryButtonColors import com.wire.android.ui.common.shimmerPlaceholder import com.wire.android.ui.theme.WireTheme @@ -74,18 +74,16 @@ fun DeviceItem( placeholder: Boolean, shouldShowVerifyLabel: Boolean, background: Color? = null, - leadingIcon: @Composable (() -> Unit), - leadingIconBorder: Dp = 1.dp, + icon: @Composable (() -> Unit), isWholeItemClickable: Boolean = false, - onRemoveDeviceClick: ((Device) -> Unit)? = null + onClickAction: ((Device) -> Unit)? = null ) { DeviceItemContent( device = device, placeholder = placeholder, background = background, - leadingIcon = leadingIcon, - leadingIconBorder = leadingIconBorder, - onRemoveDeviceClick = onRemoveDeviceClick, + icon = icon, + onClickAction = onClickAction, isWholeItemClickable = isWholeItemClickable, shouldShowVerifyLabel = shouldShowVerifyLabel ) @@ -96,9 +94,8 @@ private fun DeviceItemContent( device: Device, placeholder: Boolean, background: Color? = null, - leadingIcon: @Composable (() -> Unit), - leadingIconBorder: Dp, - onRemoveDeviceClick: ((Device) -> Unit)?, + icon: @Composable (() -> Unit), + onClickAction: ((Device) -> Unit)?, isWholeItemClickable: Boolean, shouldShowVerifyLabel: Boolean ) { @@ -107,7 +104,7 @@ private fun DeviceItemContent( modifier = (if (background != null) Modifier.background(color = background) else Modifier) .clickable(enabled = isWholeItemClickable) { if (isWholeItemClickable) { - onRemoveDeviceClick?.invoke(device) + onClickAction?.invoke(device) } } ) { @@ -128,30 +125,26 @@ private fun DeviceItemContent( .weight(1f) ) { DeviceItemTexts(device, placeholder, shouldShowVerifyLabel) } } - val (buttonTopPadding, buttonEndPadding) = getMinTouchMargins(minSize = MaterialTheme.wireDimensions.buttonSmallMinSize) - .let { - // default button touch area [48x48] is higher than button size [40x32] so it will have margins, we have to subtract - // these margins from the default item padding so that all elements are the same distance from the edge - Pair( - MaterialTheme.wireDimensions.removeDeviceItemPadding - it.calculateTopPadding(), - MaterialTheme.wireDimensions.removeDeviceItemPadding - it.calculateEndPadding(LocalLayoutDirection.current) + if (!placeholder) { + if (onClickAction != null && !isWholeItemClickable) { + WireSecondaryButton( + modifier = Modifier.testTag("remove device button"), + onClick = { onClickAction(device) }, + leadingIcon = icon, + fillMaxWidth = false, + minSize = MaterialTheme.wireDimensions.buttonSmallMinSize, + minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, + shape = RoundedCornerShape(size = MaterialTheme.wireDimensions.buttonSmallCornerSize), + contentPadding = PaddingValues(0.dp), + colors = wireSecondaryButtonColors().copy( + enabled = background ?: MaterialTheme.wireColorScheme.secondaryButtonEnabled + ) ) + } else { + Box(modifier = Modifier.padding(MaterialTheme.wireDimensions.removeDeviceItemPadding)) { + icon() + } } - if (!placeholder && onRemoveDeviceClick != null) { - WireSecondaryButton( - modifier = Modifier.testTag("remove device button"), - onClick = { onRemoveDeviceClick(device) }, - leadingIcon = leadingIcon, - fillMaxWidth = false, - minSize = MaterialTheme.wireDimensions.buttonSmallMinSize, - minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, - shape = RoundedCornerShape(size = MaterialTheme.wireDimensions.buttonSmallCornerSize), - contentPadding = PaddingValues(0.dp), - borderWidth = leadingIconBorder, - colors = wireSecondaryButtonColors().copy( - enabled = background ?: MaterialTheme.wireColorScheme.secondaryButtonEnabled - ) - ) } } } @@ -183,7 +176,10 @@ private fun DeviceItemTexts( MLSVerificationIcon(device.e2eiCertificateStatus) if (shouldShowVerifyLabel) { Spacer(modifier = Modifier.width(MaterialTheme.wireDimensions.spacing8x)) - if (device.isVerifiedProteus) ProteusVerifiedIcon(Modifier.wrapContentWidth().align(Alignment.CenterVertically)) + if (device.isVerifiedProteus) ProteusVerifiedIcon( + Modifier + .wrapContentWidth() + .align(Alignment.CenterVertically)) } } @@ -249,7 +245,7 @@ private fun DeviceItemTexts( @PreviewMultipleThemes @Composable -fun PreviewDeviceItem() { +fun PreviewDeviceItemWithActionIcon() { WireTheme { DeviceItem( device = Device(name = UIText.DynamicString("name"), isVerifiedProteus = true), @@ -260,3 +256,18 @@ fun PreviewDeviceItem() { ) {} } } + +@PreviewMultipleThemes +@Composable +fun PreviewDeviceItem() { + WireTheme { + DeviceItem( + device = Device(name = UIText.DynamicString("name"), isVerifiedProteus = true), + placeholder = false, + shouldShowVerifyLabel = true, + background = null, + isWholeItemClickable = true, + icon = Icons.Filled.ChevronRight.Icon() + ) {} + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt index a0c36cdc40d..ae337f60e78 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt @@ -188,9 +188,9 @@ private fun RemoveDeviceItemsList( DeviceItem( device = device, placeholder = placeholders, - onRemoveDeviceClick = onItemClicked, + onClickAction = onItemClicked, shouldShowVerifyLabel = false, - leadingIcon = { + icon = { Icon( painterResource(id = R.drawable.ic_remove), stringResource(R.string.content_description_remove_devices_screen_remove_icon) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsItem.kt index 95f34f6e8ad..9203dd155b6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsItem.kt @@ -60,7 +60,7 @@ fun SettingsItem( @DrawableRes trailingIcon: Int? = null, switchState: SwitchState = SwitchState.None, onRowPressed: Clickable = Clickable(false), - onIconPressed: Clickable = Clickable(false) + onIconPressed: Clickable? = null ) { RowItemTemplate( title = { diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt index 4909e6c2ea8..5ebe3d29750 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt @@ -129,9 +129,8 @@ private fun LazyListScope.folderDeviceItems( item, background = MaterialTheme.wireColorScheme.surface, placeholder = false, - onRemoveDeviceClick = onDeviceClick, - leadingIcon = Icons.Filled.ChevronRight.Icon(), - leadingIconBorder = 0.dp, + onClickAction = onDeviceClick, + icon = Icons.Filled.ChevronRight.Icon(), isWholeItemClickable = true, shouldShowVerifyLabel = shouldShowVerifyLabel ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserDevicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserDevicesScreen.kt index 36f7800537b..35121284f79 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserDevicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserDevicesScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.ui.authentication.devices.DeviceItem @@ -121,9 +120,8 @@ private fun OtherUserDevicesContent( placeholder = false, background = MaterialTheme.wireColorScheme.surface, isWholeItemClickable = true, - onRemoveDeviceClick = onDeviceClick, - leadingIcon = Icons.Filled.ChevronRight.Icon(), - leadingIconBorder = 0.dp, + onClickAction = onDeviceClick, + icon = Icons.Filled.ChevronRight.Icon(), shouldShowVerifyLabel = true ) if (index < otherUserDevices.lastIndex) WireDivider() From 51c465eced6dea84bd20ab2ab7e82bd8c123b342 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Wed, 31 Jan 2024 12:22:35 +0100 Subject: [PATCH 016/134] fix: logging level does not reflect in datadog (#2645) --- .../beta/kotlin/com/wire/android/util/DataDogLogger.kt | 10 +++++++++- .../dev/kotlin/com/wire/android/util/DataDogLogger.kt | 10 +++++++++- .../kotlin/com/wire/android/util/DataDogLogger.kt | 10 +++++++++- .../kotlin/com/wire/android/util/DataDogLogger.kt | 10 +++++++++- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/beta/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/beta/kotlin/com/wire/android/util/DataDogLogger.kt index e0c5c22169d..eb345c431ef 100644 --- a/app/src/beta/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/beta/kotlin/com/wire/android/util/DataDogLogger.kt @@ -40,6 +40,14 @@ object DataDogLogger : LogWriter() { "clientId" to userClientData.clientId, ) } ?: emptyMap() - logger.log(severity.ordinal, message, throwable, attributes) + + when (severity) { + Severity.Debug -> logger.d(message, throwable, attributes) + Severity.Info -> logger.i(message, throwable, attributes) + Severity.Warn -> logger.w(message, throwable, attributes) + Severity.Error -> logger.e(message, throwable, attributes) + Severity.Assert, + Severity.Verbose -> logger.v(message, throwable, attributes) + } } } diff --git a/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt index fd84a0bfccd..1cd511a3d5f 100644 --- a/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt @@ -42,6 +42,14 @@ object DataDogLogger : LogWriter() { "clientId" to userClientData.clientId, ) } ?: emptyMap() - logger.log(severity.ordinal, message, throwable, attributes) + + when (severity) { + Severity.Debug -> logger.d(message, throwable, attributes) + Severity.Info -> logger.i(message, throwable, attributes) + Severity.Warn -> logger.w(message, throwable, attributes) + Severity.Error -> logger.e(message, throwable, attributes) + Severity.Assert, + Severity.Verbose -> logger.v(message, throwable, attributes) + } } } diff --git a/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt index fd84a0bfccd..1cd511a3d5f 100644 --- a/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt @@ -42,6 +42,14 @@ object DataDogLogger : LogWriter() { "clientId" to userClientData.clientId, ) } ?: emptyMap() - logger.log(severity.ordinal, message, throwable, attributes) + + when (severity) { + Severity.Debug -> logger.d(message, throwable, attributes) + Severity.Info -> logger.i(message, throwable, attributes) + Severity.Warn -> logger.w(message, throwable, attributes) + Severity.Error -> logger.e(message, throwable, attributes) + Severity.Assert, + Severity.Verbose -> logger.v(message, throwable, attributes) + } } } diff --git a/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt index fd84a0bfccd..1cd511a3d5f 100644 --- a/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt @@ -42,6 +42,14 @@ object DataDogLogger : LogWriter() { "clientId" to userClientData.clientId, ) } ?: emptyMap() - logger.log(severity.ordinal, message, throwable, attributes) + + when (severity) { + Severity.Debug -> logger.d(message, throwable, attributes) + Severity.Info -> logger.i(message, throwable, attributes) + Severity.Warn -> logger.w(message, throwable, attributes) + Severity.Error -> logger.e(message, throwable, attributes) + Severity.Assert, + Severity.Verbose -> logger.v(message, throwable, attributes) + } } } From 0c4134897176c7e8d5006bbbba9b1790097cb97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:21:45 +0100 Subject: [PATCH 017/134] fix: self-deleting msg in doze mode on ConversationScreen [WPB-5894] (#2642) Co-authored-by: Yamil Medina --- .../com/wire/android/SelfDeletionTimerTest.kt | 199 +++++----- .../wire/android/GlobalObserversManager.kt | 25 ++ .../home/conversations/MessageExpiration.kt | 339 +++++++++--------- .../ui/home/conversations/MessageItem.kt | 5 +- .../home/conversations/SystemMessageItem.kt | 3 +- .../android/GlobalObserversManagerTest.kt | 88 +++++ 6 files changed, 403 insertions(+), 256 deletions(-) diff --git a/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt b/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt index a9d5dbba7c9..cca20017c6c 100644 --- a/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt +++ b/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt @@ -21,20 +21,43 @@ import androidx.test.platform.app.InstrumentationRegistry import com.wire.android.ui.home.conversations.SelfDeletionTimerHelper import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.kalium.logic.data.message.Message +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import org.junit.After +import org.junit.Before import org.junit.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds class SelfDeletionTimerTest { - private val selfDeletionTimer = SelfDeletionTimerHelper( - context = InstrumentationRegistry.getInstrumentation().targetContext - ) + private val selfDeletionTimer by lazy { + SelfDeletionTimerHelper(context = InstrumentationRegistry.getInstrumentation().targetContext) + } + private val dispatcher = StandardTestDispatcher() + private fun currentTime(): Instant = Instant.fromEpochMilliseconds(dispatcher.scheduler.currentTime) + + @Before + fun setUp() { + mockkObject(SelfDeletionTimerHelper.Companion) + every { SelfDeletionTimerHelper.Companion.currentTime() } answers { currentTime() } + } + + @After + fun cleanUp() { + unmockkObject(SelfDeletionTimerHelper.Companion) + } @Test - fun givenTimeLeftIsAboveOneHour_whenGettingTheUpdateInterval_ThenIsEqualToMinutesLeftTillWholeHour() { + fun givenTimeLeftIsAboveOneHour_whenGettingTheUpdateInterval_ThenIsEqualToMinutesLeftTillWholeHour() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours + 30.minutes, @@ -47,7 +70,7 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToWholeHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() { + fun givenTimeLeftIsEqualToWholeHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours, @@ -60,7 +83,7 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToOneHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() { + fun givenTimeLeftIsEqualToOneHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.hours, @@ -73,7 +96,7 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToOneMinute_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() { + fun givenTimeLeftIsEqualToOneMinute_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes, @@ -85,6 +108,19 @@ class SelfDeletionTimerTest { assert(interval == 1.seconds) } + @Test + fun givenTimeLeftIsEqualTo1Min10SecAnd900Millis_whenGettingTheUpdateInterval_ThenIsEqualTo10SecAnd900Millis() = runTest(dispatcher) { + val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + ExpirationStatus.Expirable( + expireAfter = 1.minutes + 10.seconds + 900.milliseconds, + selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted + ) + ) + assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) + val interval = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).updateInterval() + assert(interval == 10.seconds + 900.milliseconds) + } + @Test fun givenTimeLeftIsEqualToThirtySeconds_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( @@ -99,7 +135,7 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToFiftyDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualToFiftyDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 50.days, @@ -107,12 +143,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentySevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualToTwentySevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days, @@ -120,12 +156,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentySevenDaysAndTwelveHours_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualTo27DaysAnd12Hours_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days + 12.hours, @@ -133,12 +169,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentySevenDaysAndOneSecond_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualTo27DaysAnd1Second_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days + 1.seconds, @@ -146,12 +182,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentyEightDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualTo28Days_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 28.days, @@ -159,12 +195,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentyOneDays_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyOneLeft() { + fun givenTimeLeftIsEqualTo21Days_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyOneLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 21.days, @@ -172,12 +208,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "21 days left") } @Test - fun givenTimeLeftIsEqualToFourTeenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourTeenDaysLeft() { + fun givenTimeLeftIsEqualTo14Days_whenGettingThTimeLeftFormatted_ThenIsEqualToFourTeenDaysLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 14.days, @@ -185,12 +221,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "14 days left") } @Test - fun givenTimeLeftIsEqualToTwentyDays_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyDaysLeft() { + fun givenTimeLeftIsEqualTo20Days_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyDaysLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 20.days, @@ -198,12 +234,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "20 days left") } @Test - fun givenTimeLeftIsEqualToSevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() { + fun givenTimeLeftIsEqualToSevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 7.days, @@ -211,12 +247,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 week left") } @Test - fun givenTimeLeftIsEqualToSixDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() { + fun givenTimeLeftIsEqualToSixDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days, @@ -224,12 +260,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 week left") } @Test - fun givenTimeLeftIsEqualToSixDaysAnd12Hours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() { + fun givenTimeLeftIsEqualToSixDaysAnd12Hours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days + 12.hours, @@ -237,12 +273,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 week left") } @Test - fun givenTimeLeftIsEqualToSixDaysAndOneSecond_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() { + fun givenTimeLeftIsEqualToSixDaysAndOneSecond_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days + 1.seconds, @@ -250,12 +286,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 week left") } @Test - fun givenTimeLeftIsEqualToThirteenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToThirteenDays() { + fun givenTimeLeftIsEqualToThirteenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToThirteenDays() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 13.days, @@ -263,12 +299,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "13 days left") } @Test - fun givenTimeLeftIsEqualToOneDay_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() { + fun givenTimeLeftIsEqualToOneDay_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.days, @@ -276,12 +312,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 day left") } @Test - fun givenTimeLeftIsEqualToTwentyFourHours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() { + fun givenTimeLeftIsEqualToTwentyFourHours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 24.hours, @@ -289,12 +325,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 day left") } @Test - fun givenTimeLeftIsEqualToTwentyThreeHours_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyThreeHourLeft() { + fun givenTimeLeftIsEqualToTwentyThreeHours_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyThreeHourLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours, @@ -302,12 +338,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "23 hours left") } @Test - fun givenTimeLeftIsEqualToSixtyMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToOneHourLeft() { + fun givenTimeLeftIsEqualToSixtyMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToOneHourLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 60.minutes, @@ -315,12 +351,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 hour left") } @Test - fun givenTimeLeftIsEqualToOneMinute_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinuteLeft() { + fun givenTimeLeftIsEqualToOneMinute_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinuteLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes, @@ -328,12 +364,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 minute left") } @Test - fun givenTimeLeftIsEqualToOFiftyNineMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToFiftyNineMinutes() { + fun givenTimeLeftIsEqualToOFiftyNineMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToFiftyNineMinutes() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 59.minutes, @@ -341,12 +377,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "59 minutes left") } @Test - fun givenTimeLeftIsEqualToSixtySeconds_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinute() { + fun givenTimeLeftIsEqualToSixtySeconds_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinute() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 60.seconds, @@ -354,12 +390,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 minute left") } @Test - fun givenTimeLeftIsEqualToOneDayAndTwelveHours_whenDecreasingTimeWithInterval_thenTimeLeftIsEqualToExpecetedTimeLeft() { + fun givenTimeLeftIs1DayAnd12Hours_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.days + 12.hours, @@ -367,17 +403,19 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "1 day left") - - selfDeletionTimer.decreaseTimeLeft(selfDeletionTimer.updateInterval()) - assert(selfDeletionTimer.timeLeftFormatted() == "23 hours left") + with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "1 day left") + + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "23 hours left") + } } @Test - fun givenTimeLeftIsEqualToTwentyThreeHoursAndTwentyThreeMinutes_whenDecreasingTimeWithInterval_thenTimeLeftIsEqualToExpeceted() { + fun givenTimeLeftIs23HoursAnd23Minutes_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours + 23.minutes, @@ -385,16 +423,15 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - - val timeLeftLabel = selfDeletionTimer.timeLeftFormatted() - assert(timeLeftLabel == "23 hours left") + with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "23 hours left") + } } @Test - fun givenTimeLeftIsEqualToOneHourAndTwelveMinutes_whenDecreasingTimeWithInterval_thenTimeLeftIsEqualToExpecetedTimeLeft() { + fun givenTimeLeftIs1HourAnd12Minutes_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.hours + 12.minutes, @@ -402,18 +439,19 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "1 hour left") - selfDeletionTimer.decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "59 minutes left") + with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "1 hour left") + + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "59 minutes left") + } } @Test - fun givenTimeLeftIsEqualToOneHourAndTwentyThreeSeconds_whenDecreasingTimeWithInterval_thenTimeLeftIsEqualToExpecetedTimeLeft() { + fun givenTimeLeftIs1HourAnd23Seconds_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes + 23.seconds, @@ -421,13 +459,14 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "1 minute left") - selfDeletionTimer.decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "59 seconds left") + with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "1 minute left") + + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "59 seconds left") + } } } diff --git a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt index 954fe2a4a71..944b629225a 100644 --- a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt +++ b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt @@ -22,11 +22,13 @@ import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.KaliumCoreLogic import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager +import com.wire.android.util.CurrentScreenManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.auth.LogoutCallback +import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -35,8 +37,12 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @@ -52,6 +58,7 @@ class GlobalObserversManager @Inject constructor( private val notificationManager: WireNotificationManager, private val notificationChannelsManager: NotificationChannelsManager, private val userDataStoreProvider: UserDataStoreProvider, + private val currentScreenManager: CurrentScreenManager, ) { private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.io()) @@ -65,6 +72,7 @@ class GlobalObserversManager @Inject constructor( } } scope.handleLogouts() + scope.handleDeleteEphemeralMessageEndDate() } private suspend fun setUpNotifications() { @@ -114,4 +122,21 @@ class GlobalObserversManager @Inject constructor( awaitClose { coreLogic.getGlobalScope().logoutCallbackManager.unregister(callback) } }.launchIn(this) } + + private fun CoroutineScope.handleDeleteEphemeralMessageEndDate() { + launch { + currentScreenManager.isAppVisibleFlow() + .flatMapLatest { isAppVisible -> + if (isAppVisible) { + coreLogic.getGlobalScope().session.currentSessionFlow() + .distinctUntilChanged() + .filter { it is CurrentSessionResult.Success && it.accountInfo.isValid() } + .map { (it as CurrentSessionResult.Success).accountInfo.userId } + } else { + emptyFlow() + } + } + .collect { userId -> coreLogic.getSessionScope(userId).messages.deleteEphemeralMessageEndDate() } + } + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt index 5eadf4f0cf8..94f40bd33a1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt @@ -19,13 +19,18 @@ package com.wire.android.ui.home.conversations import android.content.Context import android.content.res.Resources +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.wire.android.R import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.android.ui.home.conversations.model.UIMessage @@ -33,12 +38,15 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.kalium.logic.data.message.Message import kotlinx.coroutines.delay import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.toDuration @Composable fun rememberSelfDeletionTimer(expirationStatus: ExpirationStatus): SelfDeletionTimerHelper.SelfDeletionTimerState { @@ -54,12 +62,11 @@ class SelfDeletionTimerHelper(private val context: Context) { fun fromExpirationStatus(expirationStatus: ExpirationStatus): SelfDeletionTimerState { return if (expirationStatus is ExpirationStatus.Expirable) { with(expirationStatus) { - val timeLeft = calculateTimeLeft(selfDeletionStatus, expireAfter) + val expireAt = calculateExpireAt(selfDeletionStatus, expireAfter) SelfDeletionTimerState.Expirable( context.resources, - timeLeft, expireAfter, - selfDeletionStatus is Message.ExpirationData.SelfDeletionStatus.Started + expireAt, ) } } else { @@ -67,35 +74,22 @@ class SelfDeletionTimerHelper(private val context: Context) { } } - private fun calculateTimeLeft( + private fun calculateExpireAt( selfDeletionStatus: Message.ExpirationData.SelfDeletionStatus?, - expireAfter: Duration - ): Duration { - return if (selfDeletionStatus is Message.ExpirationData.SelfDeletionStatus.Started) { - val timeElapsedSinceSelfDeletionStartDate = Clock.System.now() - selfDeletionStatus.selfDeletionStartDate - val timeLeft = expireAfter - timeElapsedSinceSelfDeletionStartDate - - /** - * time left for deletion, can be a negative value if the time difference between the self deletion start date and - * Clock.System.now() is greater then [expireAfter], we normalize it to 0 seconds - */ - if (timeLeft.isNegative()) { - ZERO - } else { - timeLeft - } - } else { - expireAfter + expireAfter: Duration, + ) = + if (selfDeletionStatus is Message.ExpirationData.SelfDeletionStatus.Started) selfDeletionStatus.selfDeletionStartDate + expireAfter + else { + val currentTime = currentTime() + currentTime + expireAfter } - } sealed class SelfDeletionTimerState { class Expirable( private val resources: Resources, - timeLeft: Duration, private val expireAfter: Duration, - val timerStarted: Boolean + private val expireAt: Instant, ) : SelfDeletionTimerState() { companion object { /** @@ -115,67 +109,69 @@ class SelfDeletionTimerHelper(private val context: Context) { private const val OPAQUE_BACKGROUND_COLOR_ALPHA_VALUE = 1F } - var timeLeft by mutableStateOf(timeLeft) - + var timeLeft by mutableStateOf(calculateTimeLeft()) + private set @Suppress("MagicNumber", "ComplexMethod") - fun timeLeftFormatted(): String = when { - timeLeft > 28.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 4, - 4 - ) - // 4 weeks - timeLeft >= 27.days && timeLeft <= 28.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 4, - 4 - ) - // days below 4 weeks - timeLeft <= 27.days && timeLeft > 7.days -> - resources.getQuantityString( - R.plurals.days_left, - timeLeft.inWholeDays.toInt(), - timeLeft.inWholeDays.toInt() - ) - // one week - timeLeft >= 6.days && timeLeft <= 7.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 1, - 1 - ) - // days below 1 week - timeLeft < 7.days && timeLeft >= 1.days -> - resources.getQuantityString( - R.plurals.days_left, - timeLeft.inWholeDays.toInt(), - timeLeft.inWholeDays.toInt() - ) - // hours below one day - timeLeft >= 1.hours && timeLeft < 24.hours -> - resources.getQuantityString( - R.plurals.hours_left, - timeLeft.inWholeHours.toInt(), - timeLeft.inWholeHours.toInt() - ) - // minutes below hour - timeLeft >= 1.minutes && timeLeft < 60.minutes -> - resources.getQuantityString( - R.plurals.minutes_left, - timeLeft.inWholeMinutes.toInt(), - timeLeft.inWholeMinutes.toInt() - ) - // seconds below minute - timeLeft < 60.seconds -> - resources.getQuantityString( - R.plurals.seconds_left, - timeLeft.inWholeSeconds.toInt(), - timeLeft.inWholeSeconds.toInt() - ) + val timeLeftFormatted: String by derivedStateOf { + when { + timeLeft > 28.days -> + resources.getQuantityString( + R.plurals.weeks_left, + 4, + 4 + ) + // 4 weeks + timeLeft >= 27.days && timeLeft <= 28.days -> + resources.getQuantityString( + R.plurals.weeks_left, + 4, + 4 + ) + // days below 4 weeks + timeLeft <= 27.days && timeLeft > 7.days -> + resources.getQuantityString( + R.plurals.days_left, + timeLeft.inWholeDays.toInt(), + timeLeft.inWholeDays.toInt() + ) + // one week + timeLeft >= 6.days && timeLeft <= 7.days -> + resources.getQuantityString( + R.plurals.weeks_left, + 1, + 1 + ) + // days below 1 week + timeLeft < 7.days && timeLeft >= 1.days -> + resources.getQuantityString( + R.plurals.days_left, + timeLeft.inWholeDays.toInt(), + timeLeft.inWholeDays.toInt() + ) + // hours below one day + timeLeft >= 1.hours && timeLeft < 24.hours -> + resources.getQuantityString( + R.plurals.hours_left, + timeLeft.inWholeHours.toInt(), + timeLeft.inWholeHours.toInt() + ) + // minutes below hour + timeLeft >= 1.minutes && timeLeft < 60.minutes -> + resources.getQuantityString( + R.plurals.minutes_left, + timeLeft.inWholeMinutes.toInt(), + timeLeft.inWholeMinutes.toInt() + ) + // seconds below minute + timeLeft < 60.seconds -> + resources.getQuantityString( + R.plurals.seconds_left, + timeLeft.inWholeSeconds.toInt(), + timeLeft.inWholeSeconds.toInt() + ) - else -> throw IllegalStateException("Not possible state for a time left label") + else -> throw IllegalStateException("Not possible state for a time left label") + } } /** @@ -186,48 +182,41 @@ class SelfDeletionTimerHelper(private val context: Context) { * updated every second. * @return how long until the next timer update. */ - fun updateInterval(): Duration { - val timeLeftUpdateInterval = when { - timeLeft > 24.hours -> { - val timeLeftTillWholeDay = (timeLeft.inWholeMinutes % 1.days.inWholeMinutes).minutes - if (timeLeftTillWholeDay == ZERO) { - 1.days - } else { - timeLeftTillWholeDay - } - } - - timeLeft <= 24.hours && timeLeft > 1.hours -> { - val timeLeftTillWholeHour = (timeLeft.inWholeSeconds % 1.hours.inWholeSeconds).seconds - if (timeLeftTillWholeHour == ZERO) { - 1.hours - } else { - timeLeftTillWholeHour - } - } - - timeLeft <= 1.hours && timeLeft > 1.minutes -> { - val timeLeftTillWholeMinute = (timeLeft.inWholeSeconds % 1.minutes.inWholeSeconds).seconds - if (timeLeftTillWholeMinute == ZERO) { - 1.minutes - } else { - timeLeftTillWholeMinute - } - } - - timeLeft <= 1.minutes -> { - 1.seconds - } + @VisibleForTesting + internal fun updateInterval(): Duration { + fun remainingTimeToDurationUnit(durationUnit: DurationUnit): Duration { + /* + * Function toLong returns the whole part for the given duration unit and then this whole value is converted back to + * Duration and subtracted from the original duration, which gives the remaining time to the next full duration unit. + * + * For example, if the time left is "1 day and 1 hour" and durationUnit is DAYS, then toLong will return 1L + * which means "1 full day" (just like .inWholeDays) and then it will be converted back to Duration type. + * Then this "1 day" will be subtracted from the original duration, returning "1 hour" left ("1d 1h" - "1d" = "1h"). + * So in this case it's the same as `timeLeft - timeLeft.inWholeHours.hours` + * because `timeLeft.inWholeDays` is basically `timeLeft.toLong(DurationUnit.DAYS)` + * and `1L.days` is the same as `1L.toDuration(DurationUnit.DAYS)`. + */ + val timeLeftForDurationUnit = timeLeft - timeLeft.toLong(durationUnit).toDuration(durationUnit) + return if (timeLeftForDurationUnit == ZERO) 1.toDuration(durationUnit) + else timeLeftForDurationUnit + } + val timeLeftUpdateInterval = when { + timeLeft > 24.hours -> remainingTimeToDurationUnit(DurationUnit.DAYS) + timeLeft <= 24.hours && timeLeft > 1.hours -> remainingTimeToDurationUnit(DurationUnit.HOURS) + timeLeft <= 1.hours && timeLeft > 1.minutes -> remainingTimeToDurationUnit(DurationUnit.MINUTES) + timeLeft <= 1.minutes -> remainingTimeToDurationUnit(DurationUnit.SECONDS) else -> throw IllegalStateException("Not possible state for the interval") } return timeLeftUpdateInterval } - fun decreaseTimeLeft(interval: Duration) { - if (timeLeft.inWholeSeconds != 0L) timeLeft -= interval - } + // non-negative value, returns ZERO if message is already expired + private fun calculateTimeLeft(): Duration = (expireAt - currentTime()).let { if (it.isNegative()) ZERO else it } + + @VisibleForTesting + internal fun recalculateTimeLeft() { timeLeft = calculateTimeLeft() } /** * if the time elapsed ratio is between 0.50 and 0.75 @@ -266,72 +255,80 @@ class SelfDeletionTimerHelper(private val context: Context) { OPAQUE_BACKGROUND_COLOR_ALPHA_VALUE } } - } - object NotExpirable : SelfDeletionTimerState() - } -} + @Composable + fun startDeletionTimer(message: UIMessage, onStartMessageSelfDeletion: (UIMessage) -> Unit) { + when (val messageContent = message.messageContent) { + is UIMessageContent.AssetMessage -> startAssetDeletion( + onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, + downloadStatus = messageContent.downloadStatus + ) -@Composable -fun startDeletionTimer( - message: UIMessage, - expirableTimer: SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable, - onStartMessageSelfDeletion: (UIMessage) -> Unit -) { - when (val messageContent = message.messageContent) { - is UIMessageContent.AssetMessage -> startAssetDeletion( - expirableTimer = expirableTimer, - onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, - downloadStatus = messageContent.downloadStatus - ) + is UIMessageContent.AudioAssetMessage -> startAssetDeletion( + onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, + downloadStatus = messageContent.downloadStatus + ) - is UIMessageContent.AudioAssetMessage -> startAssetDeletion( - expirableTimer = expirableTimer, - onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, - downloadStatus = messageContent.downloadStatus - ) + is UIMessageContent.ImageMessage -> startAssetDeletion( + onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, + downloadStatus = messageContent.downloadStatus + ) - is UIMessageContent.ImageMessage -> startAssetDeletion( - expirableTimer = expirableTimer, - onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, - downloadStatus = messageContent.downloadStatus - ) + else -> startRegularDeletion(message = message, onStartMessageSelfDeletion = onStartMessageSelfDeletion) + } + } - else -> { - LaunchedEffect(Unit) { - onStartMessageSelfDeletion(message) + @Composable + private fun startAssetDeletion(onSelfDeletingMessageRead: () -> Unit, downloadStatus: Message.DownloadStatus) { + LaunchedEffect(downloadStatus) { + if (downloadStatus == Message.DownloadStatus.SAVED_EXTERNALLY + || downloadStatus == Message.DownloadStatus.SAVED_INTERNALLY + ) { + onSelfDeletingMessageRead() + } + } + LaunchedEffect(key1 = timeLeft, key2 = downloadStatus) { + if (downloadStatus == Message.DownloadStatus.SAVED_EXTERNALLY + || downloadStatus == Message.DownloadStatus.SAVED_INTERNALLY + ) { + if (timeLeft != ZERO) { + delay(updateInterval()) + recalculateTimeLeft() + } + } + } + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + recalculateTimeLeft() + } + } } - LaunchedEffect(expirableTimer.timeLeft) { - with(expirableTimer) { + + @Composable + private fun startRegularDeletion(message: UIMessage, onStartMessageSelfDeletion: (UIMessage) -> Unit) { + LaunchedEffect(Unit) { + onStartMessageSelfDeletion(message) + } + LaunchedEffect(timeLeft) { if (timeLeft != ZERO) { delay(updateInterval()) - decreaseTimeLeft(updateInterval()) + recalculateTimeLeft() + } + } + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + recalculateTimeLeft() } } } } - } -} -@Composable -private fun startAssetDeletion( - expirableTimer: SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable, - onSelfDeletingMessageRead: () -> Unit, - downloadStatus: Message.DownloadStatus -) { - LaunchedEffect(downloadStatus) { - if (downloadStatus == Message.DownloadStatus.SAVED_EXTERNALLY || downloadStatus == Message.DownloadStatus.SAVED_INTERNALLY) { - onSelfDeletingMessageRead() - } + object NotExpirable : SelfDeletionTimerState() } - LaunchedEffect(expirableTimer.timeLeft, downloadStatus) { - if (downloadStatus == Message.DownloadStatus.SAVED_EXTERNALLY || downloadStatus == Message.DownloadStatus.SAVED_INTERNALLY) { - with(expirableTimer) { - if (timeLeft != ZERO) { - delay(updateInterval()) - decreaseTimeLeft(updateInterval()) - } - } - } + + companion object { + fun currentTime(): Instant = Clock.System.now() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt index 0154d6b0e2d..0d13266fdb7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt @@ -125,9 +125,8 @@ fun MessageItem( !message.isPending && !message.sendingFailed ) { - startDeletionTimer( + selfDeletionTimerState.startDeletionTimer( message = message, - expirableTimer = selfDeletionTimerState, onStartMessageSelfDeletion = onSelfDeletingMessageRead ) } @@ -226,7 +225,7 @@ fun MessageItem( MessageAuthorRow(messageHeader = message.header) } if (selfDeletionTimerState is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { - MessageExpireLabel(messageContent, selfDeletionTimerState.timeLeftFormatted()) + MessageExpireLabel(messageContent, selfDeletionTimerState.timeLeftFormatted) // if the message is marked as deleted and is [SelfDeletionTimer.SelfDeletionTimerState.Expirable] // the deletion responsibility belongs to the receiver, therefore we need to wait for the receiver diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt index 69dbac57898..5e2686e992f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt @@ -97,9 +97,8 @@ fun SystemMessageItem( !message.isPending && !message.sendingFailed ) { - startDeletionTimer( + selfDeletionTimerState.startDeletionTimer( message = message, - expirableTimer = selfDeletionTimerState, onStartMessageSelfDeletion = onSelfDeletingMessageRead ) } diff --git a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt index 3e75aac6c02..ecb71639f69 100644 --- a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt @@ -24,10 +24,18 @@ import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.framework.TestUser import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager +import com.wire.android.util.CurrentScreenManager +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.auth.PersistentWebSocketStatus +import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.team.Team import com.wire.kalium.logic.data.user.SelfUser +import com.wire.kalium.logic.feature.UserSessionScope +import com.wire.kalium.logic.feature.auth.LogoutCallbackManager +import com.wire.kalium.logic.feature.message.MessageScope +import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -35,6 +43,7 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -87,6 +96,56 @@ class GlobalObserversManagerTest { } } + @Test + fun `given app visible and valid session, when handling ephemeral messages, then call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.SELF_USER.id))) + .withAppVisibleFlow(true) + .arrange() + manager.observe() + coVerify(exactly = 1) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + + @Test + fun `given app not visible and valid session, when handling ephemeral messages, then do not call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.SELF_USER.id))) + .withAppVisibleFlow(false) + .arrange() + manager.observe() + coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + + @Test + fun `given app visible and invalid session, when handling ephemeral messages, then do not call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Success(AccountInfo.Invalid(TestUser.SELF_USER.id, LogoutReason.DELETED_ACCOUNT))) + .withAppVisibleFlow(true) + .arrange() + manager.observe() + coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + + @Test + fun `given app visible and no session, when handling ephemeral messages, then do not call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Failure.SessionNotFound) + .withAppVisibleFlow(true) + .arrange() + manager.observe() + coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + + @Test + fun `given app visible and session failure, when handling ephemeral messages, then do not call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Failure.Generic(CoreFailure.Unknown(RuntimeException("error")))) + .withAppVisibleFlow(true) + .arrange() + manager.observe() + coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + private class Arrangement { @MockK @@ -101,6 +160,18 @@ class GlobalObserversManagerTest { @MockK lateinit var userDataStoreProvider: UserDataStoreProvider + @MockK + lateinit var currentScreenManager: CurrentScreenManager + + @MockK + lateinit var logoutCallbackManager: LogoutCallbackManager + + @MockK + lateinit var userSessionScope: UserSessionScope + + @MockK + lateinit var messageScope: MessageScope + private val manager by lazy { GlobalObserversManager( dispatcherProvider = TestDispatcherProvider(), @@ -108,6 +179,7 @@ class GlobalObserversManagerTest { notificationChannelsManager = notificationChannelsManager, notificationManager = notificationManager, userDataStoreProvider = userDataStoreProvider, + currentScreenManager = currentScreenManager, ) } @@ -118,6 +190,14 @@ class GlobalObserversManagerTest { // Default empty values mockUri() every { notificationChannelsManager.createUserNotificationChannels(any()) } returns Unit + every { coreLogic.getGlobalScope().logoutCallbackManager } returns logoutCallbackManager + every { coreLogic.getSessionScope(any()) } returns userSessionScope + every { userSessionScope.messages } returns messageScope + coEvery { messageScope.deleteEphemeralMessageEndDate() } returns Unit + withPersistentWebSocketConnectionStatuses(emptyList()) + withValidAccounts(emptyList()) + withCurrentSessionFlow(CurrentSessionResult.Failure.SessionNotFound) + withAppVisibleFlow(true) } fun withValidAccounts(list: List>): Arrangement = apply { @@ -129,6 +209,14 @@ class GlobalObserversManagerTest { ObservePersistentWebSocketConnectionStatusUseCase.Result.Success(flowOf(list)) } + fun withCurrentSessionFlow(result: CurrentSessionResult): Arrangement = apply { + coEvery { coreLogic.getGlobalScope().session.currentSessionFlow() } returns flowOf(result) + } + + fun withAppVisibleFlow(isVisible: Boolean) = apply { + coEvery { currentScreenManager.isAppVisibleFlow() } returns MutableStateFlow(isVisible) + } + fun arrange() = this to manager } } From 9350830461b238ae54185e452a8857c5de5c07ca Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 31 Jan 2024 18:01:53 +0100 Subject: [PATCH 018/134] fix: not possible to search for services [WPB-5943] (#2648) Co-authored-by: Yamil Medina --- .../ui/home/conversations/search/SearchPeopleRouter.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt index 2b34fe08542..42110004980 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt @@ -178,14 +178,6 @@ fun SearchUsersAndServicesScreen( } } } - - LaunchedEffect(pagerState.isScrollInProgress, focusedTabIndex, pagerState.currentPage) { - if (!pagerState.isScrollInProgress && focusedTabIndex != pagerState.currentPage) { - keyboardController?.hide() - focusManager.clearFocus() - focusedTabIndex = pagerState.currentPage - } - } } } else { SearchAllPeopleOrContactsScreen( From 0bb42cd0e041482c61763b7e6c9ca68ae1549d0e Mon Sep 17 00:00:00 2001 From: yamilmedina Date: Thu, 1 Feb 2024 15:50:09 +0100 Subject: [PATCH 019/134] chore: kalium ref --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index cf92a574445..00a3b741fb9 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit cf92a57444563b281c2278ba6b6dbe13841bc3d6 +Subproject commit 00a3b741fb902cb58ae7a17634992cd43fa5d1d6 From 8f2f1b6ef148d2db51dc72c5d8f8355233201829 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Fri, 2 Feb 2024 13:01:23 +0100 Subject: [PATCH 020/134] ci: fix cherry pick action when last commit message contains special characters (#2654) --- .github/workflows/cherry-pick-rc-to-develop.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cherry-pick-rc-to-develop.yml b/.github/workflows/cherry-pick-rc-to-develop.yml index 48598b3d69a..afd9f7b4b1c 100644 --- a/.github/workflows/cherry-pick-rc-to-develop.yml +++ b/.github/workflows/cherry-pick-rc-to-develop.yml @@ -92,7 +92,8 @@ jobs: cd .. git add ${{ env.SUBMODULE_NAME }} git commit -m "Update submodule ${{ env.SUBMODULE_NAME }} to latest from ${{ env.TARGET_BRANCH }}" - echo "lastCommitMessage=$LAST_COMMIT_MESSAGE" >> $GITHUB_ENV + # Base64 encode the commit message to avoid issues with newlines and special characters + echo "lastCommitMessageBase64=$(echo "$LAST_COMMIT_MESSAGE" | base64)" >> $GITHUB_ENV - name: Get Cherry-pick commit id: get-cherry @@ -101,7 +102,9 @@ jobs: if [[ -n "${{ env.SUBMODULE_NAME }}" ]]; then # If SUBMODULE_NAME is set git reset --soft HEAD~2 - git commit -m "${{ env.lastCommitMessage }}" + # Decode the base64-encoded string + LAST_COMMIT_MESSAGE=$(echo "${{ env.lastCommitMessageBase64 }}" | base64 --decode) + git commit -m "$LAST_COMMIT_MESSAGE" fi # Get the SHA of the current commit (either squashed or not based on the condition above) From bfaa191751fcbf904d841e814f1568c596c528f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:33:49 +0100 Subject: [PATCH 021/134] fix: make SelfDeletionTimerTest unit instead of instrumented (#2657) --- .../home/conversations/MessageExpiration.kt | 94 ++++----- .../com/wire/android/SelfDeletionTimerTest.kt | 184 ++++++++++-------- 2 files changed, 146 insertions(+), 132 deletions(-) rename app/src/{androidTest/java => test/kotlin}/com/wire/android/SelfDeletionTimerTest.kt (68%) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt index 94f40bd33a1..cb3b04720bb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt @@ -17,8 +17,6 @@ */ package com.wire.android.ui.home.conversations -import android.content.Context -import android.content.res.Resources import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,23 +48,44 @@ import kotlin.time.toDuration @Composable fun rememberSelfDeletionTimer(expirationStatus: ExpirationStatus): SelfDeletionTimerHelper.SelfDeletionTimerState { - val context = LocalContext.current + val stringResourceProvider: StringResourceProvider = stringResourceProvider() + val currentTimeProvider: CurrentTimeProvider = { Clock.System.now() } - return remember( - (expirationStatus as? ExpirationStatus.Expirable)?.selfDeletionStatus ?: true - ) { SelfDeletionTimerHelper(context).fromExpirationStatus(expirationStatus) } + return remember((expirationStatus as? ExpirationStatus.Expirable)?.selfDeletionStatus ?: true) { + SelfDeletionTimerHelper(stringResourceProvider, currentTimeProvider) + .fromExpirationStatus(expirationStatus) + } } -class SelfDeletionTimerHelper(private val context: Context) { +@Composable +private fun stringResourceProvider(): StringResourceProvider { + with(LocalContext.current.resources) { + return object : StringResourceProvider { + override fun quantityString(type: StringResourceType, quantity: Int): String = + getQuantityString( + when (type) { + StringResourceType.WEEKS -> R.plurals.weeks_left + StringResourceType.DAYS -> R.plurals.days_left + StringResourceType.HOURS -> R.plurals.hours_left + StringResourceType.MINUTES -> R.plurals.minutes_left + StringResourceType.SECONDS -> R.plurals.seconds_left + }, quantity, quantity + ) + } + } +} + +class SelfDeletionTimerHelper(private val stringResourceProvider: StringResourceProvider, private val currentTime: CurrentTimeProvider) { fun fromExpirationStatus(expirationStatus: ExpirationStatus): SelfDeletionTimerState { return if (expirationStatus is ExpirationStatus.Expirable) { with(expirationStatus) { val expireAt = calculateExpireAt(selfDeletionStatus, expireAfter) SelfDeletionTimerState.Expirable( - context.resources, + stringResourceProvider, expireAfter, expireAt, + currentTime ) } } else { @@ -87,9 +106,10 @@ class SelfDeletionTimerHelper(private val context: Context) { sealed class SelfDeletionTimerState { class Expirable( - private val resources: Resources, + private val stringResourceProvider: StringResourceProvider, private val expireAfter: Duration, private val expireAt: Instant, + private val currentTime: CurrentTimeProvider, ) : SelfDeletionTimerState() { companion object { /** @@ -115,60 +135,28 @@ class SelfDeletionTimerHelper(private val context: Context) { val timeLeftFormatted: String by derivedStateOf { when { timeLeft > 28.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 4, - 4 - ) + stringResourceProvider.quantityString(StringResourceType.WEEKS, 4) // 4 weeks timeLeft >= 27.days && timeLeft <= 28.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 4, - 4 - ) + stringResourceProvider.quantityString(StringResourceType.WEEKS, 4) // days below 4 weeks timeLeft <= 27.days && timeLeft > 7.days -> - resources.getQuantityString( - R.plurals.days_left, - timeLeft.inWholeDays.toInt(), - timeLeft.inWholeDays.toInt() - ) + stringResourceProvider.quantityString(StringResourceType.DAYS, timeLeft.inWholeDays.toInt()) // one week timeLeft >= 6.days && timeLeft <= 7.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 1, - 1 - ) + stringResourceProvider.quantityString(StringResourceType.WEEKS, 1) // days below 1 week timeLeft < 7.days && timeLeft >= 1.days -> - resources.getQuantityString( - R.plurals.days_left, - timeLeft.inWholeDays.toInt(), - timeLeft.inWholeDays.toInt() - ) + stringResourceProvider.quantityString(StringResourceType.DAYS, timeLeft.inWholeDays.toInt()) // hours below one day timeLeft >= 1.hours && timeLeft < 24.hours -> - resources.getQuantityString( - R.plurals.hours_left, - timeLeft.inWholeHours.toInt(), - timeLeft.inWholeHours.toInt() - ) + stringResourceProvider.quantityString(StringResourceType.HOURS, timeLeft.inWholeHours.toInt()) // minutes below hour timeLeft >= 1.minutes && timeLeft < 60.minutes -> - resources.getQuantityString( - R.plurals.minutes_left, - timeLeft.inWholeMinutes.toInt(), - timeLeft.inWholeMinutes.toInt() - ) + stringResourceProvider.quantityString(StringResourceType.MINUTES, timeLeft.inWholeMinutes.toInt()) // seconds below minute timeLeft < 60.seconds -> - resources.getQuantityString( - R.plurals.seconds_left, - timeLeft.inWholeSeconds.toInt(), - timeLeft.inWholeSeconds.toInt() - ) + stringResourceProvider.quantityString(StringResourceType.SECONDS, timeLeft.inWholeSeconds.toInt()) else -> throw IllegalStateException("Not possible state for a time left label") } @@ -327,8 +315,10 @@ class SelfDeletionTimerHelper(private val context: Context) { object NotExpirable : SelfDeletionTimerState() } +} - companion object { - fun currentTime(): Instant = Clock.System.now() - } +typealias CurrentTimeProvider = () -> Instant +enum class StringResourceType { WEEKS, DAYS, HOURS, MINUTES, SECONDS; } +interface StringResourceProvider { + fun quantityString(type: StringResourceType, quantity: Int): String } diff --git a/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt b/app/src/test/kotlin/com/wire/android/SelfDeletionTimerTest.kt similarity index 68% rename from app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt rename to app/src/test/kotlin/com/wire/android/SelfDeletionTimerTest.kt index cca20017c6c..e626114820a 100644 --- a/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt +++ b/app/src/test/kotlin/com/wire/android/SelfDeletionTimerTest.kt @@ -17,20 +17,18 @@ */ package com.wire.android -import androidx.test.platform.app.InstrumentationRegistry +import com.wire.android.ui.home.conversations.CurrentTimeProvider import com.wire.android.ui.home.conversations.SelfDeletionTimerHelper +import com.wire.android.ui.home.conversations.StringResourceProvider +import com.wire.android.ui.home.conversations.StringResourceType import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.kalium.logic.data.message.Message -import io.mockk.every -import io.mockk.mockkObject -import io.mockk.unmockkObject import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant -import org.junit.After -import org.junit.Before -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds @@ -39,26 +37,12 @@ import kotlin.time.Duration.Companion.seconds class SelfDeletionTimerTest { - private val selfDeletionTimer by lazy { - SelfDeletionTimerHelper(context = InstrumentationRegistry.getInstrumentation().targetContext) - } private val dispatcher = StandardTestDispatcher() - private fun currentTime(): Instant = Instant.fromEpochMilliseconds(dispatcher.scheduler.currentTime) - - @Before - fun setUp() { - mockkObject(SelfDeletionTimerHelper.Companion) - every { SelfDeletionTimerHelper.Companion.currentTime() } answers { currentTime() } - } - - @After - fun cleanUp() { - unmockkObject(SelfDeletionTimerHelper.Companion) - } @Test fun givenTimeLeftIsAboveOneHour_whenGettingTheUpdateInterval_ThenIsEqualToMinutesLeftTillWholeHour() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (_, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours + 30.minutes, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -71,7 +55,8 @@ class SelfDeletionTimerTest { @Test fun givenTimeLeftIsEqualToWholeHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (_, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -84,7 +69,8 @@ class SelfDeletionTimerTest { @Test fun givenTimeLeftIsEqualToOneHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (_, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.hours, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -97,7 +83,8 @@ class SelfDeletionTimerTest { @Test fun givenTimeLeftIsEqualToOneMinute_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (_, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -110,7 +97,8 @@ class SelfDeletionTimerTest { @Test fun givenTimeLeftIsEqualTo1Min10SecAnd900Millis_whenGettingTheUpdateInterval_ThenIsEqualTo10SecAnd900Millis() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (_, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes + 10.seconds + 900.milliseconds, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -122,8 +110,9 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToThirtySeconds_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + fun givenTimeLeftIsEqualToThirtySeconds_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() = runTest(dispatcher) { + val (_, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 30.seconds, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -136,7 +125,8 @@ class SelfDeletionTimerTest { @Test fun givenTimeLeftIsEqualToFiftyDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 50.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -144,12 +134,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "4 weeks left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 4)) } @Test fun givenTimeLeftIsEqualToTwentySevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -157,12 +148,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "4 weeks left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 4)) } @Test fun givenTimeLeftIsEqualTo27DaysAnd12Hours_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days + 12.hours, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -170,12 +162,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "4 weeks left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 4)) } @Test fun givenTimeLeftIsEqualTo27DaysAnd1Second_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days + 1.seconds, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -183,12 +176,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "4 weeks left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 4)) } @Test fun givenTimeLeftIsEqualTo28Days_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 28.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -196,12 +190,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "4 weeks left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 4)) } @Test fun givenTimeLeftIsEqualTo21Days_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyOneLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 21.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -209,12 +204,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "21 days left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.DAYS, 21)) } @Test fun givenTimeLeftIsEqualTo14Days_whenGettingThTimeLeftFormatted_ThenIsEqualToFourTeenDaysLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 14.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -222,12 +218,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "14 days left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.DAYS, 14)) } @Test fun givenTimeLeftIsEqualTo20Days_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyDaysLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 20.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -235,12 +232,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "20 days left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.DAYS, 20)) } @Test fun givenTimeLeftIsEqualToSevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 7.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -248,12 +246,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 week left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 1)) } @Test fun givenTimeLeftIsEqualToSixDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -261,12 +260,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 week left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 1)) } @Test fun givenTimeLeftIsEqualToSixDaysAnd12Hours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days + 12.hours, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -274,12 +274,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 week left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 1)) } @Test fun givenTimeLeftIsEqualToSixDaysAndOneSecond_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days + 1.seconds, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -287,12 +288,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 week left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.WEEKS, 1)) } @Test fun givenTimeLeftIsEqualToThirteenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToThirteenDays() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 13.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -300,12 +302,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "13 days left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.DAYS, 13)) } @Test fun givenTimeLeftIsEqualToOneDay_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.days, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -313,12 +316,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 day left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.DAYS, 1)) } @Test fun givenTimeLeftIsEqualToTwentyFourHours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 24.hours, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -326,12 +330,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 day left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.DAYS, 1)) } @Test fun givenTimeLeftIsEqualToTwentyThreeHours_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyThreeHourLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -339,12 +344,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "23 hours left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.HOURS, 23)) } @Test fun givenTimeLeftIsEqualToSixtyMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToOneHourLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 60.minutes, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -352,12 +358,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 hour left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.HOURS, 1)) } @Test fun givenTimeLeftIsEqualToOneMinute_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinuteLeft() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -365,12 +372,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 minute left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.MINUTES, 1)) } @Test fun givenTimeLeftIsEqualToOFiftyNineMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToFiftyNineMinutes() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 59.minutes, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -378,12 +386,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "59 minutes left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.MINUTES, 59)) } @Test fun givenTimeLeftIsEqualToSixtySeconds_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinute() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 60.seconds, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -391,12 +400,13 @@ class SelfDeletionTimerTest { ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted - assert(timeLeftLabel == "1 minute left") + assert(timeLeftLabel == arrangement.stringsProvider.quantityString(StringResourceType.MINUTES, 1)) } @Test fun givenTimeLeftIs1DayAnd12Hours_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.days + 12.hours, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -406,17 +416,18 @@ class SelfDeletionTimerTest { with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { advanceTimeBy(updateInterval()) recalculateTimeLeft() - assert(selfDeletionTimer.timeLeftFormatted == "1 day left") + assert(selfDeletionTimer.timeLeftFormatted == arrangement.stringsProvider.quantityString(StringResourceType.DAYS, 1)) advanceTimeBy(updateInterval()) recalculateTimeLeft() - assert(selfDeletionTimer.timeLeftFormatted == "23 hours left") + assert(selfDeletionTimer.timeLeftFormatted == arrangement.stringsProvider.quantityString(StringResourceType.HOURS, 23)) } } @Test fun givenTimeLeftIs23HoursAnd23Minutes_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours + 23.minutes, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -426,13 +437,14 @@ class SelfDeletionTimerTest { with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { advanceTimeBy(updateInterval()) recalculateTimeLeft() - assert(selfDeletionTimer.timeLeftFormatted == "23 hours left") + assert(selfDeletionTimer.timeLeftFormatted == arrangement.stringsProvider.quantityString(StringResourceType.HOURS, 23)) } } @Test fun givenTimeLeftIs1HourAnd12Minutes_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.hours + 12.minutes, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -442,17 +454,18 @@ class SelfDeletionTimerTest { with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { advanceTimeBy(updateInterval()) recalculateTimeLeft() - assert(selfDeletionTimer.timeLeftFormatted == "1 hour left") + assert(selfDeletionTimer.timeLeftFormatted == arrangement.stringsProvider.quantityString(StringResourceType.HOURS, 1)) advanceTimeBy(updateInterval()) recalculateTimeLeft() - assert(selfDeletionTimer.timeLeftFormatted == "59 minutes left") + assert(selfDeletionTimer.timeLeftFormatted == arrangement.stringsProvider.quantityString(StringResourceType.MINUTES, 59)) } } @Test fun givenTimeLeftIs1HourAnd23Seconds_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { - val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + val (arrangement, selfDeletionTimerHelper) = Arrangement(dispatcher).arrange() + val selfDeletionTimer = selfDeletionTimerHelper.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes + 23.seconds, selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted @@ -462,11 +475,22 @@ class SelfDeletionTimerTest { with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { advanceTimeBy(updateInterval()) recalculateTimeLeft() - assert(selfDeletionTimer.timeLeftFormatted == "1 minute left") + assert(selfDeletionTimer.timeLeftFormatted == arrangement.stringsProvider.quantityString(StringResourceType.MINUTES, 1)) advanceTimeBy(updateInterval()) recalculateTimeLeft() - assert(selfDeletionTimer.timeLeftFormatted == "59 seconds left") + assert(selfDeletionTimer.timeLeftFormatted == arrangement.stringsProvider.quantityString(StringResourceType.SECONDS, 59)) } } + + internal class Arrangement(val dispatcher: TestDispatcher) { + + val stringsProvider: StringResourceProvider = object : StringResourceProvider { + override fun quantityString(type: StringResourceType, quantity: Int): String = "${type.name}: $quantity" + } + private val currentTime: CurrentTimeProvider = { Instant.fromEpochMilliseconds(dispatcher.scheduler.currentTime) } + + private val selfDeletionTimerHelper by lazy { SelfDeletionTimerHelper(stringsProvider, currentTime) } + fun arrange() = this to selfDeletionTimerHelper + } } From 22bd1b4d85e68febe90611b1e37ebd7c6ae307bf Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Mon, 5 Feb 2024 16:08:20 +0100 Subject: [PATCH 022/134] fix: remove trim of spaces when searching for messages inside a conversation (WPB-5834) (#2659) --- .../SearchConversationMessagesViewModel.kt | 2 +- ...SearchConversationMessagesViewModelTest.kt | 51 +++++++++++++++++++ kalium | 2 +- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt index 2203663392a..ff851cb99fc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt @@ -96,7 +96,7 @@ class SearchConversationMessagesViewModel @Inject constructor( ) if (textQueryChanged && searchQuery.text.isNotBlank()) { viewModelScope.launch { - mutableSearchQueryFlow.emit(searchQuery.text.trim()) + mutableSearchQueryFlow.emit(searchQuery.text) } } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt index b9d4d8db67a..386898793c0 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt @@ -126,6 +126,57 @@ class SearchConversationMessagesViewModelTest { } } + @Test + fun `given search term with space, when searching for messages, then search results are as expected`() = runTest { + // given + val searchTerm = "no " + val message1 = mockMessageWithText.copy( + messageContent = UIMessageContent.TextMessage( + messageBody = MessageBody( + UIText.DynamicString("not a normal text") + ) + ) + ) + val message2 = mockMessageWithText.copy( + messageContent = UIMessageContent.TextMessage( + messageBody = MessageBody( + UIText.DynamicString("this message contains a no message") + ) + ) + ) + + val messages = listOf( + message1, + message2 + ) + + val (arrangement, viewModel) = SearchConversationMessagesViewModelArrangement() + .withSuccessSearch(PagingData.from(messages)) + .arrange() + + // when + viewModel.searchQueryChanged(TextFieldValue(searchTerm)) + advanceUntilIdle() + + // then + assertEquals( + TextFieldValue(searchTerm), + viewModel.searchConversationMessagesState.searchQuery + ) + coVerify(exactly = 0) { + arrangement.getSearchMessagesForConversation( + searchTerm, + arrangement.conversationId, + any() + ) + } + viewModel.searchConversationMessagesState.searchResult.test { + awaitItem().map { + it shouldBeEqualTo message2 + } + } + } + @Test fun `given search term with empty space at start and at end, when searching for messages, then specific messages are returned`() = runTest { diff --git a/kalium b/kalium index 00a3b741fb9..84d6ceca64a 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 00a3b741fb902cb58ae7a17634992cd43fa5d1d6 +Subproject commit 84d6ceca64a54e165cfb0b52704585339107f16c From 5daa694b3d3a024def46a8abb35fd5e47887f81f Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Tue, 6 Feb 2024 12:33:50 +0100 Subject: [PATCH 023/134] chore(ci): base64 encoding adding a new line after 76 char (#2666) Co-authored-by: Oussama Hassine --- .github/workflows/cherry-pick-rc-to-develop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cherry-pick-rc-to-develop.yml b/.github/workflows/cherry-pick-rc-to-develop.yml index afd9f7b4b1c..1537a2425fb 100644 --- a/.github/workflows/cherry-pick-rc-to-develop.yml +++ b/.github/workflows/cherry-pick-rc-to-develop.yml @@ -93,7 +93,7 @@ jobs: git add ${{ env.SUBMODULE_NAME }} git commit -m "Update submodule ${{ env.SUBMODULE_NAME }} to latest from ${{ env.TARGET_BRANCH }}" # Base64 encode the commit message to avoid issues with newlines and special characters - echo "lastCommitMessageBase64=$(echo "$LAST_COMMIT_MESSAGE" | base64)" >> $GITHUB_ENV + echo "lastCommitMessageBase64=$(echo "$LAST_COMMIT_MESSAGE" | base64 -w 0 )" >> $GITHUB_ENV - name: Get Cherry-pick commit id: get-cherry From 9a2b3534147e1ff13c7b812dac9c528ec0f853f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Wed, 7 Feb 2024 10:46:48 +0100 Subject: [PATCH 024/134] fix: persistent audio state, observe senderId, edit deleted message crash [WPB-4716] (#2661) --- .../com/wire/android/mapper/MessageMapper.kt | 34 +++--- .../home/conversations/ConversationScreen.kt | 17 +-- .../ui/home/conversations/MessageItem.kt | 3 +- .../edit/EditMessageMenuItems.kt | 2 +- .../media/ConversationMediaScreen.kt | 4 +- .../conversations/media/FileAssetsContent.kt | 14 ++- .../messages/ConversationMessagesViewModel.kt | 3 +- .../messages/ConversationMessagesViewState.kt | 4 +- .../model/MessageTypesPreview.kt | 105 +++++++++--------- ...SearchConversationMessagesResultsScreen.kt | 9 +- ...GetAssetMessagesFromConversationUseCase.kt | 15 +-- ...etConversationMessagesFromSearchUseCase.kt | 12 +- .../GetMessagesForConversationUseCase.kt | 13 +-- .../usecase/GetUsersForMessageUseCase.kt | 49 ++++++++ .../wire/android/mapper/MessageMapperTest.kt | 10 +- ...nversationMessagesFromSearchUseCaseTest.kt | 9 +- .../usecase/GetUsersForMessageUseCaseTest.kt | 105 ++++++++++++++++++ kalium | 2 +- 18 files changed, 276 insertions(+), 134 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCase.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCaseTest.kt diff --git a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt index 28436f28bd4..eb79310bca2 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt @@ -55,25 +55,25 @@ class MessageMapper @Inject constructor( ) { fun memberIdList(messages: List): List = messages.flatMap { message -> - listOf(message.senderUserId).plus( - when (message) { - is Message.Regular -> { - when (val failureType = message.deliveryStatus) { - is DeliveryStatus.CompleteDelivery -> listOf() - is DeliveryStatus.PartialDelivery -> - failureType.recipientsFailedDelivery + failureType.recipientsFailedWithNoClients - } + when (message) { + is Message.Regular -> { + when (val failureType = message.deliveryStatus) { + is DeliveryStatus.CompleteDelivery -> listOf() + is DeliveryStatus.PartialDelivery -> + failureType.recipientsFailedDelivery + failureType.recipientsFailedWithNoClients } - is Message.System -> { - when (val content = message.content) { - is MessageContent.MemberChange -> content.members - is MessageContent.LegalHold.ForMembers -> content.members - else -> listOf() - } + } + + is Message.System -> { + when (val content = message.content) { + is MessageContent.MemberChange -> content.members + is MessageContent.LegalHold.ForMembers -> content.members + else -> listOf() } - is Message.Signaling -> listOf() } - ) + + is Message.Signaling -> listOf() + } }.distinct() @Suppress("LongMethod") @@ -170,7 +170,7 @@ class MessageMapper @Inject constructor( when (val status = message.status) { Message.Status.Pending -> MessageFlowStatus.Sending Message.Status.Sent -> MessageFlowStatus.Sent - is Message.Status.Read -> MessageFlowStatus.Read(status.readCount) + is Message.Status.Read -> MessageFlowStatus.Read(status.readCount) Message.Status.Failed -> MessageFlowStatus.Failure.Send.Locally(isMessageEdited) Message.Status.FailedRemotely -> MessageFlowStatus.Failure.Send.Remotely(isMessageEdited, message.conversationId.domain) Message.Status.Delivered -> MessageFlowStatus.Delivered diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index bedb879b5e8..3f1f02df25b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -144,6 +144,7 @@ import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.ConferenceCallingResult import com.wire.kalium.logic.feature.conversation.InteractionAvailability +import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -434,12 +435,12 @@ fun ConversationScreen( ) (messageComposerViewModel.sureAboutMessagingDialogState as? SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold)?.let { - LegalHoldSubjectMessageDialog( - conversationName = conversationInfoViewModel.conversationInfoViewState.conversationName.asString(), - dialogDismissed = messageComposerViewModel::dismissSureAboutSendingMessage, - sendAnywayClicked = messageComposerViewModel::acceptSureAboutSendingMessage, - ) - } + LegalHoldSubjectMessageDialog( + conversationName = conversationInfoViewModel.conversationInfoViewState.conversationName.asString(), + dialogDismissed = messageComposerViewModel::dismissSureAboutSendingMessage, + sendAnywayClicked = messageComposerViewModel::acceptSureAboutSendingMessage, + ) + } groupDetailsScreenResultRecipient.onNavResult { result -> when (result) { @@ -713,7 +714,7 @@ private fun ConversationScreenContent( conversationId: ConversationId, lastUnreadMessageInstant: Instant?, unreadEventCount: Int, - audioMessagesState: Map, + audioMessagesState: PersistentMap, selectedMessageId: String?, messageComposerStateHolder: MessageComposerStateHolder, messages: Flow>, @@ -820,7 +821,7 @@ fun MessageList( lazyPagingMessages: LazyPagingItems, lazyListState: LazyListState, lastUnreadMessageInstant: Instant?, - audioMessagesState: Map, + audioMessagesState: PersistentMap, onUpdateConversationReadDate: (String) -> Unit, onAssetItemClicked: (String) -> Unit, onImageFullScreenMode: (UIMessage.Regular, Boolean) -> Unit, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt index 0d13266fdb7..f8322e08743 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt @@ -87,6 +87,7 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.launchGeoIntent import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.user.UserId +import kotlinx.collections.immutable.PersistentMap // TODO: a definite candidate for a refactor and cleanup @Suppress("ComplexMethod") @@ -98,7 +99,7 @@ fun MessageItem( searchQuery: String = "", showAuthor: Boolean = true, useSmallBottomPadding: Boolean = false, - audioMessagesState: Map, + audioMessagesState: PersistentMap, onLongClicked: (UIMessage.Regular) -> Unit, onAssetMessageClicked: (String) -> Unit, onAudioClick: (String) -> Unit, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/EditMessageMenuItems.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/EditMessageMenuItems.kt index a01c7c4a819..098313239ba 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/EditMessageMenuItems.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/EditMessageMenuItems.kt @@ -137,7 +137,7 @@ fun EditMessageMenuItems( onDeleteClick = onDeleteItemClick, onDetailsClick = onDetailsItemClick, onReactionClick = onReactionItemClick, - onEditClick = if (message.isMyMessage) onEditItemClick else null, + onEditClick = if (message.isMyMessage && !message.isDeleted) onEditItemClick else null, onCopyClick = onCopyItemClick, onReplyClick = onReplyItemClick ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt index 1734890f00f..8e63357c09c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt @@ -65,6 +65,8 @@ import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.id.ConversationId +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.launch @RootNavGraph @@ -120,7 +122,7 @@ private fun Content( state: ConversationAssetMessagesViewState, onNavigationPressed: () -> Unit = {}, onImageFullScreenMode: (conversationId: ConversationId, messageId: String, isSelfAsset: Boolean) -> Unit, - audioMessagesState: Map = emptyMap(), + audioMessagesState: PersistentMap = persistentMapOf(), onAudioItemClicked: (String) -> Unit, onAssetItemClicked: (String) -> Unit ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt index eaf580175f1..d25aa9cd6f0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt @@ -44,12 +44,14 @@ import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.usecase.UIPagingItem import com.wire.android.ui.home.conversationslist.common.FolderHeader import com.wire.android.ui.theme.wireColorScheme +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.Flow @Composable fun FileAssetsContent( groupedAssetMessageList: Flow>, - audioMessagesState: Map = emptyMap(), + audioMessagesState: PersistentMap = persistentMapOf(), onAudioItemClicked: (String) -> Unit, onAssetItemClicked: (String) -> Unit ) { @@ -72,7 +74,7 @@ fun FileAssetsContent( @Composable private fun AssetMessagesListContent( groupedAssetMessageList: LazyPagingItems, - audioMessagesState: Map, + audioMessagesState: PersistentMap, onAudioItemClicked: (String) -> Unit, onAssetItemClicked: (String) -> Unit, ) { @@ -114,19 +116,19 @@ private fun AssetMessagesListContent( message = message, conversationDetailsData = ConversationDetailsData.None, audioMessagesState = audioMessagesState, - onAudioClick = onAudioItemClicked, - onChangeAudioPosition = { _, _ -> }, onLongClicked = { }, onAssetMessageClicked = onAssetItemClicked, + onAudioClick = onAudioItemClicked, + onChangeAudioPosition = { _, _ -> }, onImageMessageClicked = { _, _ -> }, onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, onSelfDeletingMessageRead = { }, + onLinkClick = { }, defaultBackgroundColor = colorsScheme().backgroundVariant, shouldDisplayMessageStatus = false, - shouldDisplayFooter = false, - onLinkClick = { } + shouldDisplayFooter = false ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt index f4be3695369..f0c6b9ad6b7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt @@ -58,6 +58,7 @@ import com.wire.kalium.logic.feature.message.ToggleReactionUseCase import com.wire.kalium.logic.feature.sessionreset.ResetSessionResult import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -119,7 +120,7 @@ class ConversationMessagesViewModel @Inject constructor( viewModelScope.launch { conversationAudioMessagePlayer.observableAudioMessagesState.collect { conversationViewState = conversationViewState.copy( - audioMessagesState = it + audioMessagesState = it.toPersistentMap() ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewState.kt index 068590be362..b98d75c76dd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewState.kt @@ -22,6 +22,8 @@ import androidx.paging.PagingData import com.wire.android.media.audiomessage.AudioState import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.model.UIMessage +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.datetime.Instant @@ -31,7 +33,7 @@ data class ConversationMessagesViewState( val firstUnreadInstant: Instant? = null, val firstuUnreadEventIndex: Int = 0, val downloadedAssetDialogState: DownloadedAssetDialogVisibilityState = DownloadedAssetDialogVisibilityState.Hidden, - val audioMessagesState: Map = emptyMap(), + val audioMessagesState: PersistentMap = persistentMapOf(), val searchedMessageId: String? = null ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypesPreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypesPreview.kt index ccbe8ecd548..99a78c777a0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypesPreview.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypesPreview.kt @@ -41,6 +41,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.user.UserId +import kotlinx.collections.immutable.persistentMapOf private val previewUserId = UserId("value", "domain") @@ -57,7 +58,8 @@ fun PreviewMessage() { ) ) ), - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -67,7 +69,6 @@ fun PreviewMessage() { onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None ) } } @@ -86,7 +87,8 @@ fun PreviewMessageWithReactions() { ), messageFooter = mockFooter ), - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -95,8 +97,7 @@ fun PreviewMessageWithReactions() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = {} ) } } @@ -126,7 +127,8 @@ fun PreviewMessageWithReply() { ) ) ), - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -135,8 +137,7 @@ fun PreviewMessageWithReply() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = {} ) } } @@ -156,7 +157,8 @@ fun PreviewDeletedMessage() { ) ) }, - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -165,8 +167,7 @@ fun PreviewDeletedMessage() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = { }, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = { } ) } } @@ -187,7 +188,8 @@ fun PreviewFailedSendMessage() { messageFooter = mockFooter.copy(reactions = emptyMap(), ownReactions = emptySet()) ) }, - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -196,8 +198,7 @@ fun PreviewFailedSendMessage() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = { }, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = { } ) } } @@ -218,7 +219,8 @@ fun PreviewFailedDecryptionMessage() { messageFooter = mockFooter.copy(reactions = emptyMap(), ownReactions = emptySet()) ) }, - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -227,8 +229,7 @@ fun PreviewFailedDecryptionMessage() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = { }, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = { } ) } } @@ -239,7 +240,8 @@ fun PreviewAssetMessageWithReactions() { WireTheme { MessageItem( message = mockAssetMessage().copy(messageFooter = mockFooter), - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -248,8 +250,7 @@ fun PreviewAssetMessageWithReactions() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = { }, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = { } ) } } @@ -328,7 +329,8 @@ fun PreviewImageMessageUploaded() { WireTheme { MessageItem( message = mockedImageUIMessage(Message.UploadStatus.UPLOADED), - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -337,8 +339,7 @@ fun PreviewImageMessageUploaded() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = { }, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = { } ) } } @@ -349,7 +350,8 @@ fun PreviewImageMessageUploading() { WireTheme { MessageItem( message = mockedImageUIMessage(Message.UploadStatus.UPLOAD_IN_PROGRESS), - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -358,8 +360,7 @@ fun PreviewImageMessageUploading() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = { }, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = { } ) } } @@ -376,7 +377,8 @@ fun PreviewImageMessageFailedUpload() { expirationStatus = ExpirationStatus.NotExpirable ) ), - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -385,8 +387,7 @@ fun PreviewImageMessageFailedUpload() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = { }, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = { } ) } } @@ -398,7 +399,8 @@ fun PreviewMessageWithSystemMessage() { Column { MessageItem( message = mockMessageWithText, - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -407,8 +409,7 @@ fun PreviewMessageWithSystemMessage() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = { }, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = { } ) SystemMessageItem( mockMessageWithKnock.copy( @@ -442,7 +443,8 @@ fun PreviewMessagesWithUnavailableQuotedMessage() { ) ) ), - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -451,8 +453,7 @@ fun PreviewMessagesWithUnavailableQuotedMessage() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = {} ) } } @@ -464,7 +465,8 @@ fun PreviewAggregatedMessagesWithErrorMessage() { Column { MessageItem( message = mockMessageWithText, - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -473,8 +475,7 @@ fun PreviewAggregatedMessagesWithErrorMessage() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = {} ) MessageItem( message = mockMessageWithText.copy( @@ -485,8 +486,9 @@ fun PreviewAggregatedMessagesWithErrorMessage() { ) ) ), + conversationDetailsData = ConversationDetailsData.None, showAuthor = false, - audioMessagesState = emptyMap(), + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -495,8 +497,7 @@ fun PreviewAggregatedMessagesWithErrorMessage() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = {} ) MessageItem( message = mockMessageWithText.copy( @@ -507,8 +508,9 @@ fun PreviewAggregatedMessagesWithErrorMessage() { ) ) ), + conversationDetailsData = ConversationDetailsData.None, showAuthor = false, - audioMessagesState = emptyMap(), + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -518,7 +520,6 @@ fun PreviewAggregatedMessagesWithErrorMessage() { onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None ) } } @@ -530,7 +531,8 @@ fun PreviewMessageWithMarkdownTextAndLinks() { WireTheme { MessageItem( message = mockMessageWithMarkdownTextAndLinks, - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -539,8 +541,7 @@ fun PreviewMessageWithMarkdownTextAndLinks() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = {} ) } } @@ -551,7 +552,8 @@ fun PreviewMessageWithMarkdownListAndImages() { WireTheme { MessageItem( message = mockMessageWithMarkdownListAndImages, - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -560,8 +562,7 @@ fun PreviewMessageWithMarkdownListAndImages() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = {} ) } } @@ -572,7 +573,8 @@ fun PreviewMessageWithMarkdownTablesAndBlocks() { WireTheme { MessageItem( message = mockMessageWithMarkdownTablesAndBlocks, - audioMessagesState = emptyMap(), + conversationDetailsData = ConversationDetailsData.None, + audioMessagesState = persistentMapOf(), onLongClicked = {}, onAssetMessageClicked = {}, onAudioClick = {}, @@ -581,8 +583,7 @@ fun PreviewMessageWithMarkdownTablesAndBlocks() { onOpenProfile = { _ -> }, onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, - onSelfDeletingMessageRead = {}, - conversationDetailsData = ConversationDetailsData.None + onSelfDeletingMessageRead = {} ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt index a1f0e498005..18285fac00c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt @@ -31,6 +31,7 @@ import com.wire.android.ui.home.conversations.mock.mockMessageWithText import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.flowOf @Composable @@ -54,7 +55,7 @@ fun SearchConversationMessagesResultsScreen( message = message, conversationDetailsData = ConversationDetailsData.None, searchQuery = searchQuery, - audioMessagesState = mapOf(), + audioMessagesState = persistentMapOf(), onLongClicked = { }, onAssetMessageClicked = { }, onAudioClick = { }, @@ -64,11 +65,11 @@ fun SearchConversationMessagesResultsScreen( onReactionClicked = { _, _ -> }, onResetSessionClicked = { _, _ -> }, onSelfDeletingMessageRead = { }, + isContentClickable = true, + onMessageClick = onMessageClick, defaultBackgroundColor = colorsScheme().backgroundVariant, shouldDisplayMessageStatus = false, - shouldDisplayFooter = false, - isContentClickable = true, - onMessageClick = onMessageClick + shouldDisplayFooter = false ) } is UIMessage.System -> { } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt index 6858ac48e83..9563a17deed 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt @@ -27,12 +27,9 @@ import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.asset.GetPaginatedFlowOfAssetMessageByConversationIdUseCase -import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @@ -41,7 +38,7 @@ import kotlin.math.max class GetAssetMessagesFromConversationUseCase @Inject constructor( private val getAssetMessages: GetPaginatedFlowOfAssetMessageByConversationIdUseCase, - private val observeMemberDetailsByIds: ObserveUserListByIdUseCase, + private val getUsersForMessage: GetUsersForMessageUseCase, private val messageMapper: MessageMapper, private val dispatchers: DispatcherProvider ) { @@ -69,12 +66,10 @@ class GetAssetMessagesFromConversationUseCase @Inject constructor( ).map { pagingData -> val currentTime = TimeZone.currentSystemDefault() val uiMessagePagingData: PagingData = pagingData.flatMap { messageItem -> - observeMemberDetailsByIds(messageMapper.memberIdList(listOf(messageItem))) - .mapLatest { usersList -> - messageMapper.toUIMessage(usersList, messageItem) - ?.let { listOf(UIPagingItem.Message(it, Instant.parse(messageItem.date))) } - ?: emptyList() - }.first() + val usersForMessage = getUsersForMessage(messageItem) + messageMapper.toUIMessage(usersForMessage, messageItem) + ?.let { listOf(UIPagingItem.Message(it, Instant.parse(messageItem.date))) } + ?: emptyList() }.insertSeparators { before: UIPagingItem.Message?, after: UIPagingItem.Message? -> if (before == null && after != null) { val localDateTime = after.date.toLocalDateTime(currentTime) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt index 258eaa78a6a..cc40c6b6114 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt @@ -24,20 +24,17 @@ import com.wire.android.mapper.MessageMapper import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest import javax.inject.Inject import kotlin.math.max class GetConversationMessagesFromSearchUseCase @Inject constructor( private val getMessagesSearch: GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase, - private val observeMemberDetailsByIds: ObserveUserListByIdUseCase, + private val getUsersForMessage: GetUsersForMessageUseCase, private val messageMapper: MessageMapper, private val dispatchers: DispatcherProvider ) { @@ -67,11 +64,8 @@ class GetConversationMessagesFromSearchUseCase @Inject constructor( startingOffset = max(0, lastReadIndex - PREFETCH_DISTANCE).toLong() ).map { pagingData -> pagingData.flatMap { messageItem -> - observeMemberDetailsByIds(messageMapper.memberIdList(listOf(messageItem))) - .mapLatest { usersList -> - messageMapper.toUIMessage(usersList, messageItem)?.let { listOf(it) } - ?: emptyList() - }.first() + val usersForMessage = getUsersForMessage(messageItem) + messageMapper.toUIMessage(usersForMessage, messageItem)?.let { listOf(it) } ?: emptyList() } }.flowOn(dispatchers.io()) } else { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt index a0976399255..b93a61afbca 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt @@ -25,25 +25,20 @@ import com.wire.android.mapper.MessageMapper import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesByConversationUseCase -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest import java.lang.Integer.max import javax.inject.Inject class GetMessagesForConversationUseCase @Inject constructor( private val getMessages: GetPaginatedFlowOfMessagesByConversationUseCase, - private val observeMemberDetailsByIds: ObserveUserListByIdUseCase, + private val getUsersForMessage: GetUsersForMessageUseCase, private val messageMapper: MessageMapper, private val dispatchers: DispatcherProvider, ) { - @OptIn(ExperimentalCoroutinesApi::class) suspend operator fun invoke(conversationId: ConversationId, lastReadIndex: Int): Flow> { val pagingConfig = PagingConfig( pageSize = PAGE_SIZE, @@ -56,10 +51,8 @@ class GetMessagesForConversationUseCase @Inject constructor( startingOffset = max(0, lastReadIndex - PREFETCH_DISTANCE).toLong() ).map { pagingData -> pagingData.flatMap { messageItem -> - observeMemberDetailsByIds(messageMapper.memberIdList(listOf(messageItem))) - .mapLatest { usersList -> - messageMapper.toUIMessage(usersList, messageItem)?.let { listOf(it) } ?: emptyList() - }.first() + val usersForMessage = getUsersForMessage(messageItem) + messageMapper.toUIMessage(usersForMessage, messageItem)?.let { listOf(it) } ?: emptyList() } }.flowOn(dispatchers.io()) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCase.kt new file mode 100644 index 00000000000..5d7e24b047d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCase.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 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.home.conversations.usecase + +import com.wire.android.mapper.MessageMapper +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.user.User +import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapLatest +import javax.inject.Inject + +class GetUsersForMessageUseCase @Inject constructor( + private val observeMemberDetailsByIds: ObserveUserListByIdUseCase, + private val messageMapper: MessageMapper +) { + + @OptIn(ExperimentalCoroutinesApi::class) + suspend operator fun invoke(message: Message): List { + val listWithSender: List = message.sender?.let { listOf(it) } ?: listOf() + val otherUserIdList = messageMapper.memberIdList(listOf(message)) + + return if (otherUserIdList.isNotEmpty()) { + observeMemberDetailsByIds(otherUserIdList) + .mapLatest { usersList -> + listWithSender.plus(usersList) + }.first() + } else { + listWithSender + } + } +} diff --git a/app/src/test/kotlin/com/wire/android/mapper/MessageMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/MessageMapperTest.kt index d7a2b2e91ba..abfa517dea1 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/MessageMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/MessageMapperTest.kt @@ -60,16 +60,14 @@ class MessageMapperTest { fun givenMessagesList_whenGettingMemberIdList_thenReturnCorrectList() = runTest { // Given val (_, mapper) = Arrangement().arrange() - val clientMessageAuthor = UserId("client-id", "client-domain") - val serverMessageAuthor = UserId("server-id", "server-domain") + val removedUserId = UserId("server-id", "server-domain") val messages = listOf( - TestMessage.TEXT_MESSAGE.copy(senderUserId = clientMessageAuthor), + TestMessage.TEXT_MESSAGE, TestMessage.MEMBER_REMOVED_MESSAGE.copy( - senderUserId = serverMessageAuthor, - content = MessageContent.MemberChange.Removed(listOf(serverMessageAuthor)) + content = MessageContent.MemberChange.Removed(listOf(removedUserId)) ) ) - val expected = listOf(clientMessageAuthor, serverMessageAuthor) + val expected = listOf(removedUserId) // When val list = mapper.memberIdList(messages) // Then diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt index e3c1ea2f0f2..db742c3a64a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt @@ -36,7 +36,6 @@ import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.User import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -109,7 +108,7 @@ class GetConversationMessagesFromSearchUseCaseTest { lateinit var getMessagesSearch: GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase @MockK - lateinit var observeMemberDetailsByIds: ObserveUserListByIdUseCase + lateinit var getUsersForMessages: GetUsersForMessageUseCase @MockK lateinit var messageMapper: MessageMapper @@ -117,7 +116,7 @@ class GetConversationMessagesFromSearchUseCaseTest { private val useCase: GetConversationMessagesFromSearchUseCase by lazy { GetConversationMessagesFromSearchUseCase( getMessagesSearch, - observeMemberDetailsByIds, + getUsersForMessages, messageMapper, dispatchers = TestDispatcherProvider(), ) @@ -152,9 +151,7 @@ class GetConversationMessagesFromSearchUseCaseTest { } suspend fun withMemberDetails() = apply { - coEvery { observeMemberDetailsByIds(any()) } returns flowOf( - listOf(user1, user2) - ) + coEvery { getUsersForMessages(any()) } returns listOf(user1, user2) } fun withMappedMessage(user: User, message: Message.Standalone) = apply { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCaseTest.kt new file mode 100644 index 00000000000..ec93f9da5ed --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCaseTest.kt @@ -0,0 +1,105 @@ +/* + * Wire + * Copyright (C) 2024 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.home.conversations.usecase + +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.framework.TestMessage +import com.wire.android.framework.TestUser +import com.wire.android.mapper.MessageMapper +import com.wire.kalium.logic.data.user.User +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(CoroutineTestExtension::class) +class GetUsersForMessageUseCaseTest { + + @Test + fun givenMessageWithoutAdditionalUserIds_whenInvoke_thenObserveMemberDetailsByIdsIsNotTriggered() = runTest { + val sender = TestUser.OTHER_USER + val userWithoutOtherUsers = TestMessage.TEXT_MESSAGE.copy(sender = sender, senderUserId = sender.id) + + val (arrangement, useCase) = Arrangement() + .withMemberDetails(listOf()) + .withMemberList(listOf()) + .arrange() + + val result = useCase(userWithoutOtherUsers) + + assertTrue(result.first() == sender) + coVerify(exactly = 0) { arrangement.observeMemberDetailsByIds(any()) } + } + + @Test + fun givenMessageWithAdditionalUserIds_whenInvoke_thenObserveMemberDetailsByIdsIsTriggered() = runTest { + val otherUser = TestUser.OTHER_USER + val userWithoutOtherUsers = TestMessage.MEMBER_REMOVED_MESSAGE + val user1 = TestUser.OTHER_USER.copy( + id = UserId("user-id1", "domain") + ) + val user2 = TestUser.OTHER_USER.copy( + id = UserId("user-id2", "domain") + ) + + val (arrangement, useCase) = Arrangement() + .withMemberDetails(listOf(user1, user2)) + .withMemberList(listOf(otherUser.id)) + .arrange() + + val result = useCase(userWithoutOtherUsers) + + assertTrue(result.first().equals(user1)) + coVerify(exactly = 1) { arrangement.observeMemberDetailsByIds(any()) } + } + + private class Arrangement { + + @MockK + lateinit var observeMemberDetailsByIds: ObserveUserListByIdUseCase + + @MockK + lateinit var messageMapper: MessageMapper + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + } + + suspend fun withMemberDetails(userList: List) = apply { + coEvery { observeMemberDetailsByIds(any()) } returns flowOf(userList) + } + + fun withMemberList(userIdList: List) = apply { + every { messageMapper.memberIdList(any()) } returns userIdList + } + + fun arrange() = this to GetUsersForMessageUseCase( + observeMemberDetailsByIds, messageMapper + ) + } +} diff --git a/kalium b/kalium index 84d6ceca64a..9fd46609baf 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 84d6ceca64a54e165cfb0b52704585339107f16c +Subproject commit 9fd46609baf3f4a21580eaba2a6bf467c6c033e8 From fd1b0e2241bf1258f79062c9f032b4d6c6e78fc8 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Wed, 7 Feb 2024 15:43:56 +0100 Subject: [PATCH 025/134] chore: remove jacoco and migrate to kover (ACOL-139) (#2670) --- .github/workflows/gradle-run-unit-tests.yml | 6 +- AR-builder.groovy | 1 - buildSrc/build.gradle.kts | 2 + .../main/kotlin/scripts/quality.gradle.kts | 108 +++++++----------- 4 files changed, 48 insertions(+), 69 deletions(-) diff --git a/.github/workflows/gradle-run-unit-tests.yml b/.github/workflows/gradle-run-unit-tests.yml index 8a954be3b1c..23262c6524a 100644 --- a/.github/workflows/gradle-run-unit-tests.yml +++ b/.github/workflows/gradle-run-unit-tests.yml @@ -64,19 +64,19 @@ jobs: uses: actions/upload-artifact@v4 with: name: report - path: app/build/reports/jacoco + path: app/build/reports/kover - name: Download Test Reports Folder uses: actions/download-artifact@v4 with: name: report - path: app/build/reports/jacoco + path: app/build/reports/kover merge-multiple: true - name: Upload Test Report uses: codecov/codecov-action@v3 with: - files: "app/build/reports/jacoco/jacocoReport/jacocoReport.xml" + files: "app/build/reports/kover/report.xml" - name: Cleanup Gradle Cache # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. diff --git a/AR-builder.groovy b/AR-builder.groovy index 9de771b8ed6..7bf41583ad9 100644 --- a/AR-builder.groovy +++ b/AR-builder.groovy @@ -540,7 +540,6 @@ pipeline { } } - sh './gradlew jacocoReport' wireSend(secret: env.WIRE_BOT_SECRET, message: "**[#${BUILD_NUMBER} Link](${BUILD_URL})** [${SOURCE_BRANCH}] - βœ… SUCCESS πŸŽ‰" + "\nLast 5 commits:\n```text\n$lastCommits\n```") } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index c5f8b454b4b..b6d47f377b0 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -19,6 +19,7 @@ private object Dependencies { const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0" const val detektGradlePlugin = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.0" + const val koverGradlePlugin = "org.jetbrains.kotlinx:kover-gradle-plugin:0.7.5" const val junit = "junit:junit:4.13.2" const val kluent = "org.amshove.kluent:kluent:1.73" const val spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.1.2" @@ -44,6 +45,7 @@ dependencies { implementation("com.android.tools.build:gradle:${klibs.versions.agp.get()}") implementation(Dependencies.kotlinGradlePlugin) implementation(Dependencies.detektGradlePlugin) + implementation(Dependencies.koverGradlePlugin) implementation(Dependencies.spotless) implementation(Dependencies.junit5) diff --git a/buildSrc/src/main/kotlin/scripts/quality.gradle.kts b/buildSrc/src/main/kotlin/scripts/quality.gradle.kts index 1c6415c4486..8de42f5679b 100644 --- a/buildSrc/src/main/kotlin/scripts/quality.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/quality.gradle.kts @@ -23,8 +23,8 @@ import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask plugins { id("com.android.application") apply false - id("jacoco") id("io.gitlab.arturbosch.detekt") + id("org.jetbrains.kotlinx.kover") } dependencies { @@ -83,71 +83,49 @@ tasks.register("staticCodeAnalysis") { dependsOn(detektAll) } -// Jacoco Configuration -val jacocoReport by tasks.registering(JacocoReport::class) { - group = "Quality" - description = "Reports code coverage on tests within the Wire Android codebase" - val buildVariant = "devDebug" // It's not necessary to run unit tests on every variant so we default to "devDebug" - dependsOn("test${buildVariant.capitalize()}UnitTest") - - val outputDir = "$buildDir/jacoco/html" - val classPathBuildVariant = buildVariant - - reports { - xml.required.set(true) - html.required.set(true) - html.outputLocation.set(file(outputDir)) - } - - classDirectories.setFrom( - fileTree(project.buildDir) { - include( - "**/classes/**/main/**", // This probably can be removed - "**/tmp/kotlin-classes/$classPathBuildVariant/**" - ) - exclude( - "**/R.class", - "**/R\$*.class", - "**/BuildConfig.*", - "**/Manifest*.*", - "**/Manifest$*.class", - "**/*Test*.*", - "**/Injector.*", - "android/**/*.*", - "**/*\$Lambda$*.*", - "**/*\$inlined$*.*", - "**/di/*.*", - "**/*Database.*", - "**/*Response.*", - "**/*Application.*", - "**/*Entity.*", - "**/mock/**", - "**/*Screen*", // These are composable classes - "**/*Kt*", // These are "usually" kotlin generated classes - "**/theme/**/*.*", // Ignores jetpack compose theme related code - "**/common/**/*.*", // Ignores jetpack compose common components related code - "**/navigation/**/*.*" // Ignores jetpack navigation related code - ) - } - ) - - sourceDirectories.setFrom( - fileTree(project.projectDir) { - include("src/main/java/**", "src/main/kotlin/**") - } - ) - - executionData.setFrom( - fileTree(project.buildDir) { - include("**/*.exec", "**/*.ec") - } - ) - - doLast { println("Report file: $outputDir/index.html") } -} - tasks.register("testCoverage") { group = "Quality" description = "Reports code coverage on tests within the Wire Android codebase." - dependsOn(jacocoReport) + dependsOn("koverXmlReport") +} + +koverReport { + defaults { + mergeWith("devDebug") + + filters { + excludes { + classes( + "*Fragment", + "*Fragment\$*", + "*Activity", + "*Activity\$*", + "*.databinding.*", + "*.BuildConfig", + "**/R.class", + "**/R\$*.class", + "**/Manifest*.*", + "**/Manifest$*.class", + "**/*Test*.*", + "*NavArgs*", + "*ComposableSingletons*", + "*_HiltModules*", + "*Hilt_*", + ) + packages( + "hilt_aggregated_deps", + "com.wire.android.di", + "dagger.hilt.internal.aggregatedroot.codegen", + "com.wire.android.ui.home.conversations.mock", + ) + annotatedBy( + "*Generated*", + "*HomeNavGraph*", + "*Destination*", + "*Composable*", + "*Preview*", + ) + } + } + } } From a05a3cec2e5b41cbd92dfe45143207b5d3d92bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:22:45 +0100 Subject: [PATCH 026/134] fix: crash when uploading avatar [WPB-5965] (#2673) --- .../avatarpicker/AvatarPickerViewModel.kt | 38 ++++++++++++----- .../image/AvatarPickerViewModelTest.kt | 42 ++++++++++++++++++- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt index 16eab63472e..8bdc8879867 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt @@ -67,6 +67,8 @@ class AvatarPickerViewModel @Inject constructor( var pictureState by mutableStateOf(PictureState.Empty) private set + private var initialPictureLoadingState by mutableStateOf(InitialPictureLoadingState.None) + private val _infoMessage = MutableSharedFlow() val infoMessage = _infoMessage.asSharedFlow() @@ -75,24 +77,27 @@ class AvatarPickerViewModel @Inject constructor( val temporaryAvatarUri: Uri = avatarImageManager.getShareableTempAvatarUri(defaultAvatarPath) - private lateinit var currentAvatarUri: Uri - init { loadInitialAvatarState() } + @Suppress("TooGenericExceptionCaught") fun loadInitialAvatarState() { viewModelScope.launch { + initialPictureLoadingState = InitialPictureLoadingState.Loading try { dataStore.avatarAssetId.first()?.apply { val qualifiedAsset = qualifiedIdMapper.fromStringToQualifiedID(this) val avatarRawPath = (getAvatarAsset(assetKey = qualifiedAsset) as PublicAssetResult.Success).assetPath - currentAvatarUri = avatarImageManager.getWritableAvatarUri(avatarRawPath) - - pictureState = PictureState.Initial(currentAvatarUri) + val currentAvatarUri = avatarImageManager.getWritableAvatarUri(avatarRawPath) + initialPictureLoadingState = InitialPictureLoadingState.Loaded(currentAvatarUri) + if (pictureState is PictureState.Empty) { + pictureState = PictureState.Initial(currentAvatarUri) + } } - } catch (e: ClassCastException) { + } catch (e: Exception) { appLogger.e("There was an error loading the user avatar", e) + initialPictureLoadingState = InitialPictureLoadingState.None } } } @@ -109,7 +114,6 @@ class AvatarPickerViewModel @Inject constructor( val avatarPath = defaultAvatarPath val imageDataSize = imgUri.toByteArray(appContext, dispatchers).size.toLong() - when (val result = uploadUserAvatar(avatarPath, imageDataSize)) { is UploadAvatarResult.Success -> { dataStore.updateUserAvatarAssetId(result.userAssetId.toString()) @@ -120,7 +124,12 @@ class AvatarPickerViewModel @Inject constructor( is NetworkFailure.NoNetworkConnection -> showInfoMessage(InfoMessageType.NoNetworkError) else -> showInfoMessage(InfoMessageType.UploadAvatarError) } - pictureState = PictureState.Initial(currentAvatarUri) + with(initialPictureLoadingState) { + pictureState = when (this) { + is InitialPictureLoadingState.Loaded -> PictureState.Initial(avatarUri) + else -> PictureState.Empty + } + } } } } @@ -130,16 +139,23 @@ class AvatarPickerViewModel @Inject constructor( _infoMessage.emit(type.uiText) } + @Stable + private sealed class InitialPictureLoadingState { + data object None : InitialPictureLoadingState() + data object Loading : InitialPictureLoadingState() + data class Loaded(val avatarUri: Uri) : InitialPictureLoadingState() + } + @Stable sealed class PictureState(open val avatarUri: Uri) { data class Uploading(override val avatarUri: Uri) : PictureState(avatarUri) data class Initial(override val avatarUri: Uri) : PictureState(avatarUri) data class Picked(override val avatarUri: Uri) : PictureState(avatarUri) - object Empty : PictureState("".toUri()) + data object Empty : PictureState("".toUri()) } sealed class InfoMessageType(override val uiText: UIText) : SnackBarMessage { - object UploadAvatarError : InfoMessageType(UIText.StringResource(R.string.error_uploading_user_avatar)) - object NoNetworkError : InfoMessageType(UIText.StringResource(R.string.error_no_network_message)) + data object UploadAvatarError : InfoMessageType(UIText.StringResource(R.string.error_uploading_user_avatar)) + data object NoNetworkError : InfoMessageType(UIText.StringResource(R.string.error_no_network_message)) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/image/AvatarPickerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/image/AvatarPickerViewModelTest.kt index 63b68cd51ce..94af0c5006c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/image/AvatarPickerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/image/AvatarPickerViewModelTest.kt @@ -52,6 +52,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest import okio.buffer import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -112,6 +113,32 @@ class AvatarPickerViewModelTest { } } + @Test + fun `given current avatar download failed, when uploading the asset fails, then set state as Empty`() = runTest { + // Given + val (arrangement, avatarPickerViewModel) = Arrangement() + .withFailedInitialAvatarLoad() + .withErrorUploadResponse() + .arrange() + // When + avatarPickerViewModel.uploadNewPickedAvatar(arrangement.onSuccess) + // Then + assertInstanceOf(AvatarPickerViewModel.PictureState.Empty::class.java, avatarPickerViewModel.pictureState) + } + + @Test + fun `given current avatar download succeeded, when uploading the asset fails, then set state as Initial`() = runTest { + // Given + val (arrangement, avatarPickerViewModel) = Arrangement() + .withSuccessfulInitialAvatarLoad() + .withErrorUploadResponse() + .arrange() + // When + avatarPickerViewModel.uploadNewPickedAvatar(arrangement.onSuccess) + // Then + assertInstanceOf(AvatarPickerViewModel.PictureState.Initial::class.java, avatarPickerViewModel.pictureState) + } + private class Arrangement { val userDataStore = mockk() @@ -146,9 +173,12 @@ class AvatarPickerViewModelTest { private val mockUri = mockk() + init { + MockKAnnotations.init(this, relaxUnitFun = true) + } + fun withSuccessfulInitialAvatarLoad(): Arrangement { val avatarAssetId = "avatar-value@avatar-domain" - MockKAnnotations.init(this, relaxUnitFun = true) mockkStatic(Uri::class) mockkStatic(Uri::resampleImageAndCopyToTempPath) mockkStatic(Uri::toByteArray) @@ -169,6 +199,16 @@ class AvatarPickerViewModelTest { return this } + fun withFailedInitialAvatarLoad(): Arrangement { + val avatarAssetId = "avatar-value@avatar-domain" + coEvery { getAvatarAsset(any()) } returns PublicAssetResult.Failure(Unknown(RuntimeException("some error")), false) + coEvery { avatarImageManager.getShareableTempAvatarUri(any()) } returns mockUri + every { userDataStore.avatarAssetId } returns flow { emit(avatarAssetId) } + every { qualifiedIdMapper.fromStringToQualifiedID(any()) } returns QualifiedID("avatar-value", "avatar-domain") + + return this + } + fun withSuccessfulAvatarUpload(expectedUserAssetId: UserAssetId): Arrangement { coEvery { userDataStore.updateUserAvatarAssetId(any()) } returns Unit coEvery { uploadUserAvatarUseCase(any(), any()) } returns UploadAvatarResult.Success(expectedUserAssetId) From 184a05110c8b3599e1c9c5e007404965478608ca Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Thu, 8 Feb 2024 17:54:14 +0100 Subject: [PATCH 027/134] fix: add fully qualified handle for external results (WPB-6256) (#2676) --- .../kotlin/com/wire/android/mapper/ContactMapper.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt index 4162e24aa68..91f41a6fcc2 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ContactMapper.kt @@ -29,6 +29,7 @@ import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails import com.wire.kalium.logic.data.service.ServiceDetails import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.type.UserType import javax.inject.Inject class ContactMapper @@ -76,7 +77,7 @@ class ContactMapper id = id.value, domain = id.domain, name = name ?: String.EMPTY, - label = handle ?: String.EMPTY, + label = mapUserHandle(user), avatarData = UserAvatarData( asset = previewAssetId?.let { ImageAsset.UserAvatarAsset(wireSessionImageLoader, it) } ), @@ -85,4 +86,14 @@ class ContactMapper ) } } + + /** + * Adds the fully qualified handle to the contact label in case of federated users. + */ + private fun mapUserHandle(user: UserSearchDetails): String { + return when (user.type) { + UserType.FEDERATED -> "${user.handle}@${user.id.domain}" + else -> user.handle ?: String.EMPTY + } + } } From 6b5510508234ca085f3f62b8378faf28115ebe2d Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 9 Feb 2024 07:36:31 +0100 Subject: [PATCH 028/134] chore: update kalium --- .../kotlin/com/wire/android/di/accountScoped/SearchModule.kt | 2 +- kalium | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt index f649bc17bac..fe6dc649efd 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt @@ -43,7 +43,7 @@ class SearchModule { @ViewModelScoped @Provides - fun provideSearchUsersUseCase(searchScope: SearchScope): SearchUsersUseCase = searchScope.searchUsersUseCase + fun provideSearchUsersUseCase(searchScope: SearchScope): SearchUsersUseCase = searchScope.searchUsers @ViewModelScoped @Provides diff --git a/kalium b/kalium index 9fd46609baf..c56ff5b11e5 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 9fd46609baf3f4a21580eaba2a6bf467c6c033e8 +Subproject commit c56ff5b11e5e264ec3d680068162bc75939bcef5 From 8dc59485ea0836152f9595ca26ad50e312bf5a79 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Fri, 9 Feb 2024 08:26:53 +0100 Subject: [PATCH 029/134] feat: support remote search by handle (#2647) --- .../wire/android/di/KaliumConfigsModule.kt | 1 + .../android/di/accountScoped/SearchModule.kt | 5 ++ .../conversations/search/QueryExtension.kt | 8 +-- .../search/SearchUserViewModel.kt | 27 ++++++++ .../search/SearchUserViewModelTest.kt | 64 +++++++++++++++++-- .../kotlin/customization/FeatureConfigs.kt | 4 +- default.json | 3 +- 7 files changed, 99 insertions(+), 13 deletions(-) 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 8d8d0634ece..bf89c7f0c51 100644 --- a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt @@ -64,6 +64,7 @@ class KaliumConfigsModule { wipeOnRootedDevice = BuildConfig.WIPE_ON_ROOTED_DEVICE, isWebSocketEnabledByDefault = isWebsocketEnabledByDefault(context), certPinningConfig = BuildConfig.CERTIFICATE_PINNING_CONFIG, + maxRemoteSearchResultCount = BuildConfig.MAX_REMOTE_SEARCH_RESULT_COUNT ) } } diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt index fe6dc649efd..02f80574ae6 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt @@ -22,6 +22,7 @@ import com.wire.android.di.KaliumCoreLogic import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.search.FederatedSearchParser +import com.wire.kalium.logic.feature.search.SearchByHandleUseCase import com.wire.kalium.logic.feature.search.SearchScope import com.wire.kalium.logic.feature.search.SearchUsersUseCase import dagger.Module @@ -45,6 +46,10 @@ class SearchModule { @Provides fun provideSearchUsersUseCase(searchScope: SearchScope): SearchUsersUseCase = searchScope.searchUsers + @ViewModelScoped + @Provides + fun provideSearchByHandleUseCase(searchScope: SearchScope): SearchByHandleUseCase = searchScope.searchByHandle + @ViewModelScoped @Provides fun provideFederatedSearchParser(searchScope: SearchScope): FederatedSearchParser = searchScope.federatedSearchParser diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/QueryExtension.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/QueryExtension.kt index 1f62a2dda3c..51140974697 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/QueryExtension.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/QueryExtension.kt @@ -17,10 +17,4 @@ */ package com.wire.android.ui.home.conversations.search -fun String.removeQueryPrefix(): String { - return if (startsWith("@")) { - removePrefix("@") - } else { - this - } -} +fun String.removeQueryPrefix(): String = removePrefix("@") diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt index d7169308c8c..d55b187d847 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt @@ -26,7 +26,10 @@ import androidx.lifecycle.viewModelScope import com.wire.android.mapper.ContactMapper import com.wire.android.ui.home.newconversation.model.Contact import com.wire.android.ui.navArgs +import com.wire.kalium.logic.feature.auth.ValidateUserHandleResult +import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase import com.wire.kalium.logic.feature.search.FederatedSearchParser +import com.wire.kalium.logic.feature.search.SearchByHandleUseCase import com.wire.kalium.logic.feature.search.SearchUsersUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList @@ -39,8 +42,10 @@ import javax.inject.Inject @HiltViewModel class SearchUserViewModel @Inject constructor( private val searchUserUseCase: SearchUsersUseCase, + private val searchByHandleUseCase: SearchByHandleUseCase, private val contactMapper: ContactMapper, private val federatedSearchParser: FederatedSearchParser, + private val validateUserHandle: ValidateUserHandleUseCase, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -61,7 +66,29 @@ class SearchUserViewModel @Inject constructor( @VisibleForTesting suspend fun safeSearch(query: String) { val (searchTerm, domain) = federatedSearchParser(query) + val isHandleSearch = validateUserHandle(searchTerm.removeQueryPrefix()) is ValidateUserHandleResult.Valid + if (isHandleSearch) { + searchByHandle(searchTerm, domain) + } else { + searchByName(searchTerm, domain) + } + } + + private suspend fun searchByHandle(searchTerm: String, domain: String?) { + searchByHandleUseCase( + searchTerm, + excludingConversation = addMembersSearchNavArgs?.conversationId, + customDomain = domain + ).also { userSearchEntities -> + state = state.copy( + contactsResult = userSearchEntities.connected.map(contactMapper::fromSearchUserResult).toImmutableList(), + publicResult = userSearchEntities.notConnected.map(contactMapper::fromSearchUserResult).toImmutableList() + ) + } + } + + private suspend fun searchByName(searchTerm: String, domain: String?) { searchUserUseCase( searchTerm, excludingMembersOfConversation = addMembersSearchNavArgs?.conversationId, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt index 845c3a2f721..ce86bcea5e3 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt @@ -31,7 +31,11 @@ import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType +import com.wire.kalium.logic.feature.auth.ValidateUserHandleResult +import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase import com.wire.kalium.logic.feature.search.FederatedSearchParser +import com.wire.kalium.logic.feature.search.SearchByHandleUseCase +import com.wire.kalium.logic.feature.search.SearchUserResult import com.wire.kalium.logic.feature.search.SearchUsersUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -54,7 +58,7 @@ class SearchUserViewModelTest { val (arrangement, viewModel) = Arrangement() .withAddMembersSearchNavArgsThatThrowsException() .withSearchResult( - SearchUsersUseCase.Result( + SearchUserResult( connected = listOf(), notConnected = listOf() ) @@ -65,6 +69,7 @@ class SearchUserViewModelTest { domain = "domain" ) ) + .withIsValidHandleResult(ValidateUserHandleResult.Invalid.TooLong("")) .arrange() viewModel.safeSearch(query) @@ -90,7 +95,7 @@ class SearchUserViewModelTest { val (arrangement, viewModel) = Arrangement() .withAddMembersSearchNavArgs(AddMembersSearchNavArgs(conversationId, true)) .withSearchResult( - SearchUsersUseCase.Result( + SearchUserResult( connected = listOf(), notConnected = listOf() ) @@ -101,6 +106,7 @@ class SearchUserViewModelTest { domain = "domain" ) ) + .withIsValidHandleResult(ValidateUserHandleResult.Invalid.TooLong("")) .arrange() viewModel.safeSearch(query) @@ -122,7 +128,7 @@ class SearchUserViewModelTest { fun `given searchUserUseCase returns a list of connected users, when calling the searchUseCase, then contactsResult is set`() = runTest { - val result = SearchUsersUseCase.Result( + val result = SearchUserResult( connected = listOf( UserSearchDetails( id = UserId("connected", "domain"), @@ -157,6 +163,7 @@ class SearchUserViewModelTest { domain = "domain" ) ) + .withIsValidHandleResult(ValidateUserHandleResult.Invalid.TooLong("")) .arrange() viewModel.safeSearch(query) @@ -177,6 +184,40 @@ class SearchUserViewModelTest { assertEquals(result.notConnected.map(arrangement::fromSearchUserResult), viewModel.state.publicResult) } + @Test + fun `given search term is a valid handle, when searching, then search by handle`() = runTest { + val query = "query" + val (arrangement, viewModel) = Arrangement() + .withAddMembersSearchNavArgsThatThrowsException() + .withSearchByHandleResult( + SearchUserResult( + connected = listOf(), + notConnected = listOf() + ) + ) + .withFederatedSearchParserResult( + FederatedSearchParser.Result( + searchTerm = query, + domain = "domain" + ) + ) + .withIsValidHandleResult(ValidateUserHandleResult.Valid("")) + .arrange() + + viewModel.safeSearch(query) + coVerify(exactly = 1) { + arrangement.searchByHandleUseCase.invoke( + query, + excludingConversation = null, + customDomain = "domain" + ) + } + + coVerify(exactly = 1) { + arrangement.federatedSearchParser(any()) + } + } + private class Arrangement { @MockK @@ -191,6 +232,11 @@ class SearchUserViewModelTest { @MockK lateinit var federatedSearchParser: FederatedSearchParser + @MockK + lateinit var validateUserHandle: ValidateUserHandleUseCase + + @MockK + lateinit var searchByHandleUseCase: SearchByHandleUseCase init { MockKAnnotations.init(this, relaxUnitFun = true) every { contactMapper.fromSearchUserResult(any()) } answers { @@ -232,7 +278,7 @@ class SearchUserViewModelTest { } } - fun withSearchResult(result: SearchUsersUseCase.Result) = apply { + fun withSearchResult(result: SearchUserResult) = apply { coEvery { searchUsersUseCase(any(), any(), any()) } returns result } @@ -240,13 +286,23 @@ class SearchUserViewModelTest { coEvery { federatedSearchParser(any()) } returns result } + fun withIsValidHandleResult(result: ValidateUserHandleResult) = apply { + coEvery { validateUserHandle(any()) } returns result + } + + fun withSearchByHandleResult(result: SearchUserResult) = apply { + coEvery { searchByHandleUseCase(any(), any(), any()) } returns result + } + private lateinit var searchUserViewModel: SearchUserViewModel fun arrange() = apply { searchUserViewModel = SearchUserViewModel( searchUsersUseCase, + searchByHandleUseCase, contactMapper, federatedSearchParser, + validateUserHandle, savedStateHandle ) }.run { diff --git a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt index e61fe3d0402..f46df60fdd6 100644 --- a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt +++ b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt @@ -96,5 +96,7 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) { CERTIFICATE_PINNING_CONFIG("cert_pinning_config", ConfigType.MapOfStringToListOfStrings), // TODO: Add support for default proxy configs - IS_PASSWORD_PROTECTED_GUEST_LINK_ENABLED("is_password_protected_guest_link_enabled", ConfigType.BOOLEAN) + IS_PASSWORD_PROTECTED_GUEST_LINK_ENABLED("is_password_protected_guest_link_enabled", ConfigType.BOOLEAN), + + MAX_REMOTE_SEARCH_RESULT_COUNT("max_remote_search_result_count", ConfigType.INT) } diff --git a/default.json b/default.json index 0243e34c39b..ca879d6f329 100644 --- a/default.json +++ b/default.json @@ -109,5 +109,6 @@ "is_password_protected_guest_link_enabled": false, "url_rss_release_notes": "https://medium.com/feed/wire-news/tagged/android", "team_app_lock": false, - "team_app_lock_timeout": 60 + "team_app_lock_timeout": 60, + "max_remote_search_result_count": 30 } From 6f61248fc3f8971479da0eb77f2efefcdf9fa79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:58:20 +0100 Subject: [PATCH 030/134] fix: text highlight colors [WPB-5940] (#2680) --- .../conversations/search/HighLightName.kt | 5 ++- .../search/HighLightSubtTitle.kt | 3 +- .../android/ui/markdown/MarkdownComposer.kt | 3 +- .../wire/android/ui/theme/WireColorPalette.kt | 40 +++++++++---------- .../wire/android/ui/theme/WireColorScheme.kt | 22 +++++----- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightName.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightName.kt index 3602912f963..48dc5706076 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightName.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightName.kt @@ -65,7 +65,10 @@ fun HighlightName( .forEach { highLightIndex -> if (highLightIndex.endIndex <= this.length) { addStyle( - style = SpanStyle(background = MaterialTheme.wireColorScheme.highLight.copy(alpha = 0.5f)), + style = SpanStyle( + background = MaterialTheme.wireColorScheme.highlight, + color = MaterialTheme.wireColorScheme.onHighlight, + ), start = highLightIndex.startIndex, end = highLightIndex.endIndex ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightSubtTitle.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightSubtTitle.kt index 35cb6c0e54d..21edf700ffd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightSubtTitle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/HighLightSubtTitle.kt @@ -67,7 +67,8 @@ fun HighlightSubtitle( if (highLightIndex.endIndex <= this.length) { addStyle( style = SpanStyle( - background = MaterialTheme.wireColorScheme.highLight.copy(alpha = 0.5f), + background = MaterialTheme.wireColorScheme.highlight, + color = MaterialTheme.wireColorScheme.onHighlight, ), start = highLightIndex.startIndex + suffix.length, end = highLightIndex.endIndex + suffix.length diff --git a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt index ee5dd8aa6b8..c025fd247e5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownComposer.kt @@ -306,7 +306,8 @@ fun appendLinksAndMentions( if (highLightIndex.endIndex <= length) { addStyle( style = SpanStyle( - background = nodeData.colorScheme.highLight.copy(alpha = 0.5f), + background = nodeData.colorScheme.highlight, + color = nodeData.colorScheme.onHighlight, fontFamily = nodeData.typography.body02.fontFamily, fontWeight = FontWeight.Bold ), diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt index e34148bb19c..4fb5a1af31a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt +++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt @@ -134,25 +134,25 @@ object WireColorPalette { val LightRed900 = Color(0xFF3A0006) @Stable - val LightYellow50 = Color(0xFFF3F0ED) + val LightAmber50 = Color(0xFFF3F0ED) @Stable - val LightYellow100 = Color(0xFFE5E0DA) + val LightAmber100 = Color(0xFFE5E0DA) @Stable - val LightYellow200 = Color(0xFFCCC1B5) + val LightAmber200 = Color(0xFFCCC1B5) @Stable - val LightYellow300 = Color(0xFFB2A38F) + val LightAmber300 = Color(0xFFB2A38F) @Stable - val LightYellow400 = Color(0xFF99846A) + val LightAmber400 = Color(0xFF99846A) @Stable - val LightYellow500 = Color(0xFF7F6545) + val LightAmber500 = Color(0xFF7F6545) @Stable - val LightYellow600 = Color(0xFF665137) + val LightAmber600 = Color(0xFF665137) @Stable - val LightYellow700 = Color(0xFF4C3D29) + val LightAmber700 = Color(0xFF4C3D29) @Stable - val LightYellow800 = Color(0xFF4C3D29) + val LightAmber800 = Color(0xFF4C3D29) @Stable - val LightYellow900 = Color(0xFF261E15) + val LightAmber900 = Color(0xFF261E15) @Stable val DarkBlue50 = Color(0xFFEEF7FF) @@ -261,25 +261,25 @@ object WireColorPalette { val DarkRed900 = Color(0xFF4D2422) @Stable - val DarkYellow50 = Color(0xFFFFFBEA) + val DarkAmber50 = Color(0xFFFFFBEA) @Stable - val DarkYellow100 = Color(0xFFFFF6D4) + val DarkAmber100 = Color(0xFFFFF6D4) @Stable - val DarkYellow200 = Color(0xFFFFEEA8) + val DarkAmber200 = Color(0xFFFFEEA8) @Stable - val DarkYellow300 = Color(0xFFFFE57D) + val DarkAmber300 = Color(0xFFFFE57D) @Stable - val DarkYellow400 = Color(0xFFFFDD51) + val DarkAmber400 = Color(0xFFFFDD51) @Stable - val DarkYellow500 = Color(0xFFFFD426) + val DarkAmber500 = Color(0xFFFFD426) @Stable - val DarkYellow600 = Color(0xFFCCAA1E) + val DarkAmber600 = Color(0xFFCCAA1E) @Stable - val DarkYellow700 = Color(0xFF997F17) + val DarkAmber700 = Color(0xFF997F17) @Stable - val DarkYellow800 = Color(0xFF66550F) + val DarkAmber800 = Color(0xFF66550F) @Stable - val DarkYellow900 = Color(0xFF4D400B) + val DarkAmber900 = Color(0xFF4D400B) @Stable val Gray10 = Color(0xFFFAFAFA) diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt index 67293aee148..8d3499e1fd8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt +++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt @@ -66,7 +66,7 @@ data class WireColorScheme( val scrim: Color, val labelText: Color, val badge: Color, val onBadge: Color, - val highLight: Color, + val highlight: Color, val onHighlight: Color, val uncheckedColor: Color, val disabledCheckedColor: Color, val disabledIndeterminateColor: Color, @@ -139,7 +139,7 @@ private val LightWireColorScheme = WireColorScheme( primaryVariant = WireColorPalette.LightBlue50, onPrimaryVariant = WireColorPalette.LightBlue500, error = WireColorPalette.LightRed500, onError = Color.White, errorOutline = WireColorPalette.LightRed200, - warning = WireColorPalette.LightYellow500, onWarning = Color.White, + warning = WireColorPalette.LightAmber500, onWarning = Color.White, positive = WireColorPalette.LightGreen500, onPositive = Color.White, background = WireColorPalette.Gray20, onBackground = Color.Black, backgroundVariant = WireColorPalette.Gray10, onBackgroundVariant = Color.Black, @@ -172,7 +172,7 @@ private val LightWireColorScheme = WireColorScheme( scrim = WireColorPalette.BlackAlpha55, labelText = WireColorPalette.Gray80, badge = WireColorPalette.Gray90, onBadge = Color.White, - highLight = WireColorPalette.DarkYellow300, + highlight = WireColorPalette.DarkAmber200, onHighlight = Color.Black, uncheckedColor = WireColorPalette.Gray80, disabledCheckedColor = WireColorPalette.Gray80, disabledIndeterminateColor = WireColorPalette.Gray80, @@ -197,9 +197,9 @@ private val LightWireColorScheme = WireColorScheme( WireColorPalette.LightPurple500, WireColorPalette.LightPurple700, // Yellow - Amber - WireColorPalette.LightYellow300, - WireColorPalette.LightYellow500, - WireColorPalette.LightYellow700, + WireColorPalette.LightAmber300, + WireColorPalette.LightAmber500, + WireColorPalette.LightAmber700, // Petrol WireColorPalette.LightPetrol300, WireColorPalette.LightPetrol500, @@ -248,7 +248,7 @@ private val DarkWireColorScheme = WireColorScheme( primaryVariant = WireColorPalette.DarkBlue800, onPrimaryVariant = WireColorPalette.DarkBlue300, error = WireColorPalette.DarkRed500, onError = Color.Black, errorOutline = WireColorPalette.DarkRed800, - warning = WireColorPalette.DarkYellow500, onWarning = Color.Black, + warning = WireColorPalette.DarkAmber500, onWarning = Color.Black, positive = WireColorPalette.DarkGreen500, onPositive = Color.Black, background = WireColorPalette.Gray100, onBackground = Color.White, backgroundVariant = WireColorPalette.Gray95, onBackgroundVariant = Color.White, @@ -281,7 +281,7 @@ private val DarkWireColorScheme = WireColorScheme( scrim = WireColorPalette.BlackAlpha55, labelText = WireColorPalette.Gray30, badge = WireColorPalette.Gray10, onBadge = Color.Black, - highLight = WireColorPalette.DarkYellow300, + highlight = WireColorPalette.DarkAmber300, onHighlight = Color.Black, uncheckedColor = WireColorPalette.Gray60, disabledCheckedColor = WireColorPalette.Gray80, disabledIndeterminateColor = WireColorPalette.Gray80, @@ -306,9 +306,9 @@ private val DarkWireColorScheme = WireColorScheme( WireColorPalette.DarkPurple500, WireColorPalette.DarkPurple700, // Yellow - Amber - WireColorPalette.DarkYellow300, - WireColorPalette.DarkYellow500, - WireColorPalette.DarkYellow700, + WireColorPalette.DarkAmber300, + WireColorPalette.DarkAmber500, + WireColorPalette.DarkAmber700, // Petrol WireColorPalette.DarkPetrol300, WireColorPalette.DarkPetrol500, From 876f5da21a7197bc6bbd7cb038a26a6661111e4f Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Mon, 12 Feb 2024 14:59:23 +0100 Subject: [PATCH 031/134] fix(e2ei): remove E2EI shield from remove device screen (WPB-6519) (#2685) --- .../ui/authentication/devices/DeviceItem.kt | 15 ++++++++---- .../ui/settings/devices/SelfDevicesScreen.kt | 23 +++++++++++-------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt index 112d9300e18..c171c236fa7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt @@ -73,6 +73,7 @@ fun DeviceItem( device: Device, placeholder: Boolean, shouldShowVerifyLabel: Boolean, + isCurrentClient: Boolean = false, background: Color? = null, icon: @Composable (() -> Unit), isWholeItemClickable: Boolean = false, @@ -85,7 +86,8 @@ fun DeviceItem( icon = icon, onClickAction = onClickAction, isWholeItemClickable = isWholeItemClickable, - shouldShowVerifyLabel = shouldShowVerifyLabel + shouldShowVerifyLabel = shouldShowVerifyLabel, + isCurrentClient = isCurrentClient ) } @@ -97,7 +99,8 @@ private fun DeviceItemContent( icon: @Composable (() -> Unit), onClickAction: ((Device) -> Unit)?, isWholeItemClickable: Boolean, - shouldShowVerifyLabel: Boolean + shouldShowVerifyLabel: Boolean, + isCurrentClient: Boolean ) { Row( verticalAlignment = Alignment.Top, @@ -123,7 +126,7 @@ private fun DeviceItemContent( modifier = Modifier .padding(start = MaterialTheme.wireDimensions.removeDeviceItemPadding) .weight(1f) - ) { DeviceItemTexts(device, placeholder, shouldShowVerifyLabel) } + ) { DeviceItemTexts(device, placeholder, shouldShowVerifyLabel, isCurrentClient) } } if (!placeholder) { if (onClickAction != null && !isWholeItemClickable) { @@ -154,6 +157,7 @@ private fun DeviceItemTexts( device: Device, placeholder: Boolean, shouldShowVerifyLabel: Boolean, + isCurrentClient: Boolean = false, isDebug: Boolean = BuildConfig.DEBUG ) { val displayZombieIndicator = remember { @@ -173,10 +177,10 @@ private fun DeviceItemTexts( .wrapContentWidth() .shimmerPlaceholder(visible = placeholder) ) - MLSVerificationIcon(device.e2eiCertificateStatus) if (shouldShowVerifyLabel) { + MLSVerificationIcon(device.e2eiCertificateStatus) Spacer(modifier = Modifier.width(MaterialTheme.wireDimensions.spacing8x)) - if (device.isVerifiedProteus) ProteusVerifiedIcon( + if (device.isVerifiedProteus && !isCurrentClient) ProteusVerifiedIcon( Modifier .wrapContentWidth() .align(Alignment.CenterVertically)) @@ -251,6 +255,7 @@ fun PreviewDeviceItemWithActionIcon() { device = Device(name = UIText.DynamicString("name"), isVerifiedProteus = true), placeholder = false, shouldShowVerifyLabel = true, + isCurrentClient = true, background = null, { Icon(painter = painterResource(id = R.drawable.ic_remove), contentDescription = "") } ) {} diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt index 5ebe3d29750..b451b7eadc3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt @@ -90,17 +90,20 @@ fun SelfDevicesScreenContent( false -> { state.currentDevice?.let { currentDevice -> folderDeviceItems( - context.getString(R.string.current_device_label), - listOf(currentDevice), - false, - onDeviceClick + header = context.getString(R.string.current_device_label), + items = listOf(currentDevice), + shouldShowVerifyLabel = true, + isCurrentClient = true, + onDeviceClick = onDeviceClick, + ) } folderDeviceItems( - context.getString(R.string.other_devices_label), - state.deviceList, - true, - onDeviceClick + header = context.getString(R.string.other_devices_label), + items = state.deviceList, + shouldShowVerifyLabel = true, + isCurrentClient = false, + onDeviceClick = onDeviceClick ) } } @@ -113,6 +116,7 @@ private fun LazyListScope.folderDeviceItems( header: String, items: List, shouldShowVerifyLabel: Boolean, + isCurrentClient: Boolean, onDeviceClick: (Device) -> Unit = {} ) { folderWithElements( @@ -132,7 +136,8 @@ private fun LazyListScope.folderDeviceItems( onClickAction = onDeviceClick, icon = Icons.Filled.ChevronRight.Icon(), isWholeItemClickable = true, - shouldShowVerifyLabel = shouldShowVerifyLabel + shouldShowVerifyLabel = shouldShowVerifyLabel, + isCurrentClient = isCurrentClient ) } } From 7914307be00ffa5a3f2534bd7e2e55ea60fdcedb Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 13 Feb 2024 10:44:28 +0200 Subject: [PATCH 032/134] fix: NotificationChannelGroup crash (WPB-6233) (#2687) --- .../wire/android/GlobalObserversManager.kt | 1 - .../NotificationChannelsManager.kt | 33 ++++++++++++------- .../forgot/ForgotLockScreenViewModel.kt | 1 - .../self/SelfUserProfileViewModel.kt | 1 - .../forgot/ForgotLockScreenViewModelTest.kt | 1 - 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt index 944b629225a..641b765fb2c 100644 --- a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt +++ b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt @@ -112,7 +112,6 @@ class GlobalObserversManager @Inject constructor( val callback: LogoutCallback = object : LogoutCallback { override suspend fun invoke(userId: UserId, reason: LogoutReason) { notificationManager.stopObservingOnLogout(userId) - notificationChannelsManager.deleteChannelGroup(userId) if (reason != LogoutReason.SELF_SOFT_LOGOUT) { userDataStoreProvider.getOrCreate(userId).clear() } diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt index 12b84a02203..7451d1bdc78 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt @@ -18,7 +18,6 @@ package com.wire.android.notification -import android.app.NotificationChannelGroup import android.content.ContentResolver import android.content.Context import android.media.AudioAttributes @@ -26,9 +25,9 @@ import android.net.Uri import android.os.Build import androidx.annotation.RequiresApi import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationChannelGroupCompat import androidx.core.app.NotificationManagerCompat import com.wire.android.appLogger -import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.UserId import javax.inject.Inject @@ -53,7 +52,10 @@ class NotificationChannelsManager @Inject constructor( ) } - // Creating user-specific NotificationChannels for each user, they will be grouped by User in App Settings. + /** + * Creating user-specific NotificationChannels for each user, they will be grouped by User in App Settings. + * And removing the ChannelGroups (with all the channels in it) that are not belongs to any user in a list (user logged out e.x.) + */ fun createUserNotificationChannels(allUsers: List) { appLogger.i("$TAG: creating all the notification channels for ${allUsers.size} users") if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return @@ -68,15 +70,23 @@ class NotificationChannelsManager @Inject constructor( // OngoingCall is not user specific channel, but common for all users. createOngoingNotificationChannel() + + deleteRedundantChannelGroups(allUsers) } /** - * Deletes NotificationChanelGroup (and all NotificationChannels that belongs to it) for a specific User. - * Use it on logout. + * Deletes NotificationChanelGroup (and all NotificationChannels that belongs to it) + * for the users that are not in [activeUsers] list. */ - fun deleteChannelGroup(userId: UserId) { - appLogger.i("$TAG: deleting notification channels for ${userId.toString().obfuscateId()} user") - notificationManagerCompat.deleteNotificationChannelGroup(NotificationConstants.getChanelGroupIdForUser(userId)) + private fun deleteRedundantChannelGroups(activeUsers: List) { + val groupsToKeep = activeUsers.map { NotificationConstants.getChanelGroupIdForUser(it.id) } + + notificationManagerCompat.notificationChannelGroups + .filter { group -> groupsToKeep.none { it == group.id } } + .forEach { group -> + appLogger.i("$TAG: deleting notification channels for ${group.name} group") + notificationManagerCompat.deleteNotificationChannelGroup(group.id) + } } /** @@ -85,10 +95,9 @@ class NotificationChannelsManager @Inject constructor( @RequiresApi(Build.VERSION_CODES.O) private fun createNotificationChannelGroup(userId: UserId, userName: String): String { val chanelGroupId = NotificationConstants.getChanelGroupIdForUser(userId) - val channelGroup = NotificationChannelGroup( - chanelGroupId, - getChanelGroupNameForUser(userName) - ) + val channelGroup = NotificationChannelGroupCompat.Builder(chanelGroupId) + .setName(getChanelGroupNameForUser(userName)) + .build() notificationManagerCompat.createNotificationChannelGroup(channelGroup) return chanelGroupId } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt index 3e3262e0f51..60d2e6e500d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt @@ -201,7 +201,6 @@ class ForgotLockScreenViewModel @Inject constructor( // TODO: we should have a dedicated manager to perform these required actions in AR after every LogoutUseCase call private suspend fun hardLogoutAccount(userId: UserId) { notificationManager.stopObservingOnLogout(userId) - notificationChannelsManager.deleteChannelGroup(userId) coreLogic.getSessionScope(userId).logout(reason = LogoutReason.SELF_HARD_LOGOUT, waitUntilCompletes = true) userDataStoreProvider.getOrCreate(userId).clear() } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 049607eaa6f..d8320635526 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -218,7 +218,6 @@ class SelfUserProfileViewModel @Inject constructor( } notificationManager.stopObservingOnLogout(selfUserId) - notificationChannelsManager.deleteChannelGroup(selfUserId) accountSwitch(SwitchAccountParam.TryToSwitchToNextAccount).also { if (it == SwitchAccountResult.NoOtherAccountToSwitch) { globalDataStore.clearAppLockPasscode() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelTest.kt index 3171b629f9f..582005956f1 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelTest.kt @@ -159,7 +159,6 @@ class ForgotLockScreenViewModelTest { val logoutActionsCalledExactly = if (userLogoutActionsCalled) 1 else 0 coVerify(exactly = logoutActionsCalledExactly) { logoutUseCase(any(), any()) } coVerify(exactly = logoutActionsCalledExactly) { notificationManager.stopObservingOnLogout(any()) } - coVerify(exactly = logoutActionsCalledExactly) { notificationChannelsManager.deleteChannelGroup(any()) } coVerify(exactly = logoutActionsCalledExactly) { userDataStore.clear() } } private fun testLoggingOut( From ceb0052f198d826b03dae10e83f02db757bb2afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:07:30 +0100 Subject: [PATCH 033/134] fix: serverConfig and notification crashes right after user becomes invalid [WPB-6552] [WPB-6233] (#2684) --- .../wire/android/GlobalObserversManager.kt | 24 ++++++--- .../feature/ObserveAppLockConfigUseCase.kt | 12 ++--- .../notification/WireNotificationManager.kt | 21 ++++++-- .../ui/calling/ProximitySensorManager.kt | 11 ++-- .../android/GlobalObserversManagerTest.kt | 17 ++++++ .../ObserveAppLockConfigUseCaseTest.kt | 26 +++++++++- .../WireNotificationManagerTest.kt | 52 +++++++++++++++++++ 7 files changed, 143 insertions(+), 20 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt index 641b765fb2c..71744a199f0 100644 --- a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt +++ b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt @@ -91,14 +91,26 @@ class GlobalObserversManager @Inject constructor( } coreLogic.getGlobalScope().observeValidAccounts() + .combine(persistentStatusesFlow) { list, persistentStatuses -> + val persistentStatusesMap = persistentStatuses.associate { it.userId to it.isPersistentWebSocketEnabled } + /* + Intersect both lists as they can be slightly out of sync because both lists can be updated at slightly different times. + When user is logged out, at this time one of them can still contain this invalid user - make sure that it's ignored. + When user is logged in, at this time one of them can still not contain this new user - ignore for now, + the user will be handled correctly in the next iteration when the second list becomes updated as well. + */ + list.map { (selfUser, _) -> selfUser } + .filter { persistentStatusesMap.containsKey(it.id) } + .map { it to persistentStatusesMap.getValue(it.id) } + } .distinctUntilChanged() - .combine(persistentStatusesFlow, ::Pair) - .collect { (list, persistentStatuses) -> - notificationChannelsManager.createUserNotificationChannels(list.map { it.first }) + .collectLatest { + // create notification channels for all valid users + notificationChannelsManager.createUserNotificationChannels(it.map { it.first }) - list.map { it.first.id } - // do not observe notifications for users with PersistentWebSocketEnabled, it will be done in PersistentWebSocketService - .filter { userId -> persistentStatuses.none { it.userId == userId && it.isPersistentWebSocketEnabled } } + // do not observe notifications for users with PersistentWebSocketEnabled, it will be done in PersistentWebSocketService + it.filter { (_, isPersistentWebSocketEnabled) -> !isPersistentWebSocketEnabled } + .map { (selfUser, _) -> selfUser.id } .run { notificationManager.observeNotificationsAndCallsWhileRunning(this, scope) } diff --git a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt index d946074d9b1..19896940ebb 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt @@ -37,12 +37,8 @@ class ObserveAppLockConfigUseCase @Inject constructor( ) { operator fun invoke(): Flow = channelFlow { coreLogic.getGlobalScope().session.currentSessionFlow().collectLatest { sessionResult -> - when (sessionResult) { - is CurrentSessionResult.Failure -> { - send(AppLockConfig.Disabled(DEFAULT_APP_LOCK_TIMEOUT)) - } - - is CurrentSessionResult.Success -> { + when { + sessionResult is CurrentSessionResult.Success && sessionResult.accountInfo.isValid() -> { val userId = sessionResult.accountInfo.userId val appLockTeamFeatureConfigFlow = coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver @@ -67,6 +63,10 @@ class ObserveAppLockConfigUseCase @Inject constructor( send(it) } } + + else -> { + send(AppLockConfig.Disabled(DEFAULT_APP_LOCK_TIMEOUT)) + } } } } diff --git a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt index aaabb885f29..56588ef4118 100644 --- a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt @@ -18,6 +18,7 @@ package com.wire.android.notification +import androidx.annotation.VisibleForTesting import com.wire.android.R import com.wire.android.appLogger import com.wire.android.di.KaliumCoreLogic @@ -36,6 +37,7 @@ import com.wire.kalium.logic.data.notification.LocalNotificationMessage import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.message.MarkMessagesAsNotifiedUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.DoesValidSessionExistResult import com.wire.kalium.logic.feature.session.GetAllSessionsResult import com.wire.kalium.logic.feature.user.E2EIRequiredResult import kotlinx.coroutines.CoroutineScope @@ -244,9 +246,8 @@ class WireNotificationManager @Inject constructor( return } - // start observing notifications only for new users - userIds - .filter { observingJobs.userJobs[it]?.isAllActive() != true } + // start observing notifications only for new users with valid session and without active jobs + newUsersWithValidSessionAndWithoutActiveJobs(userIds) { observingJobs.userJobs[it]?.isAllActive() == true } .forEach { userId -> val jobs = UserObservingJobs( currentScreenJob = scope.launch(dispatcherProvider.default()) { @@ -271,6 +272,20 @@ class WireNotificationManager @Inject constructor( } } + @VisibleForTesting + internal suspend fun newUsersWithValidSessionAndWithoutActiveJobs( + userIds: List, + hasActiveJobs: (UserId) -> Boolean + ): List = userIds + .filter { !hasActiveJobs(it) } + .filter { + // double check if the valid session for the given user still exists + when (val result = coreLogic.getGlobalScope().doesValidSessionExist(it)) { + is DoesValidSessionExistResult.Success -> result.doesValidSessionExist + else -> false + } + } + private fun stopObservingForUser(userId: UserId, observingJobs: ObservingJobs) { messagesNotificationManager.hideAllNotificationsForUser(userId) observingJobs.userJobs[userId]?.cancelAll() diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ProximitySensorManager.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ProximitySensorManager.kt index d72a972fb9b..b61eaccabf7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ProximitySensorManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ProximitySensorManager.kt @@ -71,8 +71,9 @@ class ProximitySensorManager @Inject constructor( override fun onSensorChanged(event: SensorEvent) { appCoroutineScope.launch { coreLogic.get().globalScope { - when (val currentSession = currentSession.get().invoke()) { - is CurrentSessionResult.Success -> { + val currentSession = currentSession.get().invoke() + when { + currentSession is CurrentSessionResult.Success && currentSession.accountInfo.isValid() -> { val userId = currentSession.accountInfo.userId val isCallRunning = coreLogic.get().getSessionScope(userId).calls.isCallRunning() val distance = event.values.first() @@ -92,8 +93,10 @@ class ProximitySensorManager @Inject constructor( } } - else -> { - // NO SESSION - Nothing to do + else -> { // NO SESSION - just release in case it's still held + if (wakeLock.isHeld) { + wakeLock.release() + } } } } diff --git a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt index ecb71639f69..e9579c3534b 100644 --- a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt @@ -146,6 +146,23 @@ class GlobalObserversManagerTest { coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } } + @Test + fun `given validAccounts and persistentStatuses are out of sync, when setting up notifications, then ignore invalid users`() { + val validAccountsList = listOf(TestUser.SELF_USER) + val persistentStatusesList = listOf( + PersistentWebSocketStatus(TestUser.SELF_USER.id, false), + PersistentWebSocketStatus(TestUser.USER_ID.copy(value = "something else"), true) + ) + val (arrangement, manager) = Arrangement() + .withValidAccounts(validAccountsList.map { it to null }) + .withPersistentWebSocketConnectionStatuses(persistentStatusesList) + .arrange() + manager.observe() + coVerify(exactly = 1) { + arrangement.notificationChannelsManager.createUserNotificationChannels(listOf(TestUser.SELF_USER)) + } + } + private class Arrangement { @MockK diff --git a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt index e0caa52dfdf..c566f67c9f7 100644 --- a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt @@ -22,6 +22,7 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.AppLockTeamConfig import com.wire.kalium.logic.data.auth.AccountInfo +import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserver @@ -54,6 +55,22 @@ class ObserveAppLockConfigUseCaseTest { } } + @Test + fun givenInvalidSession_whenObservingAppLock_thenSendDisabledStatus() = runTest { + val (_, useCase) = Arrangement() + .withInvalidSession() + .arrange() + + val result = useCase.invoke() + + result.test { + val appLockStatus = awaitItem() + + assertEquals(AppLockConfig.Disabled(timeout), appLockStatus) + awaitComplete() + } + } + @Test fun givenValidSessionAndAppLockedByTeam_whenObservingAppLock_thenSendEnforcedByTeamStatus() = runTest { @@ -142,6 +159,11 @@ class ObserveAppLockConfigUseCaseTest { flowOf(CurrentSessionResult.Failure.SessionNotFound) } + fun withInvalidSession() = apply { + coEvery { coreLogic.getGlobalScope().session.currentSessionFlow() } returns + flowOf(CurrentSessionResult.Success(accountInfoInvalid)) + } + fun withValidSession() = apply { coEvery { coreLogic.getGlobalScope().session.currentSessionFlow() } returns flowOf(CurrentSessionResult.Success(accountInfo)) @@ -177,7 +199,9 @@ class ObserveAppLockConfigUseCaseTest { } companion object { - private val accountInfo = AccountInfo.Valid(UserId("userId", "domain")) + private val userId = UserId("userId", "domain") + private val accountInfo = AccountInfo.Valid(userId) + private val accountInfoInvalid = AccountInfo.Invalid(userId, LogoutReason.DELETED_ACCOUNT) private val timeout = 60.seconds } } diff --git a/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt b/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt index 8c837d07f5f..5d8c0f2d868 100644 --- a/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt @@ -55,6 +55,7 @@ import com.wire.kalium.logic.feature.message.MessageScope import com.wire.kalium.logic.feature.message.Result import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.DoesValidSessionExistResult import com.wire.kalium.logic.feature.session.GetAllSessionsResult import com.wire.kalium.logic.feature.session.GetSessionsUseCase import com.wire.kalium.logic.feature.user.E2EIRequiredResult @@ -83,6 +84,7 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant +import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.minutes @@ -696,6 +698,51 @@ class WireNotificationManagerTest { } } + @Test + fun givenSessionExistsForTheUserAndNoActiveJobs_whenGettingUsersToObserve_thenReturnThatUser() = + runTest(dispatcherProvider.main()) { + // given + val userId = provideUserId() + val (_, manager) = Arrangement() + .withDoesValidSessionExistResult(userId, DoesValidSessionExistResult.Success(true)) + .arrange() + val hasActiveJobs: (UserId) -> Boolean = { false } + // when + val result = manager.newUsersWithValidSessionAndWithoutActiveJobs(listOf(userId), hasActiveJobs) + // then + assertEquals(listOf(userId), result) + } + + @Test + fun givenSessionExistsForTheUserButWithActiveJobs_whenGettingUsersToObserve_thenDoNotReturnThatUser() = + runTest(dispatcherProvider.main()) { + // given + val userId = provideUserId() + val (_, manager) = Arrangement() + .withDoesValidSessionExistResult(userId, DoesValidSessionExistResult.Success(true)) + .arrange() + val hasActiveJobs: (UserId) -> Boolean = { true } + // when + val result = manager.newUsersWithValidSessionAndWithoutActiveJobs(listOf(userId), hasActiveJobs) + // then + assertEquals(listOf(), result) + } + + @Test + fun givenSessionDoesNotExistForTheUserAndNoActiveJobs_whenGettingUsersToObserve_thenDoNotReturnThatUser() = + runTest(dispatcherProvider.main()) { + // given + val userId = provideUserId() + val (_, manager) = Arrangement() + .withDoesValidSessionExistResult(userId, DoesValidSessionExistResult.Success(false)) + .arrange() + val hasActiveJobs: (UserId) -> Boolean = { false } + // when + val result = manager.newUsersWithValidSessionAndWithoutActiveJobs(listOf(userId), hasActiveJobs) + // then + assertEquals(listOf(), result) + } + private inner class Arrangement { @MockK lateinit var coreLogic: CoreLogic @@ -813,6 +860,7 @@ class WireNotificationManagerTest { every { servicesManager.startOngoingCallService() } returns Unit every { servicesManager.stopOngoingCallService() } returns Unit every { pingRinger.ping(any(), any()) } returns Unit + coEvery { globalKaliumScope.doesValidSessionExist.invoke(any()) } returns DoesValidSessionExistResult.Success(true) } private fun mockSpecificUserSession( @@ -890,6 +938,10 @@ class WireNotificationManagerTest { coEvery { observeE2EIRequired.invoke() } returns flowOf(result) } + fun withDoesValidSessionExistResult(userId: UserId, result: DoesValidSessionExistResult) = apply { + coEvery { globalKaliumScope.doesValidSessionExist.invoke(userId) } returns result + } + fun arrange() = this to wireNotificationManager } From 6f7e5ca696b34d5dea65405f9a8a55cf061e7354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Tue, 13 Feb 2024 10:44:37 +0100 Subject: [PATCH 034/134] fix: welcome screen large screen [WPB-6427] (#2690) --- .../ui/authentication/welcome/WelcomeScreen.kt | 13 ++++--------- kalium | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt index 5245fe4ffed..0759a0a9aa7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt @@ -165,13 +165,7 @@ private fun WelcomeContent( ServerTitle(serverLinks = state, modifier = Modifier.padding(top = dimensions().spacing16x)) } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.weight(1f, true) - ) { - WelcomeCarousel() - } + WelcomeCarousel(modifier = Modifier.weight(1f, true)) Column( modifier = Modifier @@ -230,7 +224,7 @@ private fun WelcomeContent( @OptIn(ExperimentalFoundationApi::class) @Composable -private fun WelcomeCarousel() { +private fun WelcomeCarousel(modifier: Modifier = Modifier) { val delay = integerResource(id = R.integer.welcome_carousel_item_time_ms) val icons: List = typedArrayResource(id = R.array.welcome_carousel_icons).drawableResIdList() val texts: List = stringArrayResource(id = R.array.welcome_carousel_texts).toList() @@ -249,7 +243,7 @@ private fun WelcomeCarousel() { CompositionLocalProvider(LocalOverscrollConfiguration provides null) { HorizontalPager( state = pagerState, - modifier = Modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth() ) { page -> val (pageIconResId, pageText) = circularItemsList[page] WelcomeCarouselItem(pageIconResId = pageIconResId, pageText = pageText) @@ -300,6 +294,7 @@ private fun WelcomeCarouselItem(pageIconResId: Int, pageText: String) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() ) { Image( painter = painterResource(id = pageIconResId), diff --git a/kalium b/kalium index c56ff5b11e5..dd23b9e3849 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit c56ff5b11e5e264ec3d680068162bc75939bcef5 +Subproject commit dd23b9e38498c949db14140eeb92f9bc6114374a From ea33f8072625e4194519033ae8b4171532884225 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Tue, 13 Feb 2024 13:57:51 +0100 Subject: [PATCH 035/134] fix(e2ei): remove E2EI shield and buttons if it's disabled on your team (WPB-6520) (#2695) --- .../com/wire/android/di/CoreLogicModule.kt | 5 +++ .../ui/authentication/devices/DeviceItem.kt | 15 ++++++--- .../settings/devices/DeviceDetailsScreen.kt | 33 +++++++++++-------- .../devices/DeviceDetailsViewModel.kt | 11 +++++-- .../EndToEndIdentityCertificateItem.kt | 3 -- .../ui/settings/devices/SelfDevicesScreen.kt | 8 +++-- .../settings/devices/SelfDevicesViewModel.kt | 4 ++- .../devices/model/DeviceDetailsState.kt | 1 + .../devices/model/SelfDevicesState.kt | 3 +- .../devices/DeviceDetailsViewModelTest.kt | 8 ++++- .../devices/SelfDevicesViewModelTest.kt | 8 ++++- kalium | 2 +- 12 files changed, 71 insertions(+), 30 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 1e881239543..ba7e67af6f5 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -261,6 +261,11 @@ class UseCaseModule { fun provideIsMLSEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = coreLogic.getSessionScope(currentAccount).isMLSEnabled + @ViewModelScoped + @Provides + fun provideIsE2EIEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = + coreLogic.getSessionScope(currentAccount).isE2EIEnabled + @ViewModelScoped @Provides fun provideIsFileSharingEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt index c171c236fa7..486c28eec2a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt @@ -74,6 +74,7 @@ fun DeviceItem( placeholder: Boolean, shouldShowVerifyLabel: Boolean, isCurrentClient: Boolean = false, + shouldShowE2EIInfo: Boolean = false, background: Color? = null, icon: @Composable (() -> Unit), isWholeItemClickable: Boolean = false, @@ -87,7 +88,8 @@ fun DeviceItem( onClickAction = onClickAction, isWholeItemClickable = isWholeItemClickable, shouldShowVerifyLabel = shouldShowVerifyLabel, - isCurrentClient = isCurrentClient + isCurrentClient = isCurrentClient, + shouldShowE2EIInfo = shouldShowE2EIInfo ) } @@ -100,7 +102,8 @@ private fun DeviceItemContent( onClickAction: ((Device) -> Unit)?, isWholeItemClickable: Boolean, shouldShowVerifyLabel: Boolean, - isCurrentClient: Boolean + isCurrentClient: Boolean, + shouldShowE2EIInfo: Boolean ) { Row( verticalAlignment = Alignment.Top, @@ -126,7 +129,7 @@ private fun DeviceItemContent( modifier = Modifier .padding(start = MaterialTheme.wireDimensions.removeDeviceItemPadding) .weight(1f) - ) { DeviceItemTexts(device, placeholder, shouldShowVerifyLabel, isCurrentClient) } + ) { DeviceItemTexts(device, placeholder, shouldShowVerifyLabel, isCurrentClient, shouldShowE2EIInfo) } } if (!placeholder) { if (onClickAction != null && !isWholeItemClickable) { @@ -158,6 +161,7 @@ private fun DeviceItemTexts( placeholder: Boolean, shouldShowVerifyLabel: Boolean, isCurrentClient: Boolean = false, + shouldShowE2EIInfo: Boolean = false, isDebug: Boolean = BuildConfig.DEBUG ) { val displayZombieIndicator = remember { @@ -178,7 +182,9 @@ private fun DeviceItemTexts( .shimmerPlaceholder(visible = placeholder) ) if (shouldShowVerifyLabel) { - MLSVerificationIcon(device.e2eiCertificateStatus) + if (shouldShowE2EIInfo) { + MLSVerificationIcon(device.e2eiCertificateStatus) + } Spacer(modifier = Modifier.width(MaterialTheme.wireDimensions.spacing8x)) if (device.isVerifiedProteus && !isCurrentClient) ProteusVerifiedIcon( Modifier @@ -256,6 +262,7 @@ fun PreviewDeviceItemWithActionIcon() { placeholder = false, shouldShowVerifyLabel = true, isCurrentClient = true, + shouldShowE2EIInfo = true, background = null, { Icon(painter = painterResource(id = R.drawable.ic_remove), contentDescription = "") } ) {} diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 7078e6ad607..439fc6c5173 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -139,7 +139,7 @@ fun DeviceDetailsContent( ) { val screenState = rememberConversationScreenState() WireScaffold( - topBar = { DeviceDetailsTopBar(onNavigateBack, state.device, state.isCurrentDevice) }, + topBar = { DeviceDetailsTopBar(onNavigateBack, state.device, state.isCurrentDevice, state.isE2EIEnabled) }, bottomBar = { Column( Modifier @@ -187,17 +187,19 @@ fun DeviceDetailsContent( Divider(color = MaterialTheme.wireColorScheme.background) } } - item { - EndToEndIdentityCertificateItem( - isE2eiCertificateActivated = state.isE2eiCertificateActivated, - certificate = state.e2eiCertificate, - isCurrentDevice = state.isCurrentDevice, - isLoadingCertificate = state.isLoadingCertificate, - enrollE2eiCertificate = { enrollE2eiCertificate(context) }, - updateE2eiCertificate = {}, - showCertificate = onNavigateToE2eiCertificateDetailsScreen - ) - Divider(color = colorsScheme().background) + + if (state.isE2EIEnabled) { + item { + EndToEndIdentityCertificateItem( + isE2eiCertificateActivated = state.isE2eiCertificateActivated, + certificate = state.e2eiCertificate, + isCurrentDevice = state.isCurrentDevice, + isLoadingCertificate = state.isLoadingCertificate, + enrollE2eiCertificate = { enrollE2eiCertificate(context) }, + showCertificate = onNavigateToE2eiCertificateDetailsScreen + ) + Divider(color = colorsScheme().background) + } } item { FolderHeader( @@ -293,7 +295,8 @@ fun DeviceDetailsContent( private fun DeviceDetailsTopBar( onNavigateBack: () -> Unit, device: Device, - isCurrentDevice: Boolean + isCurrentDevice: Boolean, + shouldShowE2EIInfo: Boolean ) { WireCenterAlignedTopAppBar( onNavigationPressed = onNavigateBack, @@ -306,7 +309,9 @@ private fun DeviceDetailsTopBar( maxLines = 2 ) - MLSVerificationIcon(device.e2eiCertificateStatus) + if (shouldShowE2EIInfo) { + MLSVerificationIcon(device.e2eiCertificateStatus) + } if (!isCurrentDevice && device.isVerifiedProteus) { ProteusVerifiedIcon(Modifier.align(Alignment.CenterVertically)) diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index ae7a9ec28d3..251fd9ff7a2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -48,6 +48,7 @@ import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.e2ei.usecase.GetE2EICertificateUseCaseResult import com.wire.kalium.logic.feature.e2ei.usecase.GetE2eiCertificateUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase import com.wire.kalium.logic.functional.fold @@ -68,14 +69,20 @@ class DeviceDetailsViewModel @Inject constructor( private val updateClientVerificationStatus: UpdateClientVerificationStatusUseCase, private val observeUserInfo: ObserveUserInfoUseCase, private val e2eiCertificate: GetE2eiCertificateUseCase, - private val enrolE2EICertificateUseCase: GetE2EICertificateUseCase + private val enrolE2EICertificateUseCase: GetE2EICertificateUseCase, + isE2EIEnabledUseCase: IsE2EIEnabledUseCase ) : SavedStateViewModel(savedStateHandle) { private val deviceDetailsNavArgs: DeviceDetailsNavArgs = savedStateHandle.navArgs() private val deviceId: ClientId = deviceDetailsNavArgs.clientId private val userId: UserId = deviceDetailsNavArgs.userId - var state: DeviceDetailsState by mutableStateOf(DeviceDetailsState(isSelfClient = isSelfClient)) + var state: DeviceDetailsState by mutableStateOf( + DeviceDetailsState( + isSelfClient = isSelfClient, + isE2EIEnabled = isE2EIEnabledUseCase() + ) + ) private set init { diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt index 1e134d0aef4..169cf1277ec 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt @@ -50,7 +50,6 @@ fun EndToEndIdentityCertificateItem( isCurrentDevice: Boolean, isLoadingCertificate: Boolean, enrollE2eiCertificate: () -> Unit, - updateE2eiCertificate: () -> Unit, showCertificate: (String) -> Unit ) { Column( @@ -206,7 +205,6 @@ fun PreviewEndToEndIdentityCertificateItem() { ), isLoadingCertificate = false, enrollE2eiCertificate = {}, - updateE2eiCertificate = {}, showCertificate = {} ) } @@ -225,7 +223,6 @@ fun PreviewEndToEndIdentityCertificateSelfItem() { ), isLoadingCertificate = false, enrollE2eiCertificate = {}, - updateE2eiCertificate = {}, showCertificate = {} ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt index b451b7eadc3..9b10b2d1310 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt @@ -94,6 +94,7 @@ fun SelfDevicesScreenContent( items = listOf(currentDevice), shouldShowVerifyLabel = true, isCurrentClient = true, + isE2EIEnabled = state.isE2EIEnabled, onDeviceClick = onDeviceClick, ) @@ -103,6 +104,7 @@ fun SelfDevicesScreenContent( items = state.deviceList, shouldShowVerifyLabel = true, isCurrentClient = false, + isE2EIEnabled = state.isE2EIEnabled, onDeviceClick = onDeviceClick ) } @@ -111,12 +113,13 @@ fun SelfDevicesScreenContent( } ) } - +@Suppress("LongParameterList") private fun LazyListScope.folderDeviceItems( header: String, items: List, shouldShowVerifyLabel: Boolean, isCurrentClient: Boolean, + isE2EIEnabled: Boolean, onDeviceClick: (Device) -> Unit = {} ) { folderWithElements( @@ -137,7 +140,8 @@ private fun LazyListScope.folderDeviceItems( icon = Icons.Filled.ChevronRight.Icon(), isWholeItemClickable = true, shouldShowVerifyLabel = shouldShowVerifyLabel, - isCurrentClient = isCurrentClient + isCurrentClient = isCurrentClient, + shouldShowE2EIInfo = isE2EIEnabled ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt index dd9af69f756..6a57ced5b92 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCase import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -43,10 +44,11 @@ class SelfDevicesViewModel @Inject constructor( private val observeClientList: ObserveClientsByUserIdUseCase, private val currentClientIdUseCase: ObserveCurrentClientIdUseCase, private val getUserE2eiCertificates: GetUserE2eiCertificatesUseCase, + isE2EIEnabledUseCase: IsE2EIEnabledUseCase ) : ViewModel() { var state: SelfDevicesState by mutableStateOf( - SelfDevicesState(deviceList = listOf(), isLoadingClientsList = true, currentDevice = null) + SelfDevicesState(deviceList = listOf(), isLoadingClientsList = true, currentDevice = null, isE2EIEnabled = isE2EIEnabledUseCase()) ) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt index 1ed4b74b9a1..6400fa787aa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt @@ -36,4 +36,5 @@ data class DeviceDetailsState( val isLoadingCertificate: Boolean = false, val isE2EICertificateEnrollSuccess: Boolean = false, val isE2EICertificateEnrollError: Boolean = false, + val isE2EIEnabled: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/SelfDevicesState.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/SelfDevicesState.kt index 2b88e2f5a8c..26d12dd815c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/SelfDevicesState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/SelfDevicesState.kt @@ -23,5 +23,6 @@ import com.wire.android.ui.authentication.devices.model.Device data class SelfDevicesState ( val currentDevice: Device?, val deviceList: List, - val isLoadingClientsList: Boolean + val isLoadingClientsList: Boolean, + val isE2EIEnabled: Boolean = false ) diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt index 12af6ab551e..167a15c7e02 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt @@ -42,6 +42,7 @@ import com.wire.kalium.logic.feature.client.UpdateClientVerificationStatusUseCas import com.wire.kalium.logic.feature.e2ei.usecase.GetE2EICertificateUseCaseResult import com.wire.kalium.logic.feature.e2ei.usecase.GetE2eiCertificateUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase import io.mockk.Called @@ -319,6 +320,9 @@ class DeviceDetailsViewModelTest { @MockK(relaxed = true) lateinit var onSuccess: () -> Unit + @MockK + lateinit var isE2EIEnabledUseCase: IsE2EIEnabledUseCase + val currentUserId = UserId("currentUserId", "currentUserDomain") val viewModel by lazy { @@ -332,7 +336,8 @@ class DeviceDetailsViewModelTest { currentUserId = currentUserId, observeUserInfo = observeUserInfo, e2eiCertificate = getE2eiCertificate, - enrolE2EICertificateUseCase = enrolE2EICertificateUseCase + enrolE2EICertificateUseCase = enrolE2EICertificateUseCase, + isE2EIEnabledUseCase = isE2EIEnabledUseCase ) } @@ -341,6 +346,7 @@ class DeviceDetailsViewModelTest { withFingerprintSuccess() coEvery { observeUserInfo(any()) } returns flowOf(GetUserInfoResult.Success(TestUser.OTHER_USER, null)) coEvery { getE2eiCertificate(any()) } returns GetE2EICertificateUseCaseResult.Failure.NotActivated + coEvery { isE2EIEnabledUseCase() } returns true } fun withUserRequiresPasswordResult(result: IsPasswordRequiredUseCase.Result = IsPasswordRequiredUseCase.Result.Success(true)) = diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModelTest.kt index b47e39a3f20..bb88aabdc6a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModelTest.kt @@ -29,6 +29,7 @@ import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase import com.wire.kalium.logic.feature.client.SelfClientsResult import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import io.mockk.coEvery import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK @@ -70,6 +71,9 @@ class SelfDevicesViewModelTest { @MockK lateinit var getUserE2eiCertificates: GetUserE2eiCertificatesUseCase + @MockK + lateinit var isE2EIEnabledUseCase: IsE2EIEnabledUseCase + val selfId = UserId("selfId", "domain") private val viewModel by lazy { @@ -78,7 +82,8 @@ class SelfDevicesViewModelTest { currentAccountId = selfId, currentClientIdUseCase = currentClientId, fetchSelfClientsFromRemote = fetchSelfClientsFromRemote, - getUserE2eiCertificates = getUserE2eiCertificates + getUserE2eiCertificates = getUserE2eiCertificates, + isE2EIEnabledUseCase = isE2EIEnabledUseCase ) } @@ -95,6 +100,7 @@ class SelfDevicesViewModelTest { ) ) coEvery { getUserE2eiCertificates.invoke(any()) } returns mapOf() + coEvery { isE2EIEnabledUseCase() } returns true } fun arrange() = this to viewModel diff --git a/kalium b/kalium index dd23b9e3849..6be08c56d06 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit dd23b9e38498c949db14140eeb92f9bc6114374a +Subproject commit 6be08c56d06206d49b77b03485f43d84d7aae2d2 From f3d03c321314f0b2f932b8565a56d5ccb5e29be4 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 13 Feb 2024 16:28:22 +0200 Subject: [PATCH 036/134] fix: 2FA support dark mode (#2697) --- .../login/email/LoginEmailVerificationCodeScreen.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt index 43fb16021ca..96f61b3edfe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt @@ -38,10 +38,12 @@ import com.wire.android.R import com.wire.android.ui.authentication.verificationcode.VerificationCode import com.wire.android.ui.authentication.verificationcode.VerificationCodeState import com.wire.android.ui.common.Logo +import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.common.textfield.CodeFieldValue import com.wire.android.ui.common.typography +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText @Composable @@ -111,6 +113,7 @@ private fun MainContent( ) { Text( text = UIText.StringResource(R.string.second_factor_authentication_title).asString(), + color = colorsScheme().onBackground, style = typography().title01, textAlign = TextAlign.Start ) @@ -120,6 +123,7 @@ private fun MainContent( R.string.second_factor_authentication_instructions_label, codeState.emailUsed ).asString(), + color = colorsScheme().onBackground, style = typography().body01, textAlign = TextAlign.Start ) @@ -135,6 +139,7 @@ private fun MainContent( } @Preview(showBackground = true) +@PreviewMultipleThemes @Composable internal fun LoginEmailVerificationCodeScreenPreview() = LoginEmailVerificationCodeContent( VerificationCodeState( From 71172b772f6b176721d09fbf2d07febb747e70cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:09:46 +0100 Subject: [PATCH 037/134] fix: message background highlight colors [WPB-5940] (#2693) --- .../com/wire/android/ui/home/conversations/MessageItem.kt | 2 +- .../main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt index f8322e08743..a49b8f33be9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt @@ -147,7 +147,7 @@ fun MessageItem( } val colorAnimation = remember { Animatable(Color.Transparent) } - val highlightColor = colorsScheme().selectedMessageHighlightColor + val highlightColor = colorsScheme().primaryVariant val transparentColor = colorsScheme().primary.copy(alpha = 0F) LaunchedEffect(isSelectedMessage) { if (isSelectedMessage) { diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt index 8d3499e1fd8..dc48a7bb7f5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt +++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt @@ -102,7 +102,6 @@ data class WireColorScheme( val onScrollToBottomButtonColor: Color, val validE2eiStatusColor: Color, val mlsVerificationTextColor: Color, - val selectedMessageHighlightColor: Color ) { fun toColorScheme(): ColorScheme = ColorScheme( primary = primary, @@ -237,7 +236,6 @@ private val LightWireColorScheme = WireColorScheme( onScrollToBottomButtonColor = Color.White, validE2eiStatusColor = WireColorPalette.LightGreen550, mlsVerificationTextColor = WireColorPalette.DarkGreen700, - selectedMessageHighlightColor = WireColorPalette.DarkBlue50 ) // Dark WireColorScheme @@ -346,7 +344,6 @@ private val DarkWireColorScheme = WireColorScheme( onScrollToBottomButtonColor = Color.Black, validE2eiStatusColor = WireColorPalette.DarkGreen500, mlsVerificationTextColor = WireColorPalette.DarkGreen700, - selectedMessageHighlightColor = WireColorPalette.DarkBlue50 ) @PackagePrivate From 2fb0e05b84267a97b49d3c1d42a34cdfcf1bd602 Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Tue, 13 Feb 2024 17:46:12 +0100 Subject: [PATCH 038/134] fix: long click on deleted message (WPB-6290) (#2696) --- .../com/wire/android/ui/home/conversations/MessageItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt index a49b8f33be9..d0cf2f57272 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt @@ -172,7 +172,7 @@ fun MessageItem( }, onLongClick = remember(message) { { - if (!isContentClickable) { + if (!isContentClickable && !message.isDeleted) { onLongClicked(message) } } From a6b9a5f1495545628db80cb450dd5afab8ae0122 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Tue, 13 Feb 2024 18:27:35 +0100 Subject: [PATCH 039/134] fix: Use idp client id from remote (WPB-6494) (#2683) --- .../wire/android/feature/e2ei/OAuthUseCase.kt | 105 ++++++++---------- .../kotlin/com/wire/android/util/UriUtil.kt | 15 +++ .../com/wire/android/util/UriUtilTest.kt | 23 ++++ 3 files changed, 84 insertions(+), 59 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt index 12c2885903a..e92497dc9cf 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt @@ -22,12 +22,12 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.util.Base64 -import android.util.Log import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContracts import com.wire.android.appLogger import com.wire.android.util.deeplink.DeepLinkProcessor +import com.wire.android.util.findParameterValue import com.wire.android.util.removeQueryParams import kotlinx.serialization.json.JsonObject import net.openid.appauth.AppAuthConfiguration @@ -38,25 +38,20 @@ import net.openid.appauth.AuthorizationResponse import net.openid.appauth.AuthorizationService import net.openid.appauth.AuthorizationServiceConfiguration import net.openid.appauth.ClientAuthentication -import net.openid.appauth.ClientSecretBasic import net.openid.appauth.ResponseTypeValues import net.openid.appauth.browser.BrowserAllowList import net.openid.appauth.browser.VersionedBrowserMatcher -import net.openid.appauth.connectivity.ConnectionBuilder import org.json.JSONObject -import java.net.HttpURLConnection import java.net.URI -import java.net.URL import java.security.MessageDigest import java.security.SecureRandom -import java.security.cert.X509Certificate -import javax.net.ssl.HostnameVerifier -import javax.net.ssl.HttpsURLConnection -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager - -class OAuthUseCase(context: Context, private val authUrl: String, private val claims: JsonObject, oAuthState: String?) { + +class OAuthUseCase( + context: Context, + private val authUrl: String, + private val claims: JsonObject, + oAuthState: String? +) { private var authState: AuthState = oAuthState?.let { AuthState.jsonDeserialize(it) } ?: AuthState() @@ -64,36 +59,13 @@ class OAuthUseCase(context: Context, private val authUrl: String, private val cl private var authorizationService: AuthorizationService private lateinit var authServiceConfig: AuthorizationServiceConfiguration - // todo: this is a temporary code to ignore ssl issues on the test environment, will be removed after the preparation of the environment - // region Ignore SSL for OAuth - val naiveTrustManager = object : X509TrustManager { - override fun getAcceptedIssuers(): Array = arrayOf() - override fun checkClientTrusted(certs: Array, authType: String) = Unit - override fun checkServerTrusted(certs: Array, authType: String) = Unit - } - val insecureSocketFactory = SSLContext.getInstance("SSL").apply { - val trustAllCerts = arrayOf(naiveTrustManager) - init(null, trustAllCerts, SecureRandom()) - }.socketFactory - - private var insecureConnection = ConnectionBuilder() { uri -> - val url = URL(uri.toString()) - val connection = url.openConnection() as HttpURLConnection - if (connection is HttpsURLConnection) { - connection.hostnameVerifier = HostnameVerifier { _, _ -> true } - connection.sslSocketFactory = insecureSocketFactory - } - connection - } - // endregion - private var appAuthConfiguration: AppAuthConfiguration = AppAuthConfiguration.Builder() .setBrowserMatcher( BrowserAllowList( - VersionedBrowserMatcher.CHROME_CUSTOM_TAB, VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB + VersionedBrowserMatcher.CHROME_CUSTOM_TAB, + VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB ) ) - .setConnectionBuilder(insecureConnection) .setSkipIssuerHttpsCheck(true) .build() @@ -101,36 +73,54 @@ class OAuthUseCase(context: Context, private val authUrl: String, private val cl authorizationService = AuthorizationService(context, appAuthConfiguration) } - private fun getAuthorizationRequestIntent(): Intent = authorizationService.getAuthorizationRequestIntent(getAuthorizationRequest()) + private fun getAuthorizationRequestIntent(clientId: String): Intent = + authorizationService.getAuthorizationRequestIntent(getAuthorizationRequest(clientId)) - fun launch(activityResultRegistry: ActivityResultRegistry, resultHandler: (OAuthResult) -> Unit) { + fun launch( + activityResultRegistry: ActivityResultRegistry, + resultHandler: (OAuthResult) -> Unit + ) { authState.performActionWithFreshTokens(authorizationService) { _, idToken, exception -> if (exception != null) { - Log.e("OAuthTokenRefreshManager", "Error refreshing tokens, continue with login!", exception) + appLogger.e( + message = "OAuthTokenRefreshManager: Error refreshing tokens, continue with login!", + throwable = exception + ) launchLoginFlow(activityResultRegistry, resultHandler) } else { - resultHandler(OAuthResult.Success(idToken.toString(), authState.jsonSerializeString())) + resultHandler( + OAuthResult.Success( + idToken.toString(), + authState.jsonSerializeString() + ) + ) } } } - private fun launchLoginFlow(activityResultRegistry: ActivityResultRegistry, resultHandler: (OAuthResult) -> Unit) { + private fun launchLoginFlow( + activityResultRegistry: ActivityResultRegistry, + resultHandler: (OAuthResult) -> Unit + ) { val resultLauncher = activityResultRegistry.register( OAUTH_ACTIVITY_RESULT_KEY, ActivityResultContracts.StartActivityForResult() ) { result -> handleActivityResult(result, resultHandler) } + val clientId = URI(authUrl).findParameterValue(CLIENT_ID_QUERY_PARAM) + AuthorizationServiceConfiguration.fetchFromUrl( - Uri.parse(URI(authUrl).removeQueryParams().toString().plus(IDP_CONFIGURATION_PATH)), - { configuration, ex -> - if (ex == null) { - authServiceConfig = configuration!! - resultLauncher.launch(getAuthorizationRequestIntent()) - } else { - resultHandler(OAuthResult.Failed.InvalidActivityResult("Fetching the configurations failed! $ex")) + Uri.parse(URI(authUrl).removeQueryParams().toString().plus(IDP_CONFIGURATION_PATH)) + ) { configuration, ex -> + if (ex == null) { + authServiceConfig = configuration!! + clientId?.let { + resultLauncher.launch(getAuthorizationRequestIntent(it)) } - }, insecureConnection - ) + } else { + resultHandler(OAuthResult.Failed.InvalidActivityResult("Fetching the configurations failed! $ex")) + } + } } private fun handleActivityResult(result: ActivityResult, resultHandler: (OAuthResult) -> Unit) { @@ -143,7 +133,7 @@ class OAuthUseCase(context: Context, private val authUrl: String, private val cl private fun handleAuthorizationResponse(intent: Intent, resultHandler: (OAuthResult) -> Unit) { val authorizationResponse: AuthorizationResponse? = AuthorizationResponse.fromIntent(intent) - val clientAuth: ClientAuthentication = ClientSecretBasic(CLIENT_SECRET) + val clientAuth: ClientAuthentication = AuthState().clientAuthentication val error = AuthorizationException.fromIntent(intent) @@ -174,8 +164,8 @@ class OAuthUseCase(context: Context, private val authUrl: String, private val cl } ?: resultHandler(OAuthResult.Failed.Unknown) } - private fun getAuthorizationRequest() = AuthorizationRequest.Builder( - authServiceConfig, CLIENT_ID, ResponseTypeValues.CODE, URL_AUTH_REDIRECT + private fun getAuthorizationRequest(clientId: String) = AuthorizationRequest.Builder( + authServiceConfig, clientId, ResponseTypeValues.CODE, URL_AUTH_REDIRECT ).setCodeVerifier().setScopes( AuthorizationRequest.Scope.OPENID, AuthorizationRequest.Scope.EMAIL, @@ -216,10 +206,7 @@ class OAuthUseCase(context: Context, private val authUrl: String, private val cl companion object { const val OAUTH_ACTIVITY_RESULT_KEY = "OAuthActivityResult" - - // todo: clientId and the clientSecret will be replaced with the values from the BE once the BE provides them - const val CLIENT_ID = "wireapp" - const val CLIENT_SECRET = "dUpVSGx2dVdFdGQ0dmsxWGhDalQ0SldU" + const val CLIENT_ID_QUERY_PARAM = "client_id" const val CODE_VERIFIER_CHALLENGE_METHOD = "S256" const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" val MESSAGE_DIGEST = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) diff --git a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt index 92795d61b34..4443a6de455 100644 --- a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt @@ -66,3 +66,18 @@ fun URI.removeQueryParams(): URI { val regex = Regex("[?&][^=]+=[^&]*") return URI(this.toString().replace(regex, "")) } + +@Suppress("TooGenericExceptionCaught") +fun URI.findParameterValue(parameterName: String): String? { + return try { + rawQuery.split('&').map { + val parts = it.split('=') + val name = parts.firstOrNull() ?: "" + val value = parts.drop(1).firstOrNull() ?: "" + Pair(name, value) + }.firstOrNull { it.first == parameterName }?.second + } catch (e: NullPointerException) { + appLogger.w("Error finding parameter value: $parameterName", e) + null + } +} diff --git a/app/src/test/kotlin/com/wire/android/util/UriUtilTest.kt b/app/src/test/kotlin/com/wire/android/util/UriUtilTest.kt index f78d4f77540..e42b1878e40 100644 --- a/app/src/test/kotlin/com/wire/android/util/UriUtilTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/UriUtilTest.kt @@ -20,6 +20,7 @@ package com.wire.android.util import com.wire.android.string import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test +import java.net.URI import kotlin.random.Random class UriUtilTest { @@ -86,4 +87,26 @@ class UriUtilTest { val actual = normalizeLink(input) assertEquals(input, actual) } + + @Test + fun givenLinkWithQueryParams_whenCallingFindParameterValue_thenReturnsParamValue() { + val parameterName = "wire_client" + val parameterValue = "value1" + val url = "https://example.com?play=value&$parameterName=$parameterValue" + val actual = URI(url).findParameterValue(parameterName) + assertEquals(parameterValue, actual) + } + + @Test + fun givenLinkWithoutRequestedParam_whenCallingFindParameterValue_thenReturnsParamValue() { + val url = "https://example.com?play=value1" + val actual = URI(url).findParameterValue("wire_client") + assertEquals(null, actual) + } + @Test + fun givenLinkWithoutParams_whenCallingFindParameterValue_thenReturnsParamValue() { + val url = "https://example.com" + val actual = URI(url).findParameterValue("wire_client") + assertEquals(null, actual) + } } From 275ba259be8aaa46b9f0c64aeb87708472834b0b Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 14 Feb 2024 11:19:06 +0100 Subject: [PATCH 040/134] fix: crash when answering a call (WPB-6183) - cherrypick (#2705) --- .../com/wire/android/ui/calling/model/UICallParticipant.kt | 2 +- .../ui/calling/ongoing/participantsview/ParticipantTile.kt | 3 ++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt b/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt index 8bbeccfd793..91df9d2c8a3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt @@ -25,7 +25,7 @@ import com.wire.kalium.logic.data.id.QualifiedID data class UICallParticipant( val id: QualifiedID, val clientId: String, - val name: String = "", + val name: String? = null, val isMuted: Boolean, val isSpeaking: Boolean = false, val isCameraOn: Boolean, diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantTile.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantTile.kt index 14659df2b49..e444aa47942 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantTile.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantsview/ParticipantTile.kt @@ -94,6 +94,7 @@ fun ParticipantTile( onSelfUserVideoPreviewCreated: (view: View) -> Unit, onClearSelfUserVideoPreview: () -> Unit ) { + val defaultUserName = stringResource(id = R.string.calling_participant_tile_default_user_name) val alpha = if (participantTitleState.hasEstablishedAudio) ContentAlpha.high else ContentAlpha.medium Surface( @@ -154,7 +155,7 @@ fun ParticipantTile( end.linkTo((parent.end)) } .widthIn(max = onGoingCallTileUsernameMaxWidth), - name = participantTitleState.name, + name = participantTitleState.name ?: defaultUserName, isSpeaking = participantTitleState.isSpeaking, hasEstablishedAudio = participantTitleState.hasEstablishedAudio ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 497e9d0b39c..899ca987b78 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -842,6 +842,7 @@ Start a call Are you sure you want to call %1$s people? Call + Default Return to call Decrypting messages From 2ab8863beb50096e8e73a82e60b0d6b7569bdc3a Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 14 Feb 2024 14:44:09 +0100 Subject: [PATCH 041/134] feat: allow http calls when checking Certificate Revocation List (WPB-6493) - cherrypick (#2707) --- app/src/main/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f76f5432ac8..7f20fcc2544 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,7 +61,11 @@ android:name="android.hardware.camera" android:required="false" /> + Date: Wed, 14 Feb 2024 14:50:28 +0100 Subject: [PATCH 042/134] chore: update kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 6be08c56d06..889c1aad137 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 6be08c56d06206d49b77b03485f43d84d7aae2d2 +Subproject commit 889c1aad137be90787f44545d8d8e57c90e611c4 From b9c734b337b84d439d55afd36177234d05df69a8 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Thu, 15 Feb 2024 12:55:33 +0100 Subject: [PATCH 043/134] feat: fetch 2000 team members dring sync [WPB-6483] (#2704) --- app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt | 3 ++- buildSrc/src/main/kotlin/customization/FeatureConfigs.kt | 3 ++- default.json | 3 ++- kalium | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) 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 bf89c7f0c51..88e909b4a24 100644 --- a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt @@ -64,7 +64,8 @@ class KaliumConfigsModule { wipeOnRootedDevice = BuildConfig.WIPE_ON_ROOTED_DEVICE, isWebSocketEnabledByDefault = isWebsocketEnabledByDefault(context), certPinningConfig = BuildConfig.CERTIFICATE_PINNING_CONFIG, - maxRemoteSearchResultCount = BuildConfig.MAX_REMOTE_SEARCH_RESULT_COUNT + maxRemoteSearchResultCount = BuildConfig.MAX_REMOTE_SEARCH_RESULT_COUNT, + limitTeamMembersFetchDuringSlowSync = BuildConfig.LIMIT_TEAM_MEMBERS_FETCH_DURING_SLOW_SYNC ) } } diff --git a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt index f46df60fdd6..34daf221405 100644 --- a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt +++ b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt @@ -98,5 +98,6 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) { IS_PASSWORD_PROTECTED_GUEST_LINK_ENABLED("is_password_protected_guest_link_enabled", ConfigType.BOOLEAN), - MAX_REMOTE_SEARCH_RESULT_COUNT("max_remote_search_result_count", ConfigType.INT) + MAX_REMOTE_SEARCH_RESULT_COUNT("max_remote_search_result_count", ConfigType.INT), + LIMIT_TEAM_MEMBERS_FETCH_DURING_SLOW_SYNC("limit_team_members_fetch_during_slow_sync", ConfigType.INT), } diff --git a/default.json b/default.json index ca879d6f329..7a53dee3292 100644 --- a/default.json +++ b/default.json @@ -110,5 +110,6 @@ "url_rss_release_notes": "https://medium.com/feed/wire-news/tagged/android", "team_app_lock": false, "team_app_lock_timeout": 60, - "max_remote_search_result_count": 30 + "max_remote_search_result_count": 30, + "limit_team_members_fetch_during_slow_sync": 2000 } diff --git a/kalium b/kalium index 889c1aad137..469b32aa1a2 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 889c1aad137be90787f44545d8d8e57c90e611c4 +Subproject commit 469b32aa1a25cdfa3dd1dab801407b3b5fafc95e From d8a31c8a1fd9af2fa7788b53af00a379bba6e6f1 Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Thu, 15 Feb 2024 17:23:44 +0100 Subject: [PATCH 044/134] fix: remove browser allow list and skip of https check (WPB-6609) (#2710) Co-authored-by: Mojtaba Chenani --- .../kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt index e92497dc9cf..9f0c42c6f90 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt @@ -39,8 +39,6 @@ import net.openid.appauth.AuthorizationService import net.openid.appauth.AuthorizationServiceConfiguration import net.openid.appauth.ClientAuthentication import net.openid.appauth.ResponseTypeValues -import net.openid.appauth.browser.BrowserAllowList -import net.openid.appauth.browser.VersionedBrowserMatcher import org.json.JSONObject import java.net.URI import java.security.MessageDigest @@ -60,13 +58,6 @@ class OAuthUseCase( private lateinit var authServiceConfig: AuthorizationServiceConfiguration private var appAuthConfiguration: AppAuthConfiguration = AppAuthConfiguration.Builder() - .setBrowserMatcher( - BrowserAllowList( - VersionedBrowserMatcher.CHROME_CUSTOM_TAB, - VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB - ) - ) - .setSkipIssuerHttpsCheck(true) .build() init { From 54530b768109a1881242f3619b4fe9ce602536db Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 15 Feb 2024 22:05:43 +0200 Subject: [PATCH 045/134] fix: MLS degraded dialogs [WPB-6607] (#2712) --- .../android/ui/home/conversations/ConversationScreen.kt | 2 +- .../ui/home/conversations/MessageComposerViewModel.kt | 6 +----- .../ui/home/conversations/call/ConversationCallViewModel.kt | 2 +- kalium | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 3f1f02df25b..489031fba1d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -281,6 +281,7 @@ fun ConversationScreen( ConversationScreenDialogType.VERIFICATION_DEGRADED -> { SureAboutCallingInDegradedConversationDialog( callAnyway = { + conversationCallViewModel.onApplyConversationDegradation() startCallIfPossible( conversationCallViewModel, showDialog, @@ -293,7 +294,6 @@ fun ConversationScreen( }, onDialogDismiss = { showDialog.value = ConversationScreenDialogType.NONE } ) - conversationCallViewModel.onConversationDegradedDialogShown() } ConversationScreenDialogType.NONE -> {} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt index fbe5d4155cf..895d94e96de 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt @@ -525,11 +525,7 @@ class MessageComposerViewModel @Inject constructor( } fun dismissSureAboutSendingMessage() { - (sureAboutMessagingDialogState as? SureAboutMessagingDialogState.Visible)?.let { - viewModelScope.launch { - it.markAsNotified() - } - } + sureAboutMessagingDialogState = SureAboutMessagingDialogState.Hidden } private suspend fun SureAboutMessagingDialogState.markAsNotified() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt index 7850cdd67f2..c95e4d6a1b0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt @@ -186,7 +186,7 @@ class ConversationCallViewModel @Inject constructor( suspend fun isConferenceCallingEnabled(conversationType: Conversation.Type): ConferenceCallingResult = isConferenceCallingEnabled.invoke(conversationId, conversationType) - fun onConversationDegradedDialogShown() { + fun onApplyConversationDegradation() { viewModelScope.launch { setUserInformedAboutVerification.invoke(conversationId) } diff --git a/kalium b/kalium index 469b32aa1a2..5a54e99268d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 469b32aa1a25cdfa3dd1dab801407b3b5fafc95e +Subproject commit 5a54e99268db9a3ce8a625bffaf0fcc506da9019 From 5c7628734fd60fea59e45de10ad1add81b4fbfbe Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 16 Feb 2024 11:46:49 +0100 Subject: [PATCH 046/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 5a54e99268d..44d80621b1b 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5a54e99268db9a3ce8a625bffaf0fcc506da9019 +Subproject commit 44d80621b1be9f7eaac5f78c175d978f5060a577 From c27c5cf776f0909672b3fa374cb8e855f798b2a6 Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Fri, 16 Feb 2024 12:25:00 +0100 Subject: [PATCH 047/134] fix: leaking UI context GetE2EICertificateUseCase (WPB-6648) (#2713) --- .../di/GetE2EICertificateUseCaseProvider.kt | 48 +++++++++++++++++ .../feature/e2ei/GetE2EICertificateUseCase.kt | 7 +-- .../com/wire/android/ui/WireActivity.kt | 4 +- .../wire/android/ui/debug/DebugDataOptions.kt | 12 ++--- .../ui/e2eiEnrollment/E2EIEnrollmentScreen.kt | 4 +- .../e2eiEnrollment/E2EIEnrollmentViewModel.kt | 5 +- .../sync/FeatureFlagNotificationViewModel.kt | 52 ++++++++++--------- .../settings/devices/DeviceDetailsScreen.kt | 8 ++- .../devices/DeviceDetailsViewModel.kt | 5 +- .../FeatureFlagNotificationViewModelTest.kt | 15 ++++-- .../devices/DeviceDetailsViewModelTest.kt | 8 +-- 11 files changed, 107 insertions(+), 61 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/di/GetE2EICertificateUseCaseProvider.kt diff --git a/app/src/main/kotlin/com/wire/android/di/GetE2EICertificateUseCaseProvider.kt b/app/src/main/kotlin/com/wire/android/di/GetE2EICertificateUseCaseProvider.kt new file mode 100644 index 00000000000..d358507f8d7 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/di/GetE2EICertificateUseCaseProvider.kt @@ -0,0 +1,48 @@ +/* + * Wire + * Copyright (C) 2024 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.di + +import android.content.Context +import com.wire.android.feature.e2ei.GetE2EICertificateUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.user.UserId +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext + +class GetE2EICertificateUseCaseProvider @AssistedInject constructor( + @KaliumCoreLogic private val coreLogic: CoreLogic, + @ApplicationContext private val applicationContext: Context, + @Assisted private val userId: UserId, + @Assisted private val dispatcherProvider: DispatcherProvider +) { + + val useCase: GetE2EICertificateUseCase + get() = GetE2EICertificateUseCase( + enrollE2EI = coreLogic.getSessionScope(userId).enrollE2EI, + applicationContext = applicationContext, + dispatcherProvider = dispatcherProvider + ) + + @AssistedFactory + interface Factory { + fun create(userId: UserId, dispatcherProvider: DispatcherProvider): GetE2EICertificateUseCaseProvider + } +} diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt index 3683bc6aa7e..4d57a9d3572 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt @@ -26,6 +26,7 @@ import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.e2ei.usecase.EnrollE2EIUseCase import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -33,6 +34,7 @@ import javax.inject.Inject class GetE2EICertificateUseCase @Inject constructor( private val enrollE2EI: EnrollE2EIUseCase, + @ApplicationContext private val applicationContext: Context, val dispatcherProvider: DispatcherProvider ) { @@ -41,7 +43,6 @@ class GetE2EICertificateUseCase @Inject constructor( lateinit var enrollmentResultHandler: (Either) -> Unit operator fun invoke( - context: Context, isNewClient: Boolean, enrollmentResultHandler: (Either) -> Unit ) { @@ -52,8 +53,8 @@ class GetE2EICertificateUseCase @Inject constructor( }, { if (it is E2EIEnrollmentResult.Initialized) { initialEnrollmentResult = it - OAuthUseCase(context, it.target, it.oAuthClaims, it.oAuthState).launch( - context.getActivity()!!.activityResultRegistry, + OAuthUseCase(applicationContext, it.target, it.oAuthClaims, it.oAuthState).launch( + applicationContext.getActivity()!!.activityResultRegistry, ::oAuthResultHandler ) } else enrollmentResultHandler(Either.Right(it)) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 2c9fd2c909f..19c7a407c01 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -370,7 +370,7 @@ class WireActivity : AppCompatActivity() { E2EIRequiredDialog( e2EIRequired = e2EIRequired, isE2EILoading = isE2EILoading, - getCertificate = { featureFlagNotificationViewModel.getE2EICertificate(it, context) }, + getCertificate = { featureFlagNotificationViewModel.getE2EICertificate(it) }, snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog ) } @@ -385,7 +385,7 @@ class WireActivity : AppCompatActivity() { e2EIResult?.let { E2EIResultDialog( result = e2EIResult, - updateCertificate = { featureFlagNotificationViewModel.getE2EICertificate(it, context) }, + updateCertificate = { featureFlagNotificationViewModel.getE2EICertificate(it) }, snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog, openCertificateDetails = { navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(it))) }, dismissSuccessDialog = featureFlagNotificationViewModel::dismissSuccessE2EIdDialog, 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 8978e3ed043..682d8007a97 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 @@ -29,7 +29,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel @@ -127,8 +126,8 @@ class DebugDataOptionsViewModel } } - fun enrollE2EICertificate(context: Context) { - e2eiCertificateUseCase(context, false) { result -> + fun enrollE2EICertificate() { + e2eiCertificateUseCase(false) { result -> result.fold({ state = state.copy( certificate = (it as E2EIFailure.FailedOAuth).reason, showCertificate = true @@ -250,7 +249,7 @@ fun DebugDataOptionsContent( onRestartSlowSyncForRecovery: () -> Unit, onForceUpdateApiVersions: () -> Unit, onManualMigrationPressed: () -> Unit, - enrollE2EICertificate: (Context) -> Unit, + enrollE2EICertificate: () -> Unit, dismissCertificateDialog: () -> Unit ) { Column { @@ -352,9 +351,8 @@ fun DebugDataOptionsContent( @Composable private fun GetE2EICertificateSwitch( - enrollE2EI: (context: Context) -> Unit + enrollE2EI: () -> Unit ) { - val context = LocalContext.current Column { FolderHeader(stringResource(R.string.debug_settings_e2ei_enrollment_title)) RowItemTemplate(modifier = Modifier.wrapContentWidth(), @@ -369,7 +367,7 @@ private fun GetE2EICertificateSwitch( actions = { WirePrimaryButton( onClick = { - enrollE2EI(context) + enrollE2EI() }, text = stringResource(R.string.label_get_e2ei_cetificate), fillMaxWidth = false diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt index e341e844c95..ffd48c7cacb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -76,7 +75,6 @@ fun E2EIEnrollmentScreen( viewModel: E2EIEnrollmentViewModel = hiltViewModel(), ) { val state = viewModel.state - val context = LocalContext.current E2EIEnrollmentScreenContent( state = state, @@ -85,7 +83,7 @@ fun E2EIEnrollmentScreen( viewModel.finalizeMLSClient() }, dismissErrorDialog = viewModel::dismissErrorDialog, - enrollE2EICertificate = { viewModel.enrollE2EICertificate(context) }, + enrollE2EICertificate = { viewModel.enrollE2EICertificate() }, openCertificateDetails = { navigator.navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(state.certificate))) }, diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt index 514ca785d31..018f11da674 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.e2eiEnrollment -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -94,9 +93,9 @@ class E2EIEnrollmentViewModel @Inject constructor( } } } - fun enrollE2EICertificate(context: Context) { + fun enrollE2EICertificate() { state = state.copy(isLoading = true) - e2eiCertificateUseCase(context, true) { result -> + e2eiCertificateUseCase(true) { result -> result.fold({ state = state.copy( isLoading = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 45026da72dc..88b73d47380 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.sync -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -26,10 +25,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.GetE2EICertificateUseCaseProvider import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase -import com.wire.android.feature.e2ei.GetE2EICertificateUseCase import com.wire.android.ui.home.FeatureFlagState import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration @@ -59,6 +58,7 @@ class FeatureFlagNotificationViewModel @Inject constructor( private val currentSessionFlow: CurrentSessionFlowUseCase, private val globalDataStore: GlobalDataStore, private val disableAppLockUseCase: DisableAppLockUseCase, + private val getE2EICertificateUseCaseProvider: GetE2EICertificateUseCaseProvider.Factory, private val dispatcherProvider: DispatcherProvider ) : ViewModel() { @@ -288,35 +288,39 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun isUserAppLockSet() = globalDataStore.isAppLockPasscodeSet() - fun getE2EICertificate(e2eiRequired: FeatureFlagState.E2EIRequired, context: Context) { + fun getE2EICertificate(e2eiRequired: FeatureFlagState.E2EIRequired) { featureFlagState = featureFlagState.copy(isE2EILoading = true) currentUserId?.let { userId -> - GetE2EICertificateUseCase(coreLogic.getSessionScope(userId).enrollE2EI, dispatcherProvider).invoke( - context, - isNewClient = false - ) { result -> - result.fold({ - featureFlagState = featureFlagState.copy( - isE2EILoading = false, - e2EIRequired = null, - e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) - ) - }, { - if (it is E2EIEnrollmentResult.Finalized) { - featureFlagState = featureFlagState.copy( - isE2EILoading = false, - e2EIRequired = null, - e2EIResult = FeatureFlagState.E2EIResult.Success(it.certificate) - ) - } else if (it is E2EIEnrollmentResult.Failed) { + getE2EICertificateUseCaseProvider.create( + userId = userId, + dispatcherProvider = dispatcherProvider + ) + .useCase + .invoke( + isNewClient = false + ) { result -> + result.fold({ featureFlagState = featureFlagState.copy( isE2EILoading = false, e2EIRequired = null, e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) ) - } - }) - } + }, { + if (it is E2EIEnrollmentResult.Finalized) { + featureFlagState = featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + e2EIResult = FeatureFlagState.E2EIResult.Success(it.certificate) + ) + } else if (it is E2EIEnrollmentResult.Failed) { + featureFlagState = featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) + ) + } + }) + } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 439fc6c5173..cb1ada5625a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.settings.devices -import android.content.Context import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -132,7 +131,7 @@ fun DeviceDetailsContent( onRemoveConfirm: () -> Unit = {}, onDialogDismiss: () -> Unit = {}, onErrorDialogDismiss: () -> Unit = {}, - enrollE2eiCertificate: (Context) -> Unit = {}, + enrollE2eiCertificate: () -> Unit = {}, onUpdateClientVerification: (Boolean) -> Unit = {}, onEnrollE2EIErrorDismiss: () -> Unit = {}, onEnrollE2EISuccessDismiss: () -> Unit = {} @@ -173,7 +172,6 @@ fun DeviceDetailsContent( } } ) { internalPadding -> - val context = LocalContext.current LazyColumn( modifier = Modifier .fillMaxSize() @@ -195,7 +193,7 @@ fun DeviceDetailsContent( certificate = state.e2eiCertificate, isCurrentDevice = state.isCurrentDevice, isLoadingCertificate = state.isLoadingCertificate, - enrollE2eiCertificate = { enrollE2eiCertificate(context) }, + enrollE2eiCertificate = { enrollE2eiCertificate() }, showCertificate = onNavigateToE2eiCertificateDetailsScreen ) Divider(color = colorsScheme().background) @@ -277,7 +275,7 @@ fun DeviceDetailsContent( if (state.isE2EICertificateEnrollError) { E2EIErrorWithDismissDialog( isE2EILoading = state.isLoadingCertificate, - updateCertificate = { enrollE2eiCertificate(context) }, + updateCertificate = { enrollE2eiCertificate() }, onDismiss = onEnrollE2EIErrorDismiss ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index 251fd9ff7a2..01df6bc91f3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.settings.devices -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -128,9 +127,9 @@ class DeviceDetailsViewModel @Inject constructor( } } - fun enrollE2eiCertificate(context: Context) { + fun enrollE2eiCertificate() { state = state.copy(isLoadingCertificate = true) - enrolE2EICertificateUseCase(context, false) { result -> + enrolE2EICertificateUseCase(false) { result -> result.fold({ state = state.copy( isLoadingCertificate = false, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index 6c93b0a7b31..51cf293943d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.home.sync import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.GetE2EICertificateUseCaseProvider import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase import com.wire.android.framework.TestUser @@ -147,7 +148,7 @@ class FeatureFlagNotificationViewModelTest { @Test fun givenE2EIRequired_thenShowDialog() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.NoGracePeriod.Create) .arrange() advanceUntilIdle() @@ -174,7 +175,7 @@ class FeatureFlagNotificationViewModelTest { @Test fun givenSnoozeE2EIRequiredDialogShown_whenDismissCalled_thenItSnoozedAndDialogHidden() = runTest { val gracePeriod = 1.days - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.WithGracePeriod.Create(gracePeriod)) .arrange() viewModel.snoozeE2EIdRequiredDialog(FeatureFlagState.E2EIRequired.WithGracePeriod.Create(gracePeriod)) @@ -187,7 +188,7 @@ class FeatureFlagNotificationViewModelTest { @Test fun givenE2EIRenewRequired_thenShowDialog() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.NoGracePeriod.Renew) .arrange() advanceUntilIdle() @@ -214,7 +215,7 @@ class FeatureFlagNotificationViewModelTest { @Test fun givenSnoozeE2EIRenewDialogShown_whenDismissCalled_thenItSnoozedAndDialogHidden() = runTest { val gracePeriod = 1.days - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.WithGracePeriod.Renew(gracePeriod)) .arrange() viewModel.snoozeE2EIdRequiredDialog(FeatureFlagState.E2EIRequired.WithGracePeriod.Renew(gracePeriod)) @@ -267,7 +268,7 @@ class FeatureFlagNotificationViewModelTest { @Test fun givenE2EIRequired_whenUserLoggedOut_thenHideDialog() = runTest { val currentSessionsFlow = MutableSharedFlow(1) - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.NoGracePeriod.Create) .withCurrentSessionsFlow(currentSessionsFlow) .arrange() @@ -300,6 +301,9 @@ class FeatureFlagNotificationViewModelTest { private inner class Arrangement { + @MockK + private lateinit var getE2EICertificateUseCaseProvider: GetE2EICertificateUseCaseProvider.Factory + @MockK lateinit var currentSessionFlow: CurrentSessionFlowUseCase @@ -333,6 +337,7 @@ class FeatureFlagNotificationViewModelTest { currentSessionFlow = currentSessionFlow, globalDataStore = globalDataStore, disableAppLockUseCase = disableAppLockUseCase, + getE2EICertificateUseCaseProvider = getE2EICertificateUseCaseProvider, dispatcherProvider = TestDispatcherProvider() ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt index 167a15c7e02..687dfcc4359 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.settings.devices -import android.content.Context import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension @@ -277,19 +276,16 @@ class DeviceDetailsViewModelTest { .withClientDetailsResult(GetClientDetailsResult.Success(TestClient.CLIENT, true)) .arrange() - viewModel.enrollE2eiCertificate(arrangement.context) + viewModel.enrollE2eiCertificate() coVerify { - arrangement.enrolE2EICertificateUseCase(any(), any(), any()) + arrangement.enrolE2EICertificateUseCase(any(), any()) } assertTrue(viewModel.state.isLoadingCertificate) } private class Arrangement { - @MockK - lateinit var context: Context - @MockK lateinit var savedStateHandle: SavedStateHandle From 1a2034e0af5979b6bd7020996c4f218f85b35575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Fri, 16 Feb 2024 16:09:56 +0100 Subject: [PATCH 048/134] fix: show connection request with unavailable name [WPB-6247] (#2716) --- .../ConversationListViewModel.kt | 3 ++- .../common/ConversationItemFactory.kt | 19 +++++++++++++++++++ .../model/ConversationItem.kt | 3 ++- .../devices/DeviceDetailsViewModelTest.kt | 2 +- kalium | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index 9010ada16bf..61e40af9308 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -604,7 +604,8 @@ private fun ConversationDetails.toConversationItem( ), conversationInfo = ConversationInfo( name = otherUser?.name.orEmpty(), - membership = userTypeMapper.toMembership(userType) + membership = userTypeMapper.toMembership(userType), + isSenderUnavailable = otherUser?.isUnavailableUser ?: true ), lastMessageContent = UILastMessageContent.Connection( connection.status, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt index 6f5636852ac..6a4fe4bb7fa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt @@ -372,6 +372,25 @@ fun PreviewConnectionConversationItemWithSentConnectRequestBadge() { ) } +@Preview +@Composable +fun PreviewConnectionConversationItemWithSentConnectRequestBadgeWithUnknownSender() { + ConversationItemFactory( + conversation = ConversationItem.ConnectionConversation( + userAvatarData = UserAvatarData(), + conversationId = QualifiedID("value", "domain"), + mutedStatus = MutedConversationStatus.OnlyMentionsAndRepliesAllowed, + lastMessageContent = null, + badgeEventType = BadgeEventType.SentConnectRequest, + conversationInfo = ConversationInfo("", isSenderUnavailable = true) + ), + searchQuery = "", + isSelectableItem = false, + isChecked = false, + {}, {}, {}, {}, {}, {} + ) +} + @Preview @Composable fun PreviewPrivateConversationItemWithBlockedBadge() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt index dbae33d1b5c..179d6ae12d3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt @@ -126,5 +126,6 @@ fun ConversationItem.ConnectionConversation.toUserInfoLabel() = UserInfoLabel( labelName = conversationInfo.name, isLegalHold = isLegalHold, - membership = conversationInfo.membership + membership = conversationInfo.membership, + unavailable = conversationInfo.isSenderUnavailable ) diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt index 687dfcc4359..3ef93208574 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt @@ -341,7 +341,7 @@ class DeviceDetailsViewModelTest { MockKAnnotations.init(this, relaxUnitFun = true) withFingerprintSuccess() coEvery { observeUserInfo(any()) } returns flowOf(GetUserInfoResult.Success(TestUser.OTHER_USER, null)) - coEvery { getE2eiCertificate(any()) } returns GetE2EICertificateUseCaseResult.Failure.NotActivated + coEvery { getE2eiCertificate(any()) } returns GetE2EICertificateUseCaseResult.NotActivated coEvery { isE2EIEnabledUseCase() } returns true } diff --git a/kalium b/kalium index 44d80621b1b..a6c31af0472 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 44d80621b1be9f7eaac5f78c175d978f5060a577 +Subproject commit a6c31af0472cf977a57004ac20d030371d13495c From b9a85f3c83955091dfe02798041c0ea417f17d3b Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Mon, 19 Feb 2024 17:46:28 +0100 Subject: [PATCH 049/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index a6c31af0472..171f1e0bfb9 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit a6c31af0472cf977a57004ac20d030371d13495c +Subproject commit 171f1e0bfb99d676a13763f4078723d4010bd512 From 996103bdc497a6e7d0b8d2cb86f600da134dc15c Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Tue, 20 Feb 2024 10:10:22 +0100 Subject: [PATCH 050/134] feat: update place holder name for call participants --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 899ca987b78..d6f85f104a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -842,7 +842,7 @@ Start a call Are you sure you want to call %1$s people? Call - Default + Name not available Return to call Decrypting messages From f4fef2f82cb47f7253fb88f81d7aee4ca05b532b Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Wed, 21 Feb 2024 15:40:18 +0100 Subject: [PATCH 051/134] feat: implement use case to get default conversation creation protocol (WPB-5475) (#2722) --- .../com/wire/android/di/CoreLogicModule.kt | 5 +++ .../GroupConversationNameComponent.kt | 2 +- .../ui/common/groupname/GroupMetadataState.kt | 1 + .../NewConversationViewModel.kt | 16 ++++++-- .../NewConversationViewModelArrangement.kt | 41 ++++++++++++------- .../NewConversationViewModelTest.kt | 22 ++++++++++ kalium | 2 +- 7 files changed, 70 insertions(+), 19 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index ba7e67af6f5..7ff2cbaadc8 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -261,6 +261,11 @@ class UseCaseModule { fun provideIsMLSEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = coreLogic.getSessionScope(currentAccount).isMLSEnabled + @ViewModelScoped + @Provides + fun provideGetDefaultProtocol(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = + coreLogic.getSessionScope(currentAccount).getDefaultProtocol + @ViewModelScoped @Provides fun provideIsE2EIEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = diff --git a/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupConversationNameComponent.kt b/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupConversationNameComponent.kt index 177774939e3..215ed681280 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupConversationNameComponent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupConversationNameComponent.kt @@ -124,7 +124,7 @@ fun GroupNameScreen( WireDropDown( items = ConversationOptions.Protocol.values().map { it.name }, - defaultItemIndex = ConversationOptions.Protocol.PROTEUS.ordinal, + defaultItemIndex = defaultProtocol.ordinal, selectedItemIndex = groupProtocol.ordinal, label = stringResource(R.string.protocol), modifier = Modifier diff --git a/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupMetadataState.kt b/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupMetadataState.kt index 0a3030db861..2105ee5b3dd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupMetadataState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupMetadataState.kt @@ -32,6 +32,7 @@ data class GroupMetadataState( val animatedGroupNameError: Boolean = false, val continueEnabled: Boolean = false, val mlsEnabled: Boolean = true, + val defaultProtocol: ConversationOptions.Protocol = ConversationOptions.Protocol.PROTEUS, val isLoading: Boolean = false, val error: NewGroupError = NewGroupError.None, val mode: GroupNameMode = GroupNameMode.CREATION, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt index 8a127d0c7e0..56207022ded 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt @@ -36,6 +36,7 @@ import com.wire.kalium.logic.data.conversation.ConversationOptions import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.CreateGroupConversationUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -48,13 +49,22 @@ import javax.inject.Inject class NewConversationViewModel @Inject constructor( private val createGroupConversation: CreateGroupConversationUseCase, private val isSelfATeamMember: IsSelfATeamMemberUseCase, - isMLSEnabled: IsMLSEnabledUseCase + isMLSEnabled: IsMLSEnabledUseCase, + getDefaultProtocol: GetDefaultProtocolUseCase ) : ViewModel() { var newGroupState: GroupMetadataState by mutableStateOf( GroupMetadataState( - mlsEnabled = isMLSEnabled(), - ) + mlsEnabled = isMLSEnabled() + ).let { + val defaultProtocol = ConversationOptions + .Protocol + .fromSupportedProtocolToConversationOptionsProtocol(getDefaultProtocol()) + it.copy( + defaultProtocol = defaultProtocol, + groupProtocol = defaultProtocol + ) + } ) var groupOptionsState: GroupOptionState by mutableStateOf(GroupOptionState()) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt index 705efda75c5..997238d7e81 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt @@ -21,6 +21,7 @@ package com.wire.android.ui.home.newconversation import com.wire.android.config.mockUri import com.wire.android.framework.TestUser import com.wire.android.ui.home.newconversation.common.CreateGroupState +import com.wire.android.ui.home.newconversation.groupOptions.GroupOptionState import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.MutedConversationStatus @@ -33,10 +34,12 @@ import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.feature.conversation.CreateGroupConversationUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCaseImpl import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.every import io.mockk.impl.annotations.MockK internal class NewConversationViewModelArrangement { @@ -47,6 +50,7 @@ internal class NewConversationViewModelArrangement { // Default empty values coEvery { isMLSEnabledUseCase() } returns true coEvery { createGroupConversation(any(), any(), any()) } returns CreateGroupConversationUseCase.Result.Success(CONVERSATION) + every { getDefaultProtocol() } returns SupportedProtocol.PROTEUS } @MockK @@ -61,6 +65,13 @@ internal class NewConversationViewModelArrangement { @MockK(relaxed = true) lateinit var onGroupCreated: (ConversationId) -> Unit + @MockK + lateinit var getDefaultProtocol: GetDefaultProtocolUseCase + + private var groupOptionsState: GroupOptionState = GroupOptionState() + + private var createGroupState: CreateGroupState = CreateGroupState() + private companion object { val CONVERSATION_ID = ConversationId(value = "userId", domain = "domainId") val CONVERSATION = Conversation( @@ -128,14 +139,6 @@ internal class NewConversationViewModelArrangement { ) } - private val viewModel by lazy { - NewConversationViewModel( - createGroupConversation = createGroupConversation, - isMLSEnabled = isMLSEnabledUseCase, - isSelfATeamMember = isSelfTeamMember, - ) - } - fun withSyncFailureOnCreatingGroup() = apply { coEvery { createGroupConversation(any(), any(), any()) } returns CreateGroupConversationUseCase.Result.SyncFailure } @@ -147,7 +150,7 @@ internal class NewConversationViewModelArrangement { } fun withConflictingBackendsFailure() = apply { - viewModel.createGroupState = viewModel.createGroupState.copy( + createGroupState = createGroupState.copy( error = CreateGroupState.Error.ConflictedBackends(listOf("bella.wire.link", "foma.wire.link")) ) } @@ -157,14 +160,24 @@ internal class NewConversationViewModelArrangement { } fun withGuestEnabled(isGuestModeEnabled: Boolean) = apply { - viewModel.groupOptionsState = viewModel - .groupOptionsState - .copy(isAllowGuestEnabled = isGuestModeEnabled) + groupOptionsState = groupOptionsState.copy(isAllowGuestEnabled = isGuestModeEnabled) } fun withServicesEnabled(areServicesEnabled: Boolean) = apply { - viewModel.groupOptionsState = viewModel.groupOptionsState.copy(isAllowServicesEnabled = areServicesEnabled) + groupOptionsState = groupOptionsState.copy(isAllowServicesEnabled = areServicesEnabled) } - fun arrange() = this to viewModel + fun withDefaultProtocol(supportedProtocol: SupportedProtocol) = apply { + every { getDefaultProtocol() } returns supportedProtocol + } + + fun arrange() = this to NewConversationViewModel( + createGroupConversation = createGroupConversation, + isMLSEnabled = isMLSEnabledUseCase, + isSelfATeamMember = isSelfTeamMember, + getDefaultProtocol = getDefaultProtocol + ).also { + it.groupOptionsState = groupOptionsState + it.createGroupState = createGroupState + } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt index 77df786162a..04b6a75d253 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt @@ -24,11 +24,13 @@ import com.wire.android.config.CoroutineTestExtension import com.wire.android.ui.home.newconversation.common.CreateGroupState import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationOptions +import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserId import io.mockk.coVerify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeNull import org.junit.jupiter.api.Test @@ -143,4 +145,24 @@ class NewConversationViewModelTest { ) } } + + @Test + fun `given team settings is MLS default protocol, when getting default protocol, then result is MLS`() = runTest { + // given + val (_, viewModel) = NewConversationViewModelArrangement() + .withDefaultProtocol(SupportedProtocol.MLS) + .withIsSelfTeamMember(true) + .withServicesEnabled(false) + .withGuestEnabled(true) + .arrange() + + // when + val result = viewModel.newGroupState.defaultProtocol + + // then + assertEquals( + ConversationOptions.Protocol.MLS, + result + ) + } } diff --git a/kalium b/kalium index 171f1e0bfb9..06114716a2d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 171f1e0bfb99d676a13763f4078723d4010bd512 +Subproject commit 06114716a2d63c40960321a4502269f8b6382242 From 1e9c1e9e4fcea0885ee76826e3ca93229c4d5035 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Thu, 22 Feb 2024 17:05:29 +0100 Subject: [PATCH 052/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 06114716a2d..5f9f8a854cb 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 06114716a2d63c40960321a4502269f8b6382242 +Subproject commit 5f9f8a854cb91f4241923f04161c30343b2e021f From 5cc138fed4ba5c8caab0becb2586c4daafe57073 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Thu, 22 Feb 2024 19:33:09 +0100 Subject: [PATCH 053/134] chore: add git commit hash to external logger (#2729) --- app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt b/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt index d29ebd8205c..78cc0b1658e 100644 --- a/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt +++ b/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt @@ -30,6 +30,7 @@ import com.datadog.android.rum.tracking.ComponentPredicate import com.wire.android.datastore.GlobalDataStore import com.wire.android.ui.WireActivity import com.wire.android.util.getDeviceIdString +import com.wire.android.util.getGitBuildId import com.wire.android.util.sha256 import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking @@ -73,7 +74,8 @@ object ExternalLoggerManager { val credentials = Credentials(clientToken, environmentName, appVariantName, applicationId) val extraInfo = mapOf( - "encrypted_proteus_storage_enabled" to runBlocking { globalDataStore.isEncryptedProteusStorageEnabled().first() } + "encrypted_proteus_storage_enabled" to runBlocking { globalDataStore.isEncryptedProteusStorageEnabled().first() }, + "git_commit_hash" to context.getGitBuildId() ) Datadog.initialize(context, credentials, configuration, TrackingConsent.GRANTED) From 16b4cdb38edc9f0b24dea46238f76536aeb73ea8 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Fri, 23 Feb 2024 09:49:40 +0100 Subject: [PATCH 054/134] fix(e2ei): error handling (WPB-6271) (#2721) --- .../feature/e2ei/GetE2EICertificateUseCase.kt | 18 +++++------ .../wire/android/ui/debug/DebugDataOptions.kt | 11 ++++--- .../e2eiEnrollment/E2EIEnrollmentViewModel.kt | 5 ++++ .../sync/FeatureFlagNotificationViewModel.kt | 30 +++++++++---------- .../devices/DeviceDetailsViewModel.kt | 2 +- kalium | 2 +- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt index 4d57a9d3572..2693a11cee0 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt @@ -20,13 +20,13 @@ package com.wire.android.feature.e2ei import android.content.Context import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.extension.getActivity -import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.E2EIFailure import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.e2ei.usecase.EnrollE2EIUseCase import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.kalium.logic.functional.left import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -44,20 +44,18 @@ class GetE2EICertificateUseCase @Inject constructor( operator fun invoke( isNewClient: Boolean, - enrollmentResultHandler: (Either) -> Unit + enrollmentResultHandler: (Either) -> Unit ) { this.enrollmentResultHandler = enrollmentResultHandler scope.launch { enrollE2EI.initialEnrollment(isNewClientRegistration = isNewClient).fold({ enrollmentResultHandler(Either.Left(it)) }, { - if (it is E2EIEnrollmentResult.Initialized) { - initialEnrollmentResult = it - OAuthUseCase(applicationContext, it.target, it.oAuthClaims, it.oAuthState).launch( - applicationContext.getActivity()!!.activityResultRegistry, - ::oAuthResultHandler - ) - } else enrollmentResultHandler(Either.Right(it)) + initialEnrollmentResult = it + OAuthUseCase(applicationContext, it.target, it.oAuthClaims, it.oAuthState).launch( + applicationContext.getActivity()!!.activityResultRegistry, + ::oAuthResultHandler + ) }) } } @@ -76,7 +74,7 @@ class GetE2EICertificateUseCase @Inject constructor( } is OAuthUseCase.OAuthResult.Failed -> { - enrollmentResultHandler(Either.Left(E2EIFailure.FailedOAuth(oAuthResult.reason))) + enrollmentResultHandler(E2EIFailure.OAuth(oAuthResult.reason).left()) } } } 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 682d8007a97..df7480b6e0e 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 @@ -55,7 +55,6 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.getDeviceIdString import com.wire.android.util.getGitBuildId import com.wire.android.util.ui.PreviewMultipleThemes -import com.wire.kalium.logic.E2EIFailure import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.debug.DisableEventProcessingUseCase import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult @@ -130,13 +129,17 @@ class DebugDataOptionsViewModel e2eiCertificateUseCase(false) { result -> result.fold({ state = state.copy( - certificate = (it as E2EIFailure.FailedOAuth).reason, showCertificate = true + certificate = it.toString(), showCertificate = true ) }, { - if (it is E2EIEnrollmentResult.Finalized) { - state = state.copy( + state = if (it is E2EIEnrollmentResult.Finalized) { + state.copy( certificate = it.certificate, showCertificate = true ) + } else { + state.copy( + certificate = it.toString(), showCertificate = true + ) } }) } diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt index 018f11da674..348a96a7510 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt @@ -109,6 +109,11 @@ class E2EIEnrollmentViewModel @Inject constructor( isCertificateEnrollError = false, isLoading = false ) + } else { + state = state.copy( + isLoading = false, + isCertificateEnrollError = true + ) } }) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 88b73d47380..25591b4e118 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -306,21 +306,21 @@ class FeatureFlagNotificationViewModel @Inject constructor( e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) ) }, { - if (it is E2EIEnrollmentResult.Finalized) { - featureFlagState = featureFlagState.copy( - isE2EILoading = false, - e2EIRequired = null, - e2EIResult = FeatureFlagState.E2EIResult.Success(it.certificate) - ) - } else if (it is E2EIEnrollmentResult.Failed) { - featureFlagState = featureFlagState.copy( - isE2EILoading = false, - e2EIRequired = null, - e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) - ) - } - }) - } + featureFlagState = if (it is E2EIEnrollmentResult.Finalized) { + featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + e2EIResult = FeatureFlagState.E2EIResult.Success(it.certificate) + ) + } else { + featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) + ) + } + }) + } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index 01df6bc91f3..fb142a1571e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -139,7 +139,7 @@ class DeviceDetailsViewModel @Inject constructor( if (it is E2EIEnrollmentResult.Finalized) { getE2eiCertificate() state = state.copy(isE2EICertificateEnrollSuccess = true) - } else if (it is E2EIEnrollmentResult.Failed) { + } else { state = state.copy( isLoadingCertificate = false, isE2EICertificateEnrollError = true diff --git a/kalium b/kalium index 5f9f8a854cb..630462f7b88 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5f9f8a854cb91f4241923f04161c30343b2e021f +Subproject commit 630462f7b887f976855b6f254be628ed937717d2 From 1ab45c9fecf9b2e0590a831e538851a9a812163c Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 23 Feb 2024 11:19:52 +0100 Subject: [PATCH 055/134] chore: remove unwanted log --- .../ui/home/conversations/typing/TypingIndicatorViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt index b69e832f13f..a4b14851a27 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt @@ -51,7 +51,6 @@ class TypingIndicatorViewModel @Inject constructor( private fun observeUsersTypingState() { viewModelScope.launch { observeUsersTypingInConversation(conversationId).collect { - appLogger.d("Users typing: $it") usersTypingViewState = usersTypingViewState.copy(usersTyping = it) } } From 63fd8414e3f9a5b2da0140ae5da5d49a1bfb053d Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Fri, 23 Feb 2024 11:52:12 +0100 Subject: [PATCH 056/134] chore: fix tag logging (#2730) Co-authored-by: MohamadJaara --- .../com/wire/android/util/DataDogLogger.kt | 16 +++++++++------- .../typing/TypingIndicatorViewModel.kt | 1 - kalium | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/beta/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/beta/kotlin/com/wire/android/util/DataDogLogger.kt index eb345c431ef..547c9b0fd63 100644 --- a/app/src/beta/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/beta/kotlin/com/wire/android/util/DataDogLogger.kt @@ -34,13 +34,15 @@ object DataDogLogger : LogWriter() { .build() override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - val attributes = KaliumLogger.UserClientData.getFromTag(tag)?.let { userClientData -> - mapOf( - "userId" to userClientData.userId, - "clientId" to userClientData.clientId, - ) - } ?: emptyMap() - + val logInfo = KaliumLogger.LogAttributes.getInfoFromTagString(tag) + val userAccountData = mapOf( + "userId" to logInfo.userClientData?.userId, + "clientId" to logInfo.userClientData?.clientId, + ) + val attributes = mapOf( + "wireAccount" to userAccountData, + "tag" to logInfo.textTag + ) when (severity) { Severity.Debug -> logger.d(message, throwable, attributes) Severity.Info -> logger.i(message, throwable, attributes) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt index a4b14851a27..512f065bf8f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.appLogger import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.usecase.ObserveUsersTypingInConversationUseCase import com.wire.android.ui.navArgs diff --git a/kalium b/kalium index 630462f7b88..23d2fca42b5 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 630462f7b887f976855b6f254be628ed937717d2 +Subproject commit 23d2fca42b578a6aa12b02f8d17873038f116065 From e11de8af6b1cb12c2fde112cbfe9b0e0044eeee8 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 23 Feb 2024 11:56:48 +0100 Subject: [PATCH 057/134] chore: fix dev tag logging --- .../com/wire/android/util/DataDogLogger.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt index 1cd511a3d5f..055d776ff8f 100644 --- a/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/dev/kotlin/com/wire/android/util/DataDogLogger.kt @@ -36,13 +36,15 @@ object DataDogLogger : LogWriter() { .build() override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - val attributes = KaliumLogger.UserClientData.getFromTag(tag)?.let { userClientData -> - mapOf( - "userId" to userClientData.userId, - "clientId" to userClientData.clientId, - ) - } ?: emptyMap() - + val logInfo = KaliumLogger.LogAttributes.getInfoFromTagString(tag) + val userAccountData = mapOf( + "userId" to logInfo.userClientData?.userId, + "clientId" to logInfo.userClientData?.clientId, + ) + val attributes = mapOf( + "wireAccount" to userAccountData, + "tag" to logInfo.textTag + ) when (severity) { Severity.Debug -> logger.d(message, throwable, attributes) Severity.Info -> logger.i(message, throwable, attributes) From c9dd5912efeace861927cc5bdc449ea250aeaef8 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Fri, 23 Feb 2024 13:56:42 +0100 Subject: [PATCH 058/134] chore: add structured logs to location and fix permission handling (WPB-6358) (#2734) --- .../main/kotlin/com/wire/android/AppLogger.kt | 37 +++++++++++++++++++ .../location/LocationPickerHelper.kt | 28 ++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/AppLogger.kt b/app/src/main/kotlin/com/wire/android/AppLogger.kt index 3fe35faa35d..81311175d12 100644 --- a/app/src/main/kotlin/com/wire/android/AppLogger.kt +++ b/app/src/main/kotlin/com/wire/android/AppLogger.kt @@ -19,16 +19,53 @@ package com.wire.android import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.KaliumLogger +import com.wire.kalium.util.serialization.toJsonElement private var appLoggerConfig = KaliumLogger.Config.disabled() + // App wide global logger, carefully initialized when our application is "onCreate" internal var appLogger = KaliumLogger.disabled() + object AppLogger { fun init(config: KaliumLogger.Config) { appLoggerConfig = config appLogger = KaliumLogger(config = config, tag = "WireAppLogger") } + fun setLogLevel(level: KaliumLogLevel) { appLoggerConfig.setLogLevel(level) } } + +object AppJsonStyledLogger { + /** + * Log a structured JSON message, in the following format: + * + * Example: + * ``` + * leadingMessage: {map of key-value pairs represented as JSON} + * ``` + * @param level the severity of the log message + * @param error optional - the throwable error to be logged + * @param leadingMessage the leading message useful for later grok parsing + * @param jsonStringKeyValues the map of key-value pairs to be logged in a valid JSON format + */ + fun log( + level: KaliumLogLevel, + error: Throwable? = null, + leadingMessage: String, + jsonStringKeyValues: Map + ) = with(appLogger) { + val logJson = jsonStringKeyValues.toJsonElement() + val sanitizedLeadingMessage = if (leadingMessage.endsWith(":")) leadingMessage else "$leadingMessage:" + val logMessage = "$sanitizedLeadingMessage $logJson" + when (level) { + KaliumLogLevel.DEBUG -> d(logMessage) + KaliumLogLevel.INFO -> i(logMessage) + KaliumLogLevel.WARN -> w(logMessage) + KaliumLogLevel.ERROR -> e(logMessage, throwable = error) + KaliumLogLevel.VERBOSE -> v(logMessage) + KaliumLogLevel.DISABLED -> Unit + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt index 22c11a61c86..72595f0e151 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt @@ -27,7 +27,9 @@ import androidx.core.location.LocationManagerCompat import com.google.android.gms.location.LocationServices import com.google.android.gms.location.Priority import com.google.android.gms.tasks.CancellationTokenSource +import com.wire.android.AppJsonStyledLogger import com.wire.android.util.extension.isGoogleServicesAvailable +import com.wire.kalium.logger.KaliumLogLevel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.tasks.await import javax.inject.Inject @@ -57,12 +59,25 @@ class LocationPickerHelper @Inject constructor(@ApplicationContext val context: @SuppressLint("MissingPermission") private suspend fun getLocationWithGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { if (isLocationServicesEnabled()) { + AppJsonStyledLogger.log( + level = KaliumLogLevel.INFO, + leadingMessage = "GetLocation", + jsonStringKeyValues = mapOf("isUsingGms" to true) + ) val locationProvider = LocationServices.getFusedLocationProviderClient(context) val currentLocation = locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token).await() val address = Geocoder(context).getFromLocation(currentLocation.latitude, currentLocation.longitude, 1).orEmpty() onSuccess(GeoLocatedAddress(address.firstOrNull(), currentLocation)) } else { + AppJsonStyledLogger.log( + level = KaliumLogLevel.WARN, + leadingMessage = "GetLocation", + jsonStringKeyValues = mapOf( + "isUsingGms" to true, + "error" to "Location services are not enabled" + ) + ) onError() } } @@ -70,6 +85,11 @@ class LocationPickerHelper @Inject constructor(@ApplicationContext val context: @SuppressLint("MissingPermission") private fun getLocationWithoutGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { if (isLocationServicesEnabled()) { + AppJsonStyledLogger.log( + level = KaliumLogLevel.INFO, + leadingMessage = "GetLocation", + jsonStringKeyValues = mapOf("isUsingGms" to false) + ) val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val networkLocationListener: LocationListener = object : LocationListener { override fun onLocationChanged(location: Location) { @@ -80,6 +100,14 @@ class LocationPickerHelper @Inject constructor(@ApplicationContext val context: } locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, networkLocationListener) } else { + AppJsonStyledLogger.log( + level = KaliumLogLevel.WARN, + leadingMessage = "GetLocation", + jsonStringKeyValues = mapOf( + "isUsingGms" to false, + "error" to "Location services are not enabled" + ) + ) onError() } } From 8e38267974e47e0f341bf0d408a04be5a59fbd31 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 23 Feb 2024 17:22:34 +0100 Subject: [PATCH 059/134] chore: update app version --- build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt index 8786a24986d..6d17501b1ad 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt @@ -25,6 +25,6 @@ object AndroidSdk { object AndroidApp { const val id = "com.wire.android" - const val versionName = "4.6.0" + const val versionName = "4.6.1" val versionCode = Versionizer().versionCode } From 295d6188d592ff1b26e31fbef9363e9e4a7bd725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:12:21 +0100 Subject: [PATCH 060/134] chore: fix tag logging for internal and staging flavors (#2739) --- .../com/wire/android/util/DataDogLogger.kt | 16 +++++++++------- .../com/wire/android/util/DataDogLogger.kt | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt index 1cd511a3d5f..055d776ff8f 100644 --- a/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/internal/kotlin/com/wire/android/util/DataDogLogger.kt @@ -36,13 +36,15 @@ object DataDogLogger : LogWriter() { .build() override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - val attributes = KaliumLogger.UserClientData.getFromTag(tag)?.let { userClientData -> - mapOf( - "userId" to userClientData.userId, - "clientId" to userClientData.clientId, - ) - } ?: emptyMap() - + val logInfo = KaliumLogger.LogAttributes.getInfoFromTagString(tag) + val userAccountData = mapOf( + "userId" to logInfo.userClientData?.userId, + "clientId" to logInfo.userClientData?.clientId, + ) + val attributes = mapOf( + "wireAccount" to userAccountData, + "tag" to logInfo.textTag + ) when (severity) { Severity.Debug -> logger.d(message, throwable, attributes) Severity.Info -> logger.i(message, throwable, attributes) diff --git a/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt b/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt index 1cd511a3d5f..055d776ff8f 100644 --- a/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt +++ b/app/src/staging/kotlin/com/wire/android/util/DataDogLogger.kt @@ -36,13 +36,15 @@ object DataDogLogger : LogWriter() { .build() override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - val attributes = KaliumLogger.UserClientData.getFromTag(tag)?.let { userClientData -> - mapOf( - "userId" to userClientData.userId, - "clientId" to userClientData.clientId, - ) - } ?: emptyMap() - + val logInfo = KaliumLogger.LogAttributes.getInfoFromTagString(tag) + val userAccountData = mapOf( + "userId" to logInfo.userClientData?.userId, + "clientId" to logInfo.userClientData?.clientId, + ) + val attributes = mapOf( + "wireAccount" to userAccountData, + "tag" to logInfo.textTag + ) when (severity) { Severity.Debug -> logger.d(message, throwable, attributes) Severity.Info -> logger.i(message, throwable, attributes) From fadfac320ab63b51277037d7679b8c427efed835 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 27 Feb 2024 13:45:32 +0200 Subject: [PATCH 061/134] fix: release: Enrolling E2EI crash [WPB-6788] (#2728) --- .../di/GetE2EICertificateUseCaseProvider.kt | 48 -------- .../feature/e2ei/GetE2EICertificateUseCase.kt | 82 ------------- .../com/wire/android/ui/WireActivity.kt | 16 ++- .../wire/android/ui/debug/DebugDataOptions.kt | 57 ++++++--- .../ui/e2eiEnrollment/E2EIEnrollmentScreen.kt | 20 ++- .../e2eiEnrollment/E2EIEnrollmentViewModel.kt | 54 ++++---- .../ui/e2eiEnrollment/GetE2EICertificateUI.kt | 56 +++++++++ .../GetE2EICertificateViewModel.kt | 94 ++++++++++++++ .../com/wire/android/ui/home/E2EIDialogs.kt | 14 +-- .../wire/android/ui/home/FeatureFlagState.kt | 3 +- .../sync/FeatureFlagNotificationViewModel.kt | 116 +++++++++--------- .../settings/devices/DeviceDetailsScreen.kt | 16 ++- .../devices/DeviceDetailsViewModel.kt | 41 ++++--- .../devices/model/DeviceDetailsState.kt | 3 +- .../FeatureFlagNotificationViewModelTest.kt | 9 +- .../devices/DeviceDetailsViewModelTest.kt | 11 +- kalium | 2 +- 17 files changed, 356 insertions(+), 286 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/di/GetE2EICertificateUseCaseProvider.kt delete mode 100644 app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt diff --git a/app/src/main/kotlin/com/wire/android/di/GetE2EICertificateUseCaseProvider.kt b/app/src/main/kotlin/com/wire/android/di/GetE2EICertificateUseCaseProvider.kt deleted file mode 100644 index d358507f8d7..00000000000 --- a/app/src/main/kotlin/com/wire/android/di/GetE2EICertificateUseCaseProvider.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 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.di - -import android.content.Context -import com.wire.android.feature.e2ei.GetE2EICertificateUseCase -import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.qualifiers.ApplicationContext - -class GetE2EICertificateUseCaseProvider @AssistedInject constructor( - @KaliumCoreLogic private val coreLogic: CoreLogic, - @ApplicationContext private val applicationContext: Context, - @Assisted private val userId: UserId, - @Assisted private val dispatcherProvider: DispatcherProvider -) { - - val useCase: GetE2EICertificateUseCase - get() = GetE2EICertificateUseCase( - enrollE2EI = coreLogic.getSessionScope(userId).enrollE2EI, - applicationContext = applicationContext, - dispatcherProvider = dispatcherProvider - ) - - @AssistedFactory - interface Factory { - fun create(userId: UserId, dispatcherProvider: DispatcherProvider): GetE2EICertificateUseCaseProvider - } -} diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt deleted file mode 100644 index 2693a11cee0..00000000000 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 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.feature.e2ei - -import android.content.Context -import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.extension.getActivity -import com.wire.kalium.logic.E2EIFailure -import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult -import com.wire.kalium.logic.feature.e2ei.usecase.EnrollE2EIUseCase -import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.fold -import dagger.hilt.android.qualifiers.ApplicationContext -import com.wire.kalium.logic.functional.left -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import javax.inject.Inject - -class GetE2EICertificateUseCase @Inject constructor( - private val enrollE2EI: EnrollE2EIUseCase, - @ApplicationContext private val applicationContext: Context, - val dispatcherProvider: DispatcherProvider -) { - - private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.default()) - private lateinit var initialEnrollmentResult: E2EIEnrollmentResult.Initialized - lateinit var enrollmentResultHandler: (Either) -> Unit - - operator fun invoke( - isNewClient: Boolean, - enrollmentResultHandler: (Either) -> Unit - ) { - this.enrollmentResultHandler = enrollmentResultHandler - scope.launch { - enrollE2EI.initialEnrollment(isNewClientRegistration = isNewClient).fold({ - enrollmentResultHandler(Either.Left(it)) - }, { - initialEnrollmentResult = it - OAuthUseCase(applicationContext, it.target, it.oAuthClaims, it.oAuthState).launch( - applicationContext.getActivity()!!.activityResultRegistry, - ::oAuthResultHandler - ) - }) - } - } - - private fun oAuthResultHandler(oAuthResult: OAuthUseCase.OAuthResult) { - scope.launch { - when (oAuthResult) { - is OAuthUseCase.OAuthResult.Success -> { - enrollmentResultHandler( - enrollE2EI.finalizeEnrollment( - oAuthResult.idToken, - oAuthResult.authState, - initialEnrollmentResult - ) - ) - } - - is OAuthUseCase.OAuthResult.Failed -> { - enrollmentResultHandler(E2EIFailure.OAuth(oAuthResult.reason).left()) - } - } - } - } -} diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 19c7a407c01..3d4a4bc556a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -80,6 +80,7 @@ import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination +import com.wire.android.ui.e2eiEnrollment.GetE2EICertificateUI import com.wire.android.ui.home.E2EICertificateRevokedDialog import com.wire.android.ui.home.E2EIRequiredDialog import com.wire.android.ui.home.E2EIResultDialog @@ -165,8 +166,8 @@ class WireActivity : AppCompatActivity() { InitialAppState.NOT_MIGRATED -> MigrationScreenDestination InitialAppState.NOT_LOGGED_IN -> WelcomeScreenDestination InitialAppState.ENROLL_E2EI -> E2EIEnrollmentScreenDestination - InitialAppState.LOGGED_IN -> HomeScreenDestination - } + InitialAppState.LOGGED_IN -> HomeScreenDestination + } appLogger.i("$TAG composable content") setComposableContent(startDestination) { appLogger.i("$TAG splash hide") @@ -370,7 +371,7 @@ class WireActivity : AppCompatActivity() { E2EIRequiredDialog( e2EIRequired = e2EIRequired, isE2EILoading = isE2EILoading, - getCertificate = { featureFlagNotificationViewModel.getE2EICertificate(it) }, + getCertificate = featureFlagNotificationViewModel::enrollE2EICertificate, snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog ) } @@ -385,7 +386,7 @@ class WireActivity : AppCompatActivity() { e2EIResult?.let { E2EIResultDialog( result = e2EIResult, - updateCertificate = { featureFlagNotificationViewModel.getE2EICertificate(it) }, + updateCertificate = featureFlagNotificationViewModel::enrollE2EICertificate, snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog, openCertificateDetails = { navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(it))) }, dismissSuccessDialog = featureFlagNotificationViewModel::dismissSuccessE2EIdDialog, @@ -439,6 +440,13 @@ class WireActivity : AppCompatActivity() { featureFlagNotificationViewModel::dismissCallEndedBecauseOfConversationDegraded ) } + + if (startGettingE2EICertificate) { + GetE2EICertificateUI( + enrollmentResultHandler = { featureFlagNotificationViewModel.handleE2EIEnrollmentResult(it) }, + isNewClient = false + ) + } } } 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 df7480b6e0e..667437dc59c 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 @@ -37,7 +37,6 @@ import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.CurrentAccount -import com.wire.android.feature.e2ei.GetE2EICertificateUseCase import com.wire.android.migration.failure.UserMigrationStatus import com.wire.android.model.Clickable import com.wire.android.ui.common.RowItemTemplate @@ -47,6 +46,7 @@ import com.wire.android.ui.common.WireDialogButtonType import com.wire.android.ui.common.WireSwitch import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.dimensions +import com.wire.android.ui.e2eiEnrollment.GetE2EICertificateUI import com.wire.android.ui.home.conversationslist.common.FolderHeader import com.wire.android.ui.home.settings.SettingsItem import com.wire.android.ui.theme.wireColorScheme @@ -55,11 +55,14 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.getDeviceIdString import com.wire.android.util.getGitBuildId import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.E2EIFailure import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.debug.DisableEventProcessingUseCase import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountResult import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase +import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase @@ -80,7 +83,8 @@ data class DebugDataOptionsState( val debugId: String = "null", val commitish: String = "null", val certificate: String = "null", - val showCertificate: Boolean = false + val showCertificate: Boolean = false, + val startGettingE2EICertificate: Boolean = false ) @Suppress("LongParameterList") @@ -94,7 +98,6 @@ class DebugDataOptionsViewModel private val mlsKeyPackageCountUseCase: MLSKeyPackageCountUseCase, private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase, private val disableEventProcessingUseCase: DisableEventProcessingUseCase, - private val e2eiCertificateUseCase: GetE2EICertificateUseCase ) : ViewModel() { var state by mutableStateOf( @@ -126,23 +129,31 @@ class DebugDataOptionsViewModel } fun enrollE2EICertificate() { - e2eiCertificateUseCase(false) { result -> - result.fold({ + state = state.copy(startGettingE2EICertificate = true) + } + + fun handleE2EIEnrollmentResult(result: Either) { + result.fold({ + state = state.copy( + certificate = (it as E2EIFailure.OAuth).reason, + showCertificate = true, + startGettingE2EICertificate = false + ) + }, { + if (it is E2EIEnrollmentResult.Finalized) { state = state.copy( - certificate = it.toString(), showCertificate = true + certificate = it.certificate, + showCertificate = true, + startGettingE2EICertificate = false ) - }, { - state = if (it is E2EIEnrollmentResult.Finalized) { - state.copy( - certificate = it.certificate, showCertificate = true - ) - } else { - state.copy( - certificate = it.toString(), showCertificate = true - ) - } - }) - } + } else { + state.copy( + certificate = it.toString(), + showCertificate = true, + startGettingE2EICertificate = false + ) + } + }) } fun dismissCertificateDialog() { @@ -236,6 +247,7 @@ fun DebugDataOptions( onManualMigrationPressed = { onManualMigrationPressed(viewModel.currentAccount) }, onDisableEventProcessingChange = viewModel::disableEventProcessing, enrollE2EICertificate = viewModel::enrollE2EICertificate, + handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, dismissCertificateDialog = viewModel::dismissCertificateDialog ) } @@ -253,6 +265,7 @@ fun DebugDataOptionsContent( onForceUpdateApiVersions: () -> Unit, onManualMigrationPressed: () -> Unit, enrollE2EICertificate: () -> Unit, + handleE2EIEnrollmentResult: (Either) -> Unit, dismissCertificateDialog: () -> Unit ) { Column { @@ -349,6 +362,13 @@ fun DebugDataOptionsContent( onManualMigrationClicked = onManualMigrationPressed ) } + + if (state.startGettingE2EICertificate) { + GetE2EICertificateUI( + enrollmentResultHandler = { handleE2EIEnrollmentResult(it) }, + isNewClient = false + ) + } } } @@ -603,6 +623,7 @@ fun PreviewOtherDebugOptions() { onRestartSlowSyncForRecovery = {}, onManualMigrationPressed = {}, enrollE2EICertificate = {}, + handleE2EIEnrollmentResult = {}, dismissCertificateDialog = {}, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt index ffd48c7cacb..55f536fd97d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt @@ -64,6 +64,9 @@ import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult +import com.wire.kalium.logic.functional.Either @RootNavGraph @Destination( @@ -83,7 +86,8 @@ fun E2EIEnrollmentScreen( viewModel.finalizeMLSClient() }, dismissErrorDialog = viewModel::dismissErrorDialog, - enrollE2EICertificate = { viewModel.enrollE2EICertificate() }, + enrollE2EICertificate = viewModel::enrollE2EICertificate, + handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, openCertificateDetails = { navigator.navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(state.certificate))) }, @@ -99,6 +103,7 @@ private fun E2EIEnrollmentScreenContent( dismissSuccess: () -> Unit, dismissErrorDialog: () -> Unit, enrollE2EICertificate: () -> Unit, + handleE2EIEnrollmentResult: (Either) -> Unit, openCertificateDetails: () -> Unit, onBackButtonClicked: () -> Unit, onCancelEnrollmentClicked: () -> Unit, @@ -201,6 +206,13 @@ private fun E2EIEnrollmentScreenContent( dismissDialog = dismissSuccess ) } + + if (state.startGettingE2EICertificate) { + GetE2EICertificateUI( + enrollmentResultHandler = { handleE2EIEnrollmentResult(it) }, + isNewClient = true + ) + } } } @@ -208,7 +220,7 @@ private fun E2EIEnrollmentScreenContent( @Composable fun previewE2EIEnrollmentScreenContent() { WireTheme { - E2EIEnrollmentScreenContent(E2EIEnrollmentState(), {}, {}, {}, {}, {}, {}) { } + E2EIEnrollmentScreenContent(E2EIEnrollmentState(), {}, {}, {}, {}, {}, {}, {}) { } } } @@ -216,7 +228,7 @@ fun previewE2EIEnrollmentScreenContent() { @Composable fun previewE2EIEnrollmentScreenContentWithSuccess() { WireTheme { - E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollSuccess = true), {}, {}, {}, {}, {}, {}) { } + E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollSuccess = true), {}, {}, {}, {}, {}, {}, {}) { } } } @@ -224,6 +236,6 @@ fun previewE2EIEnrollmentScreenContentWithSuccess() { @Composable fun previewE2EIEnrollmentScreenContentWithError() { WireTheme { - E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollError = true), {}, {}, {}, {}, {}, {}) { } + E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollError = true), {}, {}, {}, {}, {}, {}, {}) { } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt index 348a96a7510..63d99974623 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt @@ -26,12 +26,13 @@ import com.wire.android.appLogger import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountActions import com.wire.android.feature.SwitchAccountParam -import com.wire.android.feature.e2ei.GetE2EICertificateUseCase +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.session.DeleteSessionUseCase +import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -43,12 +44,12 @@ data class E2EIEnrollmentState( val isLoading: Boolean = false, val isCertificateEnrollError: Boolean = false, val isCertificateEnrollSuccess: Boolean = false, - val showCancelLoginDialog: Boolean = false + val showCancelLoginDialog: Boolean = false, + val startGettingE2EICertificate: Boolean = false ) @HiltViewModel class E2EIEnrollmentViewModel @Inject constructor( - private val e2eiCertificateUseCase: GetE2EICertificateUseCase, private val finalizeMLSClientAfterE2EIEnrollment: FinalizeMLSClientAfterE2EIEnrollment, private val currentSession: CurrentSessionUseCase, private val deleteSession: DeleteSessionUseCase, @@ -78,9 +79,11 @@ class E2EIEnrollmentViewModel @Inject constructor( is CurrentSessionResult.Success -> { deleteSession(it.accountInfo.userId) } + is CurrentSessionResult.Failure.Generic -> { appLogger.e("failed to delete session") } + CurrentSessionResult.Failure.SessionNotFound -> { appLogger.e("session not found") } @@ -93,30 +96,35 @@ class E2EIEnrollmentViewModel @Inject constructor( } } } + fun enrollE2EICertificate() { - state = state.copy(isLoading = true) - e2eiCertificateUseCase(true) { result -> - result.fold({ + state = state.copy(isLoading = true, startGettingE2EICertificate = true) + } + + fun handleE2EIEnrollmentResult(result: Either) { + result.fold({ + state = state.copy( + isLoading = false, + isCertificateEnrollError = true, + startGettingE2EICertificate = false + ) + }, { + if (it is E2EIEnrollmentResult.Finalized) { state = state.copy( + certificate = it.certificate, + isCertificateEnrollSuccess = true, + isCertificateEnrollError = false, isLoading = false, - isCertificateEnrollError = true + startGettingE2EICertificate = false ) - }, { - if (it is E2EIEnrollmentResult.Finalized) { - state = state.copy( - certificate = it.certificate, - isCertificateEnrollSuccess = true, - isCertificateEnrollError = false, - isLoading = false - ) - } else { - state = state.copy( - isLoading = false, - isCertificateEnrollError = true - ) - } - }) - } + } else { + state = state.copy( + isLoading = false, + isCertificateEnrollError = true, + startGettingE2EICertificate = false + ) + } + }) } fun dismissErrorDialog() { diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt new file mode 100644 index 00000000000..ce8f36b3d5f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt @@ -0,0 +1,56 @@ +/* + * Wire + * Copyright (C) 2024 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.e2eiEnrollment + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.e2ei.OAuthUseCase +import com.wire.android.util.extension.getActivity +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult +import com.wire.kalium.logic.functional.Either +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@Composable +fun GetE2EICertificateUI( + enrollmentResultHandler: (Either) -> Unit, + isNewClient: Boolean, + viewModel: GetE2EICertificateViewModel = hiltViewModel() +) { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + // FIXME issue happens when this UI is called from WireActivity: WebView is just canceled by itself + LaunchedEffect(Unit) { + viewModel.requestOAuthFlow.onEach { + OAuthUseCase(context, it.target, it.oAuthClaims, it.oAuthState).launch( + context.getActivity()!!.activityResultRegistry + ) { result -> viewModel.handleOAuthResult(result, it) } + }.launchIn(coroutineScope) + } + + LaunchedEffect(Unit) { + viewModel.enrollmentResultFlow.onEach { enrollmentResultHandler(it) }.launchIn(coroutineScope) + } + + viewModel.getCertificate(isNewClient) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt new file mode 100644 index 00000000000..c179dd2856d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt @@ -0,0 +1,94 @@ +/* + * Wire + * Copyright (C) 2024 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.e2eiEnrollment + +import androidx.lifecycle.ViewModel +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.e2ei.OAuthUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.E2EIFailure +import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult +import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.fold +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GetE2EICertificateViewModel @Inject constructor( + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val currentSession: CurrentSessionUseCase, + val dispatcherProvider: DispatcherProvider +) : ViewModel() { + + private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.default()) + + val requestOAuthFlow = MutableSharedFlow() + val enrollmentResultFlow = MutableSharedFlow>() + + fun handleOAuthResult(oAuthResult: OAuthUseCase.OAuthResult, initialEnrollmentResult: E2EIEnrollmentResult.Initialized) { + scope.launch { + when (oAuthResult) { + is OAuthUseCase.OAuthResult.Success -> finalizeEnrollment(oAuthResult, initialEnrollmentResult) + + is OAuthUseCase.OAuthResult.Failed -> enrollmentResultFlow.emit(Either.Left(E2EIFailure.OAuth(oAuthResult.reason))) + } + } + } + + fun getCertificate(isNewClient: Boolean) { + scope.launch { + val currentSessionResult = currentSession() + if (currentSessionResult is CurrentSessionResult.Success && currentSessionResult.accountInfo.isValid()) { + coreLogic.getSessionScope(currentSessionResult.accountInfo.userId) + .enrollE2EI + .initialEnrollment(isNewClientRegistration = isNewClient) + .fold({ + enrollmentResultFlow.emit(Either.Left(it)) + }, { + if (it is E2EIEnrollmentResult.Initialized) requestOAuthFlow.emit(it) + else enrollmentResultFlow.emit(Either.Right(it)) + }) + } + } + } + + private suspend fun finalizeEnrollment( + oAuthResult: OAuthUseCase.OAuthResult.Success, + initialEnrollmentResult: E2EIEnrollmentResult.Initialized + ) { + val currentSessionResult = currentSession() + + if (currentSessionResult is CurrentSessionResult.Success && currentSessionResult.accountInfo.isValid()) { + val enrollmentResult = coreLogic.getSessionScope(currentSessionResult.accountInfo.userId) + .enrollE2EI.finalizeEnrollment( + oAuthResult.idToken, + oAuthResult.authState, + initialEnrollmentResult + ) + enrollmentResultFlow.emit(enrollmentResult) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt index 98cc6c4acf5..eedbc2a1bad 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt @@ -52,29 +52,29 @@ import kotlin.time.Duration.Companion.seconds fun E2EIRequiredDialog( e2EIRequired: FeatureFlagState.E2EIRequired, isE2EILoading: Boolean, - getCertificate: (FeatureFlagState.E2EIRequired) -> Unit, + getCertificate: () -> Unit, snoozeDialog: (FeatureFlagState.E2EIRequired.WithGracePeriod) -> Unit, ) { when (e2EIRequired) { FeatureFlagState.E2EIRequired.NoGracePeriod.Create -> E2EIRequiredNoSnoozeDialog( isLoading = isE2EILoading, - getCertificate = { getCertificate(e2EIRequired) } + getCertificate = getCertificate ) FeatureFlagState.E2EIRequired.NoGracePeriod.Renew -> E2EIRenewNoSnoozeDialog( isLoading = isE2EILoading, - updateCertificate = { getCertificate(e2EIRequired) } + updateCertificate = getCertificate ) is FeatureFlagState.E2EIRequired.WithGracePeriod.Create -> E2EIRequiredWithSnoozeDialog( isLoading = isE2EILoading, - getCertificate = { getCertificate(e2EIRequired) }, + getCertificate = getCertificate, snoozeDialog = { snoozeDialog(e2EIRequired) } ) is FeatureFlagState.E2EIRequired.WithGracePeriod.Renew -> E2EIRenewWithSnoozeDialog( isLoading = isE2EILoading, - updateCertificate = { getCertificate(e2EIRequired) }, + updateCertificate = getCertificate, snoozeDialog = { snoozeDialog(e2EIRequired) } ) } @@ -84,7 +84,7 @@ fun E2EIRequiredDialog( fun E2EIResultDialog( result: FeatureFlagState.E2EIResult, isE2EILoading: Boolean, - updateCertificate: (FeatureFlagState.E2EIRequired) -> Unit, + updateCertificate: () -> Unit, snoozeDialog: (FeatureFlagState.E2EIRequired.WithGracePeriod) -> Unit, openCertificateDetails: (String) -> Unit, dismissSuccessDialog: () -> Unit @@ -93,7 +93,7 @@ fun E2EIResultDialog( is FeatureFlagState.E2EIResult.Failure -> E2EIRenewErrorDialog( e2EIRequired = result.e2EIRequired, isE2EILoading = isE2EILoading, - updateCertificate = { updateCertificate(result.e2EIRequired) }, + updateCertificate = updateCertificate, snoozeDialog = snoozeDialog ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt b/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt index 33921138561..ed540612adf 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt @@ -37,7 +37,8 @@ data class FeatureFlagState( val e2EISnoozeInfo: E2EISnooze? = null, val e2EIResult: E2EIResult? = null, val isE2EILoading: Boolean = false, - val showCallEndedBecauseOfConversationDegraded: Boolean = false + val showCallEndedBecauseOfConversationDegraded: Boolean = false, + val startGettingE2EICertificate: Boolean = false ) { enum class SharingRestrictedState { NONE, NO_USER, RESTRICTED_IN_TEAM diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 25591b4e118..916e1a12017 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -25,14 +25,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.datastore.GlobalDataStore -import com.wire.android.di.GetE2EICertificateUseCaseProvider import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase import com.wire.android.ui.home.FeatureFlagState import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration -import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.data.message.TeamSelfDeleteTimer @@ -42,6 +41,7 @@ import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.E2EIRequiredResult +import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.coroutineScope @@ -58,8 +58,6 @@ class FeatureFlagNotificationViewModel @Inject constructor( private val currentSessionFlow: CurrentSessionFlowUseCase, private val globalDataStore: GlobalDataStore, private val disableAppLockUseCase: DisableAppLockUseCase, - private val getE2EICertificateUseCaseProvider: GetE2EICertificateUseCaseProvider.Factory, - private val dispatcherProvider: DispatcherProvider ) : ViewModel() { var featureFlagState by mutableStateOf(FeatureFlagState()) @@ -92,12 +90,14 @@ class FeatureFlagNotificationViewModel @Inject constructor( fileSharingRestrictedState = FeatureFlagState.SharingRestrictedState.NO_USER ) } + currentSessionResult is CurrentSessionResult.Success && !currentSessionResult.accountInfo.isValid() -> { appLogger.i("$TAG: Invalid current session") featureFlagState = FeatureFlagState( // invalid session, clear feature flag state to default and set NO_USER fileSharingRestrictedState = FeatureFlagState.SharingRestrictedState.NO_USER ) } + currentSessionResult is CurrentSessionResult.Success && currentSessionResult.accountInfo.isValid() -> { featureFlagState = FeatureFlagState() // new session, clear feature flag state to default and wait until synced currentSessionResult.accountInfo.userId.let { userId -> @@ -152,35 +152,35 @@ class FeatureFlagNotificationViewModel @Inject constructor( } private suspend fun setGuestRoomLinkFeatureFlag(userId: UserId) { - coreLogic.getSessionScope(userId).observeGuestRoomLinkFeatureFlag() - .collect { guestRoomLinkStatus -> - guestRoomLinkStatus.isGuestRoomLinkEnabled?.let { - featureFlagState = featureFlagState.copy(isGuestRoomLinkEnabled = it) - } - guestRoomLinkStatus.isStatusChanged?.let { - featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = it) - } + coreLogic.getSessionScope(userId).observeGuestRoomLinkFeatureFlag() + .collect { guestRoomLinkStatus -> + guestRoomLinkStatus.isGuestRoomLinkEnabled?.let { + featureFlagState = featureFlagState.copy(isGuestRoomLinkEnabled = it) } - } + guestRoomLinkStatus.isStatusChanged?.let { + featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = it) + } + } + } private suspend fun setTeamAppLockFeatureFlag(userId: UserId) { - coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver() - .distinctUntilChanged() - .collectLatest { appLockConfig -> - appLockConfig?.isStatusChanged?.let { isStatusChanged -> - val shouldBlockApp = if (isStatusChanged) { - true - } else { - (!isUserAppLockSet() && appLockConfig.isEnforced) - } - - featureFlagState = featureFlagState.copy( - isTeamAppLockEnabled = appLockConfig.isEnforced, - shouldShowTeamAppLockDialog = shouldBlockApp - ) + coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver() + .distinctUntilChanged() + .collectLatest { appLockConfig -> + appLockConfig?.isStatusChanged?.let { isStatusChanged -> + val shouldBlockApp = if (isStatusChanged) { + true + } else { + (!isUserAppLockSet() && appLockConfig.isEnforced) } + + featureFlagState = featureFlagState.copy( + isTeamAppLockEnabled = appLockConfig.isEnforced, + shouldShowTeamAppLockDialog = shouldBlockApp + ) } - } + } + } private suspend fun observeTeamSettingsSelfDeletionStatus(userId: UserId) { coreLogic.getSessionScope(userId).observeTeamSettingsSelfDeletionStatus() @@ -288,40 +288,36 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun isUserAppLockSet() = globalDataStore.isAppLockPasscodeSet() - fun getE2EICertificate(e2eiRequired: FeatureFlagState.E2EIRequired) { - featureFlagState = featureFlagState.copy(isE2EILoading = true) - currentUserId?.let { userId -> - getE2EICertificateUseCaseProvider.create( - userId = userId, - dispatcherProvider = dispatcherProvider + fun enrollE2EICertificate() { + featureFlagState = featureFlagState.copy(isE2EILoading = true, startGettingE2EICertificate = true) + } + + fun handleE2EIEnrollmentResult(result: Either) { + val e2eiRequired = featureFlagState.e2EIRequired + result.fold({ + featureFlagState = featureFlagState.copy( + isE2EILoading = false, + startGettingE2EICertificate = false, + e2EIRequired = null, + e2EIResult = e2eiRequired?.let { FeatureFlagState.E2EIResult.Failure(e2eiRequired) } ) - .useCase - .invoke( - isNewClient = false - ) { result -> - result.fold({ - featureFlagState = featureFlagState.copy( - isE2EILoading = false, - e2EIRequired = null, - e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) - ) - }, { - featureFlagState = if (it is E2EIEnrollmentResult.Finalized) { - featureFlagState.copy( - isE2EILoading = false, - e2EIRequired = null, - e2EIResult = FeatureFlagState.E2EIResult.Success(it.certificate) - ) - } else { - featureFlagState.copy( - isE2EILoading = false, - e2EIRequired = null, - e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) - ) - } - }) + }, { + featureFlagState = if (it is E2EIEnrollmentResult.Finalized) { + featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + startGettingE2EICertificate = false, + e2EIResult = FeatureFlagState.E2EIResult.Success(it.certificate) + ) + } else { + featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + startGettingE2EICertificate = false, + e2EIResult = e2eiRequired?.let { FeatureFlagState.E2EIResult.Failure(e2eiRequired) } + ) } - } + }) } fun snoozeE2EIdRequiredDialog(result: FeatureFlagState.E2EIRequired.WithGracePeriod) { diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index cb1ada5625a..ed2de9da14b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -74,6 +74,7 @@ import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.WireTopAppBarTitle import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination +import com.wire.android.ui.e2eiEnrollment.GetE2EICertificateUI import com.wire.android.ui.home.E2EIErrorWithDismissDialog import com.wire.android.ui.home.E2EISuccessDialog import com.wire.android.ui.home.conversationslist.common.FolderHeader @@ -87,7 +88,10 @@ import com.wire.android.util.extension.formatAsFingerPrint import com.wire.android.util.extension.formatAsString import com.wire.android.util.formatMediumDateTime import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult +import com.wire.kalium.logic.functional.Either @RootNavGraph @Destination( @@ -109,7 +113,8 @@ fun DeviceDetailsScreen( onErrorDialogDismiss = viewModel::clearDeleteClientError, onNavigateBack = navigator::navigateBack, onUpdateClientVerification = viewModel::onUpdateVerificationStatus, - enrollE2eiCertificate = viewModel::enrollE2eiCertificate, + enrollE2eiCertificate = viewModel::enrollE2EICertificate, + handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, onNavigateToE2eiCertificateDetailsScreen = { navigator.navigate( NavigationCommand(E2eiCertificateDetailsScreenDestination(it)) @@ -132,6 +137,7 @@ fun DeviceDetailsContent( onDialogDismiss: () -> Unit = {}, onErrorDialogDismiss: () -> Unit = {}, enrollE2eiCertificate: () -> Unit = {}, + handleE2EIEnrollmentResult: (Either) -> Unit, onUpdateClientVerification: (Boolean) -> Unit = {}, onEnrollE2EIErrorDismiss: () -> Unit = {}, onEnrollE2EISuccessDismiss: () -> Unit = {} @@ -286,6 +292,13 @@ fun DeviceDetailsContent( dismissDialog = onEnrollE2EISuccessDismiss ) } + + if (state.startGettingE2EICertificate) { + GetE2EICertificateUI( + enrollmentResultHandler = { handleE2EIEnrollmentResult(it) }, + isNewClient = false + ) + } } } @@ -569,6 +582,7 @@ fun PreviewDeviceDetailsScreen() { ), onPasswordChange = { }, enrollE2eiCertificate = { }, + handleE2EIEnrollmentResult = {}, onRemoveConfirm = { }, onDialogDismiss = { }, onErrorDialogDismiss = { } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index fb142a1571e..af5297be976 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -25,13 +25,13 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.di.CurrentAccount -import com.wire.android.feature.e2ei.GetE2EICertificateUseCase import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.authentication.devices.model.Device import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState import com.wire.android.ui.authentication.devices.remove.RemoveDeviceError import com.wire.android.ui.navArgs import com.wire.android.ui.settings.devices.model.DeviceDetailsState +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.client.ClientType import com.wire.kalium.logic.data.client.DeleteClientParam import com.wire.kalium.logic.data.conversation.ClientId @@ -50,6 +50,7 @@ import com.wire.kalium.logic.feature.user.GetUserInfoResult import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase +import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -68,7 +69,6 @@ class DeviceDetailsViewModel @Inject constructor( private val updateClientVerificationStatus: UpdateClientVerificationStatusUseCase, private val observeUserInfo: ObserveUserInfoUseCase, private val e2eiCertificate: GetE2eiCertificateUseCase, - private val enrolE2EICertificateUseCase: GetE2EICertificateUseCase, isE2EIEnabledUseCase: IsE2EIEnabledUseCase ) : SavedStateViewModel(savedStateHandle) { @@ -127,26 +127,29 @@ class DeviceDetailsViewModel @Inject constructor( } } - fun enrollE2eiCertificate() { - state = state.copy(isLoadingCertificate = true) - enrolE2EICertificateUseCase(false) { result -> - result.fold({ + fun enrollE2EICertificate() { + state = state.copy(isLoadingCertificate = true, startGettingE2EICertificate = true) + } + + fun handleE2EIEnrollmentResult(result: Either) { + result.fold({ + state = state.copy( + isLoadingCertificate = false, + startGettingE2EICertificate = false, + isE2EICertificateEnrollError = true, + ) + }, { + if (it is E2EIEnrollmentResult.Finalized) { + getE2eiCertificate() + state = state.copy(isE2EICertificateEnrollSuccess = true, startGettingE2EICertificate = false) + } else { state = state.copy( isLoadingCertificate = false, - isE2EICertificateEnrollError = true + isE2EICertificateEnrollError = true, + startGettingE2EICertificate = false, ) - }, { - if (it is E2EIEnrollmentResult.Finalized) { - getE2eiCertificate() - state = state.copy(isE2EICertificateEnrollSuccess = true) - } else { - state = state.copy( - isLoadingCertificate = false, - isE2EICertificateEnrollError = true - ) - } - }) - } + } + }) } private fun getClientFingerPrint() { diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt index 6400fa787aa..82aaaab16c5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt @@ -36,5 +36,6 @@ data class DeviceDetailsState( val isLoadingCertificate: Boolean = false, val isE2EICertificateEnrollSuccess: Boolean = false, val isE2EICertificateEnrollError: Boolean = false, - val isE2EIEnabled: Boolean = false + val isE2EIEnabled: Boolean = false, + val startGettingE2EICertificate: Boolean = false ) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index 51cf293943d..8e71c6d0401 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -18,9 +18,7 @@ package com.wire.android.ui.home.sync import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore -import com.wire.android.di.GetE2EICertificateUseCaseProvider import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase import com.wire.android.framework.TestUser @@ -301,9 +299,6 @@ class FeatureFlagNotificationViewModelTest { private inner class Arrangement { - @MockK - private lateinit var getE2EICertificateUseCaseProvider: GetE2EICertificateUseCaseProvider.Factory - @MockK lateinit var currentSessionFlow: CurrentSessionFlowUseCase @@ -336,9 +331,7 @@ class FeatureFlagNotificationViewModelTest { coreLogic = coreLogic, currentSessionFlow = currentSessionFlow, globalDataStore = globalDataStore, - disableAppLockUseCase = disableAppLockUseCase, - getE2EICertificateUseCaseProvider = getE2EICertificateUseCaseProvider, - dispatcherProvider = TestDispatcherProvider() + disableAppLockUseCase = disableAppLockUseCase ) } init { diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt index 3ef93208574..38b7d5ceaf6 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt @@ -20,7 +20,6 @@ package com.wire.android.ui.settings.devices import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension -import com.wire.android.feature.e2ei.GetE2EICertificateUseCase import com.wire.android.framework.TestClient import com.wire.android.framework.TestUser import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState @@ -276,12 +275,10 @@ class DeviceDetailsViewModelTest { .withClientDetailsResult(GetClientDetailsResult.Success(TestClient.CLIENT, true)) .arrange() - viewModel.enrollE2eiCertificate() + viewModel.enrollE2EICertificate() - coVerify { - arrangement.enrolE2EICertificateUseCase(any(), any()) - } assertTrue(viewModel.state.isLoadingCertificate) + assertTrue(viewModel.state.startGettingE2EICertificate) } private class Arrangement { @@ -310,9 +307,6 @@ class DeviceDetailsViewModelTest { @MockK lateinit var getE2eiCertificate: GetE2eiCertificateUseCase - @MockK - lateinit var enrolE2EICertificateUseCase: GetE2EICertificateUseCase - @MockK(relaxed = true) lateinit var onSuccess: () -> Unit @@ -332,7 +326,6 @@ class DeviceDetailsViewModelTest { currentUserId = currentUserId, observeUserInfo = observeUserInfo, e2eiCertificate = getE2eiCertificate, - enrolE2EICertificateUseCase = enrolE2EICertificateUseCase, isE2EIEnabledUseCase = isE2EIEnabledUseCase ) } diff --git a/kalium b/kalium index 23d2fca42b5..4d9c0d18438 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 23d2fca42b578a6aa12b02f8d17873038f116065 +Subproject commit 4d9c0d18438e00d50749282c3f111910df04abf3 From 30b80391e4586ded8c74e10cbbffd5672a544c57 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Tue, 27 Feb 2024 16:08:56 +0100 Subject: [PATCH 062/134] fix(e2ei): force login to idp to update certificate (WPB-6877) (#2742) --- .../wire/android/feature/e2ei/OAuthUseCase.kt | 31 ++++++++++--------- .../ui/e2eiEnrollment/GetE2EICertificateUI.kt | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt index 9f0c42c6f90..14e509fb0af 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt @@ -69,22 +69,25 @@ class OAuthUseCase( fun launch( activityResultRegistry: ActivityResultRegistry, - resultHandler: (OAuthResult) -> Unit + forceLoginFlow: Boolean, + resultHandler: (OAuthResult) -> Unit, ) { - authState.performActionWithFreshTokens(authorizationService) { _, idToken, exception -> - if (exception != null) { - appLogger.e( - message = "OAuthTokenRefreshManager: Error refreshing tokens, continue with login!", - throwable = exception - ) - launchLoginFlow(activityResultRegistry, resultHandler) - } else { - resultHandler( - OAuthResult.Success( - idToken.toString(), - authState.jsonSerializeString() + if (forceLoginFlow) { + launchLoginFlow(activityResultRegistry, resultHandler) + } else { + authState.performActionWithFreshTokens(authorizationService) { _, idToken, exception -> + if (exception != null) { + appLogger.e( + message = "OAuthTokenRefreshManager: Error refreshing tokens, continue with login!", throwable = exception ) - ) + launchLoginFlow(activityResultRegistry, resultHandler) + } else { + resultHandler( + OAuthResult.Success( + idToken.toString(), authState.jsonSerializeString() + ) + ) + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt index ce8f36b3d5f..65a869c0167 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt @@ -43,7 +43,7 @@ fun GetE2EICertificateUI( LaunchedEffect(Unit) { viewModel.requestOAuthFlow.onEach { OAuthUseCase(context, it.target, it.oAuthClaims, it.oAuthState).launch( - context.getActivity()!!.activityResultRegistry + context.getActivity()!!.activityResultRegistry, forceLoginFlow = true ) { result -> viewModel.handleOAuthResult(result, it) } }.launchIn(coroutineScope) } From 617beeb1c91f23baac5a8efc32282f978255f83c Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 28 Feb 2024 13:04:53 +0100 Subject: [PATCH 063/134] feat: Remove third party library for certificate decoding (WPB-6765) (#2746) --- .../ui/settings/devices/EndToEndIdentityCertificateItem.kt | 2 -- .../ui/settings/devices/model/DeviceDetailsState.kt | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt index 169cf1277ec..ebdff2f4be4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt @@ -198,7 +198,6 @@ fun PreviewEndToEndIdentityCertificateItem() { isE2eiCertificateActivated = true, isCurrentDevice = false, certificate = E2eiCertificate( - issuer = "Wire", status = CertificateStatus.VALID, serialNumber = "e5:d5:e6:75:7e:04:86:07:14:3c:a0:ed:9a:8d:e4:fd", certificateDetail = "" @@ -216,7 +215,6 @@ fun PreviewEndToEndIdentityCertificateSelfItem() { isE2eiCertificateActivated = true, isCurrentDevice = true, certificate = E2eiCertificate( - issuer = "Wire", status = CertificateStatus.VALID, serialNumber = "e5:d5:e6:75:7e:04:86:07:14:3c:a0:ed:9a:8d:e4:fd", certificateDetail = "" diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt index 82aaaab16c5..62d3de20001 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.settings.devices.model import com.wire.android.ui.authentication.devices.model.Device import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState import com.wire.android.ui.authentication.devices.remove.RemoveDeviceError +import com.wire.kalium.logic.feature.e2ei.CertificateStatus import com.wire.kalium.logic.feature.e2ei.E2eiCertificate data class DeviceDetailsState( @@ -31,7 +32,11 @@ data class DeviceDetailsState( val isSelfClient: Boolean = false, val userName: String? = null, val isE2eiCertificateActivated: Boolean = false, - val e2eiCertificate: E2eiCertificate = E2eiCertificate(), + val e2eiCertificate: E2eiCertificate = E2eiCertificate( + status = CertificateStatus.EXPIRED, + serialNumber = "", + certificateDetail = "" + ), val canBeRemoved: Boolean = false, val isLoadingCertificate: Boolean = false, val isE2EICertificateEnrollSuccess: Boolean = false, From 88c2846d082465323cf65893929e7dd97774d639 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 28 Feb 2024 13:12:07 +0100 Subject: [PATCH 064/134] chore: update kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 4d9c0d18438..e2b9a65de6d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 4d9c0d18438e00d50749282c3f111910df04abf3 +Subproject commit e2b9a65de6d850708e3d606b91c011c5cdcd0586 From 85593a6a8e91eb95fe2545e68768da4703187380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:43:03 +0100 Subject: [PATCH 065/134] fix: crash about persistent websocket being started from background [WPB-6551] (#2745) --- ...dStartPersistentWebSocketServiceUseCase.kt | 57 +++++++ .../android/ui/debug/StartServiceReceiver.kt | 47 +++--- ...rtPersistentWebSocketServiceUseCaseTest.kt | 157 ++++++++++++++++++ 3 files changed, 238 insertions(+), 23 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt create mode 100644 app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt diff --git a/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt new file mode 100644 index 00000000000..586ac8bc9ec --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt @@ -0,0 +1,57 @@ +/* + * Wire + * Copyright (C) 2024 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.feature + +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withTimeoutOrNull +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ShouldStartPersistentWebSocketServiceUseCase @Inject constructor( + @KaliumCoreLogic private val coreLogic: CoreLogic +) { + suspend operator fun invoke(): Result { + return coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus().let { result -> + when (result) { + is ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure -> Result.Failure + + is ObservePersistentWebSocketConnectionStatusUseCase.Result.Success -> { + val statusList = withTimeoutOrNull(TIMEOUT) { + val res = result.persistentWebSocketStatusListFlow.firstOrNull() + res + } + if (statusList != null && statusList.map { it.isPersistentWebSocketEnabled }.contains(true)) Result.Success(true) + else Result.Success(false) + } + } + } + } + + sealed class Result { + data object Failure : Result() + data class Success(val shouldStartPersistentWebSocketService: Boolean) : Result() + } + + companion object { + const val TIMEOUT = 10_000L + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt b/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt index 98f788c39f2..6b3e61470e0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt @@ -23,11 +23,9 @@ import android.content.Context import android.content.Intent import android.os.Build import com.wire.android.appLogger -import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.ShouldStartPersistentWebSocketServiceUseCase import com.wire.android.services.PersistentWebSocketService import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -43,8 +41,7 @@ class StartServiceReceiver : BroadcastReceiver() { lateinit var dispatcherProvider: DispatcherProvider @Inject - @KaliumCoreLogic - lateinit var coreLogic: CoreLogic + lateinit var shouldStartPersistentWebSocketServiceUseCase: ShouldStartPersistentWebSocketServiceUseCase private val scope by lazy { CoroutineScope(SupervisorJob() + dispatcherProvider.io()) @@ -52,32 +49,36 @@ class StartServiceReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { val persistentWebSocketServiceIntent = PersistentWebSocketService.newIntent(context) - appLogger.e("persistent web socket receiver") + appLogger.i("$TAG: onReceive called with action ${intent?.action}") scope.launch { - coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus().let { result -> - when (result) { - is ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure -> { - appLogger.e("Failure while fetching persistent web socket status flow from StartServiceReceiver") + shouldStartPersistentWebSocketServiceUseCase().let { + when (it) { + is ShouldStartPersistentWebSocketServiceUseCase.Result.Failure -> { + appLogger.e("$TAG: Failure while fetching persistent web socket status flow") } - - is ObservePersistentWebSocketConnectionStatusUseCase.Result.Success -> { - result.persistentWebSocketStatusListFlow.collect { status -> - if (status.map { it.isPersistentWebSocketEnabled }.contains(true)) { - appLogger.e("Starting PersistentWebsocket Service from StartServiceReceiver") - if (!PersistentWebSocketService.isServiceStarted) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context?.startForegroundService(persistentWebSocketServiceIntent) - } else { - context?.startService(persistentWebSocketServiceIntent) - } - } + is ShouldStartPersistentWebSocketServiceUseCase.Result.Success -> { + if (it.shouldStartPersistentWebSocketService) { + if (PersistentWebSocketService.isServiceStarted) { + appLogger.i("$TAG: PersistentWebsocketService already started, not starting again") } else { - context?.stopService(persistentWebSocketServiceIntent) + appLogger.i("$TAG: Starting PersistentWebsocketService") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context?.startForegroundService(persistentWebSocketServiceIntent) + } else { + context?.startService(persistentWebSocketServiceIntent) + } } + } else { + appLogger.i("$TAG: Stopping PersistentWebsocketService, no user with persistent web socket enabled found") + context?.stopService(persistentWebSocketServiceIntent) } } } } } } + + companion object { + const val TAG = "StartServiceReceiver" + } } diff --git a/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt new file mode 100644 index 00000000000..e4ac6157a8d --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCaseTest.kt @@ -0,0 +1,157 @@ +/* + * Wire + * Copyright (C) 2024 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.feature + +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.auth.PersistentWebSocketStatus +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Test + +class ShouldStartPersistentWebSocketServiceUseCaseTest { + + @Test + fun givenObservePersistentWebSocketStatusReturnsSuccessAndThereAreUsersWithPersistentFlagOn_whenInvoking_shouldReturnSuccessTrue() = + runTest { + // given + val (_, useCase) = Arrangement() + .withObservePersistentWebSocketConnectionStatusSuccess(flowOf(listOf(PersistentWebSocketStatus(userId, true)))) + .arrange() + // when + val result = useCase.invoke() + // then + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also { + assertEquals(true, it.shouldStartPersistentWebSocketService) + } + } + + @Test + fun givenObservePersistentWebSocketStatusReturnsSuccessAndThereAreNoUsersWithPersistentFlagOn_whenInvoking_shouldReturnSuccessFalse() = + runTest { + // given + val (_, useCase) = Arrangement() + .withObservePersistentWebSocketConnectionStatusSuccess(flowOf(listOf(PersistentWebSocketStatus(userId, false)))) + .arrange() + // when + val result = useCase.invoke() + // then + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also { + assertEquals(false, it.shouldStartPersistentWebSocketService) + } + } + + @Test + fun givenObservePersistentWebSocketStatusReturnsSuccessAndThereAreNoUsers_whenInvoking_shouldReturnSuccessFalse() = + runTest { + // given + val (_, useCase) = Arrangement() + .withObservePersistentWebSocketConnectionStatusSuccess(flowOf(emptyList())) + .arrange() + // when + val result = useCase.invoke() + // then + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also { + assertEquals(false, it.shouldStartPersistentWebSocketService) + } + } + + @Test + fun givenObservePersistentWebSocketStatusReturnsSuccessAndTheFlowIsEmpty_whenInvoking_shouldReturnSuccessFalse() = + runTest { + // given + val (_, useCase) = Arrangement() + .withObservePersistentWebSocketConnectionStatusSuccess(emptyFlow()) + .arrange() + // when + val result = useCase.invoke() + // then + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also { + assertEquals(false, it.shouldStartPersistentWebSocketService) + } + } + + @Test + fun givenObservePersistentWebSocketStatusReturnsSuccessAndFlowTimesOut_whenInvoking_shouldReturnSuccessFalse() = + runTest { + // given + val sharedFlow = MutableSharedFlow>() // shared flow doesn't close so we can test the timeout + val (_, useCase) = Arrangement() + .withObservePersistentWebSocketConnectionStatusSuccess(sharedFlow) + .arrange() + // when + val result = useCase.invoke() + advanceTimeBy(ShouldStartPersistentWebSocketServiceUseCase.TIMEOUT + 1000L) + // then + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Success::class.java, result).also { + assertEquals(false, it.shouldStartPersistentWebSocketService) + } + } + + @Test + fun givenObservePersistentWebSocketStatusReturnsFailure_whenInvoking_shouldReturnFailure() = + runTest { + // given + val (_, useCase) = Arrangement() + .withObservePersistentWebSocketConnectionStatusFailure() + .arrange() + // when + val result = useCase.invoke() + // then + assertInstanceOf(ShouldStartPersistentWebSocketServiceUseCase.Result.Failure::class.java, result) + } + + inner class Arrangement { + + @MockK + private lateinit var coreLogic: CoreLogic + + val useCase by lazy { + ShouldStartPersistentWebSocketServiceUseCase(coreLogic) + } + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + } + + fun arrange() = this to useCase + + fun withObservePersistentWebSocketConnectionStatusSuccess(flow: Flow>) = apply { + coEvery { coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus() } returns + ObservePersistentWebSocketConnectionStatusUseCase.Result.Success(flow) + } + fun withObservePersistentWebSocketConnectionStatusFailure() = apply { + coEvery { coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus() } returns + ObservePersistentWebSocketConnectionStatusUseCase.Result.Failure.StorageFailure + } + } + + companion object { + private val userId = UserId("userId", "domain") + } +} From d277d192a27f4b032627c29fd472ced1002cdc0c Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 28 Feb 2024 18:01:09 +0200 Subject: [PATCH 066/134] fix: Do not show waiting network in CertDetails screen (RC) (WPB-6638) (#2749) --- .../main/kotlin/com/wire/android/util/CurrentScreenManager.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt index ecc3afd2965..3161efdb414 100644 --- a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt +++ b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt @@ -35,6 +35,7 @@ import com.wire.android.ui.destinations.CreatePersonalAccountOverviewScreenDesti import com.wire.android.ui.destinations.CreateTeamAccountOverviewScreenDestination import com.wire.android.ui.destinations.Destination import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination +import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination import com.wire.android.ui.destinations.IncomingCallScreenDestination @@ -216,6 +217,7 @@ sealed class CurrentScreen { is MigrationScreenDestination, is InitialSyncScreenDestination, is E2EIEnrollmentScreenDestination, + is E2eiCertificateDetailsScreenDestination, is RegisterDeviceScreenDestination, is RemoveDeviceScreenDestination -> AuthRelated From 1d39d4a179cba6f661571ee9ed9bebb5eb2a08f6 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Thu, 29 Feb 2024 17:13:34 +0100 Subject: [PATCH 067/134] fix: some end points are not routed through the proxy server 4.6 (#2723) --- .../com/wire/android/di/CoreLogicModule.kt | 10 ----- .../feature/MigrateActiveAccountsUseCase.kt | 6 ++- .../feature/MigrateServerConfigUseCase.kt | 14 +++---- .../com/wire/android/ui/WireActivityState.kt | 11 +++--- .../wire/android/ui/WireActivityViewModel.kt | 2 +- .../android/ui/authentication/ServerTitle.kt | 23 ++++++++++-- .../create/code/CreateAccountCodeViewModel.kt | 6 ++- .../email/CreateAccountEmailViewModel.kt | 24 +----------- .../ui/authentication/login/LoginState.kt | 10 ++++- .../ui/authentication/login/LoginViewModel.kt | 7 +--- .../login/email/LoginEmailViewModel.kt | 37 +++++++++---------- .../login/sso/LoginSSOViewModel.kt | 13 +++++-- .../ui/common/dialogs/CustomServerDialog.kt | 11 ++++++ app/src/main/res/values-de/strings.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ .../MigrateServerConfigUseCaseTest.kt | 36 ++++++++++++------ .../login/sso/LoginSSOViewModelTest.kt | 2 +- kalium | 2 +- 18 files changed, 123 insertions(+), 97 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 7ff2cbaadc8..ed5677f1d35 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -206,16 +206,6 @@ class UseCaseModule { fun provideGetServerConfigUserCase(@KaliumCoreLogic coreLogic: CoreLogic) = coreLogic.getGlobalScope().fetchServerConfigFromDeepLink - @ViewModelScoped - @Provides - fun provideFetchApiVersionUserCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().fetchApiVersion - - @ViewModelScoped - @Provides - fun provideObserveServerConfigUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().observeServerConfig - @ViewModelScoped @Provides fun provideUpdateApiVersionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = diff --git a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateActiveAccountsUseCase.kt b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateActiveAccountsUseCase.kt index 29b8a5d7ffb..055000e9a0d 100644 --- a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateActiveAccountsUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateActiveAccountsUseCase.kt @@ -115,7 +115,11 @@ class MigrateActiveAccountsUseCase @Inject constructor( private suspend fun handleMissingData( serverConfig: ServerConfig, refreshToken: String, - ): Either = coreLogic.authenticationScope(serverConfig) { + ): Either = coreLogic.authenticationScope( + serverConfig, + // scala did not support proxy mode so we can pass null + proxyCredentials = null + ) { ssoLoginScope.getLoginSession(refreshToken) }.let { when (it) { diff --git a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateServerConfigUseCase.kt b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateServerConfigUseCase.kt index f2eb55a6690..1c68b612519 100644 --- a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateServerConfigUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateServerConfigUseCase.kt @@ -27,7 +27,7 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.configuration.server.CommonApiVersionType import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.failure.ServerConfigFailure -import com.wire.kalium.logic.feature.server.FetchApiVersionResult +import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.feature.server.GetServerConfigResult import com.wire.kalium.logic.feature.server.StoreServerConfigResult import com.wire.kalium.logic.functional.Either @@ -66,13 +66,13 @@ class MigrateServerConfigUseCase @Inject constructor( } private suspend fun ServerConfig.Links.fetchApiVersionAndStore(): Either = - coreLogic.getGlobalScope().fetchApiVersion(this).let { // it also already stores the fetched config + // scala did not support proxy mode so we can pass null here + coreLogic.versionedAuthenticationScope(this)(null).let { // it also already stores the fetched config when (it) { - is FetchApiVersionResult.Success -> Either.Right(it.serverConfig) - FetchApiVersionResult.Failure.TooNewVersion -> Either.Left(ServerConfigFailure.NewServerVersion) - FetchApiVersionResult.Failure.UnknownServerVersion -> Either.Left(ServerConfigFailure.UnknownServerVersion) - is FetchApiVersionResult.Failure.Generic -> Either.Left(it.genericFailure) + is AutoVersionAuthScopeUseCase.Result.Failure.Generic -> Either.Left(it.genericFailure) + AutoVersionAuthScopeUseCase.Result.Failure.TooNewVersion -> Either.Left(ServerConfigFailure.NewServerVersion) + AutoVersionAuthScopeUseCase.Result.Failure.UnknownServerVersion -> Either.Left(ServerConfigFailure.UnknownServerVersion) + is AutoVersionAuthScopeUseCase.Result.Success -> Either.Right(it.authenticationScope.currentServerConfig()) } } - } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityState.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityState.kt index db8e2921fa5..4916122814c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityState.kt @@ -18,10 +18,11 @@ package com.wire.android.ui -sealed class WireActivityState { +sealed class +WireActivityState { - data class NavigationGraph(val startNavigationRoute: String, val navigationArguments: List): WireActivityState() - data class ClientUpdateRequired(val clientUpdateUrl: String): WireActivityState() - object ServerVersionNotSupported: WireActivityState() - object Loading: WireActivityState() + data class NavigationGraph(val startNavigationRoute: String, val navigationArguments: List) : WireActivityState() + data class ClientUpdateRequired(val clientUpdateUrl: String) : WireActivityState() + object ServerVersionNotSupported : WireActivityState() + object Loading : WireActivityState() } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 8d21d615c60..99897aaecac 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -191,7 +191,7 @@ class WireActivityViewModel @Inject constructor( } private fun observeUpdateAppState() { - viewModelScope.launch(dispatchers.io()) { + viewModelScope.launch { observeIfAppUpdateRequired(BuildConfig.VERSION_CODE) .distinctUntilChanged() .collect { diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/ServerTitle.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/ServerTitle.kt index d8ce0da998f..d8524960afe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/ServerTitle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/ServerTitle.kt @@ -110,9 +110,8 @@ private fun ServerEnrollmentDialogContent( onDismiss: () -> Unit, onClick: () -> Unit, ) { - WireDialog( - title = stringResource(id = R.string.server_details_dialog_title), - text = LocalContext.current.resources.stringWithStyledArgs( + val text = if (serverLinks.apiProxy == null) { + LocalContext.current.resources.stringWithStyledArgs( R.string.server_details_dialog_body, MaterialTheme.wireTypography.body02, MaterialTheme.wireTypography.body02, @@ -120,7 +119,23 @@ private fun ServerEnrollmentDialogContent( argsColor = colorsScheme().onBackground, serverLinks.title, serverLinks.api - ), + ) + } else { + LocalContext.current.resources.stringWithStyledArgs( + R.string.server_details_dialog_body_with_proxy, + MaterialTheme.wireTypography.body02, + MaterialTheme.wireTypography.body02, + normalColor = colorsScheme().secondaryText, + argsColor = colorsScheme().onBackground, + serverLinks.title, + serverLinks.api, + serverLinks.apiProxy!!.host, + serverLinks.apiProxy!!.needsAuthentication.toString() + ) + } + WireDialog( + title = stringResource(id = R.string.server_details_dialog_title), + text = text, onDismiss = onDismiss, optionButton1Properties = WireDialogButtonProperties( stringResource(id = R.string.label_ok), diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt index c7a075c102c..7aed2837927 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt @@ -70,7 +70,8 @@ class CreateAccountCodeViewModel @Inject constructor( fun resendCode() { codeState = codeState.copy(loading = true) viewModelScope.launch { - val authScope = coreLogic.versionedAuthenticationScope(serverConfig)().let { + // create account does not support proxy yet + val authScope = coreLogic.versionedAuthenticationScope(serverConfig)(null).let { when (it) { is AutoVersionAuthScopeUseCase.Result.Success -> it.authenticationScope @@ -129,7 +130,8 @@ class CreateAccountCodeViewModel @Inject constructor( private fun onCodeContinue(onSuccess: () -> Unit) { codeState = codeState.copy(loading = true) viewModelScope.launch { - val authScope = coreLogic.versionedAuthenticationScope(serverConfig)().let { + // create account does not support proxy yet + val authScope = coreLogic.versionedAuthenticationScope(serverConfig)(null).let { when (it) { is AutoVersionAuthScopeUseCase.Result.Success -> it.authenticationScope diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt index 75fb10548db..5656091bdc5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt @@ -33,8 +33,6 @@ import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.feature.register.RequestActivationCodeResult -import com.wire.kalium.logic.feature.server.FetchApiVersionResult -import com.wire.kalium.logic.feature.server.FetchApiVersionUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -44,7 +42,6 @@ import javax.inject.Inject class CreateAccountEmailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val authServerConfigProvider: AuthServerConfigProvider, - private val fetchApiVersion: FetchApiVersionUseCase, private val validateEmail: ValidateEmailUseCase, @KaliumCoreLogic private val coreLogic: CoreLogic, ) : ViewModel() { @@ -69,25 +66,6 @@ class CreateAccountEmailViewModel @Inject constructor( fun onEmailContinue(onSuccess: () -> Unit) { emailState = emailState.copy(loading = true, continueEnabled = false) viewModelScope.launch { - fetchApiVersion(authServerConfigProvider.authServer.value).let { - when (it) { - is FetchApiVersionResult.Success -> {} - is FetchApiVersionResult.Failure.UnknownServerVersion -> { - emailState = emailState.copy(showServerVersionNotSupportedDialog = true) - return@launch - } - - is FetchApiVersionResult.Failure.TooNewVersion -> { - emailState = emailState.copy(showClientUpdateDialog = true) - return@launch - } - - is FetchApiVersionResult.Failure.Generic -> { - return@launch - } - } - } - val emailError = if (validateEmail(emailState.email.text.trim().lowercase())) CreateAccountEmailViewState.EmailError.None else CreateAccountEmailViewState.EmailError.TextFieldError.InvalidEmailError @@ -106,7 +84,7 @@ class CreateAccountEmailViewModel @Inject constructor( fun onTermsAccept(onSuccess: () -> Unit) { emailState = emailState.copy(loading = true, continueEnabled = false, termsDialogVisible = false, termsAccepted = true) viewModelScope.launch { - val authScope = coreLogic.versionedAuthenticationScope(serverConfig)().let { + val authScope = coreLogic.versionedAuthenticationScope(serverConfig)(null).let { when (it) { is AutoVersionAuthScopeUseCase.Result.Success -> it.authenticationScope diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginState.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginState.kt index 578caa2d0c9..84eb7d9f42f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginState.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.authentication.login import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.ui.common.dialogs.CustomServerDialogState +import com.wire.kalium.logic.data.auth.login.ProxyCredentials data class LoginState( val userIdentifier: TextFieldValue = TextFieldValue(""), @@ -36,7 +37,14 @@ data class LoginState( val loginError: LoginError = LoginError.None, val isProxyEnabled: Boolean = false, val customServerDialogState: CustomServerDialogState? = null, -) +) { + fun getProxyCredentials(): ProxyCredentials? = + if (proxyIdentifier.text.isNotBlank() && proxyPassword.text.isNotBlank()) { + ProxyCredentials(proxyIdentifier.text, proxyPassword.text) + } else { + null + } +} fun LoginState.updateEmailLoginEnabled() = copy( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt index 5d48582fcca..6f59359106a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt @@ -39,7 +39,6 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase import com.wire.kalium.logic.feature.auth.AuthenticationResult import com.wire.kalium.logic.feature.auth.DomainLookupUseCase -import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.client.RegisterClientUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -67,8 +66,6 @@ open class LoginViewModel @Inject constructor( } } - protected suspend fun authScope(): AutoVersionAuthScopeUseCase.Result = coreLogic.versionedAuthenticationScope(serverConfig)() - private val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs() private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle.let { if (it.isNullOrEmpty()) PreFilledUserIdentifierType.None else PreFilledUserIdentifierType.PreFilled(it) @@ -84,8 +81,8 @@ open class LoginViewModel @Inject constructor( userIdentifierEnabled = preFilledUserIdentifier is PreFilledUserIdentifierType.None, password = TextFieldValue(String.EMPTY), isProxyAuthRequired = - if (serverConfig.apiProxy?.needsAuthentication != null) serverConfig.apiProxy?.needsAuthentication!! - else false, + if (serverConfig.apiProxy?.needsAuthentication != null) serverConfig.apiProxy?.needsAuthentication!! + else false, isProxyEnabled = serverConfig.apiProxy != null ) ) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt index 85361e510b0..4fd15a6c2e3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt @@ -36,7 +36,6 @@ import com.wire.android.ui.authentication.verificationcode.VerificationCodeState import com.wire.android.ui.common.textfield.CodeFieldValue import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.auth.login.ProxyCredentials import com.wire.kalium.logic.data.auth.verification.VerifiableAction import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase import com.wire.kalium.logic.feature.auth.AuthenticationResult @@ -137,29 +136,27 @@ class LoginEmailViewModel @Inject constructor( private suspend fun resolveCurrentAuthScope(): AuthenticationScope? = coreLogic.versionedAuthenticationScope(serverConfig).invoke( - AutoVersionAuthScopeUseCase.ProxyAuthentication.UsernameAndPassword( - ProxyCredentials(loginState.proxyIdentifier.text, loginState.proxyPassword.text) - ) - ).let { - when (it) { - is AutoVersionAuthScopeUseCase.Result.Success -> it.authenticationScope - - is AutoVersionAuthScopeUseCase.Result.Failure.UnknownServerVersion -> { - updateEmailLoginError(LoginError.DialogError.ServerVersionNotSupported) - return null - } + loginState.getProxyCredentials() + ).let { + when (it) { + is AutoVersionAuthScopeUseCase.Result.Success -> it.authenticationScope + + is AutoVersionAuthScopeUseCase.Result.Failure.UnknownServerVersion -> { + updateEmailLoginError(LoginError.DialogError.ServerVersionNotSupported) + return null + } - is AutoVersionAuthScopeUseCase.Result.Failure.TooNewVersion -> { - updateEmailLoginError(LoginError.DialogError.ClientUpdateRequired) - return null - } + is AutoVersionAuthScopeUseCase.Result.Failure.TooNewVersion -> { + updateEmailLoginError(LoginError.DialogError.ClientUpdateRequired) + return null + } - is AutoVersionAuthScopeUseCase.Result.Failure.Generic -> { - updateEmailLoginError(LoginError.DialogError.GenericError(it.genericFailure)) - return null + is AutoVersionAuthScopeUseCase.Result.Failure.Generic -> { + updateEmailLoginError(LoginError.DialogError.GenericError(it.genericFailure)) + return null + } } } - } private suspend fun handleAuthenticationFailure(it: AuthenticationResult.Failure, authScope: AuthenticationScope) { when (it) { diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt index af0e111012c..0b8bc779420 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt @@ -90,7 +90,9 @@ class LoginSSOViewModel @Inject constructor( if (loginState.customServerDialogState != null) { authServerConfigProvider.updateAuthServer(loginState.customServerDialogState!!.serverLinks) - val authScope = coreLogic.versionedAuthenticationScope(loginState.customServerDialogState!!.serverLinks)().let { + // sso does not support proxy + // TODO: add proxy support + val authScope = coreLogic.versionedAuthenticationScope(loginState.customServerDialogState!!.serverLinks)(null).let { when (it) { is AutoVersionAuthScopeUseCase.Result.Failure.Generic, AutoVersionAuthScopeUseCase.Result.Failure.TooNewVersion, @@ -134,7 +136,9 @@ class LoginSSOViewModel @Inject constructor( val defaultAuthScope: AuthenticationScope = coreLogic.versionedAuthenticationScope( authServerConfigProvider.defaultServerLinks() - )().let { + // domain lockup does not support proxy + // TODO: add proxy support + )(null).let { when (it) { is AutoVersionAuthScopeUseCase.Result.Failure.Generic, AutoVersionAuthScopeUseCase.Result.Failure.TooNewVersion, @@ -168,7 +172,8 @@ class LoginSSOViewModel @Inject constructor( private fun ssoLoginWithCodeFlow() { viewModelScope.launch { val authScope = - authScope().let { + // sso does not support proxy + coreLogic.versionedAuthenticationScope(serverConfig)(null).let { when (it) { is AutoVersionAuthScopeUseCase.Result.Success -> it.authenticationScope @@ -207,7 +212,7 @@ class LoginSSOViewModel @Inject constructor( loginState = loginState.copy(ssoLoginLoading = true, loginError = LoginError.None).updateSSOLoginEnabled() viewModelScope.launch { val authScope = - authScope().let { + coreLogic.versionedAuthenticationScope(serverConfig)(null).let { when (it) { is AutoVersionAuthScopeUseCase.Result.Success -> it.authenticationScope diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt index a884f7266c8..aa130cbc332 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt @@ -86,6 +86,17 @@ internal fun CustomServerDialog( title = stringResource(id = R.string.custom_backend_dialog_body_backend_api), value = serverLinks.api ) + if (serverLinks.apiProxy != null) { + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_proxy_url), + value = serverLinks.apiProxy!!.host + ) + + CustomServerPropertyInfo( + title = stringResource(id = R.string.custom_backend_dialog_body_backend_proxy_authentication), + value = serverLinks.apiProxy!!.needsAuthentication.toString() + ) + } if (showDetails) { CustomServerPropertyInfo( title = stringResource(id = R.string.custom_backend_dialog_body_backend_websocket), diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index dfe49a4f5ac..b989aa21564 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -210,6 +210,7 @@ Wire wird unabhΓ€ngig geprΓΌft und ist ISO-, CCPA-, DSGVO- und SOX-konform Team erstellen Backend-Name:\n%1$s\n\nBackend-URL:\n%2$s + Backend name:\n%1$s\n\nBackend URL:\n%2$s\n\nProxy-URL:\n%3$s\n\nProxy-Authentifizierung:\n%4$s Lokales Backend Willkommen in unserer neuen App πŸ‘‹ Wir haben die App ΓΌberarbeitet, um sie fΓΌr alle benutzerfreundlicher zu machen.\n\nErfahren Sie mehr ΓΌber die neu gestaltete App – zusΓ€tzliche Optionen und verbesserte Barrierefreiheit bei gleichbleibend hoher Sicherheit. @@ -993,6 +994,8 @@ Wenn Sie fortfahren, wird Ihr Client an das folgende lokale Backend weitergeleitet: Backend-Name: Backend-URL: + Proxy-URL: + Proxy-Authentifizierung: Blacklist-URL: Teams-URL: Accounts-URL: diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d6f85f104a8..63d600ab807 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,6 +226,7 @@ Wire is independently audited and ISO, CCPA, GDPR, SOX-compliant Create a Team Backend name:\n%1$s\n\nBackend URL:\n%2$s + Backend name:\n%1$s\n\nBackend URL:\n%2$s\n\nProxy URL:\n%3$s\n\nProxy authentication:\n%4$s On-premises Backend Welcome To Our New Android App πŸ‘‹ We rebuilt the app to make it more usable for everyone.\n\nFind out more about Wire’s redesigned appβ€”new options and improved accessibility, with the same strong security. @@ -1013,6 +1014,8 @@ If you proceed, your client will be redirected to the following on-premises backend: Backend name: Backend URL: + Proxy URL: + Proxy authentication: Blacklist URL: Teams URL: Accounts URL: diff --git a/app/src/test/kotlin/com/wire/android/migration/MigrateServerConfigUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/migration/MigrateServerConfigUseCaseTest.kt index 7509e8c1e8c..5b12c4da989 100644 --- a/app/src/test/kotlin/com/wire/android/migration/MigrateServerConfigUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/migration/MigrateServerConfigUseCaseTest.kt @@ -27,7 +27,8 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.GlobalKaliumScope import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.configuration.server.ServerConfig -import com.wire.kalium.logic.feature.server.FetchApiVersionResult +import com.wire.kalium.logic.feature.auth.AuthenticationScope +import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.feature.server.GetServerConfigResult import com.wire.kalium.logic.feature.server.StoreServerConfigResult import com.wire.kalium.logic.functional.Either @@ -59,7 +60,6 @@ class MigrateServerConfigUseCaseTest { .arrange() val result = useCase() coVerify(exactly = 1) { arrangement.globalKaliumScope.storeServerConfig(expected.links, versionInfo) } - coVerify { arrangement.globalKaliumScope.fetchApiVersion(any()) wasNot Called } assert(result.isRight()) assertEquals(expected, (result as Either.Right).value) } @@ -69,10 +69,10 @@ class MigrateServerConfigUseCaseTest { val expected = Arrangement.serverConfig val (arrangement, useCase) = Arrangement() .withScalaServerConfig(ScalaServerConfig.Links(expected.links)) - .withFetchApiVersionResult(FetchApiVersionResult.Success(expected)) + .withCurrentServerConfig(expected) .arrange() + val result = useCase() - coVerify(exactly = 1) { arrangement.globalKaliumScope.fetchApiVersion(expected.links) } assert(result.isRight()) assertEquals(expected, (result as Either.Right).value) } @@ -84,11 +84,11 @@ class MigrateServerConfigUseCaseTest { val (arrangement, useCase) = Arrangement() .withScalaServerConfig(ScalaServerConfig.ConfigUrl(customConfigUrl)) .withFetchServerConfigFromDeepLinkResult(GetServerConfigResult.Success(expected.links)) - .withFetchApiVersionResult(FetchApiVersionResult.Success(expected)) + .withCurrentServerConfig(expected) .arrange() + val result = useCase() coVerify(exactly = 1) { arrangement.globalKaliumScope.fetchServerConfigFromDeepLink(customConfigUrl) } - coVerify(exactly = 1) { arrangement.globalKaliumScope.fetchApiVersion(expected.links) } assert(result.isRight()) assertEquals(expected, (result as Either.Right).value) } @@ -107,8 +107,10 @@ class MigrateServerConfigUseCaseTest { private class Arrangement { @MockK lateinit var coreLogic: CoreLogic + @MockK lateinit var scalaServerConfigDAO: ScalaServerConfigDAO + @MockK lateinit var globalKaliumScope: GlobalKaliumScope @@ -116,27 +118,37 @@ class MigrateServerConfigUseCaseTest { MigrateServerConfigUseCase(coreLogic, scalaServerConfigDAO) } + @MockK + lateinit var autoVersionAuthScopeUseCase: AutoVersionAuthScopeUseCase + + @MockK + lateinit var authScope: AuthenticationScope + init { MockKAnnotations.init(this, relaxUnitFun = true) every { coreLogic.getGlobalScope() } returns globalKaliumScope + every { coreLogic.versionedAuthenticationScope(any()) } returns autoVersionAuthScopeUseCase + coEvery { autoVersionAuthScopeUseCase(any()) } returns AutoVersionAuthScopeUseCase.Result.Success(authScope) + } + + fun withCurrentServerConfig(serverConfig: ServerConfig) = apply { + every { authScope.currentServerConfig() } returns serverConfig } fun withScalaServerConfig(scalaServerConfig: ScalaServerConfig): Arrangement { every { scalaServerConfigDAO.scalaServerConfig } returns scalaServerConfig return this } - fun withStoreServerConfigResult(result : StoreServerConfigResult): Arrangement { + + fun withStoreServerConfigResult(result: StoreServerConfigResult): Arrangement { coEvery { globalKaliumScope.storeServerConfig(any(), any()) } returns result return this } - fun withFetchServerConfigFromDeepLinkResult(result : GetServerConfigResult): Arrangement { + + fun withFetchServerConfigFromDeepLinkResult(result: GetServerConfigResult): Arrangement { coEvery { globalKaliumScope.fetchServerConfigFromDeepLink(any()) } returns result return this } - fun withFetchApiVersionResult(result : FetchApiVersionResult): Arrangement { - coEvery { globalKaliumScope.fetchApiVersion(any()) } returns result - return this - } fun arrange() = this to useCase diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt index e6e33468d51..3d4ca18db66 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt @@ -139,7 +139,7 @@ class LoginSSOViewModelTest { authServerConfigProvider.updateAuthServer(newServerConfig(1).links) coEvery { - autoVersionAuthScopeUseCase() + autoVersionAuthScopeUseCase(null) } returns AutoVersionAuthScopeUseCase.Result.Success( authenticationScope ) diff --git a/kalium b/kalium index e2b9a65de6d..119b02f5dd8 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e2b9a65de6d850708e3d606b91c011c5cdcd0586 +Subproject commit 119b02f5dd8e1fd294a87a70de202685bc988311 From a5ab875e009db44d907a2439341042a888b7fdc0 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Fri, 1 Mar 2024 07:27:19 +0100 Subject: [PATCH 068/134] fix(e2ei): loading e2ei state during the app initialisation (#2664) --- .../wire/android/ui/WireActivityViewModel.kt | 25 ++++++------------- .../android/ui/WireActivityViewModelTest.kt | 6 ++--- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 99897aaecac..d3ce07baa93 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -145,8 +145,12 @@ class WireActivityViewModel @Inject constructor( private val _observeSyncFlowState: MutableStateFlow = MutableStateFlow(null) val observeSyncFlowState: StateFlow = _observeSyncFlowState - private val _observeE2EIState: MutableStateFlow = MutableStateFlow(null) - private val observeE2EIState: StateFlow = _observeE2EIState + private val observeE2EIState = observeUserId + .flatMapLatest { + it?.let { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(it).observeIfE2EIIsRequiredDuringLogin() } + ?: flowOf(null) + } + .distinctUntilChanged() init { observeSyncState() @@ -154,7 +158,6 @@ class WireActivityViewModel @Inject constructor( observeNewClientState() observeScreenshotCensoringConfigState() observeAppThemeState() - observerE2EIState() } private fun observeAppThemeState() { @@ -167,18 +170,6 @@ class WireActivityViewModel @Inject constructor( } } - fun observerE2EIState() { - viewModelScope.launch(dispatchers.io()) { - observeUserId - .flatMapLatest { - it?.let { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(it).observeIfE2EIIsRequiredDuringLogin() } - ?: flowOf(null) - } - .distinctUntilChanged() - .collect { _observeE2EIState.emit(it) } - } - } - private fun observeSyncState() { viewModelScope.launch(dispatchers.io()) { observeUserId @@ -434,8 +425,8 @@ class WireActivityViewModel @Inject constructor( fun shouldLogIn(): Boolean = !hasValidCurrentSession() - fun blockedByE2EI(): Boolean { - return observeE2EIState.value == true + private fun blockedByE2EI(): Boolean = runBlocking { + observeE2EIState.first() ?: false } private fun hasValidCurrentSession(): Boolean = runBlocking { diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 7b61cf67699..d3d858a2a80 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -590,9 +590,6 @@ class WireActivityViewModelTest { private class Arrangement { - // TODO add tests for cases when observeIfE2EIIsRequiredDuringLogin emits semothing - private val observeIfE2EIIsRequiredDuringLogin = MutableSharedFlow() - init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) @@ -614,7 +611,7 @@ class WireActivityViewModelTest { coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) coEvery { globalDataStore.selectedThemeOptionFlow() } returns flowOf(ThemeOption.LIGHT) coEvery { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(any()).observeIfE2EIIsRequiredDuringLogin() } returns - observeIfE2EIIsRequiredDuringLogin + flowOf(false) } @MockK @@ -766,6 +763,7 @@ class WireActivityViewModelTest { fun withCurrentScreen(currentScreenFlow: StateFlow) = apply { coEvery { currentScreenManager.observeCurrentScreen(any()) } returns currentScreenFlow + coEvery { coreLogic.getSessionScope(TEST_ACCOUNT_INFO.userId).observeIfE2EIRequiredDuringLogin() } returns flowOf(false) } suspend fun withScreenshotCensoringConfig(result: ObserveScreenshotCensoringConfigResult) = apply { From 1265e6bc46ed2a153c1ce398c81044c97b3726e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Fri, 1 Mar 2024 09:39:19 +0100 Subject: [PATCH 069/134] fix: unexpected scrolling on selected message [WPB-6932] (#2753) --- .../ui/home/conversations/ConversationScreen.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 489031fba1d..548715b6096 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -838,12 +838,16 @@ fun MessageList( onLinkClick: (String) -> Unit, selectedMessageId: String? ) { - val mostRecentMessage = lazyPagingMessages.itemCount.takeIf { it > 0 }?.let { lazyPagingMessages[0] } - - LaunchedEffect(mostRecentMessage) { - // Most recent message changed, if the user didn't scroll up, we automatically scroll down to reveal the new message - if (lazyListState.firstVisibleItemIndex < MAXIMUM_SCROLLED_MESSAGES_UNTIL_AUTOSCROLL_STOPS) { - lazyListState.animateScrollToItem(0) + val prevItemCount = remember { mutableStateOf(lazyPagingMessages.itemCount) } + LaunchedEffect(lazyPagingMessages.itemCount) { + if (lazyPagingMessages.itemCount > prevItemCount.value && selectedMessageId == null) { + if (prevItemCount.value > 0 + && lazyListState.firstVisibleItemIndex > 0 + && lazyListState.firstVisibleItemIndex <= MAXIMUM_SCROLLED_MESSAGES_UNTIL_AUTOSCROLL_STOPS + ) { + lazyListState.animateScrollToItem(0) + } + prevItemCount.value = lazyPagingMessages.itemCount } } From f7a218618f46e2f2941b0f1dd60df95839513152 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 1 Mar 2024 10:42:31 +0100 Subject: [PATCH 070/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 119b02f5dd8..af2b4639efa 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 119b02f5dd8e1fd294a87a70de202685bc988311 +Subproject commit af2b4639efa72c25b5a4f2bb94c219177869ff9e From 79b7652a2915917cabd691d4da5060df4c12a9c0 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Sat, 2 Mar 2024 09:34:41 +0100 Subject: [PATCH 071/134] chore: update app version --- build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt index 6d17501b1ad..45ecfb9b82c 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt @@ -25,6 +25,6 @@ object AndroidSdk { object AndroidApp { const val id = "com.wire.android" - const val versionName = "4.6.1" + const val versionName = "4.6.2" val versionCode = Versionizer().versionCode } From f3b506acc93337c8c9c465f47b60820b998ae34c Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Mon, 4 Mar 2024 15:52:56 +0100 Subject: [PATCH 072/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index af2b4639efa..010fbfd3c85 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit af2b4639efa72c25b5a4f2bb94c219177869ff9e +Subproject commit 010fbfd3c85278cbe07c2e37523a5a10f7c5c59a From 332bb413f91d471ca8dd78c40cb0f94e4e4eb893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Mon, 4 Mar 2024 22:33:57 +0100 Subject: [PATCH 073/134] fix: e2e webview close [WPB-6788] (#2762) --- .../wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt index 65a869c0167..9a844bea755 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt @@ -39,7 +39,6 @@ fun GetE2EICertificateUI( val coroutineScope = rememberCoroutineScope() val context = LocalContext.current - // FIXME issue happens when this UI is called from WireActivity: WebView is just canceled by itself LaunchedEffect(Unit) { viewModel.requestOAuthFlow.onEach { OAuthUseCase(context, it.target, it.oAuthClaims, it.oAuthState).launch( @@ -51,6 +50,7 @@ fun GetE2EICertificateUI( LaunchedEffect(Unit) { viewModel.enrollmentResultFlow.onEach { enrollmentResultHandler(it) }.launchIn(coroutineScope) } - - viewModel.getCertificate(isNewClient) + LaunchedEffect(Unit) { + viewModel.getCertificate(isNewClient) + } } From 98d654e5b7a2165c16794578fa8d1aa726b815f9 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Tue, 5 Mar 2024 00:03:20 +0100 Subject: [PATCH 074/134] fix: network screen is empty on graphene os (#2760) --- .../networkSettings/NetworkSettingsScreen.kt | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt index 312e5b3f722..d46ef8a6ea9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt @@ -21,8 +21,8 @@ package com.wire.android.ui.home.settings.appsettings.networkSettings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import com.wire.android.ui.common.scaffold.WireScaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -33,6 +33,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R import com.wire.android.navigation.Navigator +import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.home.conversations.details.options.ArrowType import com.wire.android.ui.home.conversations.details.options.GroupConversationOptionsItem @@ -74,20 +75,30 @@ fun NetworkSettingsScreenContent( .fillMaxSize() .padding(internalPadding) ) { - if (!isWebsocketEnabledByDefault(LocalContext.current)) { - GroupConversationOptionsItem( - title = stringResource(R.string.settings_keep_connection_to_websocket), - subtitle = stringResource( - R.string.settings_keep_connection_to_websocket_description, - backendName - ), - switchState = SwitchState.Enabled( + val appContext = LocalContext.current.applicationContext + val isWebSocketEnforcedByDefault = remember { + isWebsocketEnabledByDefault(appContext) + } + val switchState = remember { + if (isWebSocketEnforcedByDefault) { + SwitchState.TextOnly(true) + } else { + SwitchState.Enabled( value = isWebSocketEnabled, onCheckedChange = setWebSocketState - ), - arrowType = ArrowType.NONE - ) + ) + } } + + GroupConversationOptionsItem( + title = stringResource(R.string.settings_keep_connection_to_websocket), + subtitle = stringResource( + R.string.settings_keep_connection_to_websocket_description, + backendName + ), + switchState = switchState, + arrowType = ArrowType.NONE + ) } } } From 998be7dbc725690779cf2d39f49db9db711f480f Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Tue, 5 Mar 2024 09:18:12 +0100 Subject: [PATCH 075/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 010fbfd3c85..691f69dc22e 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 010fbfd3c85278cbe07c2e37523a5a10f7c5c59a +Subproject commit 691f69dc22e4aa5cc286107be5b48d94219a5cce From b40bf8bce32a300ce42be237e8a86b42c25b8a6c Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Tue, 5 Mar 2024 14:40:32 +0100 Subject: [PATCH 076/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 691f69dc22e..0f266bf1b1b 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 691f69dc22e4aa5cc286107be5b48d94219a5cce +Subproject commit 0f266bf1b1b3e775bc88503ebde92c58c609f9a4 From 0bf115b56166c50424882973249ec97055e57022 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Wed, 6 Mar 2024 09:51:54 +0100 Subject: [PATCH 077/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 0f266bf1b1b..c73f5954973 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 0f266bf1b1b3e775bc88503ebde92c58c609f9a4 +Subproject commit c73f59549733c5376e782734222f67eef5951752 From 3db6178f2de9c9f2241fb2dc1e49098336839c56 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 6 Mar 2024 10:58:50 +0100 Subject: [PATCH 078/134] chore: explicitly restrict the app to be installed internally (#2768) --- app/src/main/AndroidManifest.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7f20fcc2544..3cb67072676 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,9 @@ + android:sharedUserId="${sharedUserId}" + android:installLocation="internalOnly" + > From a1dae98aea696e29586286b36bc9bcae97f397f0 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Wed, 6 Mar 2024 12:11:28 +0100 Subject: [PATCH 079/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index c73f5954973..e092441e5f7 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit c73f59549733c5376e782734222f67eef5951752 +Subproject commit e092441e5f7eee5c981f494996928d68af0cfe3e From 3dae65cd1db524cf908be8647e6bfba75175aae2 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Wed, 6 Mar 2024 14:29:34 +0100 Subject: [PATCH 080/134] fix: persistent ws not reflected in UI (WPB-7020) (#2770) --- .../networkSettings/NetworkSettingsScreen.kt | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt index d46ef8a6ea9..a25fb847f99 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -75,19 +74,16 @@ fun NetworkSettingsScreenContent( .fillMaxSize() .padding(internalPadding) ) { - val appContext = LocalContext.current.applicationContext - val isWebSocketEnforcedByDefault = remember { - isWebsocketEnabledByDefault(appContext) - } - val switchState = remember { - if (isWebSocketEnforcedByDefault) { - SwitchState.TextOnly(true) - } else { - SwitchState.Enabled( - value = isWebSocketEnabled, - onCheckedChange = setWebSocketState - ) - } + val appContext = LocalContext.current + val isWebSocketEnforcedByDefault = isWebsocketEnabledByDefault(appContext) + + val switchState = if (isWebSocketEnforcedByDefault) { + SwitchState.TextOnly(true) + } else { + SwitchState.Enabled( + value = isWebSocketEnabled, + onCheckedChange = setWebSocketState + ) } GroupConversationOptionsItem( From e4b36232815e74f705b4277a8d9d683bcaa6bf17 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Thu, 7 Mar 2024 14:48:33 +0100 Subject: [PATCH 081/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index e092441e5f7..8e024950f68 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e092441e5f7eee5c981f494996928d68af0cfe3e +Subproject commit 8e024950f68e136377982ab34ef22f6ab0708c02 From e2aeaad3ef263b4430538b1cdde0ced7511f9584 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Fri, 8 Mar 2024 18:49:42 +0100 Subject: [PATCH 082/134] fix: user pic is not editable for scim users (#2759) --- .../wire/android/ui/userprofile/self/SelfUserProfileScreen.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index a7b3c290e0b..ac3309f019d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -205,8 +205,7 @@ private fun SelfUserProfileContent( userName = userName, teamName = teamName, onUserProfileClick = onChangeUserProfilePicture, - editableState = if (state.isReadOnlyAccount) EditableState.NotEditable - else EditableState.IsEditable(onEditClick) + editableState = EditableState.IsEditable(onEditClick) ) } if (!state.teamName.isNullOrBlank()) { From ccaabffb7fdfeb95504b20ca97b799a193984d9a Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Mon, 11 Mar 2024 09:44:49 +0100 Subject: [PATCH 083/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 8e024950f68..565345ade4d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 8e024950f68e136377982ab34ef22f6ab0708c02 +Subproject commit 565345ade4d29e2485f9112a1c9845e100b6b4ba From 611e574bf83b7e799ef14aef72774f8a387d0629 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 11 Mar 2024 10:46:46 +0200 Subject: [PATCH 084/134] fix: Remove autologin in Keycloak in E2EI [WPB-7061] (#2774) --- .../main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt | 1 + .../android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt index 14e509fb0af..b3b96844731 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt @@ -166,6 +166,7 @@ class OAuthUseCase( AuthorizationRequest.Scope.PROFILE, AuthorizationRequest.Scope.OFFLINE_ACCESS ).setClaims(JSONObject(claims.toString())) + .setPrompt(AuthorizationRequest.Prompt.LOGIN) .build() private fun AuthorizationRequest.Builder.setCodeVerifier(): AuthorizationRequest.Builder { diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt index c179dd2856d..dda01115c8e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt @@ -68,8 +68,7 @@ class GetE2EICertificateViewModel @Inject constructor( .fold({ enrollmentResultFlow.emit(Either.Left(it)) }, { - if (it is E2EIEnrollmentResult.Initialized) requestOAuthFlow.emit(it) - else enrollmentResultFlow.emit(Either.Right(it)) + requestOAuthFlow.emit(it) }) } } From 2394e27d19ec8cc7be02cde1368267bc33acbcf9 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 11 Mar 2024 12:29:37 +0100 Subject: [PATCH 085/134] fix: breaking changes from kalium (#2778) --- .../ui/settings/devices/EndToEndIdentityCertificateItem.kt | 7 +++++-- .../ui/settings/devices/model/DeviceDetailsState.kt | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt index ebdff2f4be4..9bdafc232ee 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt @@ -42,6 +42,7 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.feature.e2ei.CertificateStatus import com.wire.kalium.logic.feature.e2ei.E2eiCertificate +import kotlinx.datetime.Instant @Composable fun EndToEndIdentityCertificateItem( @@ -200,7 +201,8 @@ fun PreviewEndToEndIdentityCertificateItem() { certificate = E2eiCertificate( status = CertificateStatus.VALID, serialNumber = "e5:d5:e6:75:7e:04:86:07:14:3c:a0:ed:9a:8d:e4:fd", - certificateDetail = "" + certificateDetail = "", + endAt = Instant.DISTANT_FUTURE ), isLoadingCertificate = false, enrollE2eiCertificate = {}, @@ -217,7 +219,8 @@ fun PreviewEndToEndIdentityCertificateSelfItem() { certificate = E2eiCertificate( status = CertificateStatus.VALID, serialNumber = "e5:d5:e6:75:7e:04:86:07:14:3c:a0:ed:9a:8d:e4:fd", - certificateDetail = "" + certificateDetail = "", + endAt = Instant.DISTANT_FUTURE ), isLoadingCertificate = false, enrollE2eiCertificate = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt index 62d3de20001..784f0d38fb1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt @@ -22,6 +22,7 @@ import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState import com.wire.android.ui.authentication.devices.remove.RemoveDeviceError import com.wire.kalium.logic.feature.e2ei.CertificateStatus import com.wire.kalium.logic.feature.e2ei.E2eiCertificate +import kotlinx.datetime.Instant data class DeviceDetailsState( val device: Device = Device(), @@ -35,7 +36,8 @@ data class DeviceDetailsState( val e2eiCertificate: E2eiCertificate = E2eiCertificate( status = CertificateStatus.EXPIRED, serialNumber = "", - certificateDetail = "" + certificateDetail = "", + endAt = Instant.DISTANT_FUTURE ), val canBeRemoved: Boolean = false, val isLoadingCertificate: Boolean = false, From b0e5621fda1f3efa1f25f2e5e05f71224ae6005a Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Mon, 11 Mar 2024 12:31:54 +0100 Subject: [PATCH 086/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 565345ade4d..5286fd1d046 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 565345ade4d29e2485f9112a1c9845e100b6b4ba +Subproject commit 5286fd1d046e0c48723e69130d78d375cf61413f From 82fceb08f50f9f5e57938bafe14ac4bf931e6786 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 11 Mar 2024 13:30:52 +0100 Subject: [PATCH 087/134] fix(calling): microphone restricted when the app goes into background on Android 14 (WPB-6307) (#2780) --- app/src/main/AndroidManifest.xml | 4 +-- .../notification/WireNotificationManager.kt | 3 +- .../android/services/OngoingCallService.kt | 33 ++++++++++++++++--- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3cb67072676..d31f77cb8d5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -41,6 +41,7 @@ + @@ -322,8 +323,7 @@ - + android:foregroundServiceType="phoneCall|microphone" /> diff --git a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt index 56588ef4118..7bf369a96ff 100644 --- a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt @@ -18,6 +18,7 @@ package com.wire.android.notification +import android.os.Build import androidx.annotation.VisibleForTesting import com.wire.android.R import com.wire.android.appLogger @@ -399,7 +400,7 @@ class WireNotificationManager @Inject constructor( private suspend fun observeOngoingCalls(currentScreenState: StateFlow) { currentScreenState .flatMapLatest { currentScreen -> - if (currentScreen !is CurrentScreen.InBackground) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && currentScreen !is CurrentScreen.InBackground) { flowOf(null) } else { coreLogic.getGlobalScope().session.currentSessionFlow() diff --git a/app/src/main/kotlin/com/wire/android/services/OngoingCallService.kt b/app/src/main/kotlin/com/wire/android/services/OngoingCallService.kt index 1baf0f220a1..d0810c4bb44 100644 --- a/app/src/main/kotlin/com/wire/android/services/OngoingCallService.kt +++ b/app/src/main/kotlin/com/wire/android/services/OngoingCallService.kt @@ -22,6 +22,7 @@ import android.app.Notification import android.app.Service import android.content.Context import android.content.Intent +import android.content.pm.ServiceInfo import android.os.IBinder import com.wire.android.appLogger import com.wire.android.di.KaliumCoreLogic @@ -47,6 +48,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject +import androidx.core.app.ServiceCompat @AndroidEntryPoint class OngoingCallService : Service() { @@ -131,17 +133,38 @@ class OngoingCallService : Service() { scope.cancel() } - private fun generateForegroundNotification(callName: String, conversationId: String, userId: UserId) { + private fun generateForegroundNotification( + callName: String, + conversationId: String, + userId: UserId + ) { appLogger.i("$TAG: generating foregroundNotification...") - val notification: Notification = callNotificationManager.builder.getOngoingCallNotification(callName, conversationId, userId) - startForeground(CALL_ONGOING_NOTIFICATION_ID, notification) + val notification: Notification = callNotificationManager.builder.getOngoingCallNotification( + callName, + conversationId, + userId + ) + ServiceCompat.startForeground( + this, + CALL_ONGOING_NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + ) + appLogger.i("$TAG: started foreground with proper notification") } private fun generatePlaceholderForegroundNotification() { appLogger.i("$TAG: generating foregroundNotification placeholder...") - val notification: Notification = callNotificationManager.builder.getOngoingCallPlaceholderNotification() - startForeground(CALL_ONGOING_NOTIFICATION_ID, notification) + val notification: Notification = + callNotificationManager.builder.getOngoingCallPlaceholderNotification() + ServiceCompat.startForeground( + this, + CALL_ONGOING_NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + ) + appLogger.i("$TAG: started foreground with placeholder notification") } From 2420b87e1b19f88caa9b6444c2e31843fd447986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Tue, 12 Mar 2024 08:48:54 +0100 Subject: [PATCH 088/134] fix: list in markdown quote [WPB-6622] (#2781) --- .../kotlin/com/wire/android/ui/markdown/MarkdownBlockQuote.kt | 4 ++++ kalium | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownBlockQuote.kt b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownBlockQuote.kt index 911032faa82..2dc3348affe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownBlockQuote.kt +++ b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownBlockQuote.kt @@ -32,6 +32,8 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography import org.commonmark.node.BlockQuote +import org.commonmark.node.BulletList +import org.commonmark.node.OrderedList @Composable fun MarkdownBlockQuote(blockQuote: BlockQuote, nodeData: NodeData) { @@ -52,6 +54,8 @@ fun MarkdownBlockQuote(blockQuote: BlockQuote, nodeData: NodeData) { while (child != null) { when (child) { is BlockQuote -> MarkdownBlockQuote(child, nodeData) + is BulletList -> MarkdownBulletList(child, nodeData) + is OrderedList -> MarkdownOrderedList(child, nodeData) else -> { val text = buildAnnotatedString { pushStyle( diff --git a/kalium b/kalium index 5286fd1d046..d77079f86de 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5286fd1d046e0c48723e69130d78d375cf61413f +Subproject commit d77079f86dedd061efcd439a97bb83008aaaeb5e From e5f2d5c2ebf7ec7c77a75d40b97de3f20d2abf3e Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 12 Mar 2024 11:24:54 +0200 Subject: [PATCH 089/134] fix: Localised DateFormat in device info (#2783) --- .../kotlin/com/wire/android/ui/WireActivityDialogs.kt | 4 ++-- .../wire/android/ui/authentication/devices/DeviceItem.kt | 6 +++--- .../authentication/devices/remove/RemoveDeviceDialog.kt | 4 ++-- .../android/ui/settings/devices/DeviceDetailsScreen.kt | 4 ++-- .../main/kotlin/com/wire/android/util/DateTimeUtil.kt | 9 +++++++++ .../kotlin/com/wire/android/util/DateTimeUtilKtTest.kt | 8 +++++++- 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt index e00b00b3754..5c85033f8b5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt @@ -43,7 +43,7 @@ import com.wire.android.ui.joinConversation.JoinConversationViaCodeState import com.wire.android.ui.joinConversation.JoinConversationViaDeepLinkDialog import com.wire.android.ui.joinConversation.JoinConversationViaInviteLinkError import com.wire.android.ui.theme.WireTheme -import com.wire.android.util.formatMediumDateTime +import com.wire.android.util.deviceDateTimeFormat import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText import com.wire.kalium.logic.configuration.server.ServerConfig @@ -319,7 +319,7 @@ fun NewClientDialog( val devicesList = data.clientsInfo.map { stringResource( R.string.new_device_dialog_message_defice_info, - it.date.formatMediumDateTime() ?: "", + it.date.deviceDateTimeFormat() ?: "", it.deviceInfo.asString() ) }.joinToString("") diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt index 486c28eec2a..2b19ebb5c43 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt @@ -64,7 +64,7 @@ import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.extension.formatAsFingerPrint import com.wire.android.util.extension.formatAsString -import com.wire.android.util.formatMediumDateTime +import com.wire.android.util.deviceDateTimeFormat import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText @@ -227,14 +227,14 @@ private fun DeviceItemTexts( stringResource( R.string.remove_device_id_and_time_label_active_label, device.clientId.formatAsString(), - device.registrationTime.formatMediumDateTime() ?: "", + device.registrationTime.deviceDateTimeFormat() ?: "", device.lastActiveDescription() ?: "" ) } else { stringResource( R.string.remove_device_id_and_time_label, device.clientId.formatAsString(), - device.registrationTime.formatMediumDateTime() ?: "" + device.registrationTime.deviceDateTimeFormat() ?: "" ) } } else { diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceDialog.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceDialog.kt index 817ec16122b..bcd7afae6c4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceDialog.kt @@ -41,7 +41,7 @@ import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.textfield.WirePasswordTextField import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.theme.wireDimensions -import com.wire.android.util.formatMediumDateTime +import com.wire.android.util.deviceDateTimeFormat @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -63,7 +63,7 @@ fun RemoveDeviceDialog( stringResource( R.string.remove_device_id_and_time_label, state.device.clientId.value, - state.device.registrationTime?.formatMediumDateTime() ?: "" + state.device.registrationTime?.deviceDateTimeFormat() ?: "" ), onDismiss = onDialogDismissHideKeyboard, dismissButtonProperties = WireDialogButtonProperties( diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index ed2de9da14b..31f87c3dd7c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -86,7 +86,7 @@ import com.wire.android.util.CustomTabsHelper import com.wire.android.util.dialogErrorStrings import com.wire.android.util.extension.formatAsFingerPrint import com.wire.android.util.extension.formatAsString -import com.wire.android.util.formatMediumDateTime +import com.wire.android.util.deviceDateTimeFormat import com.wire.android.util.ui.UIText import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.ClientId @@ -216,7 +216,7 @@ fun DeviceDetailsContent( Divider(color = MaterialTheme.wireColorScheme.background) } - state.device.registrationTime?.formatMediumDateTime()?.let { + state.device.registrationTime?.deviceDateTimeFormat()?.let { item { DeviceDetailSectionContent( stringResource(id = R.string.label_client_added_time), diff --git a/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt b/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt index a642e6635b7..b719ed1005a 100644 --- a/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt @@ -34,6 +34,8 @@ private val serverDateTimeFormat = SimpleDateFormat( ).apply { timeZone = TimeZone.getTimeZone("UTC") } private val mediumDateTimeFormat = DateFormat .getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM) +private val longDateShortTimeFormat = DateFormat + .getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) private val mediumOnlyDateTimeFormat = DateFormat .getDateInstance(DateFormat.MEDIUM) private val messageTimeFormatter = DateFormat @@ -61,6 +63,13 @@ fun String.formatMediumDateTime(): String? = null } +fun String.deviceDateTimeFormat(): String? = + try { + this.serverDate()?.let { longDateShortTimeFormat.format(it) } + } catch (e: ParseException) { + null + } + fun String.formatFullDateShortTime(): String? = try { this.serverDate()?.let { fullDateShortTimeFormatter.format(it) } diff --git a/app/src/test/kotlin/com/wire/android/util/DateTimeUtilKtTest.kt b/app/src/test/kotlin/com/wire/android/util/DateTimeUtilKtTest.kt index 79e7ad4c0e1..17c9bfd0108 100644 --- a/app/src/test/kotlin/com/wire/android/util/DateTimeUtilKtTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/DateTimeUtilKtTest.kt @@ -25,10 +25,16 @@ class DateTimeUtilKtTest { @Test fun `given a invalid date, when performing a transformation, then return null`() { - val result = "NOT_VALID".formatMediumDateTime() + val result = "NOT_VALID".deviceDateTimeFormat() assertEquals(null, result) } + @Test + fun `given a valid date, when performing a transformation for device, then return with medium format`() { + val result = "2022-03-24T18:02:30.360Z".deviceDateTimeFormat() + assertEquals("March 24, 2022, 6:02 PM", result) + } + @Test fun `given a valid date, when performing a transformation, then return with medium format`() { val result = "2022-03-24T18:02:30.360Z".formatMediumDateTime() From d2185f9de438057f68c8b6c23f02062a17e01043 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Tue, 12 Mar 2024 11:58:02 +0100 Subject: [PATCH 090/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index d77079f86de..ef701f42d83 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit d77079f86dedd061efcd439a97bb83008aaaeb5e +Subproject commit ef701f42d83fc3501452a6002c25648b7d43509d From 0fec0809ceb597218bd0fd43fd8bd1f7be8216d5 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Tue, 12 Mar 2024 13:35:58 +0100 Subject: [PATCH 091/134] fix: adding federated members to groups --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index ef701f42d83..5e5668af387 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ef701f42d83fc3501452a6002c25648b7d43509d +Subproject commit 5e5668af387d4113ba72ab5e3437e5948c1c8075 From 8bd9040940883c272cdaef67397682a473f1d46b Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 13 Mar 2024 17:17:31 +0100 Subject: [PATCH 092/134] chore: remove duplicated enroll e2ei use case (#2788) --- .../kotlin/com/wire/android/di/accountScoped/UserModule.kt | 6 ------ .../ui/e2eiEnrollment/GetE2EICertificateViewModel.kt | 2 ++ kalium | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt index a44f108d2d6..b36e3857baa 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt @@ -26,7 +26,6 @@ import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment import com.wire.kalium.logic.feature.conversation.GetAllContactsNotInConversationUseCase -import com.wire.kalium.logic.feature.e2ei.usecase.EnrollE2EIUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetE2eiCertificateUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetMembersE2EICertificateStatusesUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificateStatusUseCase @@ -113,11 +112,6 @@ class UserModule { fun providePersistReadReceiptsStatusConfig(userScope: UserScope): PersistReadReceiptsStatusConfigUseCase = userScope.persistReadReceiptsStatusConfig - @ViewModelScoped - @Provides - fun provideEnrollE2EIUseCase(userScope: UserScope): EnrollE2EIUseCase = - userScope.enrollE2EI - @ViewModelScoped @Provides fun provideFinalizeMLSClientAfterE2EIEnrollmentUseCase(userScope: UserScope): FinalizeMLSClientAfterE2EIEnrollment = diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt index dda01115c8e..118c0eb2de1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt @@ -63,6 +63,7 @@ class GetE2EICertificateViewModel @Inject constructor( val currentSessionResult = currentSession() if (currentSessionResult is CurrentSessionResult.Success && currentSessionResult.accountInfo.isValid()) { coreLogic.getSessionScope(currentSessionResult.accountInfo.userId) + .users .enrollE2EI .initialEnrollment(isNewClientRegistration = isNewClient) .fold({ @@ -82,6 +83,7 @@ class GetE2EICertificateViewModel @Inject constructor( if (currentSessionResult is CurrentSessionResult.Success && currentSessionResult.accountInfo.isValid()) { val enrollmentResult = coreLogic.getSessionScope(currentSessionResult.accountInfo.userId) + .users .enrollE2EI.finalizeEnrollment( oAuthResult.idToken, oAuthResult.authState, diff --git a/kalium b/kalium index 5e5668af387..ef09bb699e3 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5e5668af387d4113ba72ab5e3437e5948c1c8075 +Subproject commit ef09bb699e3ed918d2f6d2330a756cfa8d0ff644 From efe1ca68ed830b1052a7c1a11283533c7eda7963 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Wed, 13 Mar 2024 20:14:20 +0100 Subject: [PATCH 093/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index ef09bb699e3..f195282569d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ef09bb699e3ed918d2f6d2330a756cfa8d0ff644 +Subproject commit f195282569d516a273b5f8d548d8a670849ee8ac From b962aeb44f8ce781c9a8367c2829b9254fe93306 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 14 Mar 2024 12:57:16 +0200 Subject: [PATCH 094/134] fix: Create Group protocol not editable anymore [WPB-7109] (#2790) --- .../GroupConversationNameComponent.kt | 32 +++++++++++-------- .../ui/common/groupname/GroupMetadataState.kt | 4 +-- .../NewConversationViewModel.kt | 11 ++----- .../NewConversationViewModelArrangement.kt | 1 - .../NewConversationViewModelTest.kt | 2 +- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupConversationNameComponent.kt b/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupConversationNameComponent.kt index 215ed681280..34f86264735 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupConversationNameComponent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupConversationNameComponent.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions @@ -31,7 +32,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material3.MaterialTheme -import com.wire.android.ui.common.scaffold.WireScaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -46,19 +46,18 @@ import androidx.compose.ui.tooling.preview.Preview import com.wire.android.R import com.wire.android.ui.common.Icon import com.wire.android.ui.common.ShakeAnimation -import com.wire.android.ui.common.WireDropDown import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.groupname.GroupNameMode.CREATION import com.wire.android.ui.common.rememberBottomBarElevationState import com.wire.android.ui.common.rememberTopBarElevationState +import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography -import com.wire.kalium.logic.data.conversation.ConversationOptions @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -120,18 +119,23 @@ fun GroupNameScreen( ) } } - if (mode == CREATION && mlsEnabled) { - WireDropDown( - items = - ConversationOptions.Protocol.values().map { it.name }, - defaultItemIndex = defaultProtocol.ordinal, - selectedItemIndex = groupProtocol.ordinal, - label = stringResource(R.string.protocol), + if (mode == CREATION) { + Spacer(modifier = Modifier.height(MaterialTheme.wireDimensions.spacing16x)) + Text( + text = stringResource(R.string.protocol), + style = MaterialTheme.wireTypography.label01, modifier = Modifier - .padding(MaterialTheme.wireDimensions.spacing16x) - ) { selectedIndex -> - groupProtocol = ConversationOptions.Protocol.values()[selectedIndex] - } + .fillMaxWidth() + .padding(horizontal = MaterialTheme.wireDimensions.spacing16x) + .padding(bottom = MaterialTheme.wireDimensions.spacing4x) + ) + Text( + text = groupProtocol.name, + style = MaterialTheme.wireTypography.body02, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = MaterialTheme.wireDimensions.spacing16x) + ) } Spacer(modifier = Modifier.weight(1f)) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupMetadataState.kt b/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupMetadataState.kt index 2105ee5b3dd..0275832caea 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupMetadataState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/groupname/GroupMetadataState.kt @@ -28,11 +28,9 @@ data class GroupMetadataState( val originalGroupName: String = "", val selectedUsers: ImmutableSet = persistentSetOf(), val groupName: TextFieldValue = TextFieldValue(""), - var groupProtocol: ConversationOptions.Protocol = ConversationOptions.Protocol.PROTEUS, + val groupProtocol: ConversationOptions.Protocol = ConversationOptions.Protocol.PROTEUS, val animatedGroupNameError: Boolean = false, val continueEnabled: Boolean = false, - val mlsEnabled: Boolean = true, - val defaultProtocol: ConversationOptions.Protocol = ConversationOptions.Protocol.PROTEUS, val isLoading: Boolean = false, val error: NewGroupError = NewGroupError.None, val mode: GroupNameMode = GroupNameMode.CREATION, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt index 56207022ded..e520c76a374 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt @@ -37,7 +37,6 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.CreateGroupConversationUseCase import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase -import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableSet @@ -49,21 +48,15 @@ import javax.inject.Inject class NewConversationViewModel @Inject constructor( private val createGroupConversation: CreateGroupConversationUseCase, private val isSelfATeamMember: IsSelfATeamMemberUseCase, - isMLSEnabled: IsMLSEnabledUseCase, getDefaultProtocol: GetDefaultProtocolUseCase ) : ViewModel() { var newGroupState: GroupMetadataState by mutableStateOf( - GroupMetadataState( - mlsEnabled = isMLSEnabled() - ).let { + GroupMetadataState().let { val defaultProtocol = ConversationOptions .Protocol .fromSupportedProtocolToConversationOptionsProtocol(getDefaultProtocol()) - it.copy( - defaultProtocol = defaultProtocol, - groupProtocol = defaultProtocol - ) + it.copy(groupProtocol = defaultProtocol) } ) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt index 997238d7e81..1c30b5ec750 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt @@ -173,7 +173,6 @@ internal class NewConversationViewModelArrangement { fun arrange() = this to NewConversationViewModel( createGroupConversation = createGroupConversation, - isMLSEnabled = isMLSEnabledUseCase, isSelfATeamMember = isSelfTeamMember, getDefaultProtocol = getDefaultProtocol ).also { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt index 04b6a75d253..73199955b5b 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelTest.kt @@ -157,7 +157,7 @@ class NewConversationViewModelTest { .arrange() // when - val result = viewModel.newGroupState.defaultProtocol + val result = viewModel.newGroupState.groupProtocol // then assertEquals( From d6e94cc25a90a6eb5d994b43269d50d73c7dfc40 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Thu, 14 Mar 2024 12:57:32 +0100 Subject: [PATCH 095/134] chore: bump app version to 4.6.3 --- build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt index 45ecfb9b82c..0e131bb1f95 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt @@ -25,6 +25,6 @@ object AndroidSdk { object AndroidApp { const val id = "com.wire.android" - const val versionName = "4.6.2" + const val versionName = "4.6.3" val versionCode = Versionizer().versionCode } From ce5094b5752fa6f037b349eac371b6131ee19be8 Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Thu, 14 Mar 2024 16:19:57 +0100 Subject: [PATCH 096/134] fix: periodic checks for ws service to start if necessary (WPB-6343) (#2773) --- .../com/wire/android/WireApplication.kt | 1 + ...rtPersistentWebsocketIfNecessaryUseCase.kt | 76 ++++++++++++++++ .../wire/android/ui/WireActivityViewModel.kt | 8 +- .../android/ui/debug/StartServiceReceiver.kt | 34 +------- .../android/workmanager/WireWorkerFactory.kt | 10 +++ .../worker/PersistentWebsocketCheckWorker.kt | 74 ++++++++++++++++ ...rsistentWebsocketIfNecessaryUseCaseTest.kt | 86 +++++++++++++++++++ .../android/ui/WireActivityViewModelTest.kt | 10 ++- 8 files changed, 266 insertions(+), 33 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt create mode 100644 app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt create mode 100644 app/src/test/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCaseTest.kt diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index 5554a2e5614..71d41d122d6 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -71,6 +71,7 @@ class WireApplication : Application(), Configuration.Provider { override fun getWorkManagerConfiguration(): Configuration { return Configuration.Builder() .setWorkerFactory(wireWorkerFactory.get()) + .setMinimumLoggingLevel(android.util.Log.DEBUG) .build() } diff --git a/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt new file mode 100644 index 00000000000..b2ae0514e25 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt @@ -0,0 +1,76 @@ +/* + * Wire + * Copyright (C) 2024 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/. + */ +@file:Suppress("StringTemplate") + +package com.wire.android.feature + +import android.content.Context +import android.content.Intent +import android.os.Build +import com.wire.android.appLogger +import com.wire.android.services.PersistentWebSocketService +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class StartPersistentWebsocketIfNecessaryUseCase @Inject constructor( + @ApplicationContext private val appContext: Context, + private val shouldStartPersistentWebSocketService: ShouldStartPersistentWebSocketServiceUseCase +) { + suspend operator fun invoke() { + val persistentWebSocketServiceIntent = PersistentWebSocketService.newIntent(appContext) + shouldStartPersistentWebSocketService().let { + when (it) { + is ShouldStartPersistentWebSocketServiceUseCase.Result.Failure -> { + appLogger.e("${TAG}: Failure while fetching persistent web socket status flow") + } + + is ShouldStartPersistentWebSocketServiceUseCase.Result.Success -> { + if (it.shouldStartPersistentWebSocketService) { + startForegroundService(persistentWebSocketServiceIntent) + } else { + appLogger.i("${TAG}: Stopping PersistentWebsocketService, no user with persistent web socket enabled found") + appContext.stopService(persistentWebSocketServiceIntent) + } + } + } + } + } + + private fun startForegroundService(persistentWebSocketServiceIntent: Intent) { + when { + PersistentWebSocketService.isServiceStarted -> { + appLogger.i("${TAG}: PersistentWebsocketService already started, not starting again") + } + + else -> { + appLogger.i("${TAG}: Starting PersistentWebsocketService") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + appContext.startForegroundService(persistentWebSocketServiceIntent) + } else { + appContext.startService(persistentWebSocketServiceIntent) + } + } + } + } + + companion object { + const val TAG = "StartPersistentWebsocketIfNecessaryUseCase" + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index d3ce07baa93..12ba8586963 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.work.WorkManager import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.datastore.GlobalDataStore @@ -48,6 +49,8 @@ import com.wire.android.util.deeplink.DeepLinkProcessor import com.wire.android.util.deeplink.DeepLinkResult import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.UIText +import com.wire.android.workmanager.worker.cancelPeriodicPersistentWebsocketCheckWorker +import com.wire.android.workmanager.worker.enqueuePeriodicPersistentWebsocketCheckWorker import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.auth.AccountInfo @@ -112,7 +115,8 @@ class WireActivityViewModel @Inject constructor( private val currentScreenManager: CurrentScreenManager, private val observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory, private val globalDataStore: GlobalDataStore, - private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory + private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory, + private val workManager: WorkManager, ) : ViewModel() { var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState()) @@ -462,9 +466,11 @@ class WireActivityViewModel @Inject constructor( if (statuses.any { it.isPersistentWebSocketEnabled }) { if (!servicesManager.isPersistentWebSocketServiceRunning()) { servicesManager.startPersistentWebSocketService() + workManager.enqueuePeriodicPersistentWebsocketCheckWorker() } } else { servicesManager.stopPersistentWebSocketService() + workManager.cancelPeriodicPersistentWebsocketCheckWorker() } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt b/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt index 6b3e61470e0..93dd70ce2ce 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt @@ -21,10 +21,8 @@ package com.wire.android.ui.debug import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.os.Build import com.wire.android.appLogger -import com.wire.android.feature.ShouldStartPersistentWebSocketServiceUseCase -import com.wire.android.services.PersistentWebSocketService +import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase import com.wire.android.util.dispatchers.DispatcherProvider import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -41,41 +39,15 @@ class StartServiceReceiver : BroadcastReceiver() { lateinit var dispatcherProvider: DispatcherProvider @Inject - lateinit var shouldStartPersistentWebSocketServiceUseCase: ShouldStartPersistentWebSocketServiceUseCase + lateinit var startPersistentWebSocketService: StartPersistentWebsocketIfNecessaryUseCase private val scope by lazy { CoroutineScope(SupervisorJob() + dispatcherProvider.io()) } override fun onReceive(context: Context?, intent: Intent?) { - val persistentWebSocketServiceIntent = PersistentWebSocketService.newIntent(context) appLogger.i("$TAG: onReceive called with action ${intent?.action}") - scope.launch { - shouldStartPersistentWebSocketServiceUseCase().let { - when (it) { - is ShouldStartPersistentWebSocketServiceUseCase.Result.Failure -> { - appLogger.e("$TAG: Failure while fetching persistent web socket status flow") - } - is ShouldStartPersistentWebSocketServiceUseCase.Result.Success -> { - if (it.shouldStartPersistentWebSocketService) { - if (PersistentWebSocketService.isServiceStarted) { - appLogger.i("$TAG: PersistentWebsocketService already started, not starting again") - } else { - appLogger.i("$TAG: Starting PersistentWebsocketService") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context?.startForegroundService(persistentWebSocketServiceIntent) - } else { - context?.startService(persistentWebSocketServiceIntent) - } - } - } else { - appLogger.i("$TAG: Stopping PersistentWebsocketService, no user with persistent web socket enabled found") - context?.stopService(persistentWebSocketServiceIntent) - } - } - } - } - } + scope.launch { startPersistentWebSocketService() } } companion object { diff --git a/app/src/main/kotlin/com/wire/android/workmanager/WireWorkerFactory.kt b/app/src/main/kotlin/com/wire/android/workmanager/WireWorkerFactory.kt index e7b64e6f328..e763092fcf0 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/WireWorkerFactory.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/WireWorkerFactory.kt @@ -23,11 +23,13 @@ import androidx.work.ListenableWorker import androidx.work.WorkerFactory import androidx.work.WorkerParameters import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase import com.wire.android.migration.MigrationManager import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager import com.wire.android.workmanager.worker.MigrationWorker import com.wire.android.workmanager.worker.NotificationFetchWorker +import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker import com.wire.android.workmanager.worker.SingleUserMigrationWorker import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.sync.WrapperWorker @@ -38,6 +40,7 @@ class WireWorkerFactory @Inject constructor( private val wireNotificationManager: WireNotificationManager, private val notificationChannelsManager: NotificationChannelsManager, private val migrationManager: MigrationManager, + private val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase, @KaliumCoreLogic private val coreLogic: CoreLogic ) : WorkerFactory() { @@ -47,12 +50,19 @@ class WireWorkerFactory @Inject constructor( WrapperWorker::class.java.canonicalName -> WrapperWorkerFactory(coreLogic, WireForegroundNotificationDetailsProvider) .createWorker(appContext, workerClassName, workerParameters) + NotificationFetchWorker::class.java.canonicalName -> NotificationFetchWorker(appContext, workerParameters, wireNotificationManager, notificationChannelsManager) + MigrationWorker::class.java.canonicalName -> MigrationWorker(appContext, workerParameters, migrationManager, notificationChannelsManager) + SingleUserMigrationWorker::class.java.canonicalName -> SingleUserMigrationWorker(appContext, workerParameters, migrationManager, notificationChannelsManager) + + PersistentWebsocketCheckWorker::class.java.canonicalName -> + PersistentWebsocketCheckWorker(appContext, workerParameters, startPersistentWebsocketIfNecessary) + else -> null } } diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt new file mode 100644 index 00000000000..b3d3be9a2ef --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt @@ -0,0 +1,74 @@ +/* + * Wire + * Copyright (C) 2024 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/. + */ +@file:Suppress("StringTemplate") + +package com.wire.android.workmanager.worker + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.wire.android.appLogger +import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase +import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.NAME +import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.TAG +import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.WORK_INTERVAL +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.coroutineScope +import kotlin.time.Duration.Companion.hours +import kotlin.time.toJavaDuration + +@HiltWorker +class PersistentWebsocketCheckWorker +@AssistedInject constructor( + @Assisted private val appContext: Context, + @Assisted private val workerParams: WorkerParameters, + private val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase +) : CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result = coroutineScope { + appLogger.i("${TAG}: Starting periodic work check for persistent websocket connection") + startPersistentWebsocketIfNecessary() + Result.success() + } + + companion object { + const val NAME = "wss_check_worker" + const val TAG = "PersistentWebsocketCheckWorker" + val WORK_INTERVAL = 24.hours.toJavaDuration() + } +} + +fun WorkManager.enqueuePeriodicPersistentWebsocketCheckWorker() { + appLogger.i("${TAG}: Enqueueing periodic work for $TAG") + enqueueUniquePeriodicWork( + NAME, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + PeriodicWorkRequestBuilder(WORK_INTERVAL) + .addTag(TAG) // adds the tag so we can cancel later all related work. + .build() + ) +} + +fun WorkManager.cancelPeriodicPersistentWebsocketCheckWorker() { + appLogger.i("${TAG}: Cancelling all periodic scheduled work for the tag $TAG") + cancelAllWorkByTag(TAG) +} diff --git a/app/src/test/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCaseTest.kt new file mode 100644 index 00000000000..0288e4bae7e --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCaseTest.kt @@ -0,0 +1,86 @@ +/* + * Wire + * Copyright (C) 2024 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.feature + +import android.content.ComponentName +import android.content.Context +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +class StartPersistentWebsocketIfNecessaryUseCaseTest { + + @Test + fun givenShouldStartPersistentWebsocketTrue_whenInvoking_thenStartService() = + runTest { + // given + val (arrangement, sut) = Arrangement() + .withShouldStartPersistentWebsocketServiceResult(true) + .arrange() + + // when + sut.invoke() + + // then + verify(exactly = 1) { arrangement.applicationContext.startService(any()) } + } + + @Test + fun givenShouldStartPersistentWebsocketFalse_whenInvoking_thenDONTStartService() = + runTest { + // given + val (arrangement, sut) = Arrangement() + .withShouldStartPersistentWebsocketServiceResult(false) + .arrange() + + // when + sut.invoke() + + // then + verify(exactly = 0) { arrangement.applicationContext.startService(any()) } + } + + inner class Arrangement { + + @MockK + lateinit var shouldStartPersistentWebSocketServiceUseCase: ShouldStartPersistentWebSocketServiceUseCase + + @MockK + lateinit var applicationContext: Context + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + every { applicationContext.startService(any()) } returns ComponentName.createRelative("dummy", "class") + every { applicationContext.stopService(any()) } returns true + } + + fun arrange() = this to StartPersistentWebsocketIfNecessaryUseCase( + applicationContext, + shouldStartPersistentWebSocketServiceUseCase + ) + + fun withShouldStartPersistentWebsocketServiceResult(shouldStart: Boolean) = apply { + coEvery { shouldStartPersistentWebSocketServiceUseCase.invoke() } returns + ShouldStartPersistentWebSocketServiceUseCase.Result.Success(shouldStart) + } + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index d3d858a2a80..4e3905c7ba6 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -21,6 +21,8 @@ package com.wire.android.ui import android.content.Intent +import androidx.work.WorkManager +import androidx.work.impl.OperationImpl import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri @@ -612,6 +614,8 @@ class WireActivityViewModelTest { coEvery { globalDataStore.selectedThemeOptionFlow() } returns flowOf(ThemeOption.LIGHT) coEvery { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(any()).observeIfE2EIIsRequiredDuringLogin() } returns flowOf(false) + every { workManager.cancelAllWorkByTag(any()) } returns OperationImpl() + every { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } returns OperationImpl() } @MockK @@ -673,6 +677,9 @@ class WireActivityViewModelTest { @MockK lateinit var globalDataStore: GlobalDataStore + @MockK + lateinit var workManager: WorkManager + @MockK(relaxed = true) lateinit var onDeepLinkResult: (DeepLinkResult) -> Unit @@ -699,7 +706,8 @@ class WireActivityViewModelTest { currentScreenManager = currentScreenManager, observeScreenshotCensoringConfigUseCaseProviderFactory = observeScreenshotCensoringConfigUseCaseProviderFactory, globalDataStore = globalDataStore, - observeIfE2EIRequiredDuringLoginUseCaseProviderFactory = observeIfE2EIRequiredDuringLoginUseCaseProviderFactory + observeIfE2EIRequiredDuringLoginUseCaseProviderFactory = observeIfE2EIRequiredDuringLoginUseCaseProviderFactory, + workManager = workManager ) } From 98a0bf1033efea437bd41448b702aba14c0555d4 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Thu, 14 Mar 2024 20:17:51 +0100 Subject: [PATCH 097/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index f195282569d..21dc44c8921 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit f195282569d516a273b5f8d548d8a670849ee8ac +Subproject commit 21dc44c892130db4fb4c4c7fb390753a3082babe From f3dfb12fad90113fe25394eef6d12b742cb4fbf4 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 15 Mar 2024 01:35:01 +0100 Subject: [PATCH 098/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 21dc44c8921..cccff3879d3 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 21dc44c892130db4fb4c4c7fb390753a3082babe +Subproject commit cccff3879d3aa7a749a7abb8dfb7423b1e66066f From 0693f1c8db4c201b307f7533d791677fcf7e3595 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Thu, 21 Mar 2024 15:00:20 +0100 Subject: [PATCH 099/134] feat: add a crl revocation list to debug screen (#2793) --- .../com/wire/android/di/CoreLogicModule.kt | 5 ++ .../wire/android/ui/debug/DebugDataOptions.kt | 56 +++++++++++++++++-- .../com/wire/android/ui/debug/DebugScreen.kt | 4 +- kalium | 2 +- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index ed5677f1d35..81d813ee948 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -246,6 +246,11 @@ class UseCaseModule { @CurrentAccount currentAccount: UserId ) = coreLogic.getSessionScope(currentAccount).getPersistentWebSocketStatus + @ViewModelScoped + @Provides + fun provideCheckCrlRevocationListUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = + coreLogic.getSessionScope(currentAccount).checkCrlRevocationList + @ViewModelScoped @Provides fun provideIsMLSEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = 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 667437dc59c..81027e20b0b 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 @@ -59,6 +59,7 @@ import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.E2EIFailure import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.debug.DisableEventProcessingUseCase +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 import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase @@ -98,6 +99,7 @@ class DebugDataOptionsViewModel private val mlsKeyPackageCountUseCase: MLSKeyPackageCountUseCase, private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase, private val disableEventProcessingUseCase: DisableEventProcessingUseCase, + private val checkCrlRevocationListUseCase: CheckCrlRevocationListUseCase ) : ViewModel() { var state by mutableStateOf( @@ -114,6 +116,14 @@ class DebugDataOptionsViewModel ) } + fun checkCrlRevocationList() { + viewModelScope.launch { + checkCrlRevocationListUseCase( + forceUpdate = true + ) + } + } + fun enableEncryptedProteusStorage(enabled: Boolean) { if (enabled) { viewModelScope.launch { @@ -248,7 +258,8 @@ fun DebugDataOptions( onDisableEventProcessingChange = viewModel::disableEventProcessing, enrollE2EICertificate = viewModel::enrollE2EICertificate, handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, - dismissCertificateDialog = viewModel::dismissCertificateDialog + dismissCertificateDialog = viewModel::dismissCertificateDialog, + checkCrlRevocationList = viewModel::checkCrlRevocationList ) } @@ -266,7 +277,8 @@ fun DebugDataOptionsContent( onManualMigrationPressed: () -> Unit, enrollE2EICertificate: () -> Unit, handleE2EIEnrollmentResult: (Either) -> Unit, - dismissCertificateDialog: () -> Unit + dismissCertificateDialog: () -> Unit, + checkCrlRevocationList: () -> Unit ) { Column { @@ -303,6 +315,16 @@ fun DebugDataOptionsContent( ) if (BuildConfig.PRIVATE_BUILD) { + SettingsItem( + title = stringResource(R.string.debug_id), + text = state.debugId, + trailingIcon = R.drawable.ic_copy, + onIconPressed = Clickable( + enabled = true, + onClick = { } + ) + ) + SettingsItem( title = stringResource(R.string.debug_id), text = state.debugId, @@ -352,7 +374,8 @@ fun DebugDataOptionsContent( isEventProcessingEnabled = state.isEventProcessingDisabled, onDisableEventProcessingChange = onDisableEventProcessingChange, onRestartSlowSyncForRecovery = onRestartSlowSyncForRecovery, - onForceUpdateApiVersions = onForceUpdateApiVersions + onForceUpdateApiVersions = onForceUpdateApiVersions, + checkCrlRevocationList = checkCrlRevocationList ) } @@ -520,7 +543,8 @@ private fun DebugToolsOptions( isEventProcessingEnabled: Boolean, onDisableEventProcessingChange: (Boolean) -> Unit, onRestartSlowSyncForRecovery: () -> Unit, - onForceUpdateApiVersions: () -> Unit + onForceUpdateApiVersions: () -> Unit, + checkCrlRevocationList: () -> Unit ) { FolderHeader(stringResource(R.string.label_debug_tools_title)) Column { @@ -548,6 +572,29 @@ private fun DebugToolsOptions( ) } ) + + // checkCrlRevocationList + RowItemTemplate( + modifier = Modifier.wrapContentWidth(), + title = { + Text( + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.onBackground, + text = "CRL revocation check", + modifier = Modifier.padding(start = dimensions().spacing8x) + ) + }, + actions = { + WirePrimaryButton( + minSize = MaterialTheme.wireDimensions.buttonMediumMinSize, + minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize, + onClick = checkCrlRevocationList, + text = stringResource(R.string.debug_settings_force_api_versioning_update_button_text), + fillMaxWidth = false + ) + } + ) + RowItemTemplate( modifier = Modifier.wrapContentWidth(), title = { @@ -625,5 +672,6 @@ fun PreviewOtherDebugOptions() { enrollE2EICertificate = {}, handleE2EIEnrollmentResult = {}, dismissCertificateDialog = {}, + checkCrlRevocationList = {} ) } 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 cc6d9225d8b..fa38be1fb90 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 @@ -70,9 +70,9 @@ fun DebugScreen(navigator: Navigator) { private fun UserDebugContent( onNavigationPressed: () -> Unit, onManualMigrationPressed: (currentAccount: UserId) -> Unit, -) { + userDebugViewModel: UserDebugViewModel = hiltViewModel(), - val userDebugViewModel: UserDebugViewModel = hiltViewModel() +) { val debugContentState: DebugContentState = rememberDebugContentState(userDebugViewModel.logPath) WireScaffold( diff --git a/kalium b/kalium index cccff3879d3..4cf0b4c4f45 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit cccff3879d3aa7a749a7abb8dfb7423b1e66066f +Subproject commit 4cf0b4c4f45cea1e0224e9b50084313e6fa90ee0 From 2d3752827947652ab13eb44cff2a24244c39b821 Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 22 Mar 2024 10:34:03 +0100 Subject: [PATCH 100/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 4cf0b4c4f45..e9c9ef1a9d6 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 4cf0b4c4f45cea1e0224e9b50084313e6fa90ee0 +Subproject commit e9c9ef1a9d68ce317a73dd040d6f505fba21a932 From 4dc00e6533e4107e236f08018fc7e93771bdfed8 Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Fri, 22 Mar 2024 16:39:13 +0100 Subject: [PATCH 101/134] fix: misleading dialog copy when certificate enrolling fails (WPB-7129) (#2805) --- .../android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt | 10 ++++++---- .../kotlin/com/wire/android/ui/home/E2EIDialogs.kt | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt index 55f536fd97d..7c8c1a9073b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt @@ -57,7 +57,7 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination -import com.wire.android.ui.home.E2EIErrorWithDismissDialog +import com.wire.android.ui.home.E2EIErrorNoSnoozeDialog import com.wire.android.ui.home.E2EISuccessDialog import com.wire.android.ui.markdown.MarkdownConstants import com.wire.android.ui.theme.WireTheme @@ -193,10 +193,12 @@ private fun E2EIEnrollmentScreenContent( } if (state.isCertificateEnrollError) { - E2EIErrorWithDismissDialog( + E2EIErrorNoSnoozeDialog( isE2EILoading = state.isLoading, - updateCertificate = enrollE2EICertificate, - onDismiss = dismissErrorDialog + updateCertificate = { + dismissErrorDialog() + enrollE2EICertificate() + } ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt index eedbc2a1bad..a140afe9350 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt @@ -247,7 +247,7 @@ private fun E2EIErrorWithSnoozeDialog( } @Composable -private fun E2EIErrorNoSnoozeDialog( +fun E2EIErrorNoSnoozeDialog( isE2EILoading: Boolean, updateCertificate: () -> Unit ) { From 6833d5f14348a5e43a05dc675bad0ac282afce2c Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 25 Mar 2024 11:42:34 +0100 Subject: [PATCH 102/134] fix: Calling video not streamed when enabling camera on preview screen (WPB-7114) - cherrypick (#2808) --- .../android/di/accountScoped/CallsModule.kt | 10 ++++- .../ui/calling/SharedCallingViewModel.kt | 25 ++++------- .../ui/calling/ongoing/OngoingCallScreen.kt | 10 ++++- .../calling/ongoing/OngoingCallViewModel.kt | 29 +++++++++++- .../ui/calling/OngoingCallViewModelTest.kt | 21 +++++++++ .../ui/calling/SharedCallingViewModelTest.kt | 45 +++---------------- kalium | 2 +- 7 files changed, 83 insertions(+), 59 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt index 098f73210f6..c0f98555a70 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt @@ -37,7 +37,8 @@ import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase -import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.scopes.ViewModelScoped @@ -167,6 +168,13 @@ class CallsModule { ): UpdateVideoStateUseCase = callsScope.updateVideoState + @ViewModelScoped + @Provides + fun provideSetVideoSendStateUseCase( + callsScope: CallsScope + ): SetVideoSendStateUseCase = + callsScope.setVideoSendState + @ViewModelScoped @Provides fun provideIsCallRunningUseCase(callsScope: CallsScope) = diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt index 4eccb51976d..26d101a46e1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt @@ -52,7 +52,7 @@ import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase -import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.util.PlatformView import dagger.hilt.android.lifecycle.HiltViewModel @@ -127,8 +127,9 @@ class SharedCallingViewModel @Inject constructor( private suspend fun observeScreenState() { currentScreenManager.observeCurrentScreen(viewModelScope).collect { - if (it == CurrentScreen.InBackground) { - stopVideo() + // clear video preview when the screen is in background to avoid memory leaks + if (it == CurrentScreen.InBackground && callState.isCameraOn) { + clearVideoPreview() } } } @@ -279,6 +280,11 @@ class SharedCallingViewModel @Inject constructor( callState = callState.copy( isCameraOn = !callState.isCameraOn ) + if (callState.isCameraOn) { + updateVideoState(conversationId, VideoState.STARTED) + } else { + updateVideoState(conversationId, VideoState.STOPPED) + } } } @@ -286,7 +292,6 @@ class SharedCallingViewModel @Inject constructor( viewModelScope.launch { appLogger.i("SharedCallingViewModel: clearing video preview..") setVideoPreview(conversationId, PlatformView(null)) - updateVideoState(conversationId, VideoState.STOPPED) } } @@ -295,18 +300,6 @@ class SharedCallingViewModel @Inject constructor( appLogger.i("SharedCallingViewModel: setting video preview..") setVideoPreview(conversationId, PlatformView(null)) setVideoPreview(conversationId, PlatformView(view)) - updateVideoState(conversationId, VideoState.STARTED) - } - } - - fun stopVideo() { - viewModelScope.launch { - if (callState.isCameraOn) { - appLogger.i("SharedCallingViewModel: stopping video..") - callState = callState.copy(isCameraOn = false, isSpeakerOn = false) - clearVideoPreview() - turnLoudSpeakerOff() - } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index 1696dd17dcf..baddd56f441 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt @@ -122,8 +122,14 @@ fun OngoingCallScreen( hangUpCall = { sharedCallingViewModel.hangUpCall(navigator::navigateBack) }, toggleVideo = sharedCallingViewModel::toggleVideo, flipCamera = sharedCallingViewModel::flipCamera, - setVideoPreview = sharedCallingViewModel::setVideoPreview, - clearVideoPreview = sharedCallingViewModel::clearVideoPreview, + setVideoPreview = { + sharedCallingViewModel.setVideoPreview(it) + ongoingCallViewModel.startSendingVideoFeed() + }, + clearVideoPreview = { + sharedCallingViewModel.clearVideoPreview() + ongoingCallViewModel.stopSendingVideoFeed() + }, navigateBack = navigator::navigateBack, requestVideoStreams = ongoingCallViewModel::requestVideoStreams, hideDoubleTapToast = ongoingCallViewModel::hideDoubleTapToast diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt index 10ee083f22c..9b3abb360b7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt @@ -33,11 +33,14 @@ import com.wire.android.ui.calling.model.UICallParticipant import com.wire.android.ui.navArgs import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager +import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallClient +import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase +import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged @@ -54,7 +57,8 @@ class OngoingCallViewModel @Inject constructor( private val globalDataStore: GlobalDataStore, private val establishedCalls: ObserveEstablishedCallsUseCase, private val requestVideoStreams: RequestVideoStreamsUseCase, - private val currentScreenManager: CurrentScreenManager, + private val setVideoSendState: SetVideoSendStateUseCase, + private val currentScreenManager: CurrentScreenManager ) : ViewModel() { private val ongoingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs() @@ -70,6 +74,7 @@ class OngoingCallViewModel @Inject constructor( init { viewModelScope.launch { establishedCalls().first { it.isNotEmpty() }.run { + initCameraState(this) // We start observing once we have an ongoing call observeCurrentCall() } @@ -77,6 +82,28 @@ class OngoingCallViewModel @Inject constructor( showDoubleTapToast() } + private fun initCameraState(calls: List) { + val currentCall = calls.find { call -> call.conversationId == conversationId } + currentCall?.let { + if (it.isCameraOn) { + startSendingVideoFeed() + } else { + stopSendingVideoFeed() + } + } + } + + fun startSendingVideoFeed() { + viewModelScope.launch { + setVideoSendState(conversationId, VideoState.STARTED) + } + } + fun stopSendingVideoFeed() { + viewModelScope.launch { + setVideoSendState(conversationId, VideoState.STOPPED) + } + } + private suspend fun observeCurrentCall() { establishedCalls() .distinctUntilChanged() diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt index 37fa4e544ae..ddb7a36e4ff 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt @@ -31,12 +31,14 @@ import com.wire.android.util.CurrentScreenManager import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallClient import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase +import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -68,6 +70,9 @@ class OngoingCallViewModelTest { @MockK private lateinit var currentScreenManager: CurrentScreenManager + @MockK + private lateinit var setVideoSendState: SetVideoSendStateUseCase + @MockK private lateinit var globalDataStore: GlobalDataStore @@ -80,6 +85,7 @@ class OngoingCallViewModelTest { coEvery { establishedCall.invoke() } returns flowOf(listOf(provideCall())) coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) coEvery { globalDataStore.getShouldShowDoubleTapToast(any()) } returns false + coEvery { setVideoSendState.invoke(any(), any()) } returns Unit ongoingCallViewModel = OngoingCallViewModel( savedStateHandle = savedStateHandle, @@ -87,10 +93,25 @@ class OngoingCallViewModelTest { requestVideoStreams = requestVideoStreams, currentScreenManager = currentScreenManager, currentUserId = currentUserId, + setVideoSendState = setVideoSendState, globalDataStore = globalDataStore, ) } + @Test + fun givenAnOngoingCall_WhenTurningOnCamera_ThenSetVideoSendStateToStarted() = runTest { + ongoingCallViewModel.startSendingVideoFeed() + + coVerify(exactly = 1) { setVideoSendState.invoke(any(), VideoState.STARTED) } + } + + @Test + fun givenAnOngoingCall_WhenTurningOffCamera_ThenSetVideoSendStateToStopped() = runTest { + ongoingCallViewModel.stopSendingVideoFeed() + + coVerify { setVideoSendState.invoke(any(), VideoState.STOPPED) } + } + @Test fun givenParticipantsList_WhenRequestingVideoStream_ThenRequestItForOnlyParticipantsWithVideoEnabled() = runTest { val expectedClients = listOf( diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt index 4fae4db752c..82e7ffe8208 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt @@ -42,7 +42,7 @@ import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase -import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -261,6 +261,7 @@ class SharedCallingViewModelTest { advanceUntilIdle() sharedCallingViewModel.callState.isCameraOn shouldBeEqualTo false + coVerify(exactly = 1) { updateVideoState(any(), VideoState.STOPPED) } } @Test @@ -272,6 +273,7 @@ class SharedCallingViewModelTest { advanceUntilIdle() sharedCallingViewModel.callState.isCameraOn shouldBeEqualTo true + coVerify(exactly = 1) { updateVideoState(any(), VideoState.STARTED) } } @Test @@ -315,57 +317,24 @@ class SharedCallingViewModelTest { } @Test - fun `given an active call, when setVideoPreview is called, then set the video preview and update video state to STARTED`() = + fun `given a call, when setVideoPreview is called, then set the video preview`() = runTest { coEvery { setVideoPreview(any(), any()) } returns Unit - coEvery { updateVideoState(any(), any()) } returns Unit sharedCallingViewModel.setVideoPreview(view) advanceUntilIdle() coVerify(exactly = 2) { setVideoPreview(any(), any()) } - coVerify(exactly = 1) { updateVideoState(any(), VideoState.STARTED) } } @Test - fun `given an active call, when clearVideoPreview is called, then update video state to STOPPED`() = - runTest { - coEvery { setVideoPreview(any(), any()) } returns Unit - coEvery { updateVideoState(any(), any()) } returns Unit - - sharedCallingViewModel.clearVideoPreview() - advanceUntilIdle() - - coVerify(exactly = 1) { updateVideoState(any(), VideoState.STOPPED) } - } - - @Test - fun `given a video call, when stopping video, then clear Video Preview and turn off speaker`() = - runTest { - sharedCallingViewModel.callState = - sharedCallingViewModel.callState.copy(isCameraOn = true) - coEvery { setVideoPreview(any(), any()) } returns Unit - coEvery { updateVideoState(any(), any()) } returns Unit - coEvery { turnLoudSpeakerOff() } returns Unit - - sharedCallingViewModel.stopVideo() - advanceUntilIdle() - - coVerify(exactly = 1) { setVideoPreview(any(), any()) } - coVerify(exactly = 1) { turnLoudSpeakerOff() } - } - - @Test - fun `given an audio call, when stopVideo is invoked, then do not do anything`() = runTest { - sharedCallingViewModel.callState = sharedCallingViewModel.callState.copy(isCameraOn = false) + fun `given a call, when clearVideoPreview is called, then clear view`() = runTest { coEvery { setVideoPreview(any(), any()) } returns Unit - coEvery { turnLoudSpeakerOff() } returns Unit - sharedCallingViewModel.stopVideo() + sharedCallingViewModel.clearVideoPreview() advanceUntilIdle() - coVerify(inverse = true) { setVideoPreview(any(), any()) } - coVerify(inverse = true) { turnLoudSpeakerOff() } + coVerify(exactly = 1) { setVideoPreview(any(), any()) } } companion object { diff --git a/kalium b/kalium index e9c9ef1a9d6..e19050e052f 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e9c9ef1a9d68ce317a73dd040d6f505fba21a932 +Subproject commit e19050e052f0ea341b3e313fada4c07bf77f122b From 1a40109467dde47611e887301289e0f6117aefe1 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 25 Mar 2024 13:43:00 +0100 Subject: [PATCH 103/134] fix: Some workers not running when persistent websocket is enabled (WPB-7213) (#2803) --- .../android/di/accountScoped/UserModule.kt | 18 +++++++ .../wire/android/ui/home/AppSyncViewModel.kt | 49 +++++++++++++++++++ .../com/wire/android/ui/home/HomeScreen.kt | 23 ++++++++- .../com/wire/android/ui/home/HomeViewModel.kt | 7 ++- kalium | 2 +- 5 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt index b36e3857baa..524bbfc748f 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt @@ -26,10 +26,13 @@ import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment import com.wire.kalium.logic.feature.conversation.GetAllContactsNotInConversationUseCase +import com.wire.kalium.logic.feature.e2ei.CertificateRevocationListCheckWorker import com.wire.kalium.logic.feature.e2ei.usecase.GetE2eiCertificateUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetMembersE2EICertificateStatusesUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificateStatusUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserE2eiCertificatesUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.ObserveCertificateRevocationForSelfClientUseCase +import com.wire.kalium.logic.feature.featureConfig.FeatureFlagsSyncWorker import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase @@ -226,4 +229,19 @@ class UserModule { @Provides fun provideGetUserE2eiCertificates(userScope: UserScope): GetUserE2eiCertificatesUseCase = userScope.getUserE2eiCertificates + + @ViewModelScoped + @Provides + fun provideCertificateRevocationListCheckWorker(userScope: UserScope): CertificateRevocationListCheckWorker = + userScope.certificateRevocationListCheckWorker + + @ViewModelScoped + @Provides + fun provideFeatureFlagsSyncWorker(userScope: UserScope): FeatureFlagsSyncWorker = + userScope.featureFlagsSyncWorker + + @ViewModelScoped + @Provides + fun provideObserveCertificateRevocationForSelfClientUseCase(userScope: UserScope): ObserveCertificateRevocationForSelfClientUseCase = + userScope.observeCertificateRevocationForSelfClient } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt new file mode 100644 index 00000000000..79b2a2cbfb9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 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.home + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.wire.android.navigation.SavedStateViewModel +import com.wire.kalium.logic.feature.e2ei.CertificateRevocationListCheckWorker +import com.wire.kalium.logic.feature.e2ei.usecase.ObserveCertificateRevocationForSelfClientUseCase +import com.wire.kalium.logic.feature.featureConfig.FeatureFlagsSyncWorker +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AppSyncViewModel @Inject constructor( + override val savedStateHandle: SavedStateHandle, + private val certificateRevocationListCheckWorker: CertificateRevocationListCheckWorker, + private val observeCertificateRevocationForSelfClient: ObserveCertificateRevocationForSelfClientUseCase, + private val featureFlagsSyncWorker: FeatureFlagsSyncWorker +) : SavedStateViewModel(savedStateHandle) { + + fun startSyncingAppConfig() { + viewModelScope.launch { + certificateRevocationListCheckWorker.execute() + } + viewModelScope.launch { + observeCertificateRevocationForSelfClient.invoke() + } + viewModelScope.launch { + featureFlagsSyncWorker.execute() + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 9330e43cab0..b64521cc649 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -52,6 +53,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations @@ -96,6 +98,7 @@ import kotlinx.coroutines.launch fun HomeScreen( navigator: Navigator, homeViewModel: HomeViewModel = hiltViewModel(), + appSyncViewModel: AppSyncViewModel = hiltViewModel(), homeDrawerViewModel: HomeDrawerViewModel = hiltViewModel(), conversationListViewModel: ConversationListViewModel = hiltViewModel(), // TODO: move required elements from this one to HomeViewModel?, groupDetailsScreenResultRecipient: ResultRecipient, @@ -106,6 +109,21 @@ fun HomeScreen( val showNotificationsFlow = rememberRequestPushNotificationsPermissionFlow( onPermissionDenied = { /** TODO: Show a dialog rationale explaining why the permission is needed **/ }) + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + appSyncViewModel.startSyncingAppConfig() + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + LaunchedEffect(homeViewModel.savedStateHandle) { showNotificationsFlow.launch() } @@ -300,7 +318,10 @@ fun HomeContent( contentScale = ContentScale.FillBounds, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary), modifier = Modifier - .padding(start = dimensions().spacing4x, top = dimensions().spacing2x) + .padding( + start = dimensions().spacing4x, + top = dimensions().spacing2x + ) .size(dimensions().fabIconSize) ) }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt index ffcaa4ed585..f810fec27eb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt @@ -29,6 +29,7 @@ import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.navigation.SavedStateViewModel import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase +import com.wire.kalium.logic.feature.e2ei.CertificateRevocationListCheckWorker import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first @@ -43,7 +44,8 @@ class HomeViewModel @Inject constructor( private val getSelf: GetSelfUserUseCase, private val needsToRegisterClient: NeedsToRegisterClientUseCase, private val wireSessionImageLoader: WireSessionImageLoader, - private val shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase + private val shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase, + private val certificateRevocationListCheckWorker: CertificateRevocationListCheckWorker ) : SavedStateViewModel(savedStateHandle) { var homeState by mutableStateOf(HomeState()) @@ -51,6 +53,9 @@ class HomeViewModel @Inject constructor( init { loadUserAvatar() + viewModelScope.launch { + certificateRevocationListCheckWorker.execute() + } } fun checkRequirements(onRequirement: (HomeRequirement) -> Unit) { diff --git a/kalium b/kalium index e19050e052f..b65962a2e21 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e19050e052f0ea341b3e313fada4c07bf77f122b +Subproject commit b65962a2e21243fa12294809d2f8cb000f2474fb From bfc274cd71f1d0f99175f3cf0e1ccee1144f17db Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 25 Mar 2024 16:55:13 +0100 Subject: [PATCH 104/134] feat: adding fdroid flavor to build without GMS and Firebase (WPB-2799) (#2727) cherry pick (#2813) Co-authored-by: Lisa Marie Maginnis --- .gitignore | 4 ++ app/build.gradle.kts | 21 ++++++ app/src/fdroid/AndroidManifest.xml | 39 +++++++++++ .../location/LocationPickerHelperFlavor.kt | 32 ++++++++++ .../android/util/extension/GoogleServices.kt | 30 +++++++++ .../location/LocationPickerHelper.kt | 57 +---------------- .../location/LocationPickerViewModel.kt | 2 +- .../MessageCompositionInputStateHolder.kt | 2 +- .../settings/account/MyAccountViewModel.kt | 2 +- .../email/updateEmail/ChangeEmailViewModel.kt | 2 +- .../account/handle/ChangeHandleViewModel.kt | 2 +- .../wire/android/util/extension/Context.kt | 7 -- .../initializer/FirebaseInitializer.kt | 0 .../services/WireFirebaseMessagingService.kt | 0 .../location/LocationPickerHelperFlavor.kt | 64 +++++++++++++++++++ .../android/util/extension/GoogleServices.kt | 31 +++++++++ .../location/LocationPickerViewModelTest.kt | 2 +- build.gradle.kts | 9 ++- .../src/main/kotlin/flavor/ProductFlavors.kt | 2 + default.json | 9 ++- docker-agent/builder.sh | 47 ++++++++++++++ docker-agent/configure-project.sh | 14 ++++ docker-compose.yml | 45 +++++++++++++ kalium | 2 +- 24 files changed, 355 insertions(+), 70 deletions(-) create mode 100644 app/src/fdroid/AndroidManifest.xml create mode 100644 app/src/foss/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt create mode 100644 app/src/foss/kotlin/com/wire/android/util/extension/GoogleServices.kt rename app/src/{main => nonfree}/kotlin/com/wire/android/initializer/FirebaseInitializer.kt (100%) rename app/src/{main => nonfree}/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt (100%) create mode 100644 app/src/nonfree/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt create mode 100644 app/src/nonfree/kotlin/com/wire/android/util/extension/GoogleServices.kt create mode 100755 docker-agent/builder.sh create mode 100755 docker-agent/configure-project.sh create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index 6168c4c16e5..6dde2da7ad0 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,7 @@ lint/tmp/ # Autogenerated file with git hash information. app/src/main/assets/version.txt /intellij.gdsl + +# Editor temporary files +*~ +\#*# \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e146779449b..58fbd63098c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,8 +56,29 @@ android { jniLibs.pickFirsts.add("**/libsodium.so") } android.buildFeatures.buildConfig = true + + var fdroidBuild = gradle.startParameter.taskRequests.toString().lowercase().contains("fdroid") + sourceSets { + // Add the "foss" sourceSets for the fdroid flavor + if(fdroidBuild) { + getByName("main") { + java.srcDirs("src/foss/kotlin", "src/prod/kotlin") + resources.srcDirs("src/prod/res") + println("Building with FOSS sourceSets") + } + // For all other flavors use the "nonfree" sourceSets + } else { + getByName("main") { + java.srcDirs("src/nonfree/kotlin") + println("Building with non-free sourceSets") + } + } + } } + + + dependencies { implementation("com.wire.kalium:kalium-logic") implementation("com.wire.kalium:kalium-util") diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml new file mode 100644 index 00000000000..9d8fbbc4899 --- /dev/null +++ b/app/src/fdroid/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/app/src/foss/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt b/app/src/foss/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt new file mode 100644 index 00000000000..f50538d7437 --- /dev/null +++ b/app/src/foss/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt @@ -0,0 +1,32 @@ +/* + * Wire + * Copyright (C) 2024 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.home.messagecomposer.location + +import android.content.Context +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocationPickerHelperFlavor @Inject constructor(context: Context) : LocationPickerHelper(context) { + suspend fun getLocation(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + getLocationWithoutGms( + onSuccess = onSuccess, + onError = onError + ) + } +} diff --git a/app/src/foss/kotlin/com/wire/android/util/extension/GoogleServices.kt b/app/src/foss/kotlin/com/wire/android/util/extension/GoogleServices.kt new file mode 100644 index 00000000000..fe4dbe7d9a0 --- /dev/null +++ b/app/src/foss/kotlin/com/wire/android/util/extension/GoogleServices.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2023 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.util.extension + +import android.content.Context + +fun Context.isGoogleServicesAvailable(): Boolean { + val returnValue: Boolean = false + return returnValue +} + +fun Context.initGoogleFirebase() { /* Stub for compatibility */ } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt index 72595f0e151..f66fa5aec8a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt @@ -24,66 +24,15 @@ import android.location.Location import android.location.LocationListener import android.location.LocationManager import androidx.core.location.LocationManagerCompat -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.Priority -import com.google.android.gms.tasks.CancellationTokenSource import com.wire.android.AppJsonStyledLogger -import com.wire.android.util.extension.isGoogleServicesAvailable import com.wire.kalium.logger.KaliumLogLevel import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.tasks.await import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class LocationPickerHelper @Inject constructor(@ApplicationContext val context: Context) { - - suspend fun getLocation(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { - if (context.isGoogleServicesAvailable()) { - getLocationWithGms( - onSuccess = onSuccess, - onError = onError - ) - } else { - getLocationWithoutGms( - onSuccess = onSuccess, - onError = onError - ) - } - } - - /** - * Choosing the best location estimate by docs. - * https://developer.android.com/develop/sensors-and-location/location/retrieve-current#BestEstimate - */ - @SuppressLint("MissingPermission") - private suspend fun getLocationWithGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { - if (isLocationServicesEnabled()) { - AppJsonStyledLogger.log( - level = KaliumLogLevel.INFO, - leadingMessage = "GetLocation", - jsonStringKeyValues = mapOf("isUsingGms" to true) - ) - val locationProvider = LocationServices.getFusedLocationProviderClient(context) - val currentLocation = - locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token).await() - val address = Geocoder(context).getFromLocation(currentLocation.latitude, currentLocation.longitude, 1).orEmpty() - onSuccess(GeoLocatedAddress(address.firstOrNull(), currentLocation)) - } else { - AppJsonStyledLogger.log( - level = KaliumLogLevel.WARN, - leadingMessage = "GetLocation", - jsonStringKeyValues = mapOf( - "isUsingGms" to true, - "error" to "Location services are not enabled" - ) - ) - onError() - } - } +open class LocationPickerHelper @Inject constructor(@ApplicationContext val context: Context) { @SuppressLint("MissingPermission") - private fun getLocationWithoutGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + protected fun getLocationWithoutGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { if (isLocationServicesEnabled()) { AppJsonStyledLogger.log( level = KaliumLogLevel.INFO, @@ -112,7 +61,7 @@ class LocationPickerHelper @Inject constructor(@ApplicationContext val context: } } - private fun isLocationServicesEnabled(): Boolean { + protected fun isLocationServicesEnabled(): Boolean { val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager return LocationManagerCompat.isLocationEnabled(locationManager) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt index 353929d758f..86c8a87d1d5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class LocationPickerViewModel @Inject constructor(private val locationPickerHelper: LocationPickerHelper) : ViewModel() { +class LocationPickerViewModel @Inject constructor(private val locationPickerHelper: LocationPickerHelperFlavor) : ViewModel() { var state: LocationPickerState by mutableStateOf(LocationPickerState()) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt index 2657742d451..6b7fc7d6e66 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt @@ -17,6 +17,7 @@ */ package com.wire.android.ui.home.messagecomposer.state +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable @@ -32,7 +33,6 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max -import com.google.android.gms.common.util.VisibleForTesting import com.wire.android.R import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.textfield.WireTextFieldColors diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt index 578cf23a85d..d5af5b405b4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt @@ -18,12 +18,12 @@ package com.wire.android.ui.home.settings.account +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.google.android.gms.common.util.VisibleForTesting import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.navigation.SavedStateViewModel diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModel.kt index 8ae8561c6f7..f97da02b068 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModel.kt @@ -17,6 +17,7 @@ */ package com.wire.android.ui.home.settings.account.email.updateEmail +import androidx.annotation.VisibleForTesting import android.util.Patterns import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,7 +25,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.android.gms.common.util.VisibleForTesting import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.UpdateEmailUseCase import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModel.kt index b2284b79299..4ba506ba847 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModel.kt @@ -17,13 +17,13 @@ */ package com.wire.android.ui.home.settings.account.handle +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.android.gms.common.util.VisibleForTesting import com.wire.android.ui.authentication.create.common.handle.HandleUpdateErrorState import com.wire.kalium.logic.feature.auth.ValidateUserHandleResult import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase diff --git a/app/src/main/kotlin/com/wire/android/util/extension/Context.kt b/app/src/main/kotlin/com/wire/android/util/extension/Context.kt index 3d643d89098..67149ae0f4e 100644 --- a/app/src/main/kotlin/com/wire/android/util/extension/Context.kt +++ b/app/src/main/kotlin/com/wire/android/util/extension/Context.kt @@ -23,19 +23,12 @@ import android.content.ContextWrapper import android.content.pm.PackageManager import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability fun Context.checkPermission(permission: String): Boolean { return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED } -fun Context.isGoogleServicesAvailable(): Boolean { - val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) - return status == ConnectionResult.SUCCESS -} - fun Context.getActivity(): AppCompatActivity? = when (this) { is AppCompatActivity -> this is ContextWrapper -> baseContext.getActivity() diff --git a/app/src/main/kotlin/com/wire/android/initializer/FirebaseInitializer.kt b/app/src/nonfree/kotlin/com/wire/android/initializer/FirebaseInitializer.kt similarity index 100% rename from app/src/main/kotlin/com/wire/android/initializer/FirebaseInitializer.kt rename to app/src/nonfree/kotlin/com/wire/android/initializer/FirebaseInitializer.kt diff --git a/app/src/main/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt b/app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt similarity index 100% rename from app/src/main/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt rename to app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt diff --git a/app/src/nonfree/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt b/app/src/nonfree/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt new file mode 100644 index 00000000000..829aa040835 --- /dev/null +++ b/app/src/nonfree/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt @@ -0,0 +1,64 @@ +/* + * Wire + * Copyright (C) 2024 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.home.messagecomposer.location + +import android.annotation.SuppressLint +import android.content.Context +import android.location.Geocoder +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import com.google.android.gms.tasks.CancellationTokenSource +import com.wire.android.util.extension.isGoogleServicesAvailable +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.tasks.await + +@Singleton +class LocationPickerHelperFlavor @Inject constructor(context: Context) : LocationPickerHelper(context) { + + suspend fun getLocation(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + if (context.isGoogleServicesAvailable()) { + getLocationWithGms( + onSuccess = onSuccess, + onError = onError + ) + } else { + getLocationWithoutGms( + onSuccess = onSuccess, + onError = onError + ) + } + } + + /** + * Choosing the best location estimate by docs. + * https://developer.android.com/develop/sensors-and-location/location/retrieve-current#BestEstimate + */ + @SuppressLint("MissingPermission") + private suspend fun getLocationWithGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + if (isLocationServicesEnabled()) { + val locationProvider = LocationServices.getFusedLocationProviderClient(context) + val currentLocation = + locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token).await() + val address = Geocoder(context).getFromLocation(currentLocation.latitude, currentLocation.longitude, 1).orEmpty() + onSuccess(GeoLocatedAddress(address.firstOrNull(), currentLocation)) + } else { + onError() + } + } +} diff --git a/app/src/nonfree/kotlin/com/wire/android/util/extension/GoogleServices.kt b/app/src/nonfree/kotlin/com/wire/android/util/extension/GoogleServices.kt new file mode 100644 index 00000000000..151da8c0ddf --- /dev/null +++ b/app/src/nonfree/kotlin/com/wire/android/util/extension/GoogleServices.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2023 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.util.extension + +import android.content.Context + +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability + +fun Context.isGoogleServicesAvailable(): Boolean { + val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) + return status == ConnectionResult.SUCCESS +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelTest.kt index 899370151bc..b573830b6a0 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelTest.kt @@ -64,7 +64,7 @@ class LocationPickerViewModelTest { private class Arrangement { - val locationPickerHelper = mockk() + val locationPickerHelper = mockk() fun withGetGeoLocationSuccess() = apply { coEvery { diff --git a/build.gradle.kts b/build.gradle.kts index cdf069f0bd5..7ccb957ef57 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,13 @@ buildscript { } dependencies { classpath(libs.hilt.gradlePlugin) - classpath(libs.googleGms.gradlePlugin) + var fdroidBuild = gradle.startParameter.taskRequests.toString().lowercase().contains("fdroid") + if (fdroidBuild) { + println("Not including gms") + } else { + println("Including gms") + classpath(libs.googleGms.gradlePlugin) + } classpath(libs.aboutLibraries.gradlePlugin) } } @@ -44,3 +50,4 @@ plugins { id(ScriptPlugins.infrastructure) alias(libs.plugins.ksp) apply false // https://github.com/google/dagger/issues/3965 } + diff --git a/buildSrc/src/main/kotlin/flavor/ProductFlavors.kt b/buildSrc/src/main/kotlin/flavor/ProductFlavors.kt index 48affa5818b..e13d4bc25bd 100644 --- a/buildSrc/src/main/kotlin/flavor/ProductFlavors.kt +++ b/buildSrc/src/main/kotlin/flavor/ProductFlavors.kt @@ -35,6 +35,7 @@ sealed class ProductFlavors( object Beta : ProductFlavors("beta", "Wire Beta") object Internal : ProductFlavors("internal", "Wire Internal") object Production : ProductFlavors("prod", "Wire", shareduserId = "com.waz.userid") + object Fdroid : ProductFlavors("fdroid", "Wire", shareduserId = "com.waz.userid") companion object { val all: Collection = setOf( @@ -43,6 +44,7 @@ sealed class ProductFlavors( Beta, Internal, Production, + Fdroid, ) } } diff --git a/default.json b/default.json index 7a53dee3292..d2afb2d2c9b 100644 --- a/default.json +++ b/default.json @@ -61,8 +61,15 @@ "developer_features_enabled": true, "logging_enabled": true, "application_is_private_build": true, + "development_api_enabled": false + }, + "fdroid": { + "application_id": "com.wire", + "developer_features_enabled": false, + "logging_enabled": false, + "application_is_private_build": false, "development_api_enabled": false, - "mls_support_enabled": true + "mls_support_enabled": false } }, "application_name": "Wire", diff --git a/docker-agent/builder.sh b/docker-agent/builder.sh new file mode 100755 index 00000000000..6010b02dcc0 --- /dev/null +++ b/docker-agent/builder.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +if [ "$RUN_STATIC_CODE_ANALYSIS" = true ]; then + echo "Running Static Code Analysis" + ./gradlew detektAll + ./gradlew staticCodeAnalysis +fi + +if [ "$RUN_APP_UNIT_TESTS" = true ] ; then + echo "Running App Unit Tests" + ./gradlew runUnitTests + ./gradlew runUnitTestsFdroid +fi + +if [ "$RUN_APP_ACCEPTANCE_TESTS" = true ] ; then + echo "Running Acceptance Tests" + ./gradlew runAcceptanceTests +fi + +buildOption='' +if [ "$BUILD_WITH_STACKTRACE" = true ] ; then + buildOption="--stacktrace " + echo "Stacktrace option enabled" +fi + +if [ "$CLEAN_PROJECT_BEFORE_BUILD" = true ] ; then + echo "Cleaning the Project" + ./gradlew clean +else + echo "Cleaning the project will be skipped" +fi + +if [ "$BUILD_CLIENT" = true ] ; then + echo "Compiling the client with Flavor:${CUSTOM_FLAVOR} and BuildType:${BUILD_TYPE}" + #./gradlew ${buildOption}assemble${FLAVOR_TYPE}${BUILD_TYPE} + ./gradlew ${buildOption}assemble${CUSTOM_FLAVOR} +else + echo "Building the client will be skipped" +fi + +if [ "$SIGN_APK" = true ] ; then + echo "Signing APK with given details" + clientVersion=$(sed -ne "s/.*ANDROID_CLIENT_MAJOR_VERSION = \"\([^']*\)\"/\1/p" buildSrc/src/main/kotlin/Dependencies.kt) + /home/android-agent/android-sdk/build-tools/30.0.2/apksigner sign --ks ${HOME}/wire-android/${KEYSTORE_PATH} --ks-key-alias ${KEYSTORE_KEY_NAME} --ks-pass pass:${KSTOREPWD} --key-pass pass:${KEYPWD} "${HOME}/wire-android/app/build/outputs/apk/wire-${CUSTOM_FLAVOR,,}-${BUILD_TYPE,,}-${clientVersion}${PATCH_VERSION}.apk" +else + echo "Apk will not be signed by the builder script" +fi diff --git a/docker-agent/configure-project.sh b/docker-agent/configure-project.sh new file mode 100755 index 00000000000..84a785d95d8 --- /dev/null +++ b/docker-agent/configure-project.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +FILE=local.properties +if test -f "$FILE"; then + echo "${FILE} exists already, replacing existing sdk.dir and ndk.dir" + sed -i -r "s!sdk.dir=(.*)!sdk.dir=${ANDROID_HOME}!g" $FILE + sed -i -r "s!android.ndkPath=(.*)!android.ndkPath=${ANDROID_NDK_HOME}!g" $FILE +else + echo "sdk.dir="$ANDROID_HOME >> local.properties + echo "android.ndkPath="$ANDROID_NDK_HOME >> local.properties +fi + echo "$ANDROID_HOME has been added as sdk.dir to ${FILE}" + echo "$ANDROID_NDK_HOME has been added as ndk.dir to ${FILE}" + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..1f2e03fa488 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +version: '3.9' +services: + wire-android-build-server: + build: + context: . + dockerfile: docker-agent/AndroidAgent + image: builder-agent:latest + container_name: wire-android-build-server + environment: + - CUSTOM_FLAVOR=Fdroid + - BUILD_TYPE=Release + - PATCH_VERSION=0 + - CLEAN_PROJECT_BEFORE_BUILD=true + - RUN_APP_UNIT_TESTS=true + - RUN_STATIC_CODE_ANALYSIS=true + - RUN_STORAGE_UNIT_TESTS=true + - RUN_STORAGE_ACCEPTANCE_TESTS=true + - BUILD_CLIENT=true + #### Signing Vars (KEYSTORE_PATH's home directory is the wire-android folder inside the docker container, start from there e.g. app/keystorefile.keystore) + #- SIGN_APK=true + #- KEYSTORE_PATH=your-path-to-your-keystore-file + #- KSTOREPWD=your-keystore-password + #- KEYPWD=your-key-password + #- KEYSTORE_KEY_NAME=your-key-name + ###### needed for custom client compilation + #- CUSTOM_REPOSITORY=https://github.com/wireapp/wire-android-custom-example + #- CUSTOM_FOLDER=example-co + #- CLIENT_FOLDER=client2 + #- GRGIT_USER="your-github-api-token-or-user-name" + #- GRGIT_PASSWORD="your-github-password-only-when-using-username" #only outcomment this if you wanna use username and password instead of a github api token + #### Debug Optins + - BUILD_WITH_STACKTRACE=true + # For permissions isues with GHA + user: "${UID}:${GID}" + volumes: + - ".:/home/android-agent/wire-android" + command: bash -c "cd /home/android-agent/wire-android && /home/android-agent/wire-android/docker-agent/configure-project.sh && /home/android-agent/wire-android/docker-agent/builder.sh" + # enable this service if you wanna check out the progress of the wire-androd-build-server on a webconsole over http://localhost:9999 + #dozzle: + # container_name: dozzle + # image: amir20/dozzle:latest + # volumes: + # - /var/run/docker.sock:/var/run/docker.sock + # ports: + # - 9999:8080 diff --git a/kalium b/kalium index b65962a2e21..65dd2471624 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b65962a2e21243fa12294809d2f8cb000f2474fb +Subproject commit 65dd24716245d7624b2fe92fb4dd4f63db982283 From abd7f3411bcf7ed69788271bc34f0fc9f22b68b3 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 25 Mar 2024 16:55:23 +0100 Subject: [PATCH 105/134] feat: add fdroid to jenkins script (#2814) Co-authored-by: Lisa Marie Maginnis --- AR-builder.groovy | 2 +- Jenkinsfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AR-builder.groovy b/AR-builder.groovy index 7bf41583ad9..24e74ff0b9f 100644 --- a/AR-builder.groovy +++ b/AR-builder.groovy @@ -65,7 +65,7 @@ pipeline { string(name: 'SOURCE_BRANCH', description: 'Branch or PR name to') string(name: 'CHANGE_BRANCH', description: 'Change branch name to build only used to checkout the correct branch if you need the branch name use SOURCE_BRANCH') choice(name: 'BUILD_TYPE', choices: ['Compatrelease', 'Debug', 'Release', 'Compat'], description: 'Build Type for the Client') - choice(name: 'FLAVOR', choices: ['Prod', 'Dev', 'Staging', 'Internal', 'Beta'], description: 'Product Flavor to build') + choice(name: 'FLAVOR', choices: ['Prod', 'Fdroid', 'Dev', 'Staging', 'Internal', 'Beta'], description: 'Product Flavor to build') booleanParam(name: 'UPLOAD_TO_S3', defaultValue: false, description: 'Boolean Flag to define if the build should be uploaded to S3') booleanParam(name: 'UPLOAD_TO_PLAYSTORE_ENABLED', defaultValue: false, description: 'Boolean Flag to define if the build should be uploaded to Playstore') booleanParam(name: 'RUN_UNIT_TEST', defaultValue: true, description: 'Boolean Flag to define if the unit tests should be run') diff --git a/Jenkinsfile b/Jenkinsfile index fe00ad9c02c..4203efa6da0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ List defineFlavor() { } else if (branchName == "develop") { return ['Staging', 'Dev'] } else if (branchName == "prod") { - return ['Prod'] + return ['Prod', 'Fdroid'] } else if (branchName == "internal") { return ['Internal'] } From 4efd892915b2c9f1f5f61d154205d74fff4ec5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Tue, 26 Mar 2024 12:45:47 +0100 Subject: [PATCH 106/134] fix: update last read message on conversation opening [WPB-7208] (#2819) --- .../home/conversations/ConversationScreen.kt | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 548715b6096..bf77f9a1767 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -815,6 +815,7 @@ fun SnackBarMessage( } } +@Suppress("ComplexMethod") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun MessageList( @@ -839,6 +840,8 @@ fun MessageList( selectedMessageId: String? ) { val prevItemCount = remember { mutableStateOf(lazyPagingMessages.itemCount) } + val readLastMessageAtStartTriggered = remember { mutableStateOf(false) } + LaunchedEffect(lazyPagingMessages.itemCount) { if (lazyPagingMessages.itemCount > prevItemCount.value && selectedMessageId == null) { if (prevItemCount.value > 0 @@ -851,17 +854,20 @@ fun MessageList( } } + // update last read message when scroll ends LaunchedEffect(lazyListState.isScrollInProgress) { if (!lazyListState.isScrollInProgress && lazyPagingMessages.itemCount > 0) { val lastVisibleMessage = lazyPagingMessages[lazyListState.firstVisibleItemIndex] ?: return@LaunchedEffect + updateLastReadMessage(lastVisibleMessage, lastUnreadMessageInstant, onUpdateConversationReadDate) + } + } - val lastVisibleMessageInstant = Instant.parse(lastVisibleMessage.header.messageTime.utcISO) - - // TODO: This IF condition should be in the UseCase - // If there are no unread messages, then use distant future and don't update read date - if (lastVisibleMessageInstant > (lastUnreadMessageInstant ?: Instant.DISTANT_FUTURE)) { - onUpdateConversationReadDate(lastVisibleMessage.header.messageTime.utcISO) - } + // update last read message on start + LaunchedEffect(lazyPagingMessages.itemCount) { + if (!readLastMessageAtStartTriggered.value && lazyPagingMessages.itemSnapshotList.items.isNotEmpty()) { + val lastVisibleMessage = lazyPagingMessages[lazyListState.firstVisibleItemIndex] ?: return@LaunchedEffect + readLastMessageAtStartTriggered.value = true + updateLastReadMessage(lastVisibleMessage, lastUnreadMessageInstant, onUpdateConversationReadDate) } } @@ -929,6 +935,20 @@ fun MessageList( }) } +private fun updateLastReadMessage( + lastVisibleMessage: UIMessage, + lastUnreadMessageInstant: Instant?, + onUpdateConversationReadDate: (String) -> Unit +) { + val lastVisibleMessageInstant = Instant.parse(lastVisibleMessage.header.messageTime.utcISO) + + // TODO: This IF condition should be in the UseCase + // If there are no unread messages, then use distant future and don't update read date + if (lastVisibleMessageInstant > (lastUnreadMessageInstant ?: Instant.DISTANT_FUTURE)) { + onUpdateConversationReadDate(lastVisibleMessage.header.messageTime.utcISO) + } +} + @Composable fun JumpToLastMessageButton( coroutineScope: CoroutineScope = rememberCoroutineScope(), From b4f213f98b3c474025fd57ac09bc98f8dfba1c94 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 27 Mar 2024 20:13:47 +0100 Subject: [PATCH 107/134] fix: disable name change when e2ei is enabled (#2825) --- .../home/settings/account/MyAccountScreen.kt | 13 +++-- .../home/settings/account/MyAccountState.kt | 2 +- .../settings/account/MyAccountViewModel.kt | 12 +++-- .../account/MyAccountViewModelTest.kt | 54 ++++++++++++++++--- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt index 0d908b6f901..08d78bc53ff 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt @@ -71,6 +71,9 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper import com.wire.android.util.extension.folderWithElements import com.wire.android.util.toTitleCase +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -143,11 +146,11 @@ private fun mapToUISections( navigateToChangeDisplayName: () -> Unit, navigateToChangeHandle: () -> Unit, navigateToChangeEmail: () -> Unit -): List { +): ImmutableList { return with(state) { listOfNotNull( if (fullName.isNotBlank()) { - DisplayName(fullName, clickableActionIfPossible(state.isReadOnlyAccount, navigateToChangeDisplayName)) + DisplayName(fullName, clickableActionIfPossible(!state.isEditNameAllowed, navigateToChangeDisplayName)) } else { null }, @@ -162,7 +165,7 @@ private fun mapToUISections( ) else null, if (!teamName.isNullOrBlank()) Team(teamName) else null, if (domain.isNotBlank()) Domain(domain) else null - ) + ).toImmutableList() } } @@ -171,7 +174,7 @@ private fun clickableActionIfPossible(shouldDisableAction: Boolean, action: () - @Composable fun MyAccountContent( - accountDetailItems: List = emptyList(), + accountDetailItems: ImmutableList, forgotPasswordUrl: String?, canDeleteAccount: Boolean, onDeleteAccountClicked: () -> Unit, @@ -262,7 +265,7 @@ fun MyAccountContent( @Composable fun PreviewMyAccountScreen() { MyAccountContent( - accountDetailItems = listOf( + accountDetailItems = persistentListOf( DisplayName("Bob", Clickable(enabled = true) {}), Username("@bob_wire", Clickable(enabled = true) {}), Email("bob@wire.com", Clickable(enabled = true) {}), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountState.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountState.kt index dea7467fe1e..1b319148563 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountState.kt @@ -25,7 +25,7 @@ data class MyAccountState( val teamName: String? = null, val domain: String = "", val changePasswordUrl: String? = null, - val isReadOnlyAccount: Boolean = true, + val isEditNameAllowed: Boolean = false, val isEditEmailAllowed: Boolean = false, val isEditHandleAllowed: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt index d5af5b405b4..dde795ca0aa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt @@ -30,6 +30,7 @@ import com.wire.android.navigation.SavedStateViewModel import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.feature.team.GetUpdatedSelfTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase @@ -53,7 +54,8 @@ class MyAccountViewModel @Inject constructor( private val serverConfig: SelfServerConfigUseCase, private val isPasswordRequired: IsPasswordRequiredUseCase, private val isReadOnlyAccount: IsReadOnlyAccountUseCase, - private val dispatchers: DispatcherProvider + private val dispatchers: DispatcherProvider, + private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase ) : SavedStateViewModel(savedStateHandle) { var myAccountState by mutableStateOf(MyAccountState()) @@ -65,6 +67,9 @@ class MyAccountViewModel @Inject constructor( @VisibleForTesting var managedByWire by Delegates.notNull() + @VisibleForTesting + var isE2EIEnabled by Delegates.notNull() + init { runBlocking { hasSAMLCred = when (val result = isPasswordRequired()) { @@ -76,11 +81,12 @@ class MyAccountViewModel @Inject constructor( // is the account is read only it means it is not maneged by wire managedByWire = !isReadOnlyAccount() + isE2EIEnabled = isE2EIEnabledUseCase() } myAccountState = myAccountState.copy( - isReadOnlyAccount = !managedByWire, isEditEmailAllowed = isChangeEmailEnabledByBuild() && !hasSAMLCred && managedByWire, - isEditHandleAllowed = managedByWire + isEditNameAllowed = managedByWire && !isE2EIEnabled, + isEditHandleAllowed = managedByWire && !isE2EIEnabled ) viewModelScope.launch { fetchSelfUser() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModelTest.kt index 4911685f2b0..226b26d100c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModelTest.kt @@ -28,6 +28,7 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.feature.team.GetUpdatedSelfTeamUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase.Result.Success import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase @@ -55,9 +56,10 @@ class MyAccountViewModelTest { @Test fun `when trying to compute if the user requires password fails, then hasSAMLCred is false`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withUserRequiresPasswordResult(IsPasswordRequiredUseCase.Result.Failure(StorageFailure.DataNotFound)) .withIsReadOnlyAccountResult(true) + .withE2EIEnabledResult(false) .arrange() assertFalse(viewModel.hasSAMLCred) @@ -65,9 +67,10 @@ class MyAccountViewModelTest { @Test fun `when trying to compute if the user requires password return true, then hasSAMLCred is false`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withUserRequiresPasswordResult(Success(true)) .withIsReadOnlyAccountResult(true) + .withE2EIEnabledResult(false) .arrange() assertFalse(viewModel.hasSAMLCred) @@ -75,9 +78,10 @@ class MyAccountViewModelTest { @Test fun `when trying to compute if the user requires password return false, then hasSAMLCred is true`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withUserRequiresPasswordResult(Success(false)) .withIsReadOnlyAccountResult(true) + .withE2EIEnabledResult(false) .arrange() assertTrue(viewModel.hasSAMLCred) @@ -85,9 +89,10 @@ class MyAccountViewModelTest { @Test fun `when isAccountReadOnly return true, then managedByWire is false`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withUserRequiresPasswordResult(Success(false)) .withIsReadOnlyAccountResult(true) + .withE2EIEnabledResult(false) .arrange() assertFalse(viewModel.managedByWire) @@ -95,9 +100,10 @@ class MyAccountViewModelTest { @Test fun `when isAccountReadOnly return false, then managedByWire is true`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withUserRequiresPasswordResult(Success(false)) .withIsReadOnlyAccountResult(false) + .withE2EIEnabledResult(false) .arrange() assertTrue(viewModel.managedByWire) @@ -108,6 +114,7 @@ class MyAccountViewModelTest { val (arrangement, _) = Arrangement() .withUserRequiresPasswordResult(Success(false)) .withIsReadOnlyAccountResult(true) + .withE2EIEnabledResult(false) .arrange() verify { @@ -120,6 +127,7 @@ class MyAccountViewModelTest { val (arrangement, _) = Arrangement() .withUserRequiresPasswordResult(Success(true)) .withIsReadOnlyAccountResult(true) + .withE2EIEnabledResult(false) .arrange() coVerify(exactly = 1) { arrangement.selfServerConfigUseCase() } @@ -130,6 +138,7 @@ class MyAccountViewModelTest { val (_, viewModel) = Arrangement() .withUserRequiresPasswordResult(Success(true)) .withIsReadOnlyAccountResult(false) + .withE2EIEnabledResult(false) .arrange() assertTrue(viewModel.myAccountState.isEditHandleAllowed) @@ -140,6 +149,7 @@ class MyAccountViewModelTest { val (_, viewModel) = Arrangement() .withUserRequiresPasswordResult(Success(false)) .withIsReadOnlyAccountResult(true) + .withE2EIEnabledResult(false) .arrange() assertFalse(viewModel.myAccountState.isEditHandleAllowed) @@ -151,6 +161,7 @@ class MyAccountViewModelTest { .withUserRequiresPasswordResult(Success(true)) .withIsReadOnlyAccountResult(false) .withEmailStatusByBuild(emailEditEnabled = false) + .withE2EIEnabledResult(false) .arrange() assertFalse(viewModel.myAccountState.isEditEmailAllowed) @@ -162,11 +173,34 @@ class MyAccountViewModelTest { .withUserRequiresPasswordResult(Success(true)) .withIsReadOnlyAccountResult(false) .withEmailStatusByBuild(emailEditEnabled = true) + .withE2EIEnabledResult(false) .arrange() assertTrue(viewModel.myAccountState.isEditEmailAllowed) } + @Test + fun `given e2ei is enabled, then edit name is NOT allowed`() = runTest { + val (_, viewModel) = Arrangement() + .withUserRequiresPasswordResult(Success(true)) + .withIsReadOnlyAccountResult(false) + .withE2EIEnabledResult(true) + .arrange() + + assertFalse(viewModel.myAccountState.isEditNameAllowed) + } + + @Test + fun `given e2ei is NOT enabled, then edit name IS allowed`() = runTest { + val (_, viewModel) = Arrangement() + .withUserRequiresPasswordResult(Success(true)) + .withIsReadOnlyAccountResult(false) + .withE2EIEnabledResult(false) + .arrange() + + assertTrue(viewModel.myAccountState.isEditNameAllowed) + } + private class Arrangement { @MockK @@ -187,6 +221,9 @@ class MyAccountViewModelTest { @MockK private lateinit var savedStateHandle: SavedStateHandle + @MockK + lateinit var isE2EIEnabledUseCase: IsE2EIEnabledUseCase + private val viewModel by lazy { MyAccountViewModel( savedStateHandle, @@ -195,7 +232,8 @@ class MyAccountViewModelTest { selfServerConfigUseCase, isPasswordRequiredUseCase, isReadOnlyAccountUseCase, - TestDispatcherProvider() + TestDispatcherProvider(), + isE2EIEnabledUseCase ) } @@ -219,6 +257,10 @@ class MyAccountViewModelTest { coEvery { isReadOnlyAccountUseCase() } returns result } + fun withE2EIEnabledResult(result: Boolean) = apply { + coEvery { isE2EIEnabledUseCase() } returns result + } + fun arrange() = this to viewModel } } From 887b2e28115d64a0c27f1df52e3ad21465e777f4 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Thu, 28 Mar 2024 11:59:34 +0100 Subject: [PATCH 108/134] feat: enable encrypted proteus storage for internal builds (#2833) --- default.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/default.json b/default.json index d2afb2d2c9b..a4c58e420da 100644 --- a/default.json +++ b/default.json @@ -27,7 +27,8 @@ "default_backend_url_blacklist": "https://clientblacklist.wire.com/staging", "default_backend_url_website": "https://wire.com", "default_backend_title": "wire-staging", - "is_password_protected_guest_link_enabled": true + "is_password_protected_guest_link_enabled": true, + "encrypt_proteus_storage": true }, "staging": { "application_id": "com.waz.zclient.dev", @@ -46,7 +47,8 @@ "default_backend_url_teams": "https://wire-teams-staging.zinfra.io", "default_backend_url_blacklist": "https://clientblacklist.wire.com/staging", "default_backend_url_website": "https://wire.com", - "default_backend_title": "wire-staging" + "default_backend_title": "wire-staging", + "encrypt_proteus_storage": true }, "beta": { "application_id": "com.wire.android.internal", @@ -54,14 +56,16 @@ "logging_enabled": true, "application_is_private_build": true, "development_api_enabled": false, - "mls_support_enabled": false + "mls_support_enabled": false, + "encrypt_proteus_storage": true }, "internal": { "application_id": "com.wire.internal", "developer_features_enabled": true, "logging_enabled": true, "application_is_private_build": true, - "development_api_enabled": false + "development_api_enabled": false, + "encrypt_proteus_storage": true }, "fdroid": { "application_id": "com.wire", From bd360e102ed311dc5568f12131673c66245aa12a Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Tue, 2 Apr 2024 18:09:43 +0200 Subject: [PATCH 109/134] feat: add avs and cc version to debug screen (#2830) --- .gitignore | 3 +- .../wire/android/ui/debug/DebugDataOptions.kt | 60 +++++++++-- .../kotlin/com/wire/android/util/FileUtil.kt | 9 ++ .../main/kotlin/DependenciesVersionTask.kt | 100 ++++++++++++++++++ buildSrc/src/main/kotlin/MapTojson.kt | 31 ++++++ .../kotlin/scripts/compilation.gradle.kts | 6 ++ 6 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 buildSrc/src/main/kotlin/DependenciesVersionTask.kt create mode 100644 buildSrc/src/main/kotlin/MapTojson.kt diff --git a/.gitignore b/.gitignore index 6dde2da7ad0..f2abb861c48 100644 --- a/.gitignore +++ b/.gitignore @@ -95,8 +95,9 @@ lint/tmp/ # Autogenerated file with git hash information. app/src/main/assets/version.txt +app/src/main/assets/dependencies_version.json /intellij.gdsl # Editor temporary files *~ -\#*# \ No newline at end of file +\#*# 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 81027e20b0b..1d9fbeb0301 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 @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -52,6 +53,7 @@ import com.wire.android.ui.home.settings.SettingsItem import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.getDependenciesVersion import com.wire.android.util.getDeviceIdString import com.wire.android.util.getGitBuildId import com.wire.android.util.ui.PreviewMultipleThemes @@ -69,6 +71,9 @@ import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -85,7 +90,8 @@ data class DebugDataOptionsState( val commitish: String = "null", val certificate: String = "null", val showCertificate: Boolean = false, - val startGettingE2EICertificate: Boolean = false + val startGettingE2EICertificate: Boolean = false, + val dependencies: ImmutableMap = persistentMapOf() ) @Suppress("LongParameterList") @@ -110,10 +116,19 @@ class DebugDataOptionsViewModel observeEncryptedProteusStorageState() observeMlsMetadata() checkIfCanTriggerManualMigration() - state = state.copy( - debugId = context.getDeviceIdString() ?: "null", - commitish = context.getGitBuildId() - ) + checkDependenciesVersion() + setGitHashAndDeviceId() + } + + private fun setGitHashAndDeviceId() { + viewModelScope.launch { + val deviceId = context.getDeviceIdString() ?: "null" + val gitBuildId = context.getGitBuildId() + state = state.copy( + debugId = deviceId, + commitish = gitBuildId + ) + } } fun checkCrlRevocationList() { @@ -124,6 +139,15 @@ class DebugDataOptionsViewModel } } + fun checkDependenciesVersion() { + viewModelScope.launch { + val dependencies = context.getDependenciesVersion().toImmutableMap() + state = state.copy( + dependencies = dependencies + ) + } + } + fun enableEncryptedProteusStorage(enabled: Boolean) { if (enabled) { viewModelScope.launch { @@ -375,7 +399,8 @@ fun DebugDataOptionsContent( onDisableEventProcessingChange = onDisableEventProcessingChange, onRestartSlowSyncForRecovery = onRestartSlowSyncForRecovery, onForceUpdateApiVersions = onForceUpdateApiVersions, - checkCrlRevocationList = checkCrlRevocationList + checkCrlRevocationList = checkCrlRevocationList, + dependenciesMap = state.dependencies ) } @@ -544,7 +569,8 @@ private fun DebugToolsOptions( onDisableEventProcessingChange: (Boolean) -> Unit, onRestartSlowSyncForRecovery: () -> Unit, onForceUpdateApiVersions: () -> Unit, - checkCrlRevocationList: () -> Unit + checkCrlRevocationList: () -> Unit, + dependenciesMap: ImmutableMap ) { FolderHeader(stringResource(R.string.label_debug_tools_title)) Column { @@ -615,6 +641,17 @@ private fun DebugToolsOptions( ) } ) + RowItemTemplate( + modifier = Modifier.wrapContentWidth(), + title = { + Text( + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.onBackground, + text = prettyPrintMap(dependenciesMap), + modifier = Modifier.padding(start = dimensions().spacing8x) + ) + } + ) } } @@ -646,6 +683,15 @@ private fun DisableEventProcessingSwitch( } ) } + +@Stable +private fun prettyPrintMap(map: ImmutableMap): String = StringBuilder().apply { + append("Dependencies:\n") + map.forEach { (key, value) -> + append("$key: $value\n") + } +}.toString() + //endregion @PreviewMultipleThemes diff --git a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt index cc24748afff..4a66f7c3b2e 100644 --- a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt @@ -58,6 +58,7 @@ import com.wire.kalium.logic.util.buildFileName import com.wire.kalium.logic.util.splitFileExtensionAndCopyCounter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import okio.Path import java.io.File import java.io.FileNotFoundException @@ -421,6 +422,14 @@ fun Context.getGitBuildId(): String = runCatching { } }.getOrDefault("") +suspend fun Context.getDependenciesVersion(): Map = withContext(Dispatchers.IO) { + assets.open("dependencies_version.json").use { inputStream -> + inputStream.bufferedReader().use { it.readText() } + }.let { + Json.decodeFromString(it) + } +} + fun Context.getProviderAuthority() = "$packageName.provider" @VisibleForTesting diff --git a/buildSrc/src/main/kotlin/DependenciesVersionTask.kt b/buildSrc/src/main/kotlin/DependenciesVersionTask.kt new file mode 100644 index 00000000000..831008b87c7 --- /dev/null +++ b/buildSrc/src/main/kotlin/DependenciesVersionTask.kt @@ -0,0 +1,100 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.FileOutputStream + +/* + * Wire + * Copyright (C) 2024 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/. + */ +open class DependenciesVersionTask : DefaultTask() { + + private val VERSION_FILE = "app/src/main/assets/dependencies_version.json" + + // map of toml file and list of dependencies to extract version + private val dependencies = mapOf( + "kalium/gradle/libs.versions.toml" to listOf( + "avs", + "core-crypto" + ) + ) + + init { + group = "release" + description = "Get dependencies version and write it to the application, if possible." + } + + @TaskAction + fun processGitBuildIdentifier() { + runCatching { + println("\uD83D\uDD27 Running dependencies version parser to build.") + mutableMapOf().apply { + for ((tomlFile, dependencies) in dependencies) { + val toml = File(tomlFile).readText() + val tables = parseToml(toml) + for (dependency in dependencies) { + val version = tables["versions"]?.get(dependency) + println("\uD83D\uDD27 $dependency version: $version") + put(dependency, version) + } + } + }.toJsonString() + .also { writeToFile(it) } + }.onFailure { + println("\uD83D\uDD27 Failed to extract dependencies version: ${it.stackTraceToString()}") + writeToFile("{}") + } + } + + fun parseToml(tomlContent: String): Map> { + val table = mutableMapOf>() + var currentTable = "" + + // Regular expression to match table headers and key-value pairs + val regex = Regex("""\[(.*?)\]|\s*([^\s=]+)\s*=\s*(".*?"|[^\r\n#]+)""") + + // Iterate over lines of the TOML content + tomlContent.lines().forEach { line -> + val matchResult = regex.find(line) + + // If it's a table header + if (line.startsWith("[")) { + currentTable = matchResult?.groups?.get(1)?.value ?: "" + table[currentTable] = mutableMapOf() + } + // If it's a key-value pair + else if (matchResult != null) { + val key = matchResult.groups[2]?.value?.trim('"') + val value = matchResult.groups[3]?.value?.trim('"') + + if (!key.isNullOrBlank() && !value.isNullOrBlank()) { + table[currentTable]?.put(key, value) + } + } + } + return table + } + + /** + * Write the given [text] to the [VERSION_FILE]. + */ + private fun writeToFile(text: String) { + FileOutputStream(File(project.rootDir, VERSION_FILE)).use { + it.write(text.toByteArray()) + } + println("\u2705 Successfully wrote $text to file.") + } +} diff --git a/buildSrc/src/main/kotlin/MapTojson.kt b/buildSrc/src/main/kotlin/MapTojson.kt new file mode 100644 index 00000000000..6d4cdb2fe20 --- /dev/null +++ b/buildSrc/src/main/kotlin/MapTojson.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2024 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/. + */ + +fun Map.toJsonString (): String { + if (this.isEmpty()) return "{}" + + return StringBuilder().apply { + append("{") + this@toJsonString.forEach { (key, value) -> + append("\"$key\":\"$value\",") + } + deleteCharAt(length - 1) + append("}") + + }.toString() +} diff --git a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts index cd04c23c298..e370013fb66 100644 --- a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts @@ -18,6 +18,7 @@ package scripts +import DependenciesVersionTask import IncludeGitBuildTask plugins { @@ -29,8 +30,13 @@ project.tasks.register("includeGitBuildIdentifier", IncludeGitBuildTask::class) println("> Registering Task :includeGitBuildIdentifier") } +project.tasks.register("dependenciesVersionTask", DependenciesVersionTask::class) { + println("> Registering Task :dependenciesVersionTask") +} + project.afterEvaluate { project.tasks.matching { it.name.startsWith("bundle") || it.name.startsWith("assemble") }.configureEach { dependsOn("includeGitBuildIdentifier") + dependsOn("dependenciesVersionTask") } } From afb374b255065fabf7c55ea8a3d0271aa802a2f8 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Wed, 3 Apr 2024 15:00:59 +0200 Subject: [PATCH 110/134] refactor: simplify dependency version resource generation (#2849) --- .../main/kotlin/DependenciesVersionTask.kt | 100 ------------------ .../main/kotlin/WriteKeyValuesToFileTask.kt | 70 ++++++++++++ .../kotlin/scripts/compilation.gradle.kts | 21 ++-- settings.gradle.kts | 8 ++ 4 files changed, 92 insertions(+), 107 deletions(-) delete mode 100644 buildSrc/src/main/kotlin/DependenciesVersionTask.kt create mode 100644 buildSrc/src/main/kotlin/WriteKeyValuesToFileTask.kt diff --git a/buildSrc/src/main/kotlin/DependenciesVersionTask.kt b/buildSrc/src/main/kotlin/DependenciesVersionTask.kt deleted file mode 100644 index 831008b87c7..00000000000 --- a/buildSrc/src/main/kotlin/DependenciesVersionTask.kt +++ /dev/null @@ -1,100 +0,0 @@ -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.TaskAction -import java.io.File -import java.io.FileOutputStream - -/* - * Wire - * Copyright (C) 2024 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/. - */ -open class DependenciesVersionTask : DefaultTask() { - - private val VERSION_FILE = "app/src/main/assets/dependencies_version.json" - - // map of toml file and list of dependencies to extract version - private val dependencies = mapOf( - "kalium/gradle/libs.versions.toml" to listOf( - "avs", - "core-crypto" - ) - ) - - init { - group = "release" - description = "Get dependencies version and write it to the application, if possible." - } - - @TaskAction - fun processGitBuildIdentifier() { - runCatching { - println("\uD83D\uDD27 Running dependencies version parser to build.") - mutableMapOf().apply { - for ((tomlFile, dependencies) in dependencies) { - val toml = File(tomlFile).readText() - val tables = parseToml(toml) - for (dependency in dependencies) { - val version = tables["versions"]?.get(dependency) - println("\uD83D\uDD27 $dependency version: $version") - put(dependency, version) - } - } - }.toJsonString() - .also { writeToFile(it) } - }.onFailure { - println("\uD83D\uDD27 Failed to extract dependencies version: ${it.stackTraceToString()}") - writeToFile("{}") - } - } - - fun parseToml(tomlContent: String): Map> { - val table = mutableMapOf>() - var currentTable = "" - - // Regular expression to match table headers and key-value pairs - val regex = Regex("""\[(.*?)\]|\s*([^\s=]+)\s*=\s*(".*?"|[^\r\n#]+)""") - - // Iterate over lines of the TOML content - tomlContent.lines().forEach { line -> - val matchResult = regex.find(line) - - // If it's a table header - if (line.startsWith("[")) { - currentTable = matchResult?.groups?.get(1)?.value ?: "" - table[currentTable] = mutableMapOf() - } - // If it's a key-value pair - else if (matchResult != null) { - val key = matchResult.groups[2]?.value?.trim('"') - val value = matchResult.groups[3]?.value?.trim('"') - - if (!key.isNullOrBlank() && !value.isNullOrBlank()) { - table[currentTable]?.put(key, value) - } - } - } - return table - } - - /** - * Write the given [text] to the [VERSION_FILE]. - */ - private fun writeToFile(text: String) { - FileOutputStream(File(project.rootDir, VERSION_FILE)).use { - it.write(text.toByteArray()) - } - println("\u2705 Successfully wrote $text to file.") - } -} diff --git a/buildSrc/src/main/kotlin/WriteKeyValuesToFileTask.kt b/buildSrc/src/main/kotlin/WriteKeyValuesToFileTask.kt new file mode 100644 index 00000000000..3955a7a11d9 --- /dev/null +++ b/buildSrc/src/main/kotlin/WriteKeyValuesToFileTask.kt @@ -0,0 +1,70 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.FileOutputStream + +/* + * Wire + * Copyright (C) 2024 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/. + */ +abstract class WriteKeyValuesToFileTask : DefaultTask() { + + /** + * The JSON file where the [keyValues] will be written + */ + @get:OutputFile + abstract val outputJsonFile: Property + + /** + * Map of key-value pairs that will be written to the [outputJsonFile]. + */ + @get:Input + abstract val keyValues: MapProperty + + init { + group = "build" + description = "Write a set of key-value pairs to a desired file" + } + + @TaskAction + fun processGitBuildIdentifier() { + val outFile = outputJsonFile.get() + require(!outFile.isDirectory) { + "The specified output must be a regular file, not a directory: ${outFile.absolutePath}" + } + runCatching { + logger.debug("\uD83D\uDD27 Writing key-values to ${outFile.absolutePath}.") + keyValues.get().toJsonString().also { writeToFile(it) } + }.onFailure { + logger.error("\uD83D\uDD27 Failed to write key-values to file: ${it.stackTraceToString()}") + writeToFile("{}") + } + } + + /** + * Write the given [text] to the [outputJsonFile]. + */ + private fun writeToFile(text: String) { + FileOutputStream(outputJsonFile.get()).use { + it.write(text.toByteArray()) + } + logger.debug("\u2705 Successfully wrote '$text' to $outputJsonFile.") + } +} diff --git a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts index e370013fb66..c80051693c8 100644 --- a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts @@ -18,7 +18,7 @@ package scripts -import DependenciesVersionTask +import WriteKeyValuesToFileTask import IncludeGitBuildTask plugins { @@ -26,17 +26,24 @@ plugins { } // TODO: Extract to a convention plugin -project.tasks.register("includeGitBuildIdentifier", IncludeGitBuildTask::class) { +val gitIdTask = project.tasks.register("includeGitBuildIdentifier", IncludeGitBuildTask::class) { println("> Registering Task :includeGitBuildIdentifier") } -project.tasks.register("dependenciesVersionTask", DependenciesVersionTask::class) { - println("> Registering Task :dependenciesVersionTask") +val dependenciesVersionTask = project.tasks.register("dependenciesVersionTask", WriteKeyValuesToFileTask::class) { + outputJsonFile.set(project.file("src/main/assets/dependencies_version.json")) + val catalogs = project.extensions.getByType(VersionCatalogsExtension::class.java) + val catalog = catalogs.named("klibs") + val pairs = mapOf( + "avs" to catalog.findVersion("avs").get().requiredVersion, + "core-crypto" to catalog.findVersion("core-crypto-multiplatform").get().requiredVersion + ) + keyValues.set(pairs) } project.afterEvaluate { - project.tasks.matching { it.name.startsWith("bundle") || it.name.startsWith("assemble") }.configureEach { - dependsOn("includeGitBuildIdentifier") - dependsOn("dependenciesVersionTask") + project.tasks.matching { it.name.startsWith("merge") && it.name.endsWith("Assets") }.configureEach { + dependsOn(gitIdTask) + dependsOn(dependenciesVersionTask) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 62426aa4225..83bcfff9fc0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,6 +34,14 @@ rootDir include(":${it.name}") } +dependencyResolutionManagement { + versionCatalogs { + create("klibs") { + from(files("kalium/gradle/libs.versions.toml")) + } + } +} + // A work-around where we define the included builds in a different file // so Reloaded's Dependabot doesn't try to look into Kalium's build.gradle.kts, which is inaccessible as it is a git submodule. // See: https://github.com/dependabot/dependabot-core/issues/7201#issuecomment-1571319655 From 23d925e00b3c6b6f66405b3c1791f9d77a08f79c Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Thu, 4 Apr 2024 13:47:03 +0200 Subject: [PATCH 111/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 65dd2471624..59cb1e5a885 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 65dd24716245d7624b2fe92fb4dd4f63db982283 +Subproject commit 59cb1e5a8853f4549f7f3e6e47a60e27474a8249 From ac7786b6d37bafe9fc29bcf095bd50560323f6df Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Thu, 4 Apr 2024 14:10:31 +0200 Subject: [PATCH 112/134] fix: crash when checking audio file size limit (WPB-5961) (#2757) (#2852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Oussama Hassine Co-authored-by: MichaΕ‚ Saleniuk <30429749+saleniuk@users.noreply.github.com> --- .../recordaudio/AudioMediaRecorder.kt | 26 +++------ .../recordaudio/RecordAudioInfoMessageType.kt | 9 ++- .../recordaudio/RecordAudioViewModel.kt | 25 ++++---- app/src/main/res/values/strings.xml | 1 + .../recordaudio/RecordAudioViewModelTest.kt | 57 +++++++++++++++++-- 5 files changed, 84 insertions(+), 34 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt index 494d18e5ecc..bdfeec85706 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt @@ -24,7 +24,6 @@ import com.wire.android.appLogger import com.wire.android.util.audioFileDateTime import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.asset.KaliumFileSystem -import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.util.DateTimeUtil import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope @@ -36,14 +35,12 @@ import kotlinx.coroutines.launch import java.io.File import java.io.IOException import javax.inject.Inject -import kotlin.properties.Delegates @ViewModelScoped class AudioMediaRecorder @Inject constructor( private val context: Context, private val kaliumFileSystem: KaliumFileSystem, - private val dispatcherProvider: DispatcherProvider, - private val getAssetSizeLimit: GetAssetSizeLimitUseCase + private val dispatcherProvider: DispatcherProvider ) { private val scope by lazy { @@ -52,21 +49,13 @@ class AudioMediaRecorder @Inject constructor( private var mediaRecorder: MediaRecorder? = null - private var assetLimitInMegabyte by Delegates.notNull() - var outputFile: File? = null private val _maxFileSizeReached = MutableSharedFlow() fun getMaxFileSizeReached(): Flow = _maxFileSizeReached.asSharedFlow() - init { - scope.launch { - assetLimitInMegabyte = getAssetSizeLimit(isImage = false) - } - } - - fun setUp() { + fun setUp(assetLimitInMegabyte: Long) { if (mediaRecorder == null) { mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { MediaRecorder(context) @@ -87,22 +76,23 @@ class AudioMediaRecorder @Inject constructor( mediaRecorder?.setMaxFileSize(assetLimitInMegabyte) mediaRecorder?.setOutputFile(outputFile) - observeAudioFileSize() + observeAudioFileSize(assetLimitInMegabyte) } } - fun startRecording() { - try { + fun startRecording(): Boolean = try { mediaRecorder?.prepare() mediaRecorder?.start() + true } catch (e: IllegalStateException) { e.printStackTrace() appLogger.e("[RecordAudio] startRecording: IllegalStateException - ${e.message}") + false } catch (e: IOException) { e.printStackTrace() appLogger.e("[RecordAudio] startRecording: IOException - ${e.message}") + false } - } fun stop() { mediaRecorder?.stop() @@ -112,7 +102,7 @@ class AudioMediaRecorder @Inject constructor( mediaRecorder?.release() } - private fun observeAudioFileSize() { + private fun observeAudioFileSize(assetLimitInMegabyte: Long) { mediaRecorder?.setOnInfoListener { _, what, _ -> if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { scope.launch { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioInfoMessageType.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioInfoMessageType.kt index 3c19e698ddd..7c25f07b4a3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioInfoMessageType.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioInfoMessageType.kt @@ -24,9 +24,16 @@ import com.wire.android.util.ui.UIText sealed class RecordAudioInfoMessageType(override val uiText: UIText) : SnackBarMessage { // Unable to Record Audio due to being in a call - object UnableToRecordAudioCall : RecordAudioInfoMessageType( + data object UnableToRecordAudioCall : RecordAudioInfoMessageType( UIText.StringResource( R.string.record_audio_unable_due_to_ongoing_call ) ) + + // Unable to Record Audio due to error + data object UnableToRecordAudioError : RecordAudioInfoMessageType( + UIText.StringResource( + R.string.record_audio_unable_due_to_error + ) + ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt index a07aaab4d34..bf31b06a9a3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt @@ -31,6 +31,7 @@ import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.android.util.getAudioLengthInMs import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow @@ -47,6 +48,7 @@ import kotlin.io.path.deleteIfExists class RecordAudioViewModel @Inject constructor( private val recordAudioMessagePlayer: RecordAudioMessagePlayer, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val getAssetSizeLimit: GetAssetSizeLimitUseCase, private val currentScreenManager: CurrentScreenManager, private val audioMediaRecorder: AudioMediaRecorder ) : ViewModel() { @@ -130,17 +132,18 @@ class RecordAudioViewModel @Inject constructor( infoMessage.emit(RecordAudioInfoMessageType.UnableToRecordAudioCall.uiText) } } else { - audioMediaRecorder.setUp() - - state = state.copy( - outputFile = audioMediaRecorder.outputFile - ) - - audioMediaRecorder.startRecording() - - state = state.copy( - buttonState = RecordAudioButtonState.RECORDING - ) + viewModelScope.launch { + val assetSizeLimit = getAssetSizeLimit(false) + audioMediaRecorder.setUp(assetSizeLimit) + if (audioMediaRecorder.startRecording()) { + state = state.copy( + outputFile = audioMediaRecorder.outputFile, + buttonState = RecordAudioButtonState.RECORDING + ) + } else { + infoMessage.emit(RecordAudioInfoMessageType.UnableToRecordAudioError.uiText) + } + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63d600ab807..12f5d95cdc7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1295,6 +1295,7 @@ Recording Stopped File size for audio messages is limited to %1$d MB. You can’t record an audio message during a call. + Something went wrong while trying to record audio message. Please try again. App permission To make a call, allow Wire to access your microphone in your device settings. Not Now diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt index 8ade9e40156..32c41cde474 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt @@ -22,18 +22,22 @@ import com.wire.android.config.CoroutineTestExtension import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.media.audiomessage.AudioState import com.wire.android.media.audiomessage.RecordAudioMessagePlayer +import com.wire.android.ui.home.messagecomposer.recordaudio.RecordAudioViewModelTest.Arrangement.Companion.ASSET_SIZE_LIMIT import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -69,7 +73,7 @@ class RecordAudioViewModelTest { fun `given user is not in a call, when start recording audio, then recording screen is shown`() = runTest { // given - val (_, viewModel) = Arrangement() + val (arrangement, viewModel) = Arrangement() .arrange() // when @@ -80,6 +84,10 @@ class RecordAudioViewModelTest { RecordAudioButtonState.RECORDING, viewModel.getButtonState() ) + coVerify(exactly = 1) { arrangement.getAssetSizeLimit(false) } + verify(exactly = 1) { arrangement.audioMediaRecorder.setUp(ASSET_SIZE_LIMIT) } + verify(exactly = 1) { arrangement.audioMediaRecorder.setUp(ASSET_SIZE_LIMIT) } + verify(exactly = 1) { arrangement.audioMediaRecorder.startRecording() } } @Test @@ -216,19 +224,55 @@ class RecordAudioViewModelTest { } } + @Test + fun `given start recording succeeded, when recording audio, then recording screen is shown`() = + runTest { + // given + val (_, viewModel) = Arrangement() + .withStartRecordingSuccessful() + .arrange() + + viewModel.getInfoMessage().test { + // when + viewModel.startRecording() + // then + assertEquals(RecordAudioButtonState.RECORDING, viewModel.getButtonState()) + expectNoEvents() + } + } + + @Test + fun `given start recording failed, when recording audio, then info message is shown`() = + runTest { + // given + val (_, viewModel) = Arrangement() + .withStartRecordingFailed() + .arrange() + + viewModel.getInfoMessage().test { + // when + viewModel.startRecording() + // then + assertEquals(RecordAudioButtonState.ENABLED, viewModel.getButtonState()) + assertEquals(RecordAudioInfoMessageType.UnableToRecordAudioError.uiText, awaitItem()) + } + } + private class Arrangement { val recordAudioMessagePlayer = mockk() val audioMediaRecorder = mockk() val observeEstablishedCalls = mockk() val currentScreenManager = mockk() + val getAssetSizeLimit = mockk() val viewModel by lazy { RecordAudioViewModel( recordAudioMessagePlayer = recordAudioMessagePlayer, observeEstablishedCalls = observeEstablishedCalls, currentScreenManager = currentScreenManager, - audioMediaRecorder = audioMediaRecorder + audioMediaRecorder = audioMediaRecorder, + getAssetSizeLimit = getAssetSizeLimit, ) } @@ -237,8 +281,9 @@ class RecordAudioViewModelTest { val fakeKaliumFileSystem = FakeKaliumFileSystem() - every { audioMediaRecorder.setUp() } returns Unit - every { audioMediaRecorder.startRecording() } returns Unit + coEvery { getAssetSizeLimit.invoke(false) } returns ASSET_SIZE_LIMIT + every { audioMediaRecorder.setUp(ASSET_SIZE_LIMIT) } returns Unit + every { audioMediaRecorder.startRecording() } returns true every { audioMediaRecorder.stop() } returns Unit every { audioMediaRecorder.release() } returns Unit every { audioMediaRecorder.outputFile } returns fakeKaliumFileSystem @@ -273,9 +318,13 @@ class RecordAudioViewModelTest { ) } + fun withStartRecordingSuccessful() = apply { every { audioMediaRecorder.startRecording() } returns true } + fun withStartRecordingFailed() = apply { every { audioMediaRecorder.startRecording() } returns false } + fun arrange() = this to viewModel companion object { + const val ASSET_SIZE_LIMIT = 5L val DUMMY_CALL = Call( conversationId = ConversationId( value = "conversationId", From 272c7ee1f7c8694137ae3cf6b5566f5c1a3adda5 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Fri, 5 Apr 2024 09:01:42 +0200 Subject: [PATCH 113/134] fix: lintVitalAnalyze failing because of dependenciesVersionTask (#2858) --- .../src/main/kotlin/scripts/compilation.gradle.kts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts index c80051693c8..7070edf870a 100644 --- a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts @@ -42,8 +42,13 @@ val dependenciesVersionTask = project.tasks.register("dependenciesVersionTask", } project.afterEvaluate { - project.tasks.matching { it.name.startsWith("merge") && it.name.endsWith("Assets") }.configureEach { - dependsOn(gitIdTask) - dependsOn(dependenciesVersionTask) + project.tasks.matching { + it.name.startsWith("merge") && + it.name.endsWith("Assets") || + it.name.startsWith("lintVitalAnalyze") } + .configureEach { + dependsOn(gitIdTask) + dependsOn(dependenciesVersionTask) + } } From aeff098dd2e82bb13e08b89629287429e9907420 Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 5 Apr 2024 10:29:26 +0300 Subject: [PATCH 114/134] fix: RevokedCertificate dialog undismissable RC [WPB-7226] (#2854) Co-authored-by: Mohamad Jaara --- app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt index a140afe9350..114486d4041 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt @@ -383,7 +383,11 @@ fun E2EICertificateRevokedDialog( type = WireDialogButtonType.Secondary, ), buttonsHorizontalAlignment = false, - properties = DialogProperties(usePlatformDefaultWidth = false) + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = false, + dismissOnClickOutside = false + ) ) } From d67bf85a3995f3bdd11a6dfa1d2a35a4e6bc42ad Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 5 Apr 2024 10:58:28 +0300 Subject: [PATCH 115/134] fix: Remove NotificationDot for some notifications RC (#2856) Co-authored-by: Mohamad Jaara --- .../wire/android/notification/NotificationChannelsManager.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt index 7451d1bdc78..751cc4581bd 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt @@ -130,6 +130,7 @@ class NotificationChannelsManager @Inject constructor( .setVibrationEnabled(false) .setImportance(NotificationManagerCompat.IMPORTANCE_DEFAULT) .setSound(null, null) + .setShowBadge(false) .build() notificationManagerCompat.createNotificationChannel(notificationChannel) @@ -165,6 +166,7 @@ class NotificationChannelsManager @Inject constructor( val notificationChannel = NotificationChannelCompat .Builder(channelId, NotificationManagerCompat.IMPORTANCE_HIGH) .setName(channelName) + .setShowBadge(false) .build() notificationManagerCompat.createNotificationChannel(notificationChannel) From 13d1705f2bddca0573c818c83438b683fb6e8f1c Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Fri, 5 Apr 2024 16:31:34 +0200 Subject: [PATCH 116/134] fix: crash on GrapheneOS when downloading certificate (WPB-7407) (#2864) --- .../recordaudio/AudioMediaRecorder.kt | 4 ++-- .../e2ei/E2eiCertificateDetailsScreen.kt | 20 ++++------------ .../e2ei/E2eiCertificateDetailsViewModel.kt | 24 +++++++++++++++++++ .../com/wire/android/util/DateTimeUtil.kt | 4 ++-- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt index bdfeec85706..43d202a48ea 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt @@ -21,7 +21,7 @@ import android.content.Context import android.media.MediaRecorder import android.os.Build import com.wire.android.appLogger -import com.wire.android.util.audioFileDateTime +import com.wire.android.util.fileDateTime import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.util.DateTimeUtil @@ -118,7 +118,7 @@ class AudioMediaRecorder @Inject constructor( private companion object { fun getRecordingAudioFileName(): String = - "wire-audio-${DateTimeUtil.currentInstant().audioFileDateTime()}.m4a" + "wire-audio-${DateTimeUtil.currentInstant().fileDateTime()}.m4a" const val SIZE_OF_1MB = 1024 * 1024 const val AUDIO_CHANNELS = 1 const val SAMPLING_RATE = 44100 diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt index b883aff5c73..f8496fb52cb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign @@ -49,11 +48,9 @@ import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.util.copyLinkToClipboard import com.wire.android.util.createPemFile -import com.wire.android.util.saveFileToDownloadsFolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import okio.Path.Companion.toOkioPath @RootNavGraph @Destination( @@ -67,7 +64,7 @@ fun E2eiCertificateDetailsScreen( ) { val snackbarHostState = LocalSnackbarHostState.current val scope = rememberCoroutineScope() - val context = LocalContext.current + val downloadedString = stringResource(id = R.string.media_gallery_on_image_downloaded) WireScaffold( topBar = { @@ -92,7 +89,6 @@ fun E2eiCertificateDetailsScreen( with(e2eiCertificateDetailsViewModel) { val copiedToClipboardString = stringResource(id = R.string.e2ei_certificate_details_certificate_copied_to_clipboard) - val downloadedString = stringResource(id = R.string.media_gallery_on_image_downloaded) E2eiCertificateDetailsContent( padding = it, @@ -110,14 +106,10 @@ fun E2eiCertificateDetailsScreen( onDownload = { scope.launch { withContext(Dispatchers.IO) { - createPemFile(CERTIFICATE_FILE_NAME, getCertificate()).also { - saveFileToDownloadsFolder( - context = context, - assetName = CERTIFICATE_FILE_NAME, - assetDataPath = it.toPath().toOkioPath(), - assetDataSize = it.length() - ) - } + createPemFile( + pathname = getCertificateName(), + content = getCertificate() + ) } state.wireModalSheetState.hide() snackbarHostState.showSnackbar(downloadedString) @@ -153,5 +145,3 @@ fun E2eiCertificateDetailsContent( style = textStyle ) } - -const val CERTIFICATE_FILE_NAME = "certificate.txt" diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt index dbe4c82028a..3d195b8b38b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt @@ -22,14 +22,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.wire.android.ui.common.bottomsheet.WireModalSheetState import com.wire.android.ui.navArgs +import com.wire.android.util.fileDateTime +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.util.DateTimeUtil import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class E2eiCertificateDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + private val observerSelfUser: GetSelfUserUseCase, ) : ViewModel() { var state: E2eiCertificateDetailsState by mutableStateOf(E2eiCertificateDetailsState()) @@ -38,7 +45,24 @@ class E2eiCertificateDetailsViewModel @Inject constructor( private val e2eiCertificateDetailsScreenNavArgs: E2eiCertificateDetailsScreenNavArgs = savedStateHandle.navArgs() + private var selfUserHandle: String? = null + + init { + getSelfUserId() + } + + private fun getSelfUserId() { + viewModelScope.launch { + selfUserHandle = observerSelfUser().first().handle + } + } + fun getCertificate() = e2eiCertificateDetailsScreenNavArgs.certificateString + + fun getCertificateName(): String { + val date = DateTimeUtil.currentInstant().fileDateTime() + return "wire-certificate-$selfUserHandle-$date.txt" + } } data class E2eiCertificateDetailsState( diff --git a/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt b/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt index b719ed1005a..343346d88da 100644 --- a/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt @@ -50,7 +50,7 @@ private val readReceiptDateTimeFormat = SimpleDateFormat( Locale.getDefault() ).apply { timeZone = TimeZone.getDefault() } -private val audioFileDateTimeFormat = SimpleDateFormat( +private val fileDateTimeFormat = SimpleDateFormat( "yyyy-MM-dd-hh-mm-ss", Locale.getDefault() ).apply { timeZone = TimeZone.getDefault() } @@ -96,7 +96,7 @@ fun Date.toMediumOnlyDateTime(): String = mediumOnlyDateTimeFormat.format(this) fun Instant.uiReadReceiptDateTime(): String = readReceiptDateTimeFormat.format(Date(this.toEpochMilliseconds())) -fun Instant.audioFileDateTime(): String = audioFileDateTimeFormat +fun Instant.fileDateTime(): String = fileDateTimeFormat .format(Date(this.toEpochMilliseconds())) fun getCurrentParsedDateTime(): String = mediumDateTimeFormat.format(System.currentTimeMillis()) From 8565a3e9cb2e535c7c67d3557989753078b5c64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Mon, 8 Apr 2024 16:38:36 +0200 Subject: [PATCH 117/134] fix: close properly keyboard in conversation screen [WPB-7630] (#2872) --- .../home/conversations/ConversationScreen.kt | 11 +- .../messagecomposer/EnabledMessageComposer.kt | 10 +- .../messagecomposer/MessageComposerInput.kt | 23 ++++ .../state/AdditionalOptionMenuState.kt | 3 +- .../state/MessageComposerStateHolder.kt | 2 +- .../MessageCompositionInputStateHolder.kt | 8 +- .../MessageCompositionInputStateHolderTest.kt | 130 +++++++++--------- 7 files changed, 107 insertions(+), 80 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index bf77f9a1767..2481bf024c1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -56,9 +56,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -188,7 +186,6 @@ fun ConversationScreen( val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current val showDialog = remember { mutableStateOf(ConversationScreenDialogType.NONE) } - val focusManager = LocalFocusManager.current val conversationScreenState = rememberConversationScreenState() val messageComposerViewState = messageComposerViewModel.messageComposerViewState val messageComposerStateHolder = rememberMessageComposerStateHolder( @@ -374,7 +371,7 @@ fun ConversationScreen( } } }, - onBackButtonClick = { conversationScreenOnBackButtonClick(messageComposerViewModel, focusManager, navigator) }, + onBackButtonClick = { conversationScreenOnBackButtonClick(messageComposerViewModel, messageComposerStateHolder, navigator) }, composerMessages = messageComposerViewModel.infoMessage, conversationMessages = conversationMessagesViewModel.infoMessage, conversationMessagesViewModel = conversationMessagesViewModel, @@ -403,7 +400,7 @@ fun ConversationScreen( }, onTypingEvent = messageComposerViewModel::sendTypingEvent ) - BackHandler { conversationScreenOnBackButtonClick(messageComposerViewModel, focusManager, navigator) } + BackHandler { conversationScreenOnBackButtonClick(messageComposerViewModel, messageComposerStateHolder, navigator) } DeleteMessageDialog( state = messageComposerViewModel.deleteMessageDialogsState, actions = messageComposerViewModel.deleteMessageHelper @@ -499,11 +496,11 @@ fun ConversationScreen( private fun conversationScreenOnBackButtonClick( messageComposerViewModel: MessageComposerViewModel, - focusManager: FocusManager, + messageComposerStateHolder: MessageComposerStateHolder, navigator: Navigator ) { messageComposerViewModel.sendTypingEvent(TypingIndicatorMode.STOPPED) - focusManager.clearFocus(true) + messageComposerStateHolder.messageCompositionInputStateHolder.collapseComposer(null) navigator.navigateBack() } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt index 8cdec9c3d7a..3261fcb3439 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt @@ -111,7 +111,7 @@ fun EnabledMessageComposer( messageCompositionInputStateHolder.clearFocus() } else if (additionalOptionStateHolder.selectedOption == AdditionalOptionSelectItem.SelfDeleting) { messageCompositionInputStateHolder.requestFocus() - additionalOptionStateHolder.hideAdditionalOptionsMenu() + additionalOptionStateHolder.unselectAdditionalOptionsMenu() } } @@ -178,6 +178,7 @@ fun EnabledMessageComposer( inputFocused = messageCompositionInputStateHolder.inputFocused, onInputFocusedChanged = ::onInputFocusedChanged, onToggleInputSize = messageCompositionInputStateHolder::toggleInputSize, + onTextCollapse = messageCompositionInputStateHolder::collapseText, onCancelReply = messageCompositionHolder::clearReply, onCancelEdit = ::cancelEdit, onMessageTextChanged = { @@ -247,7 +248,7 @@ fun EnabledMessageComposer( onAdditionalOptionsMenuClicked = { if (!isKeyboardMoving) { if (additionalOptionStateHolder.selectedOption == AdditionalOptionSelectItem.AttachFile) { - additionalOptionStateHolder.hideAdditionalOptionsMenu() + additionalOptionStateHolder.unselectAdditionalOptionsMenu() messageCompositionInputStateHolder.toComposing() } else { showAdditionalOptionsMenu() @@ -293,10 +294,7 @@ fun EnabledMessageComposer( cancelEdit() } BackHandler(isImeVisible || inputStateHolder.optionsVisible) { - inputStateHolder.handleBackPressed( - isImeVisible, - additionalOptionStateHolder.additionalOptionsSubMenuState - ) + inputStateHolder.collapseComposer(additionalOptionStateHolder.additionalOptionsSubMenuState) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt index e05f159ea58..41ed3ab5586 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt @@ -47,6 +47,9 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.nativeKeyCode +import androidx.compose.ui.input.key.onPreInterceptKeyBeforeSoftKeyboard import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -79,6 +82,7 @@ fun ActiveMessageComposerInput( onEditButtonClicked: () -> Unit, onChangeSelfDeletionClicked: () -> Unit, onToggleInputSize: () -> Unit, + onTextCollapse: () -> Unit, onCancelReply: () -> Unit, onCancelEdit: () -> Unit, onInputFocusedChanged: (Boolean) -> Unit, @@ -138,6 +142,7 @@ fun ActiveMessageComposerInput( onLineBottomYCoordinateChanged = onLineBottomYCoordinateChanged, showOptions = showOptions, onPlusClick = onPlusClick, + onTextCollapse = onTextCollapse, modifier = stretchToMaxParentConstraintHeightOrWithInBoundary, ) } @@ -162,6 +167,7 @@ fun ActiveMessageComposerInput( onLineBottomYCoordinateChanged = onLineBottomYCoordinateChanged, showOptions = showOptions, onPlusClick = onPlusClick, + onTextCollapse = onTextCollapse, modifier = stretchToMaxParentConstraintHeightOrWithInBoundary ) } @@ -196,6 +202,7 @@ private fun InputContent( onLineBottomYCoordinateChanged: (Float) -> Unit, showOptions: Boolean, onPlusClick: () -> Unit, + onTextCollapse: () -> Unit, modifier: Modifier, ) { if (!showOptions && inputType is MessageCompositionType.Composing) { @@ -209,6 +216,7 @@ private fun InputContent( } MessageComposerTextInput( + isTextExpanded = isTextExpanded, inputFocused = inputFocused, colors = inputType.inputTextColor(), messageText = messageComposition.messageTextFieldValue, @@ -218,6 +226,7 @@ private fun InputContent( onFocusChanged = onInputFocusedChanged, onSelectedLineIndexChanged = onSelectedLineIndexChanged, onLineBottomYCoordinateChanged = onLineBottomYCoordinateChanged, + onTextCollapse = onTextCollapse, modifier = modifier ) @@ -254,6 +263,7 @@ private fun InputContent( @OptIn(ExperimentalComposeUiApi::class) @Composable private fun MessageComposerTextInput( + isTextExpanded: Boolean, inputFocused: Boolean, colors: WireTextFieldColors, singleLine: Boolean, @@ -263,6 +273,7 @@ private fun MessageComposerTextInput( onFocusChanged: (Boolean) -> Unit = {}, onSelectedLineIndexChanged: (Int) -> Unit = { }, onLineBottomYCoordinateChanged: (Float) -> Unit = { }, + onTextCollapse: () -> Unit, modifier: Modifier = Modifier ) { val keyboardController = LocalSoftwareKeyboardController.current @@ -304,6 +315,18 @@ private fun MessageComposerTextInput( if (focusState.isFocused) { onFocusChanged(focusState.isFocused) } + } + .onPreInterceptKeyBeforeSoftKeyboard { event -> + if (event.key.nativeKeyCode == android.view.KeyEvent.KEYCODE_BACK) { + if (isTextExpanded) { + onTextCollapse() + true + } else { + false + } + } else { + false + } }, interactionSource = interactionSource, onSelectedLineIndexChanged = onSelectedLineIndexChanged, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/AdditionalOptionMenuState.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/AdditionalOptionMenuState.kt index 02c0a1dfb32..93a8365e703 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/AdditionalOptionMenuState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/AdditionalOptionMenuState.kt @@ -64,7 +64,7 @@ class AdditionalOptionStateHolder { additionalOptionsSubMenuState = AdditionalOptionSubMenuState.AttachFile } - fun hideAdditionalOptionsMenu() { + fun unselectAdditionalOptionsMenu() { selectedOption = AdditionalOptionSelectItem.None } @@ -88,6 +88,7 @@ class AdditionalOptionStateHolder { fun toAttachmentAndAdditionalOptionsMenu() { additionalOptionState = AdditionalOptionMenuState.AttachmentAndAdditionalOptionsMenu + unselectAdditionalOptionsMenu() } fun toSelfDeletingOptionsMenu() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt index a4913c6a586..981e20bbc76 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt @@ -112,7 +112,7 @@ class MessageComposerStateHolder( fun onInputFocusedChanged(onFocused: Boolean) { if (onFocused) { - additionalOptionStateHolder.hideAdditionalOptionsMenu() + additionalOptionStateHolder.unselectAdditionalOptionsMenu() messageCompositionInputStateHolder.requestFocus() } else { messageCompositionInputStateHolder.clearFocus() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt index 6b7fc7d6e66..12ebdb41068 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolder.kt @@ -149,6 +149,10 @@ class MessageCompositionInputStateHolder( isTextExpanded = !isTextExpanded } + fun collapseText() { + isTextExpanded = false + } + fun clearFocus() { inputFocused = false } @@ -168,8 +172,8 @@ class MessageCompositionInputStateHolder( clearFocus() } - fun handleBackPressed(isImeVisible: Boolean, additionalOptionsSubMenuState: AdditionalOptionSubMenuState) { - if ((isImeVisible || optionsVisible) && additionalOptionsSubMenuState != AdditionalOptionSubMenuState.RecordAudio) { + fun collapseComposer(additionalOptionsSubMenuState: AdditionalOptionSubMenuState? = null) { + if (additionalOptionsSubMenuState != AdditionalOptionSubMenuState.RecordAudio) { optionsVisible = false subOptionsVisible = false isTextExpanded = false diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolderTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolderTest.kt index 5febb3ae68c..cd9e57a4fa4 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolderTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionInputStateHolderTest.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp import com.wire.android.config.CoroutineTestExtension import com.wire.kalium.logic.data.message.SelfDeletionTimer import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -46,7 +47,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when IME is visible, showOptions should be set to true`() { + fun `when IME is visible, showOptions should be set to true`() = runTest { // Given val isImeVisible = true @@ -58,7 +59,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when IME is hidden and showSubOptions is true, showOptions remains unchanged`() { + fun `when IME is hidden and showSubOptions is true, showOptions remains unchanged`() = runTest { // Given val isImeVisible = false state.updateValuesForTesting(showSubOptions = true, showOptions = false) @@ -71,7 +72,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when IME is hidden and showSubOptions is false, showOptions should be set to false`() { + fun `when IME is hidden and showSubOptions is false, showOptions should be set to false`() = runTest { // Given val isImeVisible = false state.updateValuesForTesting(showSubOptions = false) @@ -84,7 +85,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when offset increases and is bigger than previous and options height, options height is updated`() { + fun `when offset increases and is bigger than previous and options height, options height is updated`() = runTest { // When state.handleOffsetChange( 50.dp, @@ -99,7 +100,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when offset decreases and showSubOptions is false, options height is updated`() { + fun `when offset decreases and showSubOptions is false, options height is updated`() = runTest { // Given state.updateValuesForTesting(previousOffset = 50.dp) @@ -116,7 +117,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when offset decreases to zero, showOptions and isTextExpanded are set to false`() { + fun `when offset decreases to zero, showOptions and isTextExpanded are set to false`() = runTest { // Given state.updateValuesForTesting(previousOffset = 50.dp) @@ -134,7 +135,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when offset equals keyboard height, showSubOptions is set to false`() { + fun `when offset equals keyboard height, showSubOptions is set to false`() = runTest { // Given state.updateValuesForTesting(keyboardHeight = 30.dp) @@ -151,7 +152,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when offset is greater than keyboard height, keyboardHeight is updated`() { + fun `when offset is greater than keyboard height, keyboardHeight is updated`() = runTest { // Given state.updateValuesForTesting(keyboardHeight = 20.dp) @@ -168,7 +169,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when offset increases and is greater than keyboardHeight but is less than previousOffset, keyboardHeight is updated`() { + fun `when offset increases and is greater than keyboardHeight but is less than previousOffset, keyboardHeight is updated`() = runTest { // Given state.updateValuesForTesting(previousOffset = 50.dp, keyboardHeight = 20.dp) @@ -186,51 +187,53 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `when offset decreases, showSubOptions is true, and actualOffset is greater than optionsHeight, values remain unchanged`() { - // Given - state.updateValuesForTesting( - previousOffset = 50.dp, - keyboardHeight = 20.dp, - showSubOptions = true, - optionsHeight = 10.dp - ) - - // When - state.handleOffsetChange( - 30.dp, - NAVIGATION_BAR_HEIGHT, - SOURCE, - TARGET - ) - - // Then - state.optionsHeight shouldBeEqualTo 10.dp - } + fun `when offset decreases, showSubOptions is true, and actualOffset is greater than optionsHeight, values remain unchanged`() = + runTest { + // Given + state.updateValuesForTesting( + previousOffset = 50.dp, + keyboardHeight = 20.dp, + showSubOptions = true, + optionsHeight = 10.dp + ) + + // When + state.handleOffsetChange( + 30.dp, + NAVIGATION_BAR_HEIGHT, + SOURCE, + TARGET + ) + + // Then + state.optionsHeight shouldBeEqualTo 10.dp + } @Test - fun `when offset decreases, showSubOptions is false, and actualOffset is greater than optionsHeight, optionsHeight is updated`() { - // Given - state.updateValuesForTesting( - previousOffset = 50.dp, - keyboardHeight = 20.dp, - showSubOptions = false, - optionsHeight = 10.dp - ) - - // When - state.handleOffsetChange( - 30.dp, - NAVIGATION_BAR_HEIGHT, - SOURCE, - TARGET - ) - - // Then - state.optionsHeight shouldBeEqualTo 30.dp - } + fun `when offset decreases, showSubOptions is false, and actualOffset is greater than optionsHeight, optionsHeight is updated`() = + runTest { + // Given + state.updateValuesForTesting( + previousOffset = 50.dp, + keyboardHeight = 20.dp, + showSubOptions = false, + optionsHeight = 10.dp + ) + + // When + state.handleOffsetChange( + 30.dp, + NAVIGATION_BAR_HEIGHT, + SOURCE, + TARGET + ) + + // Then + state.optionsHeight shouldBeEqualTo 30.dp + } @Test - fun `when offset is the same as previousOffset and greater than current keyboardHeight, keyboardHeight is updated`() { + fun `when offset is the same as previousOffset and greater than current keyboardHeight, keyboardHeight is updated`() = runTest { // Given state.updateValuesForTesting(previousOffset = 40.dp, keyboardHeight = 20.dp) @@ -248,7 +251,7 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `given first keyboard appear when source equals target, then initialKeyboardHeight is set`() { + fun `given first keyboard appear when source equals target, then initialKeyboardHeight is set`() = runTest { // Given val imeValue = 50.dp state.updateValuesForTesting(initialKeyboardHeight = 0.dp) @@ -261,22 +264,23 @@ class MessageCompositionInputStateHolderTest { } @Test - fun `given extended keyboard height when attachment button is clicked, then keyboardHeight is set to initialKeyboardHeight`() { - // Given - val initialKeyboardHeight = 10.dp - state.updateValuesForTesting(previousOffset = 40.dp, keyboardHeight = 20.dp, initialKeyboardHeight = initialKeyboardHeight) + fun `given extended keyboard height when attachment button is clicked, then keyboardHeight is set to initialKeyboardHeight`() = + runTest { + // Given + val initialKeyboardHeight = 10.dp + state.updateValuesForTesting(previousOffset = 40.dp, keyboardHeight = 20.dp, initialKeyboardHeight = initialKeyboardHeight) - // When - state.showOptions() - state.handleOffsetChange(0.dp, NAVIGATION_BAR_HEIGHT, source = TARGET, target = SOURCE) + // When + state.showOptions() + state.handleOffsetChange(0.dp, NAVIGATION_BAR_HEIGHT, source = TARGET, target = SOURCE) - // Then - state.keyboardHeight shouldBeEqualTo 20.dp - state.optionsHeight shouldBeEqualTo initialKeyboardHeight - } + // Then + state.keyboardHeight shouldBeEqualTo 20.dp + state.optionsHeight shouldBeEqualTo initialKeyboardHeight + } @Test - fun `when offset decreases but is not zero, only optionsHeight is updated`() { + fun `when offset decreases but is not zero, only optionsHeight is updated`() = runTest { // Given state.updateValuesForTesting(previousOffset = 50.dp) From ec371a71686c179b7172fd826b87ed5b867c7ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Tue, 9 Apr 2024 09:23:16 +0200 Subject: [PATCH 118/134] fix: read conversation on short list [WPB-7432] (#2876) --- .../ui/home/conversations/ConversationScreen.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 2481bf024c1..d7447d68f78 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -812,7 +812,7 @@ fun SnackBarMessage( } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "ComplexCondition") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun MessageList( @@ -859,11 +859,15 @@ fun MessageList( } } - // update last read message on start + // update last read message on start or when list is not scrollable LaunchedEffect(lazyPagingMessages.itemCount) { - if (!readLastMessageAtStartTriggered.value && lazyPagingMessages.itemSnapshotList.items.isNotEmpty()) { + if ((!readLastMessageAtStartTriggered.value || (!lazyListState.canScrollBackward && !lazyListState.canScrollForward)) + && lazyPagingMessages.itemSnapshotList.items.isNotEmpty() + ) { val lastVisibleMessage = lazyPagingMessages[lazyListState.firstVisibleItemIndex] ?: return@LaunchedEffect - readLastMessageAtStartTriggered.value = true + if (!readLastMessageAtStartTriggered.value) { + readLastMessageAtStartTriggered.value = true + } updateLastReadMessage(lastVisibleMessage, lastUnreadMessageInstant, onUpdateConversationReadDate) } } From 8366a0e9a70cac2d5a76133011335865f8e90be4 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 9 Apr 2024 11:36:28 +0300 Subject: [PATCH 119/134] fix: Display verified E2EI icon other user devices list [WPB-6974] (#2868) --- .../android/ui/userprofile/other/OtherUserDevicesScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserDevicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserDevicesScreen.kt index 35121284f79..ffe69c37d60 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserDevicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserDevicesScreen.kt @@ -50,6 +50,7 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper import com.wire.android.util.ui.LinkText import com.wire.android.util.ui.LinkTextData +import com.wire.kalium.logic.feature.e2ei.CertificateStatus @Composable fun OtherUserDevicesScreen( @@ -122,7 +123,8 @@ private fun OtherUserDevicesContent( isWholeItemClickable = true, onClickAction = onDeviceClick, icon = Icons.Filled.ChevronRight.Icon(), - shouldShowVerifyLabel = true + shouldShowVerifyLabel = true, + shouldShowE2EIInfo = item.e2eiCertificateStatus == CertificateStatus.VALID ) if (index < otherUserDevices.lastIndex) WireDivider() } From 3e294644c8285117dfc7f7e1842dc6e8dec4733f Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Tue, 9 Apr 2024 11:10:49 +0200 Subject: [PATCH 120/134] chore: update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 59cb1e5a885..76b84684a9f 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 59cb1e5a8853f4549f7f3e6e47a60e27474a8249 +Subproject commit 76b84684a9fd12b9fd6ab1d95b20f0ccb70e6fcf From bc12ea5f76098e5989b37d1462985668690480d1 Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Thu, 11 Apr 2024 15:57:22 +0200 Subject: [PATCH 121/134] fix: misleading e2ei certificate error dialog (WPB-7129) (#2883) Signed-off-by: alexandreferris --- .../ui/e2eiEnrollment/E2EIEnrollmentScreen.kt | 10 ++--- .../com/wire/android/ui/home/E2EIDialogs.kt | 38 +++++++++++++++++-- .../settings/devices/DeviceDetailsScreen.kt | 4 +- app/src/main/res/values/strings.xml | 2 + 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt index 7c8c1a9073b..cc11e227b11 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt @@ -57,7 +57,7 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination -import com.wire.android.ui.home.E2EIErrorNoSnoozeDialog +import com.wire.android.ui.home.E2EIEnrollmentErrorWithDismissDialog import com.wire.android.ui.home.E2EISuccessDialog import com.wire.android.ui.markdown.MarkdownConstants import com.wire.android.ui.theme.WireTheme @@ -193,12 +193,10 @@ private fun E2EIEnrollmentScreenContent( } if (state.isCertificateEnrollError) { - E2EIErrorNoSnoozeDialog( + E2EIEnrollmentErrorWithDismissDialog( isE2EILoading = state.isLoading, - updateCertificate = { - dismissErrorDialog() - enrollE2EICertificate() - } + onClick = enrollE2EICertificate, + onDismiss = dismissErrorDialog ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt index 114486d4041..1d5206ceed3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt @@ -195,14 +195,46 @@ fun E2EISuccessDialog( } @Composable -fun E2EIErrorWithDismissDialog( +fun E2EIUpdateErrorWithDismissDialog( isE2EILoading: Boolean, updateCertificate: () -> Unit, onDismiss: () -> Unit ) { - WireDialog( + E2EIErrorWithDismissDialog( title = stringResource(id = R.string.end_to_end_identity_renew_error_dialog_title), text = stringResource(id = R.string.end_to_end_identity_renew_error_dialog_text), + isE2EILoading = isE2EILoading, + updateCertificate = updateCertificate, + onDismiss = onDismiss + ) +} + +@Composable +fun E2EIEnrollmentErrorWithDismissDialog( + isE2EILoading: Boolean, + onClick: () -> Unit, + onDismiss: () -> Unit +) { + E2EIErrorWithDismissDialog( + title = stringResource(id = R.string.end_to_end_identity_enrollment_error_dialog_title), + text = stringResource(id = R.string.end_to_end_identity_enrollment_error_dialog_text), + isE2EILoading = isE2EILoading, + updateCertificate = onClick, + onDismiss = onDismiss + ) +} + +@Composable +private fun E2EIErrorWithDismissDialog( + title: String, + text: String, + isE2EILoading: Boolean, + updateCertificate: () -> Unit, + onDismiss: () -> Unit +) { + WireDialog( + title = title, + text = text, onDismiss = onDismiss, optionButton1Properties = WireDialogButtonProperties( onClick = updateCertificate, @@ -247,7 +279,7 @@ private fun E2EIErrorWithSnoozeDialog( } @Composable -fun E2EIErrorNoSnoozeDialog( +private fun E2EIErrorNoSnoozeDialog( isE2EILoading: Boolean, updateCertificate: () -> Unit ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 31f87c3dd7c..029a283e75a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -75,8 +75,8 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.WireTopAppBarTitle import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.e2eiEnrollment.GetE2EICertificateUI -import com.wire.android.ui.home.E2EIErrorWithDismissDialog import com.wire.android.ui.home.E2EISuccessDialog +import com.wire.android.ui.home.E2EIUpdateErrorWithDismissDialog import com.wire.android.ui.home.conversationslist.common.FolderHeader import com.wire.android.ui.settings.devices.model.DeviceDetailsState import com.wire.android.ui.theme.wireColorScheme @@ -279,7 +279,7 @@ fun DeviceDetailsContent( } if (state.isE2EICertificateEnrollError) { - E2EIErrorWithDismissDialog( + E2EIUpdateErrorWithDismissDialog( isE2EILoading = state.isLoadingCertificate, updateCertificate = { enrollE2eiCertificate() }, onDismiss = onEnrollE2EIErrorDismiss diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 12f5d95cdc7..2edbab382a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1277,6 +1277,8 @@ Certificate updated The certificate is updated and your device is verified. Certificate Details + Certificate couldn’t be issued. + Please try again, or reach out to your team admin. Certificate Details End-to-end certificate revoked Log out to reduce security risks. Then log in again, get a new certificate, and reset your password.\n\nIf you keep using this device, your conversations are no longer verified. From 19bce82a8f1f6e33d47240371ba21f9ef0098989 Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Fri, 12 Apr 2024 11:14:21 +0200 Subject: [PATCH 122/134] fix: remove dot from title string (#2887) Signed-off-by: alexandreferris --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2edbab382a4..2e526d1d4e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1277,7 +1277,7 @@ Certificate updated The certificate is updated and your device is verified. Certificate Details - Certificate couldn’t be issued. + Certificate couldn’t be issued Please try again, or reach out to your team admin. Certificate Details End-to-end certificate revoked From 844f58f4ad71c176bcfba8897ed739640b86099f Mon Sep 17 00:00:00 2001 From: Lisa Marie Maginnis Date: Fri, 12 Apr 2024 17:54:11 +0200 Subject: [PATCH 123/134] fix: startup crash with fdroid [WPB-7286] (#2845) Co-authored-by: Mohamad Jaara --- app/build.gradle.kts | 11 ++++++++--- app/src/fdroid/AndroidManifest.xml | 6 ++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58fbd63098c..1629cd19cd6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -162,10 +162,15 @@ dependencies { implementation(libs.bundlizer.core) // firebase - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.fcm) + var fdroidBuild = gradle.startParameter.taskRequests.toString().lowercase().contains("fdroid") + if (!fdroidBuild) { + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.fcm) + implementation(libs.googleGms.location) + } else { + println("Excluding FireBase for FDroid build") + } implementation(libs.androidx.work) - implementation(libs.googleGms.location) // commonMark implementation(libs.commonmark.core) diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml index 9d8fbbc4899..2be4aeab9f9 100644 --- a/app/src/fdroid/AndroidManifest.xml +++ b/app/src/fdroid/AndroidManifest.xml @@ -35,5 +35,11 @@ android:name="com.wire.android.initializer.FirebaseInitializer" tools:node="remove" /> + + From 480a5a6f79536cb8372b4a363857e7da39d68022 Mon Sep 17 00:00:00 2001 From: Lisa Marie Maginnis Date: Fri, 12 Apr 2024 18:44:12 +0200 Subject: [PATCH 124/134] fix: wrong color of fdroid app icon (WPB-7287) (#2886) Co-authored-by: Mohamad Jaara --- app/build.gradle.kts | 4 ++-- kalium | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1629cd19cd6..b2508233717 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -61,9 +61,9 @@ android { sourceSets { // Add the "foss" sourceSets for the fdroid flavor if(fdroidBuild) { - getByName("main") { + getByName("fdroid") { java.srcDirs("src/foss/kotlin", "src/prod/kotlin") - resources.srcDirs("src/prod/res") + res.srcDirs("src/prod/res") println("Building with FOSS sourceSets") } // For all other flavors use the "nonfree" sourceSets diff --git a/kalium b/kalium index 76b84684a9f..b1520bec82e 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 76b84684a9fd12b9fd6ab1d95b20f0ccb70e6fcf +Subproject commit b1520bec82e8b2625837887f8b806b1e7e16bb5d From fb657e966c8a10507405906ef0a8dd6179434541 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 15 Apr 2024 11:04:43 +0200 Subject: [PATCH 125/134] fix: error in deciding whether the current build should use open source only dependencies or not (#2890) --- app/build.gradle.kts | 19 ++++++++++++++----- build.gradle.kts | 7 ++++++- .../kotlin/scripts/infrastructure.gradle.kts | 4 ++-- .../main/kotlin/scripts/variants.gradle.kts | 13 ++++++++++--- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b2508233717..067b4b33943 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import scripts.Variants_gradle + /* * Wire * Copyright (C) 2024 Wire Swiss GmbH @@ -42,6 +44,11 @@ repositories { google() } +fun isFossSourceSet(): Boolean { + return (Variants_gradle.Default.explicitBuildFlavor() ?: gradle.startParameter.taskRequests.toString()) + .lowercase() + .contains("fdroid") +} android { // Most of the configuration is done in the build-logic // through the Wire Application convention plugin @@ -57,16 +64,17 @@ android { } android.buildFeatures.buildConfig = true - var fdroidBuild = gradle.startParameter.taskRequests.toString().lowercase().contains("fdroid") + val fdroidBuild = isFossSourceSet() + sourceSets { // Add the "foss" sourceSets for the fdroid flavor - if(fdroidBuild) { + if (fdroidBuild) { getByName("fdroid") { java.srcDirs("src/foss/kotlin", "src/prod/kotlin") res.srcDirs("src/prod/res") println("Building with FOSS sourceSets") } - // For all other flavors use the "nonfree" sourceSets + // For all other flavors use the "nonfree" sourceSets } else { getByName("main") { java.srcDirs("src/nonfree/kotlin") @@ -162,8 +170,9 @@ dependencies { implementation(libs.bundlizer.core) // firebase - var fdroidBuild = gradle.startParameter.taskRequests.toString().lowercase().contains("fdroid") - if (!fdroidBuild) { + var fdroidBuild = isFossSourceSet() + + if (!fdroidBuild) { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.fcm) implementation(libs.googleGms.location) diff --git a/build.gradle.kts b/build.gradle.kts index 7ccb957ef57..3cd06b21ce7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,12 @@ buildscript { } dependencies { classpath(libs.hilt.gradlePlugin) - var fdroidBuild = gradle.startParameter.taskRequests.toString().lowercase().contains("fdroid") + val fdroidBuild = (System.getenv("flavor") + ?: System.getenv("FLAVOR") + ?: gradle.startParameter.taskRequests.toString()) + .lowercase() + .contains("fdroid") + if (fdroidBuild) { println("Not including gms") } else { diff --git a/buildSrc/src/main/kotlin/scripts/infrastructure.gradle.kts b/buildSrc/src/main/kotlin/scripts/infrastructure.gradle.kts index 6703c3919c4..d5ed870a37a 100644 --- a/buildSrc/src/main/kotlin/scripts/infrastructure.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/infrastructure.gradle.kts @@ -38,7 +38,7 @@ tasks.register("runUnitTests") { tasks.register("runAcceptanceTests") { description = "Runs all Acceptance Tests in the connected device." - dependsOn(":app:connected${Default.BUILD_FLAVOR.capitalize()}DebugAndroidTest") + dependsOn(":app:connected${Default.resolvedBuildFlavor().capitalize()}DebugAndroidTest") } tasks.register("assembleApp") { @@ -71,7 +71,7 @@ tasks.register("runApp", Exec::class) { val sdkDir = properties["sdk.dir"] val adb = "${sdkDir}/platform-tools/adb" - val applicationPackage = "com.wire.android.${Default.BUILD_FLAVOR}" + val applicationPackage = "com.wire.android.${Default.resolvedBuildFlavor()}" val launchActivity = "com.wire.android.feature.launch.ui.LauncherActivity" commandLine(adb, "shell", "am", "start", "-n", "${applicationPackage}/${launchActivity}") diff --git a/buildSrc/src/main/kotlin/scripts/variants.gradle.kts b/buildSrc/src/main/kotlin/scripts/variants.gradle.kts index e61bd559d92..c9fa0382723 100644 --- a/buildSrc/src/main/kotlin/scripts/variants.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/variants.gradle.kts @@ -40,10 +40,17 @@ object BuildTypes { } object Default { - val BUILD_FLAVOR: String = System.getenv("flavor") ?: System.getenv("FLAVOR") ?: ProductFlavors.Dev.buildName - val BUILD_TYPE = System.getenv("buildType") ?: System.getenv("BUILD_TYPE") ?: BuildTypes.DEBUG + fun explicitBuildFlavor(): String? = System.getenv("flavor") + ?: System.getenv("FLAVOR") - val BUILD_VARIANT = "${BUILD_FLAVOR.capitalize()}${BUILD_TYPE.capitalize()}" + fun resolvedBuildFlavor(): String = explicitBuildFlavor() ?: ProductFlavors.Dev.buildName + + fun explicitBuildType(): String? = System.getenv("buildType") + ?: System.getenv("BUILD_TYPE") + + fun resolvedBuildType(): String = explicitBuildType() ?: BuildTypes.DEBUG + + val BUILD_VARIANT = "${resolvedBuildFlavor().capitalize()}${resolvedBuildType().capitalize()}" } fun NamedDomainObjectContainer.createAppFlavour( From 2d5066dab30ab2d157d5f1e227cbc5f52536cc7d Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 15 Apr 2024 14:04:02 +0300 Subject: [PATCH 126/134] fix: Fetch MLS status on every conversation opening [WPB-8610] (#2884) Co-authored-by: Mojtaba Chenani --- .../main/kotlin/com/wire/android/di/CoreLogicModule.kt | 8 ++++++++ .../home/conversations/info/ConversationInfoViewModel.kt | 9 +++++++++ .../info/ConversationInfoViewModelArrangement.kt | 6 ++++++ kalium | 2 +- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 81d813ee948..bea47ad95d2 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -35,6 +35,7 @@ import com.wire.kalium.logic.feature.connection.BlockUserUseCase import com.wire.kalium.logic.feature.connection.UnblockUserUseCase import com.wire.kalium.logic.feature.conversation.ObserveOtherUserSecurityClassificationLabelUseCase import com.wire.kalium.logic.feature.conversation.ObserveSecurityClassificationLabelUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveTeamSettingsSelfDeletingStatusUseCase @@ -445,4 +446,11 @@ class UseCaseModule { fun provideObserveIsAppLockEditableUseCase( @KaliumCoreLogic coreLogic: CoreLogic ): ObserveIsAppLockEditableUseCase = coreLogic.getGlobalScope().observeIsAppLockEditableUseCase + + @ViewModelScoped + @Provides + fun provideFetchConversationMLSVerificationStatusUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId + ): FetchConversationMLSVerificationStatusUseCase = coreLogic.getSessionScope(currentAccount).fetchConversationMLSVerificationStatus } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt index f7ba4647941..a0d6c7cc135 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt @@ -39,6 +39,7 @@ import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first @@ -52,6 +53,7 @@ class ConversationInfoViewModel @Inject constructor( override val savedStateHandle: SavedStateHandle, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val observerSelfUser: GetSelfUserUseCase, + private val fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase, private val wireSessionImageLoader: WireSessionImageLoader, ) : SavedStateViewModel(savedStateHandle) { @@ -64,6 +66,13 @@ class ConversationInfoViewModel @Inject constructor( init { getSelfUserId() + fetchMLSVerificationStatus() + } + + private fun fetchMLSVerificationStatus() { + viewModelScope.launch { + fetchConversationMLSVerificationStatus(conversationId) + } } private fun getSelfUserId() { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt index 5a17b46ecb8..397c3e3107b 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -59,6 +60,9 @@ class ConversationInfoViewModelArrangement { @MockK lateinit var observerSelfUser: GetSelfUserUseCase + @MockK + lateinit var fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase + @MockK private lateinit var wireSessionImageLoader: WireSessionImageLoader @@ -71,6 +75,7 @@ class ConversationInfoViewModelArrangement { savedStateHandle, observeConversationDetails, observerSelfUser, + fetchConversationMLSVerificationStatus, wireSessionImageLoader ) } @@ -86,6 +91,7 @@ class ConversationInfoViewModelArrangement { coEvery { observeConversationDetails(any()) } returns conversationDetailsChannel.consumeAsFlow().map { ObserveConversationDetailsUseCase.Result.Success(it) } + coEvery { fetchConversationMLSVerificationStatus.invoke(any()) } returns Unit } suspend fun withConversationDetailUpdate(conversationDetails: ConversationDetails) = apply { diff --git a/kalium b/kalium index b1520bec82e..2931055c9f9 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b1520bec82e8b2625837887f8b806b1e7e16bb5d +Subproject commit 2931055c9f9465dd0edd161fa5b2ea29beed3ebd From 30e14ee6f4ebc436356b86a6a7558c95bc5e3079 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Tue, 16 Apr 2024 18:09:45 +0200 Subject: [PATCH 127/134] feat: display avs and CC version on all builds (#2894) --- .../wire/android/ui/debug/DebugDataOptions.kt | 264 +++--------------- .../android/ui/debug/DebugDataOptionsState.kt | 36 +++ .../ui/debug/DebugDataOptionsViewModel.kt | 215 ++++++++++++++ app/src/main/res/values/strings.xml | 1 + .../kotlin/scripts/compilation.gradle.kts | 2 +- 5 files changed, 287 insertions(+), 231 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt 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 1d9fbeb0301..a51e152fecb 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 @@ -17,7 +17,6 @@ */ package com.wire.android.ui.debug -import android.content.Context import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -26,19 +25,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.wire.android.BuildConfig import com.wire.android.R -import com.wire.android.datastore.GlobalDataStore -import com.wire.android.di.CurrentAccount -import com.wire.android.migration.failure.UserMigrationStatus import com.wire.android.model.Clickable import com.wire.android.ui.common.RowItemTemplate import com.wire.android.ui.common.WireDialog @@ -53,214 +45,13 @@ import com.wire.android.ui.home.settings.SettingsItem import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography -import com.wire.android.util.getDependenciesVersion -import com.wire.android.util.getDeviceIdString -import com.wire.android.util.getGitBuildId import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.CoreFailure -import com.wire.kalium.logic.E2EIFailure import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.debug.DisableEventProcessingUseCase -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 -import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.fold -import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler -import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toImmutableMap -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.inject.Inject - -//region DebugDataOptionsViewModel -data class DebugDataOptionsState( - val isEncryptedProteusStorageEnabled: Boolean = false, - val isEventProcessingDisabled: Boolean = false, - val keyPackagesCount: Int = 0, - val mslClientId: String = "null", - val mlsErrorMessage: String = "null", - val isManualMigrationAllowed: Boolean = false, - val debugId: String = "null", - val commitish: String = "null", - val certificate: String = "null", - val showCertificate: Boolean = false, - val startGettingE2EICertificate: Boolean = false, - val dependencies: ImmutableMap = persistentMapOf() -) - -@Suppress("LongParameterList") -@HiltViewModel -class DebugDataOptionsViewModel -@Inject constructor( - @ApplicationContext private val context: Context, - @CurrentAccount val currentAccount: UserId, - private val globalDataStore: GlobalDataStore, - private val updateApiVersions: UpdateApiVersionsScheduler, - private val mlsKeyPackageCountUseCase: MLSKeyPackageCountUseCase, - private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase, - private val disableEventProcessingUseCase: DisableEventProcessingUseCase, - private val checkCrlRevocationListUseCase: CheckCrlRevocationListUseCase -) : ViewModel() { - - var state by mutableStateOf( - DebugDataOptionsState() - ) - - init { - observeEncryptedProteusStorageState() - observeMlsMetadata() - checkIfCanTriggerManualMigration() - checkDependenciesVersion() - setGitHashAndDeviceId() - } - - private fun setGitHashAndDeviceId() { - viewModelScope.launch { - val deviceId = context.getDeviceIdString() ?: "null" - val gitBuildId = context.getGitBuildId() - state = state.copy( - debugId = deviceId, - commitish = gitBuildId - ) - } - } - - fun checkCrlRevocationList() { - viewModelScope.launch { - checkCrlRevocationListUseCase( - forceUpdate = true - ) - } - } - - fun checkDependenciesVersion() { - viewModelScope.launch { - val dependencies = context.getDependenciesVersion().toImmutableMap() - state = state.copy( - dependencies = dependencies - ) - } - } - - fun enableEncryptedProteusStorage(enabled: Boolean) { - if (enabled) { - viewModelScope.launch { - globalDataStore.setEncryptedProteusStorageEnabled(true) - } - } - } - - fun restartSlowSyncForRecovery() { - viewModelScope.launch { - restartSlowSyncProcessForRecovery() - } - } - - fun enrollE2EICertificate() { - state = state.copy(startGettingE2EICertificate = true) - } - - fun handleE2EIEnrollmentResult(result: Either) { - result.fold({ - state = state.copy( - certificate = (it as E2EIFailure.OAuth).reason, - showCertificate = true, - startGettingE2EICertificate = false - ) - }, { - if (it is E2EIEnrollmentResult.Finalized) { - state = state.copy( - certificate = it.certificate, - showCertificate = true, - startGettingE2EICertificate = false - ) - } else { - state.copy( - certificate = it.toString(), - showCertificate = true, - startGettingE2EICertificate = false - ) - } - }) - } - - fun dismissCertificateDialog() { - state = state.copy( - showCertificate = false, - ) - } - - fun forceUpdateApiVersions() { - updateApiVersions.scheduleImmediateApiVersionUpdate() - } - - fun disableEventProcessing(disabled: Boolean) { - viewModelScope.launch { - disableEventProcessingUseCase(disabled) - state = state.copy(isEventProcessingDisabled = disabled) - } - } - - //region Private - private fun observeEncryptedProteusStorageState() { - viewModelScope.launch { - globalDataStore.isEncryptedProteusStorageEnabled().collect { - state = state.copy(isEncryptedProteusStorageEnabled = it) - } - } - } - - // If status is NoNeed, it means that the user has already been migrated in and older app version, - // or it is a new install - // this is why we check the existence of the database file - private fun checkIfCanTriggerManualMigration() { - viewModelScope.launch { - globalDataStore.getUserMigrationStatus(currentAccount.value).first() - .let { migrationStatus -> - if (migrationStatus != UserMigrationStatus.NoNeed) { - context.getDatabasePath(currentAccount.value).let { - state = state.copy( - isManualMigrationAllowed = (it.exists() && it.isFile) - ) - } - } - } - } - } - - private fun observeMlsMetadata() { - viewModelScope.launch { - mlsKeyPackageCountUseCase().let { - when (it) { - is MLSKeyPackageCountResult.Success -> { - state = state.copy( - keyPackagesCount = it.count, - mslClientId = it.clientId.value - ) - } - - is MLSKeyPackageCountResult.Failure.NetworkCallFailure -> { - state = state.copy(mlsErrorMessage = "Network Error!") - } - - is MLSKeyPackageCountResult.Failure.FetchClientIdFailure -> { - state = state.copy(mlsErrorMessage = "ClientId Fetch Error!") - } - - is MLSKeyPackageCountResult.Failure.Generic -> {} - } - } - } - } - //endregion -} -//endregion @Composable fun DebugDataOptions( @@ -283,7 +74,8 @@ fun DebugDataOptions( enrollE2EICertificate = viewModel::enrollE2EICertificate, handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, dismissCertificateDialog = viewModel::dismissCertificateDialog, - checkCrlRevocationList = viewModel::checkCrlRevocationList + checkCrlRevocationList = viewModel::checkCrlRevocationList, + dependenciesMap = viewModel.state.dependencies ) } @@ -302,7 +94,8 @@ fun DebugDataOptionsContent( enrollE2EICertificate: () -> Unit, handleE2EIEnrollmentResult: (Either) -> Unit, dismissCertificateDialog: () -> Unit, - checkCrlRevocationList: () -> Unit + checkCrlRevocationList: () -> Unit, + dependenciesMap: ImmutableMap ) { Column { @@ -337,6 +130,7 @@ fun DebugDataOptionsContent( onClick = { onCopyText(state.commitish) } ) ) + DependenciesItem(dependenciesMap) if (BuildConfig.PRIVATE_BUILD) { SettingsItem( @@ -399,8 +193,7 @@ fun DebugDataOptionsContent( onDisableEventProcessingChange = onDisableEventProcessingChange, onRestartSlowSyncForRecovery = onRestartSlowSyncForRecovery, onForceUpdateApiVersions = onForceUpdateApiVersions, - checkCrlRevocationList = checkCrlRevocationList, - dependenciesMap = state.dependencies + checkCrlRevocationList = checkCrlRevocationList ) } @@ -569,8 +362,7 @@ private fun DebugToolsOptions( onDisableEventProcessingChange: (Boolean) -> Unit, onRestartSlowSyncForRecovery: () -> Unit, onForceUpdateApiVersions: () -> Unit, - checkCrlRevocationList: () -> Unit, - dependenciesMap: ImmutableMap + checkCrlRevocationList: () -> Unit ) { FolderHeader(stringResource(R.string.label_debug_tools_title)) Column { @@ -641,20 +433,31 @@ private fun DebugToolsOptions( ) } ) - RowItemTemplate( - modifier = Modifier.wrapContentWidth(), - title = { - Text( - style = MaterialTheme.wireTypography.body01, - color = MaterialTheme.wireColorScheme.onBackground, - text = prettyPrintMap(dependenciesMap), - modifier = Modifier.padding(start = dimensions().spacing8x) - ) - } - ) } } +/** + * + */ +@Composable +fun DependenciesItem(dependencies: ImmutableMap) { + val title = stringResource(id = R.string.item_dependencies_title) + val text = remember { + prettyPrintMap(dependencies, title) + } + RowItemTemplate( + modifier = Modifier.wrapContentWidth(), + title = { + Text( + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.onBackground, + text = text, + modifier = Modifier.padding(start = dimensions().spacing8x) + ) + } + ) +} + @Composable private fun DisableEventProcessingSwitch( isEnabled: Boolean = false, @@ -685,8 +488,8 @@ private fun DisableEventProcessingSwitch( } @Stable -private fun prettyPrintMap(map: ImmutableMap): String = StringBuilder().apply { - append("Dependencies:\n") +private fun prettyPrintMap(map: Map, title: String): String = StringBuilder().apply { + append("$title\n") map.forEach { (key, value) -> append("$key: $value\n") } @@ -718,6 +521,7 @@ fun PreviewOtherDebugOptions() { enrollE2EICertificate = {}, handleE2EIEnrollmentResult = {}, dismissCertificateDialog = {}, - checkCrlRevocationList = {} + checkCrlRevocationList = {}, + dependenciesMap = persistentMapOf() ) } 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 new file mode 100644 index 00000000000..c0f648184dc --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt @@ -0,0 +1,36 @@ +/* + * Wire + * Copyright (C) 2024 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 + +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf + +data class DebugDataOptionsState( + val isEncryptedProteusStorageEnabled: Boolean = false, + val isEventProcessingDisabled: Boolean = false, + val keyPackagesCount: Int = 0, + val mslClientId: String = "null", + val mlsErrorMessage: String = "null", + val isManualMigrationAllowed: Boolean = false, + val debugId: String = "null", + val commitish: String = "null", + val certificate: String = "null", + val showCertificate: Boolean = false, + val startGettingE2EICertificate: Boolean = false, + val dependencies: ImmutableMap = persistentMapOf() +) 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 new file mode 100644 index 00000000000..944c27e0336 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt @@ -0,0 +1,215 @@ +/* + * Wire + * Copyright (C) 2024 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 + +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.CurrentAccount +import com.wire.android.migration.failure.UserMigrationStatus +import com.wire.android.util.getDependenciesVersion +import com.wire.android.util.getDeviceIdString +import com.wire.android.util.getGitBuildId +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.E2EIFailure +import com.wire.kalium.logic.data.user.UserId +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 +import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler +import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.toImmutableMap +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@HiltViewModel +class DebugDataOptionsViewModel +@Inject constructor( + @ApplicationContext private val context: Context, + @CurrentAccount val currentAccount: UserId, + private val globalDataStore: GlobalDataStore, + private val updateApiVersions: UpdateApiVersionsScheduler, + private val mlsKeyPackageCount: MLSKeyPackageCountUseCase, + private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase, + private val checkCrlRevocationList: CheckCrlRevocationListUseCase +) : ViewModel() { + + var state by mutableStateOf( + DebugDataOptionsState() + ) + + init { + observeEncryptedProteusStorageState() + observeMlsMetadata() + checkIfCanTriggerManualMigration() + setGitHashAndDeviceId() + checkDependenciesVersion() + } + + private fun checkDependenciesVersion() { + viewModelScope.launch { + val dependencies = context.getDependenciesVersion().toImmutableMap() + state = state.copy( + dependencies = dependencies + ) + } + } + + private fun setGitHashAndDeviceId() { + viewModelScope.launch { + val deviceId = context.getDeviceIdString() ?: "null" + val gitBuildId = context.getGitBuildId() + state = state.copy( + debugId = deviceId, + commitish = gitBuildId + ) + } + } + + fun checkCrlRevocationList() { + viewModelScope.launch { + checkCrlRevocationList( + forceUpdate = true + ) + } + } + + fun enableEncryptedProteusStorage(enabled: Boolean) { + if (enabled) { + viewModelScope.launch { + globalDataStore.setEncryptedProteusStorageEnabled(true) + } + } + } + + fun restartSlowSyncForRecovery() { + viewModelScope.launch { + restartSlowSyncProcessForRecovery() + } + } + + fun enrollE2EICertificate() { + state = state.copy(startGettingE2EICertificate = true) + } + + fun handleE2EIEnrollmentResult(result: Either) { + result.fold({ + state = state.copy( + certificate = (it as E2EIFailure.OAuth).reason, + showCertificate = true, + startGettingE2EICertificate = false + ) + }, { + if (it is E2EIEnrollmentResult.Finalized) { + state = state.copy( + certificate = it.certificate, + showCertificate = true, + startGettingE2EICertificate = false + ) + } else { + state.copy( + certificate = it.toString(), + showCertificate = true, + startGettingE2EICertificate = false + ) + } + }) + } + + fun dismissCertificateDialog() { + state = state.copy( + showCertificate = false, + ) + } + + fun forceUpdateApiVersions() { + updateApiVersions.scheduleImmediateApiVersionUpdate() + } + + fun disableEventProcessing(disabled: Boolean) { + viewModelScope.launch { + disableEventProcessing(disabled) + state = state.copy(isEventProcessingDisabled = disabled) + } + } + + //region Private + private fun observeEncryptedProteusStorageState() { + viewModelScope.launch { + globalDataStore.isEncryptedProteusStorageEnabled().collect { + state = state.copy(isEncryptedProteusStorageEnabled = it) + } + } + } + + // If status is NoNeed, it means that the user has already been migrated in and older app version, + // or it is a new install + // this is why we check the existence of the database file + private fun checkIfCanTriggerManualMigration() { + viewModelScope.launch { + globalDataStore.getUserMigrationStatus(currentAccount.value).first() + .let { migrationStatus -> + if (migrationStatus != UserMigrationStatus.NoNeed) { + context.getDatabasePath(currentAccount.value).let { + state = state.copy( + isManualMigrationAllowed = (it.exists() && it.isFile) + ) + } + } + } + } + } + + private fun observeMlsMetadata() { + viewModelScope.launch { + mlsKeyPackageCount().let { + when (it) { + is MLSKeyPackageCountResult.Success -> { + state = state.copy( + keyPackagesCount = it.count, + mslClientId = it.clientId.value + ) + } + + is MLSKeyPackageCountResult.Failure.NetworkCallFailure -> { + state = state.copy(mlsErrorMessage = "Network Error!") + } + + is MLSKeyPackageCountResult.Failure.FetchClientIdFailure -> { + state = state.copy(mlsErrorMessage = "ClientId Fetch Error!") + } + + is MLSKeyPackageCountResult.Failure.Generic -> {} + } + } + } + } + //endregion +} +//endregion diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e526d1d4e4..e190e8ba290 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -199,6 +199,7 @@ API VERSIONING E2EI Manual Enrollment Force API versioning update + Dependencies: Update Support Back up & Restore Conversations diff --git a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts index 7070edf870a..421db62bed7 100644 --- a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts @@ -36,7 +36,7 @@ val dependenciesVersionTask = project.tasks.register("dependenciesVersionTask", val catalog = catalogs.named("klibs") val pairs = mapOf( "avs" to catalog.findVersion("avs").get().requiredVersion, - "core-crypto" to catalog.findVersion("core-crypto-multiplatform").get().requiredVersion + "core-crypto" to catalog.findVersion("core-crypto").get().requiredVersion ) keyValues.set(pairs) } From 0991c31279831f7d638f00556b3dcc4fab964367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:10:34 +0200 Subject: [PATCH 128/134] fix: secure switching to invalid account and disable composer [WPB-7369] (#2906) --- .../android/feature/AccountSwitchUseCase.kt | 41 ++++++++++++++--- .../conversations/MessageComposerViewModel.kt | 31 ++++++++++--- .../feature/AccountSwitchUseCaseTest.kt | 45 ++++++++++++++++--- .../MessageComposerViewModelArrangement.kt | 16 ++++++- .../MessageComposerViewModelTest.kt | 15 +++++++ 5 files changed, 130 insertions(+), 18 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt index 307cf2be77f..17ce4b5dad8 100644 --- a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt @@ -68,12 +68,39 @@ class AccountSwitchUseCase @Inject constructor( val current = currentAccount.await() appLogger.i("$TAG Switching account invoked: ${params.toLogString()}, current account: ${current?.userId?.toLogString() ?: "-"}") return when (params) { - is SwitchAccountParam.SwitchToAccount -> switch(params.userId, current) + is SwitchAccountParam.SwitchToAccount -> checkAccountAndSwitchIfPossible(params.userId, current) SwitchAccountParam.TryToSwitchToNextAccount -> getNextAccountIfPossibleAndSwitch(current) SwitchAccountParam.Clear -> switch(null, current) } } + private suspend fun checkAccountAndSwitchIfPossible(userId: UserId, current: AccountInfo?): SwitchAccountResult = + getSessions().let { + when (it) { + is GetAllSessionsResult.Success -> { + val isAccountLoggedInAndValid = it.sessions.any { + accountInfo -> (accountInfo is AccountInfo.Valid) && (accountInfo.userId == userId) + } + if (isAccountLoggedInAndValid) { + switch(userId, current) + } else { + appLogger.i("$TAG Given account is not logged in or invalid: ${userId.toLogString()}") + return SwitchAccountResult.GivenAccountIsInvalid + } + } + + is GetAllSessionsResult.Failure.Generic -> { + appLogger.i("$TAG Failure when switching account to: ${userId.toLogString()}") + SwitchAccountResult.Failure + } + + GetAllSessionsResult.Failure.NoSessionFound -> { + appLogger.i("$TAG Given account is not found: ${userId.toLogString()}") + SwitchAccountResult.GivenAccountIsInvalid + } + } + } + private suspend fun getNextAccountIfPossibleAndSwitch(current: AccountInfo?): SwitchAccountResult { val nextSessionId: UserId? = getSessions().let { when (it) { @@ -103,7 +130,10 @@ class AccountSwitchUseCase @Inject constructor( } successResult } - is UpdateCurrentSessionUseCase.Result.Failure -> SwitchAccountResult.Failure + is UpdateCurrentSessionUseCase.Result.Failure -> { + appLogger.i("$TAG Failure when switching account to: ${userId?.toLogString() ?: "-"}") + SwitchAccountResult.Failure + } } } @@ -161,9 +191,10 @@ sealed class SwitchAccountParam { } sealed class SwitchAccountResult { - object Failure : SwitchAccountResult() - object SwitchedToAnotherAccount : SwitchAccountResult() - object NoOtherAccountToSwitch : SwitchAccountResult() + data object Failure : SwitchAccountResult() + data object SwitchedToAnotherAccount : SwitchAccountResult() + data object NoOtherAccountToSwitch : SwitchAccountResult() + data object GivenAccountIsInvalid : SwitchAccountResult() fun callAction(actions: SwitchAccountActions) = when (this) { NoOtherAccountToSwitch -> actions.noOtherAccountToSwitch() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt index 895d94e96de..ff1c167bf58 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt @@ -76,13 +76,19 @@ import com.wire.kalium.logic.feature.message.SendTextMessageUseCase import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.onFailure import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.datetime.Instant @@ -120,6 +126,7 @@ class MessageComposerViewModel @Inject constructor( private val setNotifiedAboutConversationUnderLegalHold: SetNotifiedAboutConversationUnderLegalHoldUseCase, private val observeConversationUnderLegalHoldNotified: ObserveConversationUnderLegalHoldNotifiedUseCase, private val sendLocation: SendLocationUseCase, + private val currentSessionFlowUseCase: CurrentSessionFlowUseCase, ) : SavedStateViewModel(savedStateHandle) { var messageComposerViewState = mutableStateOf(MessageComposerViewState()) @@ -188,14 +195,24 @@ class MessageComposerViewModel @Inject constructor( } private fun observeIsTypingAvailable() = viewModelScope.launch { - observeConversationInteractionAvailability(conversationId).collect { result -> - messageComposerViewState.value = messageComposerViewState.value.copy( - interactionAvailability = when (result) { - is IsInteractionAvailableResult.Failure -> InteractionAvailability.DISABLED - is IsInteractionAvailableResult.Success -> result.interactionAvailability + currentSessionFlowUseCase() + .flatMapLatest { + when (it) { + is CurrentSessionResult.Success -> { + observeConversationInteractionAvailability(conversationId) + .mapLatest { result -> + when (result) { + is IsInteractionAvailableResult.Failure -> InteractionAvailability.DISABLED + is IsInteractionAvailableResult.Success -> result.interactionAvailability + } + } + } + else -> flowOf(InteractionAvailability.DISABLED) } - ) - } + } + .collectLatest { + messageComposerViewState.value = messageComposerViewState.value.copy(interactionAvailability = it) + } } private fun observeSelfDeletingMessagesStatus() = viewModelScope.launch { diff --git a/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt index 0e8364e5bbb..b89654c3853 100644 --- a/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt @@ -55,6 +55,7 @@ class AccountSwitchUseCaseTest { val (arrangement, switchAccount) = Arrangement(testScope) .withGetCurrentSession(CurrentSessionResult.Success(ACCOUNT_VALID_1)) + .withGetAllSessions(GetAllSessionsResult.Success(listOf(ACCOUNT_VALID_1, ACCOUNT_VALID_2))) .withUpdateCurrentSession(UpdateCurrentSessionUseCase.Result.Success) .arrange() @@ -78,7 +79,7 @@ class AccountSwitchUseCaseTest { Arrangement(testScope) .withGetCurrentSession(CurrentSessionResult.Success(ACCOUNT_VALID_1)) .withUpdateCurrentSession(UpdateCurrentSessionUseCase.Result.Success) - .withGetAllSessions(GetAllSessionsResult.Success(emptyList())) + .withGetAllSessions(GetAllSessionsResult.Success(listOf(ACCOUNT_VALID_1))) .withServerConfigForAccount(ServerConfigForAccountUseCase.Result.Success(serverConfig)) .arrange() @@ -95,7 +96,7 @@ class AccountSwitchUseCaseTest { @Test fun givenCurrentSessionIsInvalid_whenSwitchingToAccount_thenUpdateCurrentSessionAndDeleteTheOldOne() = testScope.runTest { val currentAccount = ACCOUNT_INVALID_3 - val switchTO = ACCOUNT_VALID_2 + val switchTo = ACCOUNT_VALID_2 val expectedResult = SwitchAccountResult.SwitchedToAnotherAccount @@ -103,21 +104,55 @@ class AccountSwitchUseCaseTest { Arrangement(testScope) .withGetCurrentSession(CurrentSessionResult.Success(currentAccount)) .withUpdateCurrentSession(UpdateCurrentSessionUseCase.Result.Success) - .withGetAllSessions(GetAllSessionsResult.Success(emptyList())) + .withGetAllSessions(GetAllSessionsResult.Success(listOf(currentAccount, switchTo))) .withDeleteSession(currentAccount.userId, DeleteSessionUseCase.Result.Success) .arrange() - val result = switchAccount(SwitchAccountParam.SwitchToAccount(switchTO.userId)) + val result = switchAccount(SwitchAccountParam.SwitchToAccount(switchTo.userId)) testScope.advanceUntilIdle() assertEquals(expectedResult, result) coVerify(exactly = 1) { arrangement.currentSession() - arrangement.updateCurrentSession(switchTO.userId) + arrangement.updateCurrentSession(switchTo.userId) arrangement.deleteSession(currentAccount.userId) } } + @Test + fun givenProvidedAccountIsNotFound_whenSwitchingToAccount_thenReturnGivenAccountIsInvalid() = testScope.runTest { + val (arrangement, switchAccount) = + Arrangement(testScope) + .withGetCurrentSession(CurrentSessionResult.Success(ACCOUNT_VALID_1)) + .withGetAllSessions(GetAllSessionsResult.Success(listOf(ACCOUNT_VALID_1))) + .arrange() + + val result = switchAccount(SwitchAccountParam.SwitchToAccount(ACCOUNT_VALID_2.userId)) + + assertEquals(SwitchAccountResult.GivenAccountIsInvalid, result) + coVerify(exactly = 1) { + arrangement.currentSession() + arrangement.getSessions() + } + } + + @Test + fun givenProvidedAccountIsNotValid_whenSwitchingToAccount_thenReturnGivenAccountIsInvalid() = testScope.runTest { + val (arrangement, switchAccount) = + Arrangement(testScope) + .withGetCurrentSession(CurrentSessionResult.Success(ACCOUNT_VALID_1)) + .withGetAllSessions(GetAllSessionsResult.Success(listOf(ACCOUNT_VALID_1, ACCOUNT_INVALID_3))) + .arrange() + + val result = switchAccount(SwitchAccountParam.SwitchToAccount(ACCOUNT_INVALID_3.userId)) + + assertEquals(SwitchAccountResult.GivenAccountIsInvalid, result) + coVerify(exactly = 1) { + arrangement.currentSession() + arrangement.getSessions() + } + } + private companion object { val ACCOUNT_VALID_1 = AccountInfo.Valid(UserId("userId_valid_1", "domain_valid_1")) val ACCOUNT_VALID_2 = AccountInfo.Valid(UserId("userId_valid_2", "domain_valid_2")) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModelArrangement.kt index 27e0e2cbde5..4d7fe9ab41b 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModelArrangement.kt @@ -24,6 +24,7 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.framework.TestConversation +import com.wire.android.framework.TestUser import com.wire.android.mapper.ContactMapper import com.wire.android.media.PingRinger import com.wire.android.model.UserAvatarData @@ -41,6 +42,7 @@ import com.wire.android.util.ImageUtil import com.wire.android.util.ui.UIText import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.configuration.FileSharingStatus +import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId @@ -77,6 +79,8 @@ import com.wire.kalium.logic.feature.message.SendTextMessageUseCase import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.sync.ObserveSyncStateUseCase @@ -85,6 +89,7 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import okio.Path @@ -113,6 +118,7 @@ internal class MessageComposerViewModelArrangement { coEvery { observeDegradedConversationNotifiedUseCase(any()) } returns flowOf(true) coEvery { setNotifiedAboutConversationUnderLegalHold(any()) } returns Unit coEvery { observeConversationUnderLegalHoldNotified(any()) } returns flowOf(true) + coEvery { currentSessionFlowUseCase() } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) } @MockK @@ -202,6 +208,9 @@ internal class MessageComposerViewModelArrangement { @MockK lateinit var sendLocation: SendLocationUseCase + @MockK + lateinit var currentSessionFlowUseCase: CurrentSessionFlowUseCase + private val fakeKaliumFileSystem = FakeKaliumFileSystem() private val viewModel by lazy { @@ -232,7 +241,8 @@ internal class MessageComposerViewModelArrangement { observeDegradedConversationNotified = observeDegradedConversationNotifiedUseCase, setNotifiedAboutConversationUnderLegalHold = setNotifiedAboutConversationUnderLegalHold, observeConversationUnderLegalHoldNotified = observeConversationUnderLegalHoldNotified, - sendLocation = sendLocation + sendLocation = sendLocation, + currentSessionFlowUseCase = currentSessionFlowUseCase, ) } @@ -363,6 +373,10 @@ internal class MessageComposerViewModelArrangement { coEvery { retryFailedMessageUseCase(any(), any()) } returns Either.Right(Unit) } + fun withCurrentSessionFlowResult(resultFlow: Flow) = apply { + coEvery { currentSessionFlowUseCase() } returns resultFlow + } + fun arrange() = this to viewModel } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModelTest.kt index 2077a39c3df..c93b3293189 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModelTest.kt @@ -34,9 +34,12 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.failure.LegalHoldEnabledForConversationFailure import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCaseImpl.Companion.ASSET_SIZE_DEFAULT_LIMIT_BYTES +import com.wire.kalium.logic.feature.conversation.InteractionAvailability +import com.wire.kalium.logic.feature.session.CurrentSessionResult import io.mockk.coVerify import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import okio.Path.Companion.toPath @@ -818,4 +821,16 @@ class MessageComposerViewModelTest { coVerify(exactly = 1) { arrangement.sendLocation.invoke(any(), any(), any(), any(), any()) } assertEquals(SureAboutMessagingDialogState.Hidden, viewModel.sureAboutMessagingDialogState) } + + @Test + fun `given no current session, then disable interaction`() = runTest { + // given + val (_, viewModel) = MessageComposerViewModelArrangement() + .withSuccessfulViewModelInit() + .withCurrentSessionFlowResult(flowOf(CurrentSessionResult.Failure.SessionNotFound)) + .arrange() + advanceUntilIdle() + // then + assertEquals(InteractionAvailability.DISABLED, viewModel.messageComposerViewState.value.interactionAvailability) + } } From de8ade9d1a0fe609c7498d251d5f61d533b07bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:11:00 +0200 Subject: [PATCH 129/134] refactor: make learn more links clickable for automation [WPB-5888] (#2915) --- .../ui/authentication/login/LoginScreen.kt | 51 +++--- .../android/ui/common/TextWithLinkSuffix.kt | 167 ++++++++++++++++++ .../com/wire/android/ui/common/WireDialog.kt | 55 ++++-- .../home/conversations/SystemMessageItem.kt | 45 ++--- .../common/CreateGroupErrorDialog.kt | 62 +++---- .../com/wire/android/util/CoreFailureUtil.kt | 5 +- 6 files changed, 263 insertions(+), 122 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/TextWithLinkSuffix.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt index acad24526f9..ecb66047e12 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.window.DialogProperties import androidx.hilt.navigation.compose.hiltViewModel @@ -242,22 +243,22 @@ fun LoginErrorDialog( ) { val dialogErrorData: LoginDialogErrorData = when (error) { is LoginError.DialogError.InvalidCredentialsError -> LoginDialogErrorData( - stringResource(R.string.login_error_invalid_credentials_title), - stringResource(R.string.login_error_invalid_credentials_message), - onDialogDismiss + title = stringResource(R.string.login_error_invalid_credentials_title), + body = AnnotatedString(stringResource(R.string.login_error_invalid_credentials_message)), + onDismiss = onDialogDismiss ) is LoginError.DialogError.UserAlreadyExists -> LoginDialogErrorData( - stringResource(R.string.login_error_user_already_logged_in_title), - stringResource(R.string.login_error_user_already_logged_in_message), - onDialogDismiss + title = stringResource(R.string.login_error_user_already_logged_in_title), + body = AnnotatedString(stringResource(R.string.login_error_user_already_logged_in_message)), + onDismiss = onDialogDismiss ) is LoginError.DialogError.ProxyError -> { LoginDialogErrorData( - stringResource(R.string.error_socket_title), - stringResource(R.string.error_socket_message), - onDialogDismiss + title = stringResource(R.string.error_socket_title), + body = AnnotatedString(stringResource(R.string.error_socket_message)), + onDismiss = onDialogDismiss ) } @@ -265,36 +266,36 @@ fun LoginErrorDialog( val strings = error.coreFailure.dialogErrorStrings(LocalContext.current.resources) LoginDialogErrorData( strings.title, - strings.message, + strings.annotatedMessage, onDialogDismiss ) } is LoginError.DialogError.InvalidSSOCodeError -> LoginDialogErrorData( - stringResource(R.string.login_error_invalid_credentials_title), - stringResource(R.string.login_error_invalid_sso_code), - onDialogDismiss + title = stringResource(R.string.login_error_invalid_credentials_title), + body = AnnotatedString(stringResource(R.string.login_error_invalid_sso_code)), + onDismiss = onDialogDismiss ) is LoginError.DialogError.InvalidSSOCookie -> LoginDialogErrorData( - stringResource(R.string.login_sso_error_invalid_cookie_title), - stringResource(R.string.login_sso_error_invalid_cookie_message), - onDialogDismiss + title = stringResource(R.string.login_sso_error_invalid_cookie_title), + body = AnnotatedString(stringResource(R.string.login_sso_error_invalid_cookie_message)), + onDismiss = onDialogDismiss ) is LoginError.DialogError.SSOResultError -> { with(ssoLoginResult as DeepLinkResult.SSOLogin.Failure) { LoginDialogErrorData( - stringResource(R.string.sso_error_dialog_title), - stringResource(R.string.sso_error_dialog_message, this.ssoError.errorCode), - onDialogDismiss + title = stringResource(R.string.sso_error_dialog_title), + body = AnnotatedString(stringResource(R.string.sso_error_dialog_message, this.ssoError.errorCode)), + onDismiss = onDialogDismiss ) } } is LoginError.DialogError.ServerVersionNotSupported -> LoginDialogErrorData( title = stringResource(R.string.api_versioning_server_version_not_supported_title), - body = stringResource(R.string.api_versioning_server_version_not_supported_message), + body = AnnotatedString(stringResource(R.string.api_versioning_server_version_not_supported_message)), onDismiss = onDialogDismiss, actionTextId = R.string.label_close, dismissOnClickOutside = false @@ -302,7 +303,7 @@ fun LoginErrorDialog( is LoginError.DialogError.ClientUpdateRequired -> LoginDialogErrorData( title = stringResource(R.string.api_versioning_client_update_required_title), - body = stringResource(R.string.api_versioning_client_update_required_message), + body = AnnotatedString(stringResource(R.string.api_versioning_client_update_required_message)), onDismiss = onDialogDismiss, actionTextId = R.string.label_update, onAction = updateTheApp, @@ -312,9 +313,9 @@ fun LoginErrorDialog( LoginError.DialogError.PasswordNeededToRegisterClient -> TODO() else -> LoginDialogErrorData( - stringResource(R.string.error_unknown_title), - stringResource(R.string.error_unknown_message), - onDialogDismiss + title = stringResource(R.string.error_unknown_title), + body = AnnotatedString(stringResource(R.string.error_unknown_message)), + onDismiss = onDialogDismiss ) } @@ -337,7 +338,7 @@ fun LoginErrorDialog( data class LoginDialogErrorData( val title: String, - val body: String, + val body: AnnotatedString, val onDismiss: () -> Unit, @StringRes val actionTextId: Int = R.string.label_ok, val onAction: () -> Unit = onDismiss, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/TextWithLinkSuffix.kt b/app/src/main/kotlin/com/wire/android/ui/common/TextWithLinkSuffix.kt new file mode 100644 index 00000000000..67f3b163aa0 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/TextWithLinkSuffix.kt @@ -0,0 +1,167 @@ +/* + * Wire + * Copyright (C) 2024 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.common + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.Dp +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +fun TextWithLinkSuffix( + text: AnnotatedString, + linkText: String? = null, + onLinkClick: () -> Unit = {}, + linkTag: String = "link", + textStyle: TextStyle = MaterialTheme.wireTypography.body01, + textColor: Color = MaterialTheme.wireColorScheme.onBackground, + linkStyle: TextStyle = MaterialTheme.wireTypography.body02, + linkColor: Color = MaterialTheme.wireColorScheme.primary, + linkDecoration: TextDecoration = TextDecoration.Underline, + onTextLayout: (TextLayoutResult) -> Unit = {}, + modifier: Modifier = Modifier, +) { + val textMeasurer = rememberTextMeasurer() + val linkId = "link" + val inlineText = if (linkText != null) { + text.plus( + buildAnnotatedString { + append(" ") + appendInlineContent(linkId, "[link]") + } + ) + } else text + val inlineContent = buildMap { + if (linkText != null) { + val textLayoutResult: TextLayoutResult = textMeasurer.measure( + text = linkText, + style = linkStyle.copy(textDecoration = linkDecoration), + ) + val textSize = textLayoutResult.size + val density = LocalDensity.current + val (linkWidthSp, linkHeightSp) = with(density) { textSize.width.toSp() to textSize.height.toSp() } + + put(linkId, InlineTextContent( + placeholder = Placeholder( + width = linkWidthSp, + height = linkHeightSp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Bottom + ), + children = { + Text( + text = linkText, + style = linkStyle, + color = linkColor, + textDecoration = linkDecoration, + modifier = Modifier + .testTag(linkTag) + .clickable(onClick = onLinkClick) + ) + } + ) + ) + } + } + + Text( + text = inlineText, + style = textStyle, + color = textColor, + inlineContent = inlineContent, + onTextLayout = onTextLayout, + modifier = modifier, + ) +} + +@Composable +private fun PreviewTextWithLinkSuffixBuilder( + textLines: List = listOf("This is a text with a link"), + linkText: String = "link", + calculateWidth: (lastTextLineWidthDp: Dp, linkWidthDp: Dp) -> Dp +) { + val textStyle = MaterialTheme.wireTypography.body01 + val linkStyle = MaterialTheme.wireTypography.body02.copy(textDecoration = TextDecoration.Underline) + val textMeasurer = rememberTextMeasurer() + val lastTextLineLayoutResult = textMeasurer.measure(text = "${textLines.last()} ", style = textStyle) + val linkLayoutResult = textMeasurer.measure(text = linkText, style = linkStyle) + val density = LocalDensity.current + val lastTextLineWidthDp = with(density) { lastTextLineLayoutResult.size.width.toDp() } + val linkWidthDp = with(density) { linkLayoutResult.size.width.toDp() } + TextWithLinkSuffix( + text = AnnotatedString(textLines.joinToString(separator = "\n")), + linkText = linkText, + onLinkClick = {}, + modifier = Modifier.width(calculateWidth(lastTextLineWidthDp, linkWidthDp)) + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewTextWithLinkSuffixWithoutALink() = WireTheme { + TextWithLinkSuffix(text = AnnotatedString("This is a text without a link")) +} + +@PreviewMultipleThemes +@Composable +fun PreviewTextWithLinkSuffixFittingInSameLine() = WireTheme { + PreviewTextWithLinkSuffixBuilder { lastTextLineWidthDp, linkWidthDp -> lastTextLineWidthDp + linkWidthDp } +} + +@PreviewMultipleThemes +@Composable +fun PreviewTextWithLinkSuffixNotFittingInSameLine() = WireTheme { + PreviewTextWithLinkSuffixBuilder { lastTextLineWidthDp, linkWidthDp -> lastTextLineWidthDp + (linkWidthDp / 2) } +} + +@PreviewMultipleThemes +@Composable +fun PreviewTextWithLinkSuffixMultilineFittingInLastLine() = WireTheme { + PreviewTextWithLinkSuffixBuilder( + textLines = listOf("This is a text with a link", "This is a text with a"), + linkText = "link", + ) { lastTextLineWidthDp, linkWidthDp -> lastTextLineWidthDp + linkWidthDp } +} + +@PreviewMultipleThemes +@Composable +fun PreviewTextWithLinkSuffixMultilineNotFittingInLastLine() = WireTheme { + PreviewTextWithLinkSuffixBuilder( + textLines = listOf("This is a text with a", "This is a text with a"), + linkText = "link" + ) { lastTextLineWidthDp, linkWidthDp -> lastTextLineWidthDp + (linkWidthDp / 2) } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt index 14f24856b50..2bfbc916103 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalUriHandler @@ -51,7 +50,6 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.wire.android.ui.common.button.WireButtonState @@ -60,11 +58,11 @@ import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.button.WireTertiaryButton import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.common.textfield.WirePasswordTextField -import com.wire.android.ui.markdown.MarkdownConstants import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes @Stable fun wireDialogPropertiesBuilder( @@ -81,6 +79,7 @@ fun wireDialogPropertiesBuilder( fun WireDialog( title: String, text: String, + textSuffixLink: DialogTextSuffixLink? = null, onDismiss: () -> Unit, optionButton1Properties: WireDialogButtonProperties? = null, optionButton2Properties: WireDialogButtonProperties? = null, @@ -116,6 +115,7 @@ fun WireDialog( ) withStyle(style) { append(text) } }, + textSuffixLink = textSuffixLink, centerContent = centerContent, content = content ) @@ -125,6 +125,7 @@ fun WireDialog( fun WireDialog( title: String, text: AnnotatedString? = null, + textSuffixLink: DialogTextSuffixLink? = null, onDismiss: () -> Unit, optionButton1Properties: WireDialogButtonProperties? = null, optionButton2Properties: WireDialogButtonProperties? = null, @@ -153,6 +154,7 @@ fun WireDialog( title = title, titleLoading = titleLoading, text = text, + textSuffixLink = textSuffixLink, centerContent = centerContent, content = content ) @@ -164,6 +166,7 @@ private fun WireDialogContent( title: String, titleLoading: Boolean = false, text: AnnotatedString? = null, + textSuffixLink: DialogTextSuffixLink? = null, optionButton1Properties: WireDialogButtonProperties? = null, optionButton2Properties: WireDialogButtonProperties? = null, dismissButtonProperties: WireDialogButtonProperties? = null, @@ -203,17 +206,11 @@ private fun WireDialogContent( ) { text?.let { item { - ClickableText( + TextWithLinkSuffix( text = text, - style = MaterialTheme.wireTypography.body01, - modifier = Modifier.padding(bottom = MaterialTheme.wireDimensions.dialogTextsSpacing), - onClick = { offset -> - text.getStringAnnotations( - tag = MarkdownConstants.TAG_URL, - start = offset, - end = offset, - ).firstOrNull()?.let { result -> uriHandler.openUri(result.item) } - } + linkText = textSuffixLink?.linkText, + onLinkClick = { textSuffixLink?.linkUrl?.let { uriHandler.openUri(it) } }, + modifier = Modifier.padding(bottom = MaterialTheme.wireDimensions.dialogTextsSpacing) ) } } @@ -299,8 +296,7 @@ private fun WireDialogButtonProperties?.getButton(modifier: Modifier = Modifier) } } -@OptIn(ExperimentalComposeUiApi::class) -@Preview(showBackground = true) +@PreviewMultipleThemes @Composable fun PreviewWireDialog() { var password by remember { mutableStateOf(TextFieldValue("")) } @@ -342,8 +338,28 @@ fun PreviewWireDialog() { } } -@OptIn(ExperimentalComposeUiApi::class) -@Preview(showBackground = true) +@PreviewMultipleThemes +@Composable +fun PreviewWireDialogWithSuffixLink() { + WireTheme { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + WireDialogContent( + dismissButtonProperties = WireDialogButtonProperties( + text = "OK", + onClick = { } + ), + title = "title", + text = AnnotatedString("This is a long text with a link on a second line.\nThis is a second line."), + textSuffixLink = DialogTextSuffixLink("link", "https://www.wire.com"), + ) + } + } +} + +@PreviewMultipleThemes @Composable fun PreviewWireDialogWith2OptionButtons() { var password by remember { mutableStateOf(TextFieldValue("")) } @@ -392,8 +408,7 @@ fun PreviewWireDialogWith2OptionButtons() { } } -@OptIn(ExperimentalComposeUiApi::class) -@Preview(showBackground = true) +@PreviewMultipleThemes @Composable fun PreviewWireDialogCentered() { var password by remember { mutableStateOf(TextFieldValue("")) } @@ -445,3 +460,5 @@ data class WireDialogButtonProperties( val type: WireDialogButtonType = WireDialogButtonType.Secondary, val loading: Boolean = false ) + +data class DialogTextSuffixLink(val linkText: String, val linkUrl: String) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt index 5e2686e992f..90e6e81cadd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt @@ -38,7 +38,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -55,11 +54,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextDecoration import com.wire.android.R +import com.wire.android.ui.common.TextWithLinkSuffix import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions @@ -162,37 +159,18 @@ fun SystemMessageItem( errorColor = MaterialTheme.wireColorScheme.error, isErrorString = message.addingFailed, ) - val learnMoreAnnotatedString = message.messageContent.learnMoreResId?.let { - val learnMoreLink = stringResource(id = message.messageContent.learnMoreResId) - val learnMoreText = stringResource(id = R.string.label_learn_more) - buildAnnotatedString { - append(learnMoreText) - addStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ), - start = 0, - end = learnMoreText.length - ) - addStringAnnotation(tag = TAG_LEARN_MORE, annotation = learnMoreLink, start = 0, end = learnMoreText.length) - } - } - val fullAnnotatedString = - if (learnMoreAnnotatedString != null) annotatedString + AnnotatedString(" ") + learnMoreAnnotatedString - else annotatedString - - ClickableText( - modifier = Modifier.defaultMinSize(minHeight = dimensions().spacing20x), - text = fullAnnotatedString, - onClick = { offset -> - fullAnnotatedString.getStringAnnotations(TAG_LEARN_MORE, offset, offset) - .firstOrNull()?.let { result -> CustomTabsHelper.launchUrl(context, result.item) } - }, - style = MaterialTheme.wireTypography.body02, + val learnMoreLink = message.messageContent.learnMoreResId?.let { stringResource(id = it) } + + TextWithLinkSuffix( + text = annotatedString, + linkText = learnMoreLink?.let { stringResource(id = R.string.label_learn_more) }, + textColor = MaterialTheme.wireColorScheme.secondaryText, + linkColor = MaterialTheme.wireColorScheme.onBackground, + onLinkClick = { learnMoreLink?.let { CustomTabsHelper.launchUrl(context, it) } }, onTextLayout = { centerOfFirstLine = if (it.lineCount == 0) 0f else ((it.getLineTop(0) + it.getLineBottom(0)) / 2) - } + }, + modifier = Modifier.defaultMinSize(minHeight = dimensions().spacing20x), ) if ((message.addingFailed && expanded) || message.singleUserAddFailed) { @@ -794,4 +772,3 @@ private fun SystemMessage.MemberFailedToAdd.toFailedToAddMarkdownText( private const val EXPANDABLE_THRESHOLD = 4 private const val SINGLE_EXPANDABLE_THRESHOLD = 1 -private const val TAG_LEARN_MORE = "tag_learn_more" diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupErrorDialog.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupErrorDialog.kt index 6d9e206f16e..1e7caa2b4e9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupErrorDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/common/CreateGroupErrorDialog.kt @@ -19,18 +19,13 @@ package com.wire.android.ui.home.newconversation.common import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withStyle import com.wire.android.R +import com.wire.android.ui.common.DialogTextSuffixLink 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.colorsScheme -import com.wire.android.ui.markdown.MarkdownConstants import com.wire.android.ui.theme.WireTheme -import com.wire.android.util.DialogAnnotatedErrorStrings +import com.wire.android.util.DialogErrorStrings import com.wire.android.util.ui.PreviewMultipleThemes @Composable @@ -40,51 +35,34 @@ fun CreateGroupErrorDialog( onAccept: () -> Unit, onCancel: () -> Unit ) { - val dialogStrings = when (error) { - is CreateGroupState.Error.LackingConnection -> DialogAnnotatedErrorStrings( - stringResource(R.string.error_no_network_title), - buildAnnotatedString { append(stringResource(R.string.error_no_network_message)) } - ) + val (dialogStrings, dialogSuffixLink) = when (error) { + is CreateGroupState.Error.LackingConnection -> DialogErrorStrings( + title = stringResource(R.string.error_no_network_title), + message = stringResource(R.string.error_no_network_message), + ) to null - is CreateGroupState.Error.Unknown -> DialogAnnotatedErrorStrings( - stringResource(R.string.error_unknown_title), - buildAnnotatedString { append(stringResource(R.string.error_unknown_message)) } - ) + is CreateGroupState.Error.Unknown -> DialogErrorStrings( + title = stringResource(R.string.error_unknown_title), + message = stringResource(R.string.error_unknown_message), + ) to null - is CreateGroupState.Error.ConflictedBackends -> DialogAnnotatedErrorStrings( + is CreateGroupState.Error.ConflictedBackends -> DialogErrorStrings( title = stringResource(id = R.string.group_can_not_be_created_title), - annotatedMessage = buildAnnotatedString { - val description = stringResource( + message = stringResource( id = R.string.group_can_not_be_created_federation_conflict_description, error.domains.dropLast(1).joinToString(", "), error.domains.last() - ) - val learnMore = stringResource(id = R.string.label_learn_more) - - append(description) - append(' ') - - withStyle( - style = SpanStyle( - color = colorsScheme().primary, - textDecoration = TextDecoration.Underline - ) - ) { - append(learnMore) - } - addStringAnnotation( - tag = MarkdownConstants.TAG_URL, - annotation = stringResource(id = R.string.url_message_details_offline_backends_learn_more), - start = description.length + 1, - end = description.length + 1 + learnMore.length - ) - } + ), + ) to DialogTextSuffixLink( + linkText = stringResource(id = R.string.label_learn_more), + linkUrl = stringResource(id = R.string.url_message_details_offline_backends_learn_more) ) } WireDialog( - dialogStrings.title, - dialogStrings.annotatedMessage, + title = dialogStrings.title, + text = dialogStrings.annotatedMessage, + textSuffixLink = dialogSuffixLink, onDismiss = onDismiss, buttonsHorizontalAlignment = false, optionButton1Properties = WireDialogButtonProperties( diff --git a/app/src/main/kotlin/com/wire/android/util/CoreFailureUtil.kt b/app/src/main/kotlin/com/wire/android/util/CoreFailureUtil.kt index 85717698ea0..ac3f60ae358 100644 --- a/app/src/main/kotlin/com/wire/android/util/CoreFailureUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/CoreFailureUtil.kt @@ -48,5 +48,6 @@ fun CoreFailure.uiText(): UIText = when (this) { else -> UIText.StringResource(R.string.error_unknown_message) } -data class DialogErrorStrings(val title: String, val message: String) -data class DialogAnnotatedErrorStrings(val title: String, val annotatedMessage: AnnotatedString) +data class DialogErrorStrings(val title: String, val annotatedMessage: AnnotatedString) { + constructor(title: String, message: String) : this(title, AnnotatedString(message)) +} From 2c071dd42dd361060a7f1c400736071479cc3f17 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Tue, 23 Apr 2024 10:50:32 +0200 Subject: [PATCH 130/134] feat(config): separate lower KeyPackage limit and set to false (WPB-8685) (#2927) --- app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt | 2 +- buildSrc/src/main/kotlin/customization/FeatureConfigs.kt | 1 + default.json | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) 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 88e909b4a24..b6b2dc73003 100644 --- a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt @@ -51,7 +51,7 @@ class KaliumConfigsModule { forceConstantBitrateCalls = BuildConfig.FORCE_CONSTANT_BITRATE_CALLS, // we use upsert, available from SQL3.24, which is supported from Android API30, so for older APIs we have to use SQLCipher shouldEncryptData = !BuildConfig.DEBUG || Build.VERSION.SDK_INT < Build.VERSION_CODES.R, - lowerKeyPackageLimits = BuildConfig.PRIVATE_BUILD, + lowerKeyPackageLimits = BuildConfig.LOWER_KEYPACKAGE_LIMIT, lowerKeyingMaterialsUpdateThreshold = BuildConfig.PRIVATE_BUILD, isMLSSupportEnabled = BuildConfig.MLS_SUPPORT_ENABLED, developmentApiEnabled = BuildConfig.DEVELOPMENT_API_ENABLED, diff --git a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt index 34daf221405..3d6bbf921a6 100644 --- a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt +++ b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt @@ -54,6 +54,7 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) { * Security/Cryptography stuff */ MLS_SUPPORT_ENABLED("mls_support_enabled", ConfigType.BOOLEAN), + LOWER_KEYPACKAGE_LIMIT("lower_keypackage_limit", ConfigType.BOOLEAN), ENCRYPT_PROTEUS_STORAGE("encrypt_proteus_storage", ConfigType.BOOLEAN), WIPE_ON_COOKIE_INVALID("wipe_on_cookie_invalid", ConfigType.BOOLEAN), WIPE_ON_ROOTED_DEVICE("wipe_on_rooted_device", ConfigType.BOOLEAN), diff --git a/default.json b/default.json index a4c58e420da..47e1a9c53ab 100644 --- a/default.json +++ b/default.json @@ -89,6 +89,7 @@ "force_constant_bitrate_calls": false, "ignore_ssl_certificates": false, "mls_support_enabled": true, + "lower_keypackage_limit": false, "encrypt_proteus_storage": false, "self_deleting_messages": true, "wipe_on_cookie_invalid": false, From ac56dd35f390c8c7d6da498362bf8502dc9f4f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Wed, 24 Apr 2024 08:36:17 +0200 Subject: [PATCH 131/134] fix: remove duplicated debug id in settings screen [WPB-8626] (#2926) --- .../com/wire/android/ui/debug/DebugDataOptions.kt | 10 ---------- 1 file changed, 10 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 a51e152fecb..7cc855f5187 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 @@ -133,16 +133,6 @@ fun DebugDataOptionsContent( DependenciesItem(dependenciesMap) if (BuildConfig.PRIVATE_BUILD) { - SettingsItem( - title = stringResource(R.string.debug_id), - text = state.debugId, - trailingIcon = R.drawable.ic_copy, - onIconPressed = Clickable( - enabled = true, - onClick = { } - ) - ) - SettingsItem( title = stringResource(R.string.debug_id), text = state.debugId, From 413a82b68b0f2ff582c65e6ce5a9ad563de0d407 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 25 Apr 2024 16:38:06 +0300 Subject: [PATCH 132/134] fix: Handle 1o1 conversations when no key packages [WPB-6936] (#2936) --- .../di/accountScoped/ConversationModule.kt | 8 ++- .../ui/connection/ConnectionActionButton.kt | 60 ++++++++++++++++++- .../ConnectionActionButtonViewModel.kt | 10 +++- .../other/OtherUserProfileScreen.kt | 2 + .../other/OtherUserProfileScreenViewModel.kt | 9 +++ .../other/OtherUserProfileState.kt | 3 +- app/src/main/res/values/strings.xml | 4 ++ .../ConnectionActionButtonViewModelTest.kt | 28 ++++++++- .../OtherUserProfileViewModelArrangement.kt | 6 ++ kalium | 2 +- 10 files changed, 122 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt index 42dba9c29a2..060a17f4141 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt @@ -30,6 +30,7 @@ import com.wire.kalium.logic.feature.conversation.CreateGroupConversationUseCase import com.wire.kalium.logic.feature.conversation.GetConversationUnreadEventsCountUseCase import com.wire.kalium.logic.feature.conversation.GetOneToOneConversationUseCase import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase +import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase import com.wire.kalium.logic.feature.conversation.JoinConversationViaCodeUseCase import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase @@ -38,9 +39,9 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseC import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationUnderLegalHoldNotifiedUseCase import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase import com.wire.kalium.logic.feature.conversation.ObserveIsSelfUserMemberUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationUnderLegalHoldNotifiedUseCase import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase import com.wire.kalium.logic.feature.conversation.ObserveUsersTypingUseCase import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase @@ -288,4 +289,9 @@ class ConversationModule { fun provideObserveLegalHoldWithChangeNotifiedForConversationUseCase( conversationScope: ConversationScope, ): ObserveConversationUnderLegalHoldNotifiedUseCase = conversationScope.observeConversationUnderLegalHoldNotified + + @ViewModelScoped + @Provides + fun provideIsOneToOneConversationCreatedUseCase(conversationScope: ConversationScope): IsOneToOneConversationCreatedUseCase = + conversationScope.isOneToOneConversationCreatedUseCase } diff --git a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt index 690d0ef4183..748808d8d3b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt @@ -23,24 +23,34 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.wire.android.R import com.wire.android.di.hiltViewModelScoped import com.wire.android.model.ClickBlockParams +import com.wire.android.ui.common.VisibilityState +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.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dialogs.UnblockUserDialogContent import com.wire.android.ui.common.dialogs.UnblockUserDialogState import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.snackbar.collectAndShowSnackbar +import com.wire.android.ui.common.visbility.VisibilityState import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireTypography import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.android.util.ui.stringWithStyledArgs import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId @@ -49,7 +59,9 @@ import com.wire.kalium.logic.data.user.UserId fun ConnectionActionButton( userId: UserId, userName: String, + fullName: String, connectionStatus: ConnectionState, + isConversationStarted: Boolean, onConnectionRequestIgnored: (String) -> Unit = {}, onOpenConversation: (ConversationId) -> Unit = {}, viewModel: ConnectionActionButtonViewModel = @@ -59,6 +71,7 @@ fun ConnectionActionButton( ) { LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = viewModel.infoMessage) val unblockUserDialogState = rememberVisibilityState() + val unableStartConversationDialogState = rememberVisibilityState() UnblockUserDialogContent( dialogState = unblockUserDialogState, @@ -66,6 +79,8 @@ fun ConnectionActionButton( isLoading = viewModel.actionableState().isPerformingAction, ) + UnableStartConversationDialogContent(dialogState = unableStartConversationDialogState) + if (!viewModel.actionableState().isPerformingAction) { unblockUserDialogState.dismiss() } @@ -79,9 +94,13 @@ fun ConnectionActionButton( ) ConnectionState.ACCEPTED -> WirePrimaryButton( - text = stringResource(R.string.label_open_conversation), + text = stringResource(if (isConversationStarted) R.string.label_open_conversation else R.string.label_start_conversation), loading = viewModel.actionableState().isPerformingAction, - onClick = { viewModel.onOpenConversation(onOpenConversation) }, + onClick = { + viewModel.onOpenConversation(onOpenConversation) { + unableStartConversationDialogState.show(UnableStartConversationDialogState(fullName)) + } + }, ) ConnectionState.IGNORED -> WirePrimaryButton( @@ -167,6 +186,31 @@ fun ConnectionActionButton( } } +@Composable +fun UnableStartConversationDialogContent(dialogState: VisibilityState) { + VisibilityState(dialogState) { state -> + WireDialog( + title = stringResource(id = R.string.missing_keypackage_dialog_title), + text = LocalContext.current.resources.stringWithStyledArgs( + R.string.missing_keypackage_dialog_body, + MaterialTheme.wireTypography.body01, + MaterialTheme.wireTypography.body02, + colorsScheme().onBackground, + colorsScheme().onBackground, + state.userName + ), + onDismiss = dialogState::dismiss, + optionButton1Properties = WireDialogButtonProperties( + onClick = dialogState::dismiss, + text = stringResource(id = R.string.label_ok), + type = WireDialogButtonType.Primary, + ), + ) + } +} + +data class UnableStartConversationDialogState(val userName: String) + @Composable @PreviewMultipleThemes fun PreviewOtherUserConnectionActionButtonPending() { @@ -174,7 +218,9 @@ fun PreviewOtherUserConnectionActionButtonPending() { ConnectionActionButton( userId = UserId("value", "domain"), userName = "Username", + fullName = "some user", connectionStatus = ConnectionState.PENDING, + isConversationStarted = false ) } } @@ -186,7 +232,9 @@ fun PreviewOtherUserConnectionActionButtonNotConnected() { ConnectionActionButton( userId = UserId("value", "domain"), userName = "Username", + fullName = "some user", connectionStatus = ConnectionState.NOT_CONNECTED, + isConversationStarted = false ) } } @@ -198,7 +246,9 @@ fun PreviewOtherUserConnectionActionButtonBlocked() { ConnectionActionButton( userId = UserId("value", "domain"), userName = "Username", + fullName = "some user", connectionStatus = ConnectionState.BLOCKED, + isConversationStarted = false ) } } @@ -210,7 +260,9 @@ fun PreviewOtherUserConnectionActionButtonCanceled() { ConnectionActionButton( userId = UserId("value", "domain"), userName = "Username", + fullName = "some user", connectionStatus = ConnectionState.CANCELLED, + isConversationStarted = false ) } } @@ -222,7 +274,9 @@ fun PreviewOtherUserConnectionActionButtonAccepted() { ConnectionActionButton( userId = UserId("value", "domain"), userName = "Username", + fullName = "some user", connectionStatus = ConnectionState.ACCEPTED, + isConversationStarted = false ) } } @@ -234,7 +288,9 @@ fun PreviewOtherUserConnectionActionButtonSent() { ConnectionActionButton( userId = UserId("value", "domain"), userName = "Username", + fullName = "some user", connectionStatus = ConnectionState.SENT, + isConversationStarted = false ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModel.kt index 101e05b9a8f..dedd2ee27b8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModel.kt @@ -26,13 +26,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.ViewModelScopedPreview import com.wire.android.di.scopedArgs import com.wire.android.model.ActionableState import com.wire.android.model.finishAction import com.wire.android.model.performAction -import com.wire.android.di.ViewModelScopedPreview import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.connection.AcceptConnectionRequestUseCase @@ -59,13 +60,14 @@ import javax.inject.Inject interface ConnectionActionButtonViewModel { val infoMessage: SharedFlow get() = MutableSharedFlow() + fun actionableState(): ActionableState = ActionableState() fun onSendConnectionRequest() {} fun onCancelConnectionRequest() {} fun onAcceptConnectionRequest() {} fun onIgnoreConnectionRequest(onSuccess: (userName: String) -> Unit) {} fun onUnblockUser() {} - fun onOpenConversation(onSuccess: (conversationId: ConversationId) -> Unit) {} + fun onOpenConversation(onSuccess: (conversationId: ConversationId) -> Unit, onMissingKeyPackages: () -> Unit) {} } @Suppress("LongParameterList", "TooManyFunctions") @@ -191,17 +193,19 @@ class ConnectionActionButtonViewModelImpl @Inject constructor( } } - override fun onOpenConversation(onSuccess: (conversationId: ConversationId) -> Unit) { + override fun onOpenConversation(onSuccess: (conversationId: ConversationId) -> Unit, onMissingKeyPackages: () -> Unit) { viewModelScope.launch { state = state.performAction() when (val result = withContext(dispatchers.io()) { getOrCreateOneToOneConversation(userId) }) { is CreateConversationResult.Failure -> { appLogger.d(("Couldn't retrieve or create the conversation")) state = state.finishAction() + if (result.coreFailure is CoreFailure.MissingKeyPackages) onMissingKeyPackages() } is CreateConversationResult.Success -> onSuccess(result.conversation.id) } + state.finishAction() } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index e7dd1482b01..17a47102684 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -531,7 +531,9 @@ private fun ContentFooter( ConnectionActionButton( state.userId, state.userName, + state.fullName, state.connectionState, + state.isConversationStarted, onIgnoreConnectionRequest, onOpenConversation ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index 654aa596a3b..5a990572c02 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -63,6 +63,7 @@ import com.wire.kalium.logic.feature.conversation.ArchiveStatusUpdateResult import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase import com.wire.kalium.logic.feature.conversation.ConversationUpdateStatusResult import com.wire.kalium.logic.feature.conversation.GetOneToOneConversationUseCase +import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleResult @@ -107,6 +108,7 @@ class OtherUserProfileScreenViewModel @Inject constructor( private val updateConversationArchivedStatus: UpdateConversationArchivedStatusUseCase, private val getUserE2eiCertificateStatus: GetUserE2eiCertificateStatusUseCase, private val getUserE2eiCertificates: GetUserE2eiCertificatesUseCase, + private val isOneToOneConversationCreated: IsOneToOneConversationCreatedUseCase, savedStateHandle: SavedStateHandle ) : ViewModel(), OtherUserProfileEventsHandler, OtherUserProfileBottomSheetEventsHandler { @@ -134,6 +136,13 @@ class OtherUserProfileScreenViewModel @Inject constructor( observeUserInfoAndUpdateViewState() persistClients() getMLSVerificationStatus() + getIfConversationExist() + } + + private fun getIfConversationExist() { + viewModelScope.launch { + state = state.copy(isConversationStarted = isOneToOneConversationCreated(userId)) + } } private fun getMLSVerificationStatus() { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt index a8b751828d3..5c0f958d69e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileState.kt @@ -49,7 +49,8 @@ data class OtherUserProfileState( val otherUserDevices: List = listOf(), val blockingState: BlockingState = BlockingState.CAN_NOT_BE_BLOCKED, val isProteusVerified: Boolean = false, - val isMLSVerified: Boolean = false + val isMLSVerified: Boolean = false, + val isConversationStarted: Boolean = false ) { fun updateMuteStatus(status: MutedConversationStatus): OtherUserProfileState { return conversationSheetContent?.let { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e190e8ba290..d50b8a80171 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -789,6 +789,7 @@ HorizontalBouncingWritingPen transition Collapse button rotation degree transition Open Conversation + Start Conversation Email Phone @@ -871,6 +872,9 @@ Ignore Get certainty about the identity of %s\'s before connecting. Please verify the person\'s identity before accepting the connection request. + + Unable to start conversation + You can’t start the conversation with %1$s right now. %1$s needs to open Wire or log in again first. Please try again later. Media Gallery Saved to Downloads folder diff --git a/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt index c9c350c6352..ad8f66719c9 100644 --- a/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt @@ -248,13 +248,14 @@ class ConnectionActionButtonViewModelTest { .arrange() // when - viewModel.onOpenConversation(arrangement.onOpenConversation) + viewModel.onOpenConversation(arrangement.onOpenConversation, arrangement.onMissingKeyPackages) // then coVerify { arrangement.getOrCreateOneToOneConversation(TestUser.USER_ID) } verify { arrangement.onOpenConversation(any()) } + verify { arrangement.onMissingKeyPackages wasNot Called } } @Test @@ -266,13 +267,33 @@ class ConnectionActionButtonViewModelTest { .arrange() // when - viewModel.onOpenConversation(arrangement.onOpenConversation) + viewModel.onOpenConversation(arrangement.onOpenConversation, arrangement.onMissingKeyPackages) // then coVerify { arrangement.getOrCreateOneToOneConversation(TestUser.USER_ID) } verify { arrangement.onOpenConversation wasNot Called } + verify { arrangement.onMissingKeyPackages wasNot Called } + } + + @Test + fun `given a conversationId, when trying to open the conversation and fails with MissingKeyPackages, then call MissingKeyPackage()`() = + runTest { + // given + val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + .withGetOneToOneConversation(CreateConversationResult.Failure(CoreFailure.MissingKeyPackages(setOf()))) + .arrange() + + // when + viewModel.onOpenConversation(arrangement.onOpenConversation, arrangement.onMissingKeyPackages) + + // then + coVerify { + arrangement.getOrCreateOneToOneConversation(TestUser.USER_ID) + } + verify { arrangement.onOpenConversation wasNot Called } + verify { arrangement.onMissingKeyPackages() } } companion object { @@ -315,6 +336,9 @@ internal class ConnectionActionButtonHiltArrangement { @MockK(relaxed = true) lateinit var onOpenConversation: (conversationId: ConversationId) -> Unit + @MockK(relaxed = true) + lateinit var onMissingKeyPackages: () -> Unit + private val viewModel by lazy { ConnectionActionButtonViewModelImpl( TestDispatcherProvider(), diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt index 1729d4aa114..3dd80464f49 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt @@ -38,6 +38,7 @@ import com.wire.kalium.logic.feature.connection.UnblockUserUseCase import com.wire.kalium.logic.feature.conversation.ArchiveStatusUpdateResult import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase import com.wire.kalium.logic.feature.conversation.GetOneToOneConversationUseCase +import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleResult @@ -112,6 +113,9 @@ internal class OtherUserProfileViewModelArrangement { @MockK lateinit var getUserE2eiCertificates: GetUserE2eiCertificatesUseCase + @MockK + lateinit var isOneToOneConversationCreated: IsOneToOneConversationCreatedUseCase + private val viewModel by lazy { OtherUserProfileScreenViewModel( TestDispatcherProvider(), @@ -131,6 +135,7 @@ internal class OtherUserProfileViewModelArrangement { updateConversationArchivedStatus, getUserE2eiCertificateStatus, getUserE2eiCertificates, + isOneToOneConversationCreated, savedStateHandle, ) } @@ -161,6 +166,7 @@ internal class OtherUserProfileViewModelArrangement { ) coEvery { getUserE2eiCertificateStatus.invoke(any()) } returns GetUserE2eiCertificateStatusResult.Success(CertificateStatus.VALID) coEvery { getUserE2eiCertificates.invoke(any()) } returns mapOf() + coEvery { isOneToOneConversationCreated.invoke(any()) } returns true } suspend fun withBlockUserResult(result: BlockUserResult) = apply { diff --git a/kalium b/kalium index 2931055c9f9..91be23a81d9 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 2931055c9f9465dd0edd161fa5b2ea29beed3ebd +Subproject commit 91be23a81d973fdc30df33e42c88c531b965eb6a From 8c1b2548d1c0cc8444486cc55009c0a74cb5dd90 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Fri, 26 Apr 2024 08:56:08 +0200 Subject: [PATCH 133/134] fix(e2ei): set user display name correctly for the certificate downloaded file --- app/src/main/kotlin/com/wire/android/ui/WireActivity.kt | 2 +- .../wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt | 2 +- .../android/ui/settings/devices/DeviceDetailsScreen.kt | 6 ++++-- .../devices/e2ei/E2eiCertificateDetailsScreenNavArgs.kt | 4 +++- .../devices/e2ei/E2eiCertificateDetailsViewModel.kt | 7 ++++--- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 3d4a4bc556a..c90046edd39 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -388,7 +388,7 @@ class WireActivity : AppCompatActivity() { result = e2EIResult, updateCertificate = featureFlagNotificationViewModel::enrollE2EICertificate, snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog, - openCertificateDetails = { navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(it))) }, + openCertificateDetails = { navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(it, true))) }, dismissSuccessDialog = featureFlagNotificationViewModel::dismissSuccessE2EIdDialog, isE2EILoading = isE2EILoading ) diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt index cc11e227b11..b2557824f93 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt @@ -89,7 +89,7 @@ fun E2EIEnrollmentScreen( enrollE2EICertificate = viewModel::enrollE2EICertificate, handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, openCertificateDetails = { - navigator.navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(state.certificate))) + navigator.navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(state.certificate, true))) }, onBackButtonClicked = viewModel::onBackButtonClicked, onCancelEnrollmentClicked = { viewModel.onCancelEnrollmentClicked(NavigationSwitchAccountActions(navigator::navigate)) }, diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 029a283e75a..081bccdce72 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -83,10 +83,10 @@ import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper +import com.wire.android.util.deviceDateTimeFormat import com.wire.android.util.dialogErrorStrings import com.wire.android.util.extension.formatAsFingerPrint import com.wire.android.util.extension.formatAsString -import com.wire.android.util.deviceDateTimeFormat import com.wire.android.util.ui.UIText import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.ClientId @@ -117,7 +117,9 @@ fun DeviceDetailsScreen( handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, onNavigateToE2eiCertificateDetailsScreen = { navigator.navigate( - NavigationCommand(E2eiCertificateDetailsScreenDestination(it)) + NavigationCommand( + E2eiCertificateDetailsScreenDestination(it, viewModel.state.isSelfClient, viewModel.state.userName) + ) ) }, onEnrollE2EIErrorDismiss = viewModel::hideEnrollE2EICertificateError, diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreenNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreenNavArgs.kt index 340caeb21de..bc764d5e581 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreenNavArgs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreenNavArgs.kt @@ -18,5 +18,7 @@ package com.wire.android.ui.settings.devices.e2ei data class E2eiCertificateDetailsScreenNavArgs( - val certificateString: String + val certificateString: String, + val isSelfUser: Boolean, + val otherUserName: String? = null ) diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt index 3d195b8b38b..61522adeb05 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt @@ -45,7 +45,7 @@ class E2eiCertificateDetailsViewModel @Inject constructor( private val e2eiCertificateDetailsScreenNavArgs: E2eiCertificateDetailsScreenNavArgs = savedStateHandle.navArgs() - private var selfUserHandle: String? = null + private var selfUserName: String? = null init { getSelfUserId() @@ -53,7 +53,7 @@ class E2eiCertificateDetailsViewModel @Inject constructor( private fun getSelfUserId() { viewModelScope.launch { - selfUserHandle = observerSelfUser().first().handle + selfUserName = observerSelfUser().first().name } } @@ -61,7 +61,8 @@ class E2eiCertificateDetailsViewModel @Inject constructor( fun getCertificateName(): String { val date = DateTimeUtil.currentInstant().fileDateTime() - return "wire-certificate-$selfUserHandle-$date.txt" + val userName = if(e2eiCertificateDetailsScreenNavArgs.isSelfUser) selfUserName else e2eiCertificateDetailsScreenNavArgs.otherUserName + return "wire-certificate-$userName-$date.txt" } } From 3b6111081c54906030d32982f1b40da9f6c4ccc1 Mon Sep 17 00:00:00 2001 From: Mojtaba Chenani Date: Fri, 26 Apr 2024 10:11:33 +0200 Subject: [PATCH 134/134] fix: code style --- .../devices/e2ei/E2eiCertificateDetailsViewModel.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt index 61522adeb05..c81bb2a9b33 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt @@ -61,7 +61,12 @@ class E2eiCertificateDetailsViewModel @Inject constructor( fun getCertificateName(): String { val date = DateTimeUtil.currentInstant().fileDateTime() - val userName = if(e2eiCertificateDetailsScreenNavArgs.isSelfUser) selfUserName else e2eiCertificateDetailsScreenNavArgs.otherUserName + val userName = + if (e2eiCertificateDetailsScreenNavArgs.isSelfUser) { + selfUserName + } else { + e2eiCertificateDetailsScreenNavArgs.otherUserName + } return "wire-certificate-$userName-$date.txt" } }