From 88fcbc9e66f0b4a2ba525f36c6249a58a5afc6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:02:18 +0000 Subject: [PATCH 1/3] Commit with unresolved merge conflicts outside of submodules --- app/build.gradle.kts | 2 + .../location/LocationPickerHelperFlavor.kt | 9 +- .../kotlin/com/wire/android/di/AppModule.kt | 9 + .../location/GeocoderHelper.kt | 37 ++++ .../location/LocationPickerHelper.kt | 83 +++++++- .../location/LocationPickerHelperFlavor.kt | 20 +- .../wire/android/mapper/UserTypeMapperTest.kt | 2 +- .../EditGuestAccessViewModelTest.kt | 153 +++++++++++++- .../location/GeocoderHelperTest.kt | 97 +++++++++ .../LocationPickerHelperFlavorTest.kt | 184 ++++++++++++++++ .../location/LocationPickerHelperTest.kt | 198 ++++++++++++++++++ gradle/libs.versions.toml | 11 + 12 files changed, 782 insertions(+), 23 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelper.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelperTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavorTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e014018aea5..06e8c3dd9b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -210,6 +210,8 @@ dependencies { testRuntimeOnly(libs.junit5.vintage.engine) testRuntimeOnly(libs.junit5.engine) testImplementation(libs.androidx.paging.testing) + testImplementation(libs.robolectric) + testRuntimeOnly(libs.junit.vintage.engine) // Acceptance/Functional tests dependencies androidTestImplementation(libs.androidx.test.runner) 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 index f50538d7437..fe07d70e78d 100644 --- 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 @@ -17,16 +17,17 @@ */ 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) { +class LocationPickerHelperFlavor @Inject constructor( + private val locationPickerHelper: LocationPickerHelper, +) { suspend fun getLocation(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { - getLocationWithoutGms( + locationPickerHelper.getLocationWithoutGms( onSuccess = onSuccess, - onError = onError + onError = onError, ) } } diff --git a/app/src/main/kotlin/com/wire/android/di/AppModule.kt b/app/src/main/kotlin/com/wire/android/di/AppModule.kt index c73e8ecf7e9..2d64387d68e 100644 --- a/app/src/main/kotlin/com/wire/android/di/AppModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/AppModule.kt @@ -20,12 +20,14 @@ package com.wire.android.di import android.app.NotificationManager import android.content.Context +import android.location.Geocoder import android.media.AudioAttributes import android.media.MediaPlayer import androidx.core.app.NotificationManagerCompat import com.wire.android.BuildConfig import com.wire.android.mapper.MessageResourceProvider import com.wire.android.ui.home.appLock.CurrentTimestampProvider +import com.wire.android.ui.home.messagecomposer.location.LocationPickerParameters import com.wire.android.util.dispatchers.DefaultDispatcherProvider import com.wire.android.util.dispatchers.DispatcherProvider import dagger.Module @@ -82,4 +84,11 @@ object AppModule { @Singleton @Provides fun provideCurrentTimestampProvider(): CurrentTimestampProvider = { System.currentTimeMillis() } + + @Provides + fun provideGeocoder(appContext: Context): Geocoder = Geocoder(appContext) + + @Singleton + @Provides + fun provideLocationPickerParameters(): LocationPickerParameters = LocationPickerParameters() } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelper.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelper.kt new file mode 100644 index 00000000000..b738d2d65d9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelper.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.ui.home.messagecomposer.location + +import android.location.Geocoder +import android.location.Location +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GeocoderHelper @Inject constructor(private val geocoder: Geocoder) { + + @Suppress("TooGenericExceptionCaught") + fun getGeoLocatedAddress(location: Location): GeoLocatedAddress = + try { + geocoder.getFromLocation(location.latitude, location.longitude, 1).orEmpty() + } catch (e: Exception) { + emptyList() + }.let { addressList -> + GeoLocatedAddress(addressList.firstOrNull(), location) + } +} 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 f66fa5aec8a..5836283a942 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 @@ -19,20 +19,41 @@ 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 android.os.Build +import android.os.CancellationSignal +import androidx.annotation.VisibleForTesting import androidx.core.location.LocationManagerCompat import com.wire.android.AppJsonStyledLogger +import com.wire.android.di.ApplicationScope +import com.wire.android.ui.home.appLock.CurrentTimestampProvider import com.wire.kalium.logger.KaliumLogLevel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.function.Consumer import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds -open class LocationPickerHelper @Inject constructor(@ApplicationContext val context: Context) { +@SuppressLint("MissingPermission") +@Singleton +class LocationPickerHelper @Inject constructor( + @ApplicationContext private val context: Context, + @ApplicationScope private val scope: CoroutineScope, + private val currentTimestampProvider: CurrentTimestampProvider, + private val geocoderHelper: GeocoderHelper, + private val parameters: LocationPickerParameters, +) { - @SuppressLint("MissingPermission") - protected fun getLocationWithoutGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + @VisibleForTesting + fun getLocationWithoutGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { if (isLocationServicesEnabled()) { AppJsonStyledLogger.log( level = KaliumLogLevel.INFO, @@ -40,14 +61,17 @@ open class LocationPickerHelper @Inject constructor(@ApplicationContext val cont jsonStringKeyValues = mapOf("isUsingGms" to false) ) 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.getLastKnownLocation(LocationManager.FUSED_PROVIDER).let { lastLocation -> + if ( + lastLocation != null + && currentTimestampProvider() - lastLocation.time <= parameters.lastLocationTimeLimit.inWholeMilliseconds + ) { + // use last known location if present and not older than given limit + onSuccess(geocoderHelper.getGeoLocatedAddress(lastLocation)) + } else { + locationManager.requestCurrentLocationWithoutGms(onSuccess, onError) } } - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, networkLocationListener) } else { AppJsonStyledLogger.log( level = KaliumLogLevel.WARN, @@ -61,8 +85,45 @@ open class LocationPickerHelper @Inject constructor(@ApplicationContext val cont } } - protected fun isLocationServicesEnabled(): Boolean { + private fun LocationManager.requestCurrentLocationWithoutGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { + val cancellationSignal = CancellationSignal() + val timeoutJob = scope.launch(start = CoroutineStart.LAZY) { + delay(parameters.requestLocationTimeout) + cancellationSignal.cancel() + onError() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val executor = context.mainExecutor + val consumer: Consumer = Consumer { location -> + timeoutJob.cancel() + if (location != null) { + onSuccess(geocoderHelper.getGeoLocatedAddress(location)) + } else { + onError() + } + } + this.getCurrentLocation(LocationManager.FUSED_PROVIDER, cancellationSignal, executor, consumer) + } else { + val listener = LocationListener { location -> + timeoutJob.cancel() + onSuccess(geocoderHelper.getGeoLocatedAddress(location)) + } + cancellationSignal.setOnCancelListener { + this.removeUpdates(listener) + } + this.requestSingleUpdate(LocationManager.FUSED_PROVIDER, listener, null) + } + timeoutJob.start() + } + + internal fun isLocationServicesEnabled(): Boolean { val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager return LocationManagerCompat.isLocationEnabled(locationManager) } } + +data class LocationPickerParameters( + val lastLocationTimeLimit: Duration = 1.minutes, + val requestLocationTimeout: Duration = 10.seconds, +) 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 index 5cf954d7d99..d9c02454cbc 100644 --- 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 @@ -19,20 +19,25 @@ 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.AppJsonStyledLogger import com.wire.android.util.extension.isGoogleServicesAvailable +<<<<<<< HEAD import com.wire.kalium.logger.KaliumLogLevel +======= +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) import kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton @Singleton -class LocationPickerHelperFlavor @Inject constructor(context: Context) : LocationPickerHelper(context) { - +class LocationPickerHelperFlavor @Inject constructor( + private val context: Context, + private val geocoderHelper: GeocoderHelper, + private val locationPickerHelper: LocationPickerHelper, +) { suspend fun getLocation(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { if (context.isGoogleServicesAvailable()) { getLocationWithGms( @@ -40,7 +45,7 @@ class LocationPickerHelperFlavor @Inject constructor(context: Context) : Locatio onError = onError ) } else { - getLocationWithoutGms( + locationPickerHelper.getLocationWithoutGms( onSuccess = onSuccess, onError = onError ) @@ -53,17 +58,20 @@ class LocationPickerHelperFlavor @Inject constructor(context: Context) : Locatio */ @SuppressLint("MissingPermission") private suspend fun getLocationWithGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { +<<<<<<< HEAD if (isLocationServicesEnabled()) { AppJsonStyledLogger.log( level = KaliumLogLevel.INFO, leadingMessage = "GetLocation", jsonStringKeyValues = mapOf("isUsingGms" to true) ) +======= + if (locationPickerHelper.isLocationServicesEnabled()) { +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) 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)) + onSuccess(geocoderHelper.getGeoLocatedAddress(currentLocation)) } else { AppJsonStyledLogger.log( level = KaliumLogLevel.WARN, diff --git a/app/src/test/kotlin/com/wire/android/mapper/UserTypeMapperTest.kt b/app/src/test/kotlin/com/wire/android/mapper/UserTypeMapperTest.kt index 09108ce3fb1..54718bc081c 100644 --- a/app/src/test/kotlin/com/wire/android/mapper/UserTypeMapperTest.kt +++ b/app/src/test/kotlin/com/wire/android/mapper/UserTypeMapperTest.kt @@ -46,7 +46,7 @@ class UserTypeMapperTest { } @Test - fun `given internal as a user type correctly map to none as membership`() { + fun `given internal as a user type correctly map to standard as membership`() { val result = userTypeMapper.toMembership(UserType.INTERNAL) assertEquals(Membership.Standard, result) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt index 855aabfe7fa..d03c0246419 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt @@ -24,12 +24,19 @@ import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider +<<<<<<< HEAD +======= +import com.wire.android.framework.TestConversation +import com.wire.android.framework.TestConversationDetails +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase import com.wire.android.ui.navArgs import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.configuration.GuestRoomLinkStatus +import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationCodeUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUseCase @@ -40,21 +47,34 @@ import com.wire.kalium.logic.feature.conversation.guestroomlink.ObserveGuestRoom import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkResult import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkUseCase import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase +<<<<<<< HEAD +======= +import com.wire.kalium.logic.functional.Either +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) 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 +<<<<<<< HEAD import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals +======= +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.BeforeEach +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) +<<<<<<< HEAD @ExtendWith(CoroutineTestExtension::class) @ExtendWith(NavigationTestExtension::class) class EditGuestAccessViewModelTest { @@ -67,6 +87,93 @@ class EditGuestAccessViewModelTest { .withUpdateConversationAccessRoleResult(UpdateConversationAccessRoleUseCase.Result.Success) .arrange() advanceUntilIdle() +======= +@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class) +class EditGuestAccessViewModelTest { + + val dispatcher = TestDispatcherProvider() + + @MockK + private lateinit var savedStateHandle: SavedStateHandle + + @MockK + lateinit var updateConversationAccessRoleUseCase: UpdateConversationAccessRoleUseCase + + @MockK + lateinit var observeConversationDetails: ObserveConversationDetailsUseCase + + @MockK + lateinit var observeConversationMembers: ObserveParticipantsForConversationUseCase + + @MockK + lateinit var generateGuestRoomLink: GenerateGuestRoomLinkUseCase + + @MockK + lateinit var observeGuestRoomLink: ObserveGuestRoomLinkUseCase + + @MockK + lateinit var revokeGuestRoomLink: RevokeGuestRoomLinkUseCase + + @MockK + lateinit var observeGuestRoomLinkFeatureFlag: ObserveGuestRoomLinkFeatureFlagUseCase + + @MockK + lateinit var canCreatePasswordProtectedLinks: CanCreatePasswordProtectedLinksUseCase + + private lateinit var editGuestAccessViewModel: EditGuestAccessViewModel + + private val conversationDetailsChannel = Channel(capacity = Channel.UNLIMITED) + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this, relaxUnitFun = true) + coEvery { savedStateHandle.navArgs() } returns EditGuestAccessNavArgs( + conversationId = TestConversation.ID, + editGuessAccessParams = EditGuestAccessParams( + isGuestAccessAllowed = true, + isServicesAllowed = true, + isUpdatingGuestAccessAllowed = true + ) + ) + coEvery { + observeConversationDetails(any()) + } returns conversationDetailsChannel.consumeAsFlow() + coEvery { + observeConversationMembers(any()) + } returns flowOf(ConversationParticipantsData()) + coEvery { + observeGuestRoomLink(any()) + } returns flowOf(Either.Right(null)) + coEvery { + observeGuestRoomLinkFeatureFlag() + } returns flowOf(GuestRoomLinkStatus(null, null)) + coEvery { + canCreatePasswordProtectedLinks() + } returns true + + editGuestAccessViewModel = EditGuestAccessViewModel( + observeConversationDetails = observeConversationDetails, + observeConversationMembers = observeConversationMembers, + updateConversationAccessRole = updateConversationAccessRoleUseCase, + generateGuestRoomLink = generateGuestRoomLink, + revokeGuestRoomLink = revokeGuestRoomLink, + observeGuestRoomLink = observeGuestRoomLink, + savedStateHandle = savedStateHandle, + observeGuestRoomLinkFeatureFlag = observeGuestRoomLinkFeatureFlag, + canCreatePasswordProtectedLinks = canCreatePasswordProtectedLinks, + dispatcher = dispatcher, + ) + conversationDetailsChannel.trySend(ObserveConversationDetailsUseCase.Result.Success(TestConversationDetails.GROUP)) + } + + @Test + fun `given updateConversationAccessRole use case runs successfully, when trying to enable guest access, then enable guest access`() = + runTest(dispatcher.default()) { + editGuestAccessViewModel.editGuestAccessState = editGuestAccessViewModel.editGuestAccessState.copy(isGuestAccessAllowed = false) + coEvery { + updateConversationAccessRoleUseCase(any(), any(), any()) + } returns UpdateConversationAccessRoleUseCase.Result.Success +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.updateGuestAccess(true) @@ -112,12 +219,19 @@ class EditGuestAccessViewModelTest { } @Test +<<<<<<< HEAD fun `given useCase runs with success, when_generating guest link, then invoke it once`() = runTest { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withGenerateGuestRoomResult(GenerateGuestRoomLinkResult.Success) .arrange() advanceUntilIdle() +======= + fun `given useCase runs with success, when_generating guest link, then invoke it once`() = runTest(dispatcher.default()) { + coEvery { + generateGuestRoomLink.invoke(any(), any()) + } returns GenerateGuestRoomLinkResult.Success +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.onRequestGuestRoomLink() @@ -128,6 +242,7 @@ class EditGuestAccessViewModelTest { } @Test +<<<<<<< HEAD fun `given useCase runs with failure, when generating guest link, then show dialog error`() = runTest { // given val (arrangement, editGuestAccessViewModel) = Arrangement() @@ -135,6 +250,12 @@ class EditGuestAccessViewModelTest { GenerateGuestRoomLinkResult.Failure(NetworkFailure.NoNetworkConnection(RuntimeException("no network"))) ).arrange() advanceUntilIdle() +======= + fun `given useCase runs with failure, when generating guest link, then show dialog error`() = runTest(dispatcher.default()) { + coEvery { + generateGuestRoomLink(any(), any()) + } returns GenerateGuestRoomLinkResult.Failure(NetworkFailure.NoNetworkConnection(null)) +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.onRequestGuestRoomLink() @@ -145,12 +266,19 @@ class EditGuestAccessViewModelTest { } @Test +<<<<<<< HEAD fun `given useCase runs with success, when revoking guest link, then invoke it once`() = runTest { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withRevokeGuestRoomLinkResult(RevokeGuestRoomLinkResult.Success) .arrange() advanceUntilIdle() +======= + fun `given useCase runs with success, when revoking guest link, then invoke it once`() = runTest(dispatcher.default()) { + coEvery { + revokeGuestRoomLink(any()) + } returns RevokeGuestRoomLinkResult.Success +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.removeGuestLink() @@ -161,12 +289,19 @@ class EditGuestAccessViewModelTest { } @Test +<<<<<<< HEAD fun `given useCase runs with failure when revoking guest link then show dialog error`() = runTest { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withRevokeGuestRoomLinkResult(RevokeGuestRoomLinkResult.Failure(CoreFailure.MissingClientRegistration)) .arrange() advanceUntilIdle() +======= + fun `given useCase runs with failure when revoking guest link then show dialog error`() = runTest(dispatcher.default()) { + coEvery { + revokeGuestRoomLink(any()) + } returns RevokeGuestRoomLinkResult.Failure(CoreFailure.MissingClientRegistration) +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.removeGuestLink() @@ -179,12 +314,28 @@ class EditGuestAccessViewModelTest { @Test fun `given updateConversationAccessRole use case runs successfully, when trying to disable guest access, then disable guest access`() = +<<<<<<< HEAD runTest { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withUpdateConversationAccessRoleResult(UpdateConversationAccessRoleUseCase.Result.Success) .arrange() advanceUntilIdle() +======= + runTest(dispatcher.default()) { + editGuestAccessViewModel.editGuestAccessState = editGuestAccessViewModel.editGuestAccessState.copy(isGuestAccessAllowed = true) + coEvery { + updateConversationAccessRoleUseCase(any(), any(), any()) + } coAnswers { + val accessRoles = secondArg>() + val newConversationDetails = TestConversationDetails.GROUP.copy( + conversation = TestConversationDetails.GROUP.conversation.copy(accessRole = accessRoles.toList()) + ) + // mock emitting updated conversation details with new access roles + conversationDetailsChannel.send(ObserveConversationDetailsUseCase.Result.Success(newConversationDetails)) + UpdateConversationAccessRoleUseCase.Result.Success + } +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.onGuestDialogConfirm() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelperTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelperTest.kt new file mode 100644 index 00000000000..23cd5603817 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelperTest.kt @@ -0,0 +1,97 @@ +/* + * 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.Address +import android.location.Geocoder +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import okio.IOException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class GeocoderHelperTest { + + @Test + fun `given non-null result, when getting geocoder address, then return result with address`() = runTest { + // given + val location = mockLocation(latitude = 1.0, longitude = 1.0) + val address = mockAddress(addressFirstLine = "address") + val (_, geocoderHelper) = Arrangement() + .withGetFromLocation(1.0, 1.0, address) + .arrange() + + // when + val result = geocoderHelper.getGeoLocatedAddress(location) + + // then + assertEquals(address, result.address) + } + + @Test + fun `given empty result, when getting geocoder address, then return result without address`() = runTest { + // given + val location = mockLocation(latitude = 1.0, longitude = 1.0) + val (_, geocoderHelper) = Arrangement() + .withGetFromLocation(1.0, 1.0, null) + .arrange() + + // when + val result = geocoderHelper.getGeoLocatedAddress(location) + + // then + assertEquals(null, result.address) + } + + @Test + fun `given failure, when getting geocoder address, then return result without address`() = runTest { + // given + val location = mockLocation(latitude = 1.0, longitude = 1.0) + val (_, geocoderHelper) = Arrangement() + .withGetFromLocationFailure() + .arrange() + + // when + val result = geocoderHelper.getGeoLocatedAddress(location) + + // then + assertEquals(null, result.address) + } + + inner class Arrangement { + + @MockK + lateinit var geocoder: Geocoder + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + } + + fun withGetFromLocation(latitude: Double, longitude: Double, result: Address?) = apply { + coEvery { geocoder.getFromLocation(latitude, longitude, 1) } returns listOfNotNull(result) + } + + fun withGetFromLocationFailure() = apply { + coEvery { geocoder.getFromLocation(any(), any(), any()) } throws IOException() + } + + fun arrange() = this to GeocoderHelper(geocoder) + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavorTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavorTest.kt new file mode 100644 index 00000000000..467b8ad282a --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavorTest.kt @@ -0,0 +1,184 @@ +/* + * 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 android.location.Address +import android.location.Location +import android.location.LocationManager +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.google.android.gms.tasks.CancellationToken +import com.google.android.gms.tasks.CancellationTokenSource +import com.google.android.gms.tasks.Task +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.util.extension.isGoogleServicesAvailable +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.mockkStatic +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class LocationPickerHelperFlavorTest { + + private val dispatcher = StandardTestDispatcher() + + @Test + fun `given GMS not available, when getting location, then execute getLocationWithoutGms`() = + runTest(dispatcher) { + // given + val (arrangement, locationPickerHelperFlavor) = Arrangement() + .withIsGoogleServicesAvailable(false) + .arrange() + + // when + locationPickerHelperFlavor.getLocation(onSuccess = arrangement.onSuccess, onError = arrangement.onError) + + // then + coVerify(exactly = 1) { + arrangement.locationPickerHelper.getLocationWithoutGms(any(), any()) + } + } + + @Test + fun `given GMS available and location service disabled, when getting location, then execute onError`() = + runTest(dispatcher) { + // given + val (arrangement, locationPickerHelperFlavor) = Arrangement() + .withIsGoogleServicesAvailable(true) + .withIsLocationServiceEnabled(false) + .arrange() + + // when + locationPickerHelperFlavor.getLocation(onSuccess = arrangement.onSuccess, onError = arrangement.onError) + + // then + coVerify(exactly = 0) { + arrangement.onSuccess(any()) + } + coVerify(exactly = 1) { + arrangement.onError() + } + } + + @Test + fun `given GMS available and location service enabled, when getting location, then execute onSuccess with location`() = + runTest(dispatcher) { + // given + val location = mockLocation(latitude = 1.0, longitude = 1.0) + val address = mockAddress(addressFirstLine = "address") + val (arrangement, locationPickerHelperFlavor) = Arrangement() + .withIsGoogleServicesAvailable(true) + .withIsLocationServiceEnabled(true) + .withGetCurrentLocation(location) + .withGetGeoLocatedAddress(location, address) + .arrange() + + // when + locationPickerHelperFlavor.getLocation(onSuccess = arrangement.onSuccess, onError = arrangement.onError) + + // then + coVerify(exactly = 1) { + arrangement.onSuccess(match { it.location == location && it.address == address }) + } + coVerify(exactly = 0) { + arrangement.onError() + } + } + + inner class Arrangement { + + @MockK + private lateinit var context: Context + + @MockK + private lateinit var locationManager: LocationManager + + @MockK + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + + @MockK + private lateinit var geocoderHelper: GeocoderHelper + + @MockK + lateinit var locationPickerHelper: LocationPickerHelper + + val onSuccess: (GeoLocatedAddress) -> Unit = mockk() + val onError: () -> Unit = mockk() + + private val locationPickerHelperFlavor by lazy { + LocationPickerHelperFlavor( + context = context, + geocoderHelper = geocoderHelper, + locationPickerHelper = locationPickerHelper, + ) + } + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + mockkStatic(LocationServices::getFusedLocationProviderClient) + coEvery { LocationServices.getFusedLocationProviderClient(context) } returns fusedLocationProviderClient + coEvery { context.getSystemService(Context.LOCATION_SERVICE) } returns locationManager + coEvery { onSuccess(any()) } returns Unit + coEvery { onError() } returns Unit + coEvery { locationPickerHelper.getLocationWithoutGms(any(), any()) } returns Unit + } + + fun withIsGoogleServicesAvailable(isAvailable: Boolean) = apply { + mockkStatic("com.wire.android.util.extension.GoogleServicesKt") + coEvery { context.isGoogleServicesAvailable() } returns isAvailable + } + + fun withIsLocationServiceEnabled(isEnabled: Boolean) = apply { + coEvery { locationPickerHelper.isLocationServicesEnabled() } returns isEnabled + } + + fun withGetCurrentLocation(location: Location) = apply { + val task: Task = mockk() + mockkStatic("kotlinx.coroutines.tasks.TasksKt") + coEvery { task.await() } returns location + mockkConstructor(CancellationTokenSource::class) + coEvery { anyConstructed().token } returns mockk() + coEvery { fusedLocationProviderClient.getCurrentLocation(any(), any()) } returns task + } + + fun withGetGeoLocatedAddress(location: Location, result: Address) = apply { + coEvery { geocoderHelper.getGeoLocatedAddress(location) } returns GeoLocatedAddress(result, location) + } + + fun arrange() = this to locationPickerHelperFlavor + } +} + +fun mockLocation(latitude: Double, longitude: Double) = mockk().let { + coEvery { it.latitude } returns latitude + coEvery { it.longitude } returns longitude + it +} + +fun mockAddress(addressFirstLine: String) = mockk
().also { + coEvery { it.getAddressLine(0) } returns addressFirstLine +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperTest.kt index d219d9edf10..0de8298e5c2 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperTest.kt @@ -19,16 +19,33 @@ package com.wire.android.ui.home.messagecomposer.location import android.app.Application import android.content.Context +<<<<<<< HEAD import android.location.LocationManager import android.os.Build import androidx.test.core.app.ApplicationProvider import kotlinx.coroutines.test.runTest import org.junit.Assert.assertTrue +======= +import android.location.Address +import android.location.Geocoder +import android.location.Location +import android.location.LocationManager +import android.os.Build +import android.os.Looper +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config +<<<<<<< HEAD @RunWith(RobolectricTestRunner::class) @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) @@ -88,5 +105,186 @@ class LocationPickerHelperTest { } fun arrange() = this to LocationPickerHelperFlavor(context) +======= +import org.robolectric.shadows.ShadowSystemClock +import java.util.Locale +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicInteger +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class LocationPickerHelperTest { + + private val dispatcher = StandardTestDispatcher() + + @Test + @Config(sdk = [Build.VERSION_CODES.P, Build.VERSION_CODES.R]) + fun `given last location not too old, then emit last location`() = runTest(dispatcher) { + // given + val resultHandler = ResultHandler() + val (arrangement, locationPickerHelper) = Arrangement().arrange() + val location = Location(LocationManager.FUSED_PROVIDER).apply { + latitude = 1.0 + longitude = 1.0 + elapsedRealtimeNanos = arrangement.lastLocationTimeLimit.inWholeNanoseconds - 1.seconds.inWholeNanoseconds + time = dispatcher.scheduler.currentTime - elapsedRealtimeNanos.nanoseconds.inWholeMilliseconds + bearing = 0f + } + arrangement.updateLocation(location) + shadowOf(Looper.getMainLooper()).idle() + + // when + locationPickerHelper.getLocationWithoutGms(resultHandler::onSuccess, resultHandler::onError) + + // then + resultHandler.assert(expectedErrorCount = 0, expectedLocations = listOf(GeoLocatedAddress(arrangement.address, location))) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.P, Build.VERSION_CODES.R]) + fun `given last location too old, when new location comes before timeout, then emit new location`() = runTest(dispatcher) { + // given + val resultHandler = ResultHandler() + val (arrangement, locationPickerHelper) = Arrangement().arrange() + val lastLocation = Location(LocationManager.FUSED_PROVIDER).apply { + latitude = 1.0 + longitude = 1.0 + elapsedRealtimeNanos = arrangement.lastLocationTimeLimit.inWholeNanoseconds + 1.seconds.inWholeNanoseconds + time = dispatcher.scheduler.currentTime - elapsedRealtimeNanos.nanoseconds.inWholeMilliseconds + } + arrangement.updateLocation(lastLocation) + shadowOf(Looper.getMainLooper()).idle() + + // when + locationPickerHelper.getLocationWithoutGms(resultHandler::onSuccess, resultHandler::onError) + advanceTimeBy(arrangement.requestLocationTimeout - 1.seconds) + + val newLocation = Location(LocationManager.FUSED_PROVIDER).apply { + latitude = 2.0 + longitude = 2.0 + elapsedRealtimeNanos = 0 + time = dispatcher.scheduler.currentTime + } + arrangement.updateLocation(newLocation) + shadowOf(Looper.getMainLooper()).idle() + + // then + resultHandler.assert(expectedErrorCount = 0, expectedLocations = listOf(GeoLocatedAddress(arrangement.address, newLocation))) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.P, Build.VERSION_CODES.R]) + fun `given last location too old, when new location times out, then emit error`() = runTest(dispatcher) { + // given + val resultHandler = ResultHandler() + val (arrangement, locationPickerHelper) = Arrangement().arrange() + val lastLocation = Location(LocationManager.FUSED_PROVIDER).apply { + latitude = 1.0 + longitude = 1.0 + elapsedRealtimeNanos = arrangement.lastLocationTimeLimit.inWholeNanoseconds + 1.seconds.inWholeNanoseconds + time = dispatcher.scheduler.currentTime - elapsedRealtimeNanos.nanoseconds.inWholeMilliseconds + } + arrangement.updateLocation(lastLocation) + shadowOf(Looper.getMainLooper()).idle() + + // when + locationPickerHelper.getLocationWithoutGms(resultHandler::onSuccess, resultHandler::onError) + advanceTimeBy(arrangement.requestLocationTimeout + 1.seconds) + + // then + resultHandler.assert(expectedErrorCount = 1, expectedLocations = emptyList()) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.R]) // null location can happen only for R and above after some timeout + fun `given no last location, when new location is null, then emit error`() = runTest(dispatcher) { + // given + val resultHandler = ResultHandler() + val (arrangement, locationPickerHelper) = Arrangement( + requestLocationTimeout = 1.minutes + ) + .arrange() + + // when + locationPickerHelper.getLocationWithoutGms(resultHandler::onSuccess, resultHandler::onError) + + shadowOf(Looper.getMainLooper()) + .idleFor(shadowOf(Looper.getMainLooper()).lastScheduledTaskTime) // this is how the timeout is simulated + + // then + resultHandler.assert(expectedErrorCount = 1, expectedLocations = emptyList()) + } + + inner class Arrangement( + val lastLocationTimeLimit: Duration = 1.minutes, + val requestLocationTimeout: Duration = 10.seconds + ) { + private val context: Context = ApplicationProvider.getApplicationContext() + private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) + private val geocoder: Geocoder = Geocoder(context) + private val geocoderHelper: GeocoderHelper = GeocoderHelper(geocoder) + private val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val address = Address(Locale.getDefault()).apply { + setAddressLine(0, "address") + } + + private val locationPickerHelper by lazy { + LocationPickerHelper( + context = context, + scope = scope, + currentTimestampProvider = dispatcher.scheduler::currentTime, + geocoderHelper = geocoderHelper, + parameters = LocationPickerParameters( + lastLocationTimeLimit = lastLocationTimeLimit, + requestLocationTimeout = requestLocationTimeout + ), + ) + } + + init { + shadowOf(geocoder).setFromLocation(listOf(address)) + shadowOf(locationManager).setProviderEnabled(LocationManager.FUSED_PROVIDER, true) + + // update the system clock to not start with 0 and prevent from having negative time for some locations + dispatcher.scheduler.advanceTimeBy(1.hours) + ShadowSystemClock.advanceBy(1.hours.toJavaDuration()) + } + + fun updateLocation(location: Location) = apply { + shadowOf(locationManager).simulateLocation(location) + } + + fun arrange() = this to locationPickerHelper + } + + class ResultHandler { + private val locations = ConcurrentLinkedQueue() + private val errorCount = AtomicInteger(0) + + fun onSuccess(geoLocatedAddress: GeoLocatedAddress) { + locations.add(geoLocatedAddress) + } + + fun onError() { + errorCount.incrementAndGet() + } + + fun assert(expectedErrorCount: Int = 0, expectedLocations: List = emptyList()) { + assertEquals(expectedErrorCount, errorCount.get()) + assertEquals(expectedLocations.size, locations.size) + locations.forEachIndexed { index, geoLocatedAddress -> + assertEquals(expectedLocations[index].address, geoLocatedAddress.address) + assertEquals(expectedLocations[index].location.latitude, geoLocatedAddress.location.latitude) + assertEquals(expectedLocations[index].location.longitude, geoLocatedAddress.location.longitude) + assertEquals(expectedLocations[index].location.time, geoLocatedAddress.location.time) + } + } +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 263e44ac439..69042eb57e6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -97,10 +97,17 @@ androidx-text-archCore = "2.2.0" junit4 = "4.13.2" junit5 = "5.10.2" kluent = "1.73" +<<<<<<< HEAD mockk = "1.13.10" okio = "3.9.0" turbine = "1.1.0" robolectric = "4.12.1" +======= +mockk = "1.13.9" +okio = "3.6.0" +turbine = "1.0.0" +robolectric = "4.12.2" +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) [plugins] # 3rd Party plugins @@ -269,3 +276,7 @@ mockk-core = { module = "io.mockk:mockk", version.ref = "mockk" } okio-fakeFileSystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +<<<<<<< HEAD +======= +junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } # needed for tests that use Robolectric because it doesn't yet support JUnit5 +>>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) From a2ea5fd98f9621982598067a93af861bf02bea16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 28 Jun 2024 14:28:07 +0200 Subject: [PATCH 2/3] conflicts resolved --- app/build.gradle.kts | 4 - .../location/LocationPickerHelperFlavor.kt | 9 +- .../EditGuestAccessViewModelTest.kt | 181 ++---------------- .../location/LocationPickerHelperTest.kt | 70 ------- gradle/libs.versions.toml | 13 +- 5 files changed, 16 insertions(+), 261 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 06e8c3dd9b0..85bcb65b91b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,3 @@ -import scripts.Variants_gradle - /* * Wire * Copyright (C) 2024 Wire Swiss GmbH @@ -210,8 +208,6 @@ dependencies { testRuntimeOnly(libs.junit5.vintage.engine) testRuntimeOnly(libs.junit5.engine) testImplementation(libs.androidx.paging.testing) - testImplementation(libs.robolectric) - testRuntimeOnly(libs.junit.vintage.engine) // Acceptance/Functional tests dependencies androidTestImplementation(libs.androidx.test.runner) 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 index d9c02454cbc..00a0d48bdc4 100644 --- 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 @@ -24,10 +24,7 @@ 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 -<<<<<<< HEAD import com.wire.kalium.logger.KaliumLogLevel -======= ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) import kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton @@ -58,16 +55,12 @@ class LocationPickerHelperFlavor @Inject constructor( */ @SuppressLint("MissingPermission") private suspend fun getLocationWithGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { -<<<<<<< HEAD - if (isLocationServicesEnabled()) { + if (locationPickerHelper.isLocationServicesEnabled()) { AppJsonStyledLogger.log( level = KaliumLogLevel.INFO, leadingMessage = "GetLocation", jsonStringKeyValues = mapOf("isUsingGms" to true) ) -======= - if (locationPickerHelper.isLocationServicesEnabled()) { ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) val locationProvider = LocationServices.getFusedLocationProviderClient(context) val currentLocation = locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, CancellationTokenSource().token).await() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt index d03c0246419..bdc79f781d2 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt @@ -24,19 +24,12 @@ import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider -<<<<<<< HEAD -======= -import com.wire.android.framework.TestConversation -import com.wire.android.framework.TestConversationDetails ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase import com.wire.android.ui.navArgs import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure -import com.wire.kalium.logic.configuration.GuestRoomLinkStatus -import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationCodeUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUseCase @@ -47,133 +40,34 @@ import com.wire.kalium.logic.feature.conversation.guestroomlink.ObserveGuestRoom import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkResult import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkUseCase import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase -<<<<<<< HEAD -======= -import com.wire.kalium.logic.functional.Either ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) 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 -<<<<<<< HEAD import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals -======= -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.internal.assertEquals -import org.junit.jupiter.api.BeforeEach ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) -<<<<<<< HEAD -@ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) +@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class) class EditGuestAccessViewModelTest { + val dispatcher = TestDispatcherProvider() + @Test fun `given updateConversationAccessRole use case runs successfully, when trying to enable guest access, then enable guest access`() = - runTest { + runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withUpdateConversationAccessRoleResult(UpdateConversationAccessRoleUseCase.Result.Success) .arrange() advanceUntilIdle() -======= -@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class) -class EditGuestAccessViewModelTest { - - val dispatcher = TestDispatcherProvider() - - @MockK - private lateinit var savedStateHandle: SavedStateHandle - - @MockK - lateinit var updateConversationAccessRoleUseCase: UpdateConversationAccessRoleUseCase - - @MockK - lateinit var observeConversationDetails: ObserveConversationDetailsUseCase - - @MockK - lateinit var observeConversationMembers: ObserveParticipantsForConversationUseCase - - @MockK - lateinit var generateGuestRoomLink: GenerateGuestRoomLinkUseCase - - @MockK - lateinit var observeGuestRoomLink: ObserveGuestRoomLinkUseCase - - @MockK - lateinit var revokeGuestRoomLink: RevokeGuestRoomLinkUseCase - - @MockK - lateinit var observeGuestRoomLinkFeatureFlag: ObserveGuestRoomLinkFeatureFlagUseCase - - @MockK - lateinit var canCreatePasswordProtectedLinks: CanCreatePasswordProtectedLinksUseCase - - private lateinit var editGuestAccessViewModel: EditGuestAccessViewModel - - private val conversationDetailsChannel = Channel(capacity = Channel.UNLIMITED) - - @BeforeEach - fun setUp() { - MockKAnnotations.init(this, relaxUnitFun = true) - coEvery { savedStateHandle.navArgs() } returns EditGuestAccessNavArgs( - conversationId = TestConversation.ID, - editGuessAccessParams = EditGuestAccessParams( - isGuestAccessAllowed = true, - isServicesAllowed = true, - isUpdatingGuestAccessAllowed = true - ) - ) - coEvery { - observeConversationDetails(any()) - } returns conversationDetailsChannel.consumeAsFlow() - coEvery { - observeConversationMembers(any()) - } returns flowOf(ConversationParticipantsData()) - coEvery { - observeGuestRoomLink(any()) - } returns flowOf(Either.Right(null)) - coEvery { - observeGuestRoomLinkFeatureFlag() - } returns flowOf(GuestRoomLinkStatus(null, null)) - coEvery { - canCreatePasswordProtectedLinks() - } returns true - - editGuestAccessViewModel = EditGuestAccessViewModel( - observeConversationDetails = observeConversationDetails, - observeConversationMembers = observeConversationMembers, - updateConversationAccessRole = updateConversationAccessRoleUseCase, - generateGuestRoomLink = generateGuestRoomLink, - revokeGuestRoomLink = revokeGuestRoomLink, - observeGuestRoomLink = observeGuestRoomLink, - savedStateHandle = savedStateHandle, - observeGuestRoomLinkFeatureFlag = observeGuestRoomLinkFeatureFlag, - canCreatePasswordProtectedLinks = canCreatePasswordProtectedLinks, - dispatcher = dispatcher, - ) - conversationDetailsChannel.trySend(ObserveConversationDetailsUseCase.Result.Success(TestConversationDetails.GROUP)) - } - - @Test - fun `given updateConversationAccessRole use case runs successfully, when trying to enable guest access, then enable guest access`() = - runTest(dispatcher.default()) { - editGuestAccessViewModel.editGuestAccessState = editGuestAccessViewModel.editGuestAccessState.copy(isGuestAccessAllowed = false) - coEvery { - updateConversationAccessRoleUseCase(any(), any(), any()) - } returns UpdateConversationAccessRoleUseCase.Result.Success ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.updateGuestAccess(true) @@ -185,7 +79,7 @@ class EditGuestAccessViewModelTest { @Test fun `given a failure when running updateConversationAccessRole, when trying to enable guest access, then do not enable guest access`() = - runTest { + runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withUpdateConversationAccessRoleResult( @@ -203,7 +97,7 @@ class EditGuestAccessViewModelTest { @Test fun `given guest access is activated, when trying to disable guest access, then display dialog before disabling guest access`() = - runTest { + runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withUpdateConversationAccessRoleResult(UpdateConversationAccessRoleUseCase.Result.Success) @@ -214,24 +108,17 @@ class EditGuestAccessViewModelTest { editGuestAccessViewModel.updateGuestAccess(false) // then - coVerify(inverse = true) { arrangement.updateConversationAccessRoleUseCase(any(), any(), any()) } + coVerify(inverse = true) { arrangement.updateConversationAccessRole(any(), any(), any()) } assertEquals(true, editGuestAccessViewModel.editGuestAccessState.shouldShowGuestAccessChangeConfirmationDialog) } @Test -<<<<<<< HEAD - fun `given useCase runs with success, when_generating guest link, then invoke it once`() = runTest { + fun `given useCase runs with success, when_generating guest link, then invoke it once`() = runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withGenerateGuestRoomResult(GenerateGuestRoomLinkResult.Success) .arrange() advanceUntilIdle() -======= - fun `given useCase runs with success, when_generating guest link, then invoke it once`() = runTest(dispatcher.default()) { - coEvery { - generateGuestRoomLink.invoke(any(), any()) - } returns GenerateGuestRoomLinkResult.Success ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.onRequestGuestRoomLink() @@ -242,20 +129,13 @@ class EditGuestAccessViewModelTest { } @Test -<<<<<<< HEAD - fun `given useCase runs with failure, when generating guest link, then show dialog error`() = runTest { + fun `given useCase runs with failure, when generating guest link, then show dialog error`() = runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withGenerateGuestRoomResult( GenerateGuestRoomLinkResult.Failure(NetworkFailure.NoNetworkConnection(RuntimeException("no network"))) ).arrange() advanceUntilIdle() -======= - fun `given useCase runs with failure, when generating guest link, then show dialog error`() = runTest(dispatcher.default()) { - coEvery { - generateGuestRoomLink(any(), any()) - } returns GenerateGuestRoomLinkResult.Failure(NetworkFailure.NoNetworkConnection(null)) ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.onRequestGuestRoomLink() @@ -266,19 +146,12 @@ class EditGuestAccessViewModelTest { } @Test -<<<<<<< HEAD - fun `given useCase runs with success, when revoking guest link, then invoke it once`() = runTest { + fun `given useCase runs with success, when revoking guest link, then invoke it once`() = runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withRevokeGuestRoomLinkResult(RevokeGuestRoomLinkResult.Success) .arrange() advanceUntilIdle() -======= - fun `given useCase runs with success, when revoking guest link, then invoke it once`() = runTest(dispatcher.default()) { - coEvery { - revokeGuestRoomLink(any()) - } returns RevokeGuestRoomLinkResult.Success ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.removeGuestLink() @@ -289,19 +162,12 @@ class EditGuestAccessViewModelTest { } @Test -<<<<<<< HEAD - fun `given useCase runs with failure when revoking guest link then show dialog error`() = runTest { + fun `given useCase runs with failure when revoking guest link then show dialog error`() = runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withRevokeGuestRoomLinkResult(RevokeGuestRoomLinkResult.Failure(CoreFailure.MissingClientRegistration)) .arrange() advanceUntilIdle() -======= - fun `given useCase runs with failure when revoking guest link then show dialog error`() = runTest(dispatcher.default()) { - coEvery { - revokeGuestRoomLink(any()) - } returns RevokeGuestRoomLinkResult.Failure(CoreFailure.MissingClientRegistration) ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.removeGuestLink() @@ -314,28 +180,12 @@ class EditGuestAccessViewModelTest { @Test fun `given updateConversationAccessRole use case runs successfully, when trying to disable guest access, then disable guest access`() = -<<<<<<< HEAD - runTest { + runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withUpdateConversationAccessRoleResult(UpdateConversationAccessRoleUseCase.Result.Success) .arrange() advanceUntilIdle() -======= - runTest(dispatcher.default()) { - editGuestAccessViewModel.editGuestAccessState = editGuestAccessViewModel.editGuestAccessState.copy(isGuestAccessAllowed = true) - coEvery { - updateConversationAccessRoleUseCase(any(), any(), any()) - } coAnswers { - val accessRoles = secondArg>() - val newConversationDetails = TestConversationDetails.GROUP.copy( - conversation = TestConversationDetails.GROUP.conversation.copy(accessRole = accessRoles.toList()) - ) - // mock emitting updated conversation details with new access roles - conversationDetailsChannel.send(ObserveConversationDetailsUseCase.Result.Success(newConversationDetails)) - UpdateConversationAccessRoleUseCase.Result.Success - } ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) // when editGuestAccessViewModel.onGuestDialogConfirm() @@ -347,7 +197,7 @@ class EditGuestAccessViewModelTest { @Test fun `given a failure running updateConversationAccessRole, when trying to disable guest access, then do not disable guest access`() = - runTest { + runTest(dispatcher.default()) { // given val (arrangement, editGuestAccessViewModel) = Arrangement() .withUpdateConversationAccessRoleResult( @@ -368,9 +218,6 @@ class EditGuestAccessViewModelTest { @MockK lateinit var savedStateHandle: SavedStateHandle - @MockK - lateinit var updateConversationAccessRoleUseCase: UpdateConversationAccessRoleUseCase - @MockK lateinit var observeConversationDetails: ObserveConversationDetailsUseCase diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperTest.kt index 0de8298e5c2..bcc30d16b97 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperTest.kt @@ -19,13 +19,6 @@ package com.wire.android.ui.home.messagecomposer.location import android.app.Application import android.content.Context -<<<<<<< HEAD -import android.location.LocationManager -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertTrue -======= import android.location.Address import android.location.Geocoder import android.location.Location @@ -39,73 +32,11 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config -<<<<<<< HEAD - -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [Build.VERSION_CODES.TIRAMISU]) -class LocationPickerHelperTest { - - @Test - fun `given user has device location disabled, when sharing location, then error lambda is called`() = runTest { - // given - val (arrangement, locationHelper) = Arrangement() - .withLocationEnabled(false) - .arrange() - - // when - then - locationHelper.getLocation( - onSuccess = { - assertTrue(false) // this should not be called, so it will fail the test otherwise. - }, - onError = { assertTrue(true) } - ) - } - - @Test - fun `given user has device location enabled, when sharing location, then on success lambda is called`() = runTest { - // given - val (arrangement, locationHelper) = Arrangement() - .withLocationEnabled(true) - .arrange() - - // when - then - locationHelper.getLocation( - onSuccess = { - assertTrue(true) - }, - onError = { - assertTrue(false) // this should not be called, so it will fail the test otherwise. - } - ) - } - - private class Arrangement { - val context: Context = ApplicationProvider.getApplicationContext() - val locationManager: LocationManager = context.getSystemService(Application.LOCATION_SERVICE) as LocationManager - - init { - shadowOf(locationManager).apply { - setProviderEnabled(LocationManager.GPS_PROVIDER, true) - setProviderEnabled(LocationManager.NETWORK_PROVIDER, true) - } - } - - fun withLocationEnabled(enabled: Boolean) = apply { - locationManager.apply { - shadowOf(this).apply { - setLocationEnabled(enabled) - } - } - } - - fun arrange() = this to LocationPickerHelperFlavor(context) -======= import org.robolectric.shadows.ShadowSystemClock import java.util.Locale import java.util.concurrent.ConcurrentLinkedQueue @@ -285,6 +216,5 @@ class LocationPickerHelperTest { assertEquals(expectedLocations[index].location.time, geoLocatedAddress.location.time) } } ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69042eb57e6..8627af88862 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -97,17 +97,10 @@ androidx-text-archCore = "2.2.0" junit4 = "4.13.2" junit5 = "5.10.2" kluent = "1.73" -<<<<<<< HEAD mockk = "1.13.10" okio = "3.9.0" turbine = "1.1.0" robolectric = "4.12.1" -======= -mockk = "1.13.9" -okio = "3.6.0" -turbine = "1.0.0" -robolectric = "4.12.2" ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) [plugins] # 3rd Party plugins @@ -268,7 +261,7 @@ androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents" junit4 = { module = "junit:junit", version.ref = "junit4" } junit5-core = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } -junit5-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } +junit5-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } # needed for tests that use Robolectric because it doesn't yet support JUnit5 kluent-android = { module = "org.amshove.kluent:kluent-android", version.ref = "kluent" } kluent-core = { module = "org.amshove.kluent:kluent", version.ref = "kluent" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } @@ -276,7 +269,3 @@ mockk-core = { module = "io.mockk:mockk", version.ref = "mockk" } okio-fakeFileSystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } -<<<<<<< HEAD -======= -junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } # needed for tests that use Robolectric because it doesn't yet support JUnit5 ->>>>>>> 395395269 (fix: location sharing without gms when not moving [WPB-9724] (#3136)) From 042640db6204d16899742a01a643d1087de38e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 28 Jun 2024 14:47:33 +0200 Subject: [PATCH 3/3] disable lint MissingTranslation --- .../kotlin/com/wire/android/gradle/KotlinAndroidConfiguration.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/build-logic/plugins/src/main/kotlin/com/wire/android/gradle/KotlinAndroidConfiguration.kt b/build-logic/plugins/src/main/kotlin/com/wire/android/gradle/KotlinAndroidConfiguration.kt index 800829bb620..91fc66da10a 100644 --- a/build-logic/plugins/src/main/kotlin/com/wire/android/gradle/KotlinAndroidConfiguration.kt +++ b/build-logic/plugins/src/main/kotlin/com/wire/android/gradle/KotlinAndroidConfiguration.kt @@ -88,6 +88,7 @@ private fun CommonExtension<*, *, *, *, *>.configureLint(project: Project) { disable.add("IconDensities") // For testing purpose. This is safe to remove. disable.add("IconMissingDensityFolder") // For testing purpose. This is safe to remove. disable.add("ComposePreviewPublic") // Needed for screenshot testing. + disable.add("MissingTranslation") // We don't want to hardcode translations in English for other languages. baseline = project.file("lint-baseline.xml") }