Skip to content

Commit

Permalink
fix: location sharing without gms when not moving [WPB-9724] 🍒 🍒 (#3145)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Michał Saleniuk <[email protected]>
Co-authored-by: Mojtaba Chenani <[email protected]>
  • Loading branch information
3 people authored Jul 2, 2024
1 parent 2a4cc0b commit 29b6de2
Show file tree
Hide file tree
Showing 12 changed files with 584 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@
*/
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,
)
}
}
8 changes: 8 additions & 0 deletions app/src/main/kotlin/com/wire/android/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,4 +84,10 @@ object AppModule {
@Singleton
@Provides
fun provideCurrentTimestampProvider(): CurrentTimestampProvider = { System.currentTimeMillis() }

@Provides
fun provideGeocoder(appContext: Context): Geocoder = Geocoder(appContext)

@Provides
fun provideLocationPickerParameters(): LocationPickerParameters = LocationPickerParameters()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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

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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,57 @@ 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 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")
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,
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) {
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,
Expand All @@ -61,8 +83,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<Location?> = 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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ 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
Expand All @@ -28,19 +27,20 @@ import com.wire.android.util.extension.isGoogleServicesAvailable
import com.wire.kalium.logger.KaliumLogLevel
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(
onSuccess = onSuccess,
onError = onError
)
} else {
getLocationWithoutGms(
locationPickerHelper.getLocationWithoutGms(
onSuccess = onSuccess,
onError = onError
)
Expand All @@ -53,7 +53,7 @@ class LocationPickerHelperFlavor @Inject constructor(context: Context) : Locatio
*/
@SuppressLint("MissingPermission")
private suspend fun getLocationWithGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) {
if (isLocationServicesEnabled()) {
if (locationPickerHelper.isLocationServicesEnabled()) {
AppJsonStyledLogger.log(
level = KaliumLogLevel.INFO,
leadingMessage = "GetLocation",
Expand All @@ -62,8 +62,7 @@ class LocationPickerHelperFlavor @Inject constructor(context: Context) : Locatio
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(CoroutineTestExtension::class)
@ExtendWith(NavigationTestExtension::class)
@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class)
class EditGuestAccessViewModelTest {
private val dispatcher = TestDispatcherProvider()

Expand Down Expand Up @@ -108,7 +107,7 @@ 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)
}

Expand Down Expand Up @@ -218,9 +217,6 @@ class EditGuestAccessViewModelTest {
@MockK
lateinit var savedStateHandle: SavedStateHandle

@MockK
lateinit var updateConversationAccessRoleUseCase: UpdateConversationAccessRoleUseCase

@MockK
lateinit var observeConversationDetails: ObserveConversationDetailsUseCase

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 29b6de2

Please sign in to comment.