Skip to content

Commit

Permalink
fix: location sharing without gms when not moving [WPB-9724] (#3136)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk authored Jun 28, 2024
1 parent f5b8521 commit d768fce
Show file tree
Hide file tree
Showing 14 changed files with 706 additions and 54 deletions.
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
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,59 @@ 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,
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 +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<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,26 +19,28 @@ package com.wire.android.ui.home.messagecomposer.location

import android.annotation.SuppressLint
import android.content.Context
import android.location.Geocoder
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.google.android.gms.tasks.CancellationTokenSource
import com.wire.android.util.extension.isGoogleServicesAvailable
import 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 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 @@ -51,12 +53,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()
onSuccess(GeoLocatedAddress(address.firstOrNull(), currentLocation))
onSuccess(geocoderHelper.getGeoLocatedAddress(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

0 comments on commit d768fce

Please sign in to comment.