Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: location sharing without gms when not moving [WPB-9724] #3136

Merged
merged 9 commits into from
Jun 28, 2024
Merged
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ dependencies {
testImplementation(libs.okio.fakeFileSystem)
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
9 changes: 9 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,11 @@ object AppModule {
@Singleton
@Provides
fun provideCurrentTimestampProvider(): CurrentTimestampProvider = { System.currentTimeMillis() }

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

@Singleton
@Provides
fun provideLocationPickerParameters(): LocationPickerParameters = LocationPickerParameters()
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ class EditGuestAccessViewModel @Inject constructor(
conversationDetailsFlow,
isSelfAdminFlow
) { conversationDetails, isSelfAnAdmin ->
isSelfAnAdmin to conversationDetails
}.collect { (isSelfAnAdmin, conversationDetails) ->

val isGuestAllowed =
conversationDetails.conversation.isGuestAllowed() || conversationDetails.conversation.isNonTeamMemberAllowed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,55 @@ 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 geocoder: Geocoder,
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
) {
onSuccess(lastLocation.toGeoLocatedAddress()) // use last known location if present and not older than given limit
} else {
locationManager.requestCurrentLocationWithoutGms(onSuccess, onError)
}
}
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, networkLocationListener)
} else {
AppJsonStyledLogger.log(
level = KaliumLogLevel.WARN,
Expand All @@ -61,8 +85,50 @@ 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(location.toGeoLocatedAddress())
} else {
onError()
}
}
this.getCurrentLocation(LocationManager.FUSED_PROVIDER, cancellationSignal, executor, consumer)
} else {
val listener = LocationListener { location ->
timeoutJob.cancel()
onSuccess(location.toGeoLocatedAddress())
}
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)
}

private fun Location.toGeoLocatedAddress(): GeoLocatedAddress =
geocoder.getFromLocation(latitude, longitude, 1).orEmpty().let { addressList ->
GeoLocatedAddress(addressList.firstOrNull(), this)
}
}

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 @@ -24,21 +24,24 @@ 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 kotlinx.coroutines.tasks.await
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.tasks.await

@Singleton
class LocationPickerHelperFlavor @Inject constructor(context: Context) : LocationPickerHelper(context) {

class LocationPickerHelperFlavor @Inject constructor(
private val context: Context,
private val geocoder: Geocoder,
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 @@ -51,11 +54,11 @@ class LocationPickerHelperFlavor @Inject constructor(context: Context) : Locatio
*/
@SuppressLint("MissingPermission")
private suspend fun getLocationWithGms(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) {
if (isLocationServicesEnabled()) {
if (locationPickerHelper.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()
val address = geocoder.getFromLocation(currentLocation.latitude, currentLocation.longitude, 1).orEmpty()
onSuccess(GeoLocatedAddress(address.firstOrNull(), currentLocation))
} else {
onError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ package com.wire.android.mapper
import com.wire.android.ui.home.conversationslist.model.Membership
import com.wire.kalium.logic.data.user.type.UserType
import org.amshove.kluent.internal.assertEquals
import org.junit.Test
import org.junit.jupiter.api.Test

class UserTypeMapperTest {

Expand All @@ -46,9 +46,9 @@ 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.None, result)
assertEquals(Membership.Standard, result)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import com.wire.android.config.CoroutineTestExtension
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.UserId
import org.amshove.kluent.internal.assertEquals
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CoroutineTestExtension::class)
Expand Down
Loading
Loading