Merge pull request #5923 from vector-im/fix/mna/issue-5913-location-tracker
[Location tracker] - Fix some location tracking issues (PSF-1000)
This commit is contained in:
commit
cd74f09d70
3
changelog.d/5913.misc
Normal file
3
changelog.d/5913.misc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Notify of the latest known location in LocationTracker to avoid multiple locations at start
|
||||||
|
- Debounce location updates
|
||||||
|
- Improve location providers access
|
@ -198,7 +198,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocationProviderIsNotAvailable() {
|
override fun onNoLocationProviderAvailable() {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
@ -182,7 +182,7 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocationProviderIsNotAvailable() {
|
override fun onNoLocationProviderAvailable() {
|
||||||
_viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError)
|
_viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,13 +21,19 @@ import android.content.Context
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.location.LocationListenerCompat
|
import androidx.core.location.LocationListenerCompat
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
|
import im.vector.app.core.utils.Debouncer
|
||||||
|
import im.vector.app.core.utils.createBackgroundHandler
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
private const val BKG_HANDLER_NAME = "LocationTracker.BKG_HANDLER_NAME"
|
||||||
|
private const val LOCATION_DEBOUNCE_ID = "LocationTracker.LOCATION_DEBOUNCE_ID"
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class LocationTracker @Inject constructor(
|
class LocationTracker @Inject constructor(
|
||||||
context: Context
|
context: Context
|
||||||
@ -36,43 +42,50 @@ class LocationTracker @Inject constructor(
|
|||||||
private val locationManager = context.getSystemService<LocationManager>()
|
private val locationManager = context.getSystemService<LocationManager>()
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
|
/**
|
||||||
|
* Called on every location update.
|
||||||
|
*/
|
||||||
fun onLocationUpdate(locationData: LocationData)
|
fun onLocationUpdate(locationData: LocationData)
|
||||||
fun onLocationProviderIsNotAvailable()
|
|
||||||
|
/**
|
||||||
|
* Called when no location provider is available to request location updates.
|
||||||
|
*/
|
||||||
|
fun onNoLocationProviderAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callbacks = mutableListOf<Callback>()
|
@VisibleForTesting
|
||||||
|
val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
private var hasGpsProviderLiveLocation = false
|
@VisibleForTesting
|
||||||
|
var hasLocationFromFusedProvider = false
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
var hasLocationFromGPSProvider = false
|
||||||
|
|
||||||
private var lastLocation: LocationData? = null
|
private var lastLocation: LocationData? = null
|
||||||
|
|
||||||
|
private val debouncer = Debouncer(createBackgroundHandler(BKG_HANDLER_NAME))
|
||||||
|
|
||||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||||
fun start() {
|
fun start() {
|
||||||
Timber.d("## LocationTracker. start()")
|
Timber.d("start()")
|
||||||
hasGpsProviderLiveLocation = false
|
|
||||||
|
|
||||||
if (locationManager == null) {
|
if (locationManager == null) {
|
||||||
callbacks.forEach { it.onLocationProviderIsNotAvailable() }
|
Timber.v("LocationManager is not available")
|
||||||
Timber.v("## LocationTracker. LocationManager is not available")
|
onNoLocationProviderAvailable()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
locationManager.allProviders
|
val providers = locationManager.allProviders
|
||||||
.takeIf { it.isNotEmpty() }
|
|
||||||
// Take GPS first
|
|
||||||
?.sortedByDescending { if (it == LocationManager.GPS_PROVIDER) 1 else 0 }
|
|
||||||
?.forEach { provider ->
|
|
||||||
Timber.d("## LocationTracker. track location using $provider")
|
|
||||||
|
|
||||||
// Send last known location without waiting location updates
|
if (providers.isEmpty()) {
|
||||||
locationManager.getLastKnownLocation(provider)?.let { lastKnownLocation ->
|
Timber.v("There is no location provider available")
|
||||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
onNoLocationProviderAvailable()
|
||||||
Timber.d("## LocationTracker. lastKnownLocation: $lastKnownLocation")
|
|
||||||
} else {
|
} else {
|
||||||
Timber.d("## LocationTracker. lastKnownLocation: ${lastKnownLocation.provider}")
|
// Take GPS first
|
||||||
}
|
providers.sortedByDescending(::getProviderPriority)
|
||||||
notifyLocation(lastKnownLocation, isLive = false)
|
.mapNotNull { provider ->
|
||||||
}
|
Timber.d("track location using $provider")
|
||||||
|
|
||||||
locationManager.requestLocationUpdates(
|
locationManager.requestLocationUpdates(
|
||||||
provider,
|
provider,
|
||||||
@ -80,34 +93,59 @@ class LocationTracker @Inject constructor(
|
|||||||
MIN_DISTANCE_TO_UPDATE_LOCATION_METERS,
|
MIN_DISTANCE_TO_UPDATE_LOCATION_METERS,
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
|
|
||||||
|
locationManager.getLastKnownLocation(provider)
|
||||||
}
|
}
|
||||||
?: run {
|
.maxByOrNull { location -> location.time }
|
||||||
callbacks.forEach { it.onLocationProviderIsNotAvailable() }
|
?.let { latestKnownLocation ->
|
||||||
Timber.v("## LocationTracker. There is no location provider available")
|
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||||
|
Timber.d("lastKnownLocation: $latestKnownLocation")
|
||||||
|
} else {
|
||||||
|
Timber.d("lastKnownLocation: ${latestKnownLocation.provider}")
|
||||||
}
|
}
|
||||||
|
notifyLocation(latestKnownLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the priority of the given provider name.
|
||||||
|
* @return an integer representing the priority: the higher the value, the higher the priority is.
|
||||||
|
*/
|
||||||
|
private fun getProviderPriority(provider: String): Int = when (provider) {
|
||||||
|
LocationManager.FUSED_PROVIDER -> 2
|
||||||
|
LocationManager.GPS_PROVIDER -> 1
|
||||||
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||||
fun stop() {
|
fun stop() {
|
||||||
Timber.d("## LocationTracker. stop()")
|
Timber.d("stop()")
|
||||||
locationManager?.removeUpdates(this)
|
locationManager?.removeUpdates(this)
|
||||||
|
synchronized(this) {
|
||||||
callbacks.clear()
|
callbacks.clear()
|
||||||
}
|
}
|
||||||
|
debouncer.cancelAll()
|
||||||
|
hasLocationFromGPSProvider = false
|
||||||
|
hasLocationFromFusedProvider = false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the last known location. It will be given async through Callback.
|
* Request the last known location. It will be given async through Callback.
|
||||||
* Please ensure adding a callback to receive the value.
|
* Please ensure adding a callback to receive the value.
|
||||||
*/
|
*/
|
||||||
fun requestLastKnownLocation() {
|
fun requestLastKnownLocation() {
|
||||||
lastLocation?.let { location -> callbacks.forEach { it.onLocationUpdate(location) } }
|
lastLocation?.let { locationData -> onLocationUpdate(locationData) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun addCallback(callback: Callback) {
|
fun addCallback(callback: Callback) {
|
||||||
if (!callbacks.contains(callback)) {
|
if (!callbacks.contains(callback)) {
|
||||||
callbacks.add(callback)
|
callbacks.add(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun removeCallback(callback: Callback) {
|
fun removeCallback(callback: Callback) {
|
||||||
callbacks.remove(callback)
|
callbacks.remove(callback)
|
||||||
if (callbacks.size == 0) {
|
if (callbacks.size == 0) {
|
||||||
@ -117,34 +155,86 @@ class LocationTracker @Inject constructor(
|
|||||||
|
|
||||||
override fun onLocationChanged(location: Location) {
|
override fun onLocationChanged(location: Location) {
|
||||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||||
Timber.d("## LocationTracker. onLocationChanged: $location")
|
Timber.d("onLocationChanged: $location")
|
||||||
} else {
|
} else {
|
||||||
Timber.d("## LocationTracker. onLocationChanged: ${location.provider}")
|
Timber.d("onLocationChanged: ${location.provider}")
|
||||||
}
|
|
||||||
notifyLocation(location, isLive = true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyLocation(location: Location, isLive: Boolean) {
|
|
||||||
when (location.provider) {
|
when (location.provider) {
|
||||||
|
LocationManager.FUSED_PROVIDER -> {
|
||||||
|
hasLocationFromFusedProvider = true
|
||||||
|
}
|
||||||
LocationManager.GPS_PROVIDER -> {
|
LocationManager.GPS_PROVIDER -> {
|
||||||
hasGpsProviderLiveLocation = isLive
|
if (hasLocationFromFusedProvider) {
|
||||||
|
hasLocationFromGPSProvider = false
|
||||||
|
// Ignore this update
|
||||||
|
Timber.d("ignoring location from ${location.provider}, we have location from fused provider")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
hasLocationFromGPSProvider = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (hasGpsProviderLiveLocation) {
|
if (hasLocationFromFusedProvider || hasLocationFromGPSProvider) {
|
||||||
// Ignore this update
|
// Ignore this update
|
||||||
Timber.d("## LocationTracker. ignoring location from ${location.provider}, we have gps live location")
|
Timber.d("ignoring location from ${location.provider}, we have location from GPS provider")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debouncer.debounce(LOCATION_DEBOUNCE_ID, MIN_TIME_TO_UPDATE_LOCATION_MILLIS) {
|
||||||
|
notifyLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyLocation(location: Location) {
|
||||||
|
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||||
|
Timber.d("notify location: $location")
|
||||||
|
} else {
|
||||||
|
Timber.d("notify location: ${location.provider}")
|
||||||
|
}
|
||||||
|
|
||||||
val locationData = location.toLocationData()
|
val locationData = location.toLocationData()
|
||||||
lastLocation = locationData
|
lastLocation = locationData
|
||||||
callbacks.forEach { it.onLocationUpdate(locationData) }
|
onLocationUpdate(locationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onProviderDisabled(provider: String) {
|
override fun onProviderDisabled(provider: String) {
|
||||||
Timber.d("## LocationTracker. onProviderDisabled: $provider")
|
Timber.d("onProviderDisabled: $provider")
|
||||||
callbacks.forEach { it.onLocationProviderIsNotAvailable() }
|
when (provider) {
|
||||||
|
LocationManager.FUSED_PROVIDER -> hasLocationFromFusedProvider = false
|
||||||
|
LocationManager.GPS_PROVIDER -> hasLocationFromGPSProvider = false
|
||||||
|
}
|
||||||
|
|
||||||
|
locationManager?.allProviders
|
||||||
|
?.takeIf { it.isEmpty() }
|
||||||
|
?.let {
|
||||||
|
Timber.e("all providers have been disabled")
|
||||||
|
onNoLocationProviderAvailable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun onNoLocationProviderAvailable() {
|
||||||
|
callbacks.forEach {
|
||||||
|
try {
|
||||||
|
it.onNoLocationProviderAvailable()
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Timber.e(error, "error in onNoLocationProviderAvailable callback $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun onLocationUpdate(locationData: LocationData) {
|
||||||
|
callbacks.forEach {
|
||||||
|
try {
|
||||||
|
it.onLocationUpdate(locationData)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Timber.e(error, "error in onLocationUpdate callback $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Location.toLocationData(): LocationData {
|
private fun Location.toLocationData(): LocationData {
|
||||||
|
@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.location
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.location.Location
|
||||||
|
import android.location.LocationManager
|
||||||
|
import im.vector.app.core.utils.Debouncer
|
||||||
|
import im.vector.app.core.utils.createBackgroundHandler
|
||||||
|
import im.vector.app.test.fakes.FakeContext
|
||||||
|
import im.vector.app.test.fakes.FakeHandler
|
||||||
|
import im.vector.app.test.fakes.FakeLocationManager
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkConstructor
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import io.mockk.verify
|
||||||
|
import io.mockk.verifyOrder
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_LATITUDE = 1.2
|
||||||
|
private const val A_LONGITUDE = 44.0
|
||||||
|
private const val AN_ACCURACY = 5.0f
|
||||||
|
|
||||||
|
class LocationTrackerTest {
|
||||||
|
|
||||||
|
private val fakeHandler = FakeHandler()
|
||||||
|
private val fakeLocationManager = FakeLocationManager()
|
||||||
|
private val fakeContext = FakeContext().also {
|
||||||
|
it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var locationTracker: LocationTracker
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockkConstructor(Debouncer::class)
|
||||||
|
every { anyConstructed<Debouncer>().cancelAll() } just runs
|
||||||
|
val runnable = slot<Runnable>()
|
||||||
|
every { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, capture(runnable)) } answers {
|
||||||
|
runnable.captured.run()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
mockkStatic("im.vector.app.core.utils.HandlerKt")
|
||||||
|
every { createBackgroundHandler(any()) } returns fakeHandler.instance
|
||||||
|
locationTracker = LocationTracker(fakeContext.instance)
|
||||||
|
fakeLocationManager.givenRemoveUpdates(locationTracker)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given available list of providers when starting then location updates are requested in priority order`() {
|
||||||
|
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
|
||||||
|
mockAvailableProviders(providers)
|
||||||
|
|
||||||
|
locationTracker.start()
|
||||||
|
|
||||||
|
verifyOrder {
|
||||||
|
fakeLocationManager.instance.requestLocationUpdates(
|
||||||
|
LocationManager.FUSED_PROVIDER,
|
||||||
|
MIN_TIME_TO_UPDATE_LOCATION_MILLIS,
|
||||||
|
MIN_DISTANCE_TO_UPDATE_LOCATION_METERS,
|
||||||
|
locationTracker
|
||||||
|
)
|
||||||
|
fakeLocationManager.instance.requestLocationUpdates(
|
||||||
|
LocationManager.GPS_PROVIDER,
|
||||||
|
MIN_TIME_TO_UPDATE_LOCATION_MILLIS,
|
||||||
|
MIN_DISTANCE_TO_UPDATE_LOCATION_METERS,
|
||||||
|
locationTracker
|
||||||
|
)
|
||||||
|
fakeLocationManager.instance.requestLocationUpdates(
|
||||||
|
LocationManager.NETWORK_PROVIDER,
|
||||||
|
MIN_TIME_TO_UPDATE_LOCATION_MILLIS,
|
||||||
|
MIN_DISTANCE_TO_UPDATE_LOCATION_METERS,
|
||||||
|
locationTracker
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given available list of providers when list is empty then callbacks are notified`() {
|
||||||
|
val providers = emptyList<String>()
|
||||||
|
val callback = mockCallback()
|
||||||
|
|
||||||
|
locationTracker.addCallback(callback)
|
||||||
|
fakeLocationManager.givenActiveProviders(providers)
|
||||||
|
|
||||||
|
locationTracker.start()
|
||||||
|
|
||||||
|
verify { callback.onNoLocationProviderAvailable() }
|
||||||
|
locationTracker.removeCallback(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when adding or removing a callback then it is added into or removed from the list of callbacks`() {
|
||||||
|
val callback1 = mockCallback()
|
||||||
|
val callback2 = mockCallback()
|
||||||
|
|
||||||
|
locationTracker.addCallback(callback1)
|
||||||
|
locationTracker.addCallback(callback2)
|
||||||
|
|
||||||
|
locationTracker.callbacks.size shouldBeEqualTo 2
|
||||||
|
locationTracker.callbacks.first() shouldBeEqualTo callback1
|
||||||
|
locationTracker.callbacks[1] shouldBeEqualTo callback2
|
||||||
|
|
||||||
|
locationTracker.removeCallback(callback1)
|
||||||
|
|
||||||
|
locationTracker.callbacks.size shouldBeEqualTo 1
|
||||||
|
|
||||||
|
locationTracker.removeCallback(callback2)
|
||||||
|
|
||||||
|
locationTracker.callbacks.size shouldBeEqualTo 0
|
||||||
|
verify { locationTracker.stop() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when location updates are received from fused provider then fused locations are taken in priority`() {
|
||||||
|
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
|
||||||
|
mockAvailableProviders(providers)
|
||||||
|
val callback = mockCallback()
|
||||||
|
locationTracker.addCallback(callback)
|
||||||
|
locationTracker.start()
|
||||||
|
|
||||||
|
val fusedLocation = mockLocation(
|
||||||
|
provider = LocationManager.FUSED_PROVIDER,
|
||||||
|
latitude = 1.0,
|
||||||
|
longitude = 3.0,
|
||||||
|
accuracy = 4f
|
||||||
|
)
|
||||||
|
val gpsLocation = mockLocation(
|
||||||
|
provider = LocationManager.GPS_PROVIDER
|
||||||
|
)
|
||||||
|
|
||||||
|
val networkLocation = mockLocation(
|
||||||
|
provider = LocationManager.NETWORK_PROVIDER
|
||||||
|
)
|
||||||
|
locationTracker.onLocationChanged(fusedLocation)
|
||||||
|
locationTracker.onLocationChanged(gpsLocation)
|
||||||
|
locationTracker.onLocationChanged(networkLocation)
|
||||||
|
|
||||||
|
val expectedLocationData = LocationData(
|
||||||
|
latitude = 1.0,
|
||||||
|
longitude = 3.0,
|
||||||
|
uncertainty = 4.0
|
||||||
|
)
|
||||||
|
verify { callback.onLocationUpdate(expectedLocationData) }
|
||||||
|
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) }
|
||||||
|
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo true
|
||||||
|
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when location updates are received from gps provider then gps locations are taken if none are received from fused provider`() {
|
||||||
|
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
|
||||||
|
mockAvailableProviders(providers)
|
||||||
|
val callback = mockCallback()
|
||||||
|
locationTracker.addCallback(callback)
|
||||||
|
locationTracker.start()
|
||||||
|
|
||||||
|
val gpsLocation = mockLocation(
|
||||||
|
provider = LocationManager.GPS_PROVIDER,
|
||||||
|
latitude = 1.0,
|
||||||
|
longitude = 3.0,
|
||||||
|
accuracy = 4f
|
||||||
|
)
|
||||||
|
|
||||||
|
val networkLocation = mockLocation(
|
||||||
|
provider = LocationManager.NETWORK_PROVIDER
|
||||||
|
)
|
||||||
|
locationTracker.onLocationChanged(gpsLocation)
|
||||||
|
locationTracker.onLocationChanged(networkLocation)
|
||||||
|
|
||||||
|
val expectedLocationData = LocationData(
|
||||||
|
latitude = 1.0,
|
||||||
|
longitude = 3.0,
|
||||||
|
uncertainty = 4.0
|
||||||
|
)
|
||||||
|
verify { callback.onLocationUpdate(expectedLocationData) }
|
||||||
|
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) }
|
||||||
|
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
|
||||||
|
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when location updates are received from network provider then network locations are taken if none are received from fused or gps provider`() {
|
||||||
|
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
|
||||||
|
mockAvailableProviders(providers)
|
||||||
|
val callback = mockCallback()
|
||||||
|
locationTracker.addCallback(callback)
|
||||||
|
locationTracker.start()
|
||||||
|
|
||||||
|
val networkLocation = mockLocation(
|
||||||
|
provider = LocationManager.NETWORK_PROVIDER,
|
||||||
|
latitude = 1.0,
|
||||||
|
longitude = 3.0,
|
||||||
|
accuracy = 4f
|
||||||
|
)
|
||||||
|
locationTracker.onLocationChanged(networkLocation)
|
||||||
|
|
||||||
|
val expectedLocationData = LocationData(
|
||||||
|
latitude = 1.0,
|
||||||
|
longitude = 3.0,
|
||||||
|
uncertainty = 4.0
|
||||||
|
)
|
||||||
|
verify { callback.onLocationUpdate(expectedLocationData) }
|
||||||
|
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) }
|
||||||
|
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
|
||||||
|
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when requesting the last location then last location is notified via callback`() {
|
||||||
|
val providers = listOf(LocationManager.GPS_PROVIDER)
|
||||||
|
fakeLocationManager.givenActiveProviders(providers)
|
||||||
|
val lastLocation = mockLocation(provider = LocationManager.GPS_PROVIDER)
|
||||||
|
fakeLocationManager.givenLastLocationForProvider(provider = LocationManager.GPS_PROVIDER, location = lastLocation)
|
||||||
|
fakeLocationManager.givenRequestUpdatesForProvider(provider = LocationManager.GPS_PROVIDER, listener = locationTracker)
|
||||||
|
val callback = mockCallback()
|
||||||
|
locationTracker.addCallback(callback)
|
||||||
|
locationTracker.start()
|
||||||
|
|
||||||
|
locationTracker.requestLastKnownLocation()
|
||||||
|
|
||||||
|
val expectedLocationData = LocationData(
|
||||||
|
latitude = A_LATITUDE,
|
||||||
|
longitude = A_LONGITUDE,
|
||||||
|
uncertainty = AN_ACCURACY.toDouble()
|
||||||
|
)
|
||||||
|
verify { callback.onLocationUpdate(expectedLocationData) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when stopping then location updates are stopped and callbacks are cleared`() {
|
||||||
|
locationTracker.stop()
|
||||||
|
|
||||||
|
verify { fakeLocationManager.instance.removeUpdates(locationTracker) }
|
||||||
|
verify { anyConstructed<Debouncer>().cancelAll() }
|
||||||
|
locationTracker.callbacks.isEmpty() shouldBeEqualTo true
|
||||||
|
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
|
||||||
|
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mockAvailableProviders(providers: List<String>) {
|
||||||
|
fakeLocationManager.givenActiveProviders(providers)
|
||||||
|
providers.forEach { provider ->
|
||||||
|
fakeLocationManager.givenLastLocationForProvider(provider = provider, location = null)
|
||||||
|
fakeLocationManager.givenRequestUpdatesForProvider(provider = provider, listener = locationTracker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mockCallback(): LocationTracker.Callback {
|
||||||
|
return mockk<LocationTracker.Callback>().also {
|
||||||
|
every { it.onNoLocationProviderAvailable() } just runs
|
||||||
|
every { it.onLocationUpdate(any()) } just runs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mockLocation(
|
||||||
|
provider: String,
|
||||||
|
latitude: Double = A_LATITUDE,
|
||||||
|
longitude: Double = A_LONGITUDE,
|
||||||
|
accuracy: Float = AN_ACCURACY
|
||||||
|
): Location {
|
||||||
|
return mockk<Location>().also {
|
||||||
|
every { it.time } returns 123
|
||||||
|
every { it.latitude } returns latitude
|
||||||
|
every { it.longitude } returns longitude
|
||||||
|
every { it.accuracy } returns accuracy
|
||||||
|
every { it.provider } returns provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -56,7 +56,7 @@ class FakeContext(
|
|||||||
givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
|
givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> givenService(name: String, klass: Class<T>, service: T) {
|
fun <T> givenService(name: String, klass: Class<T>, service: T) {
|
||||||
every { instance.getSystemService(name) } returns service
|
every { instance.getSystemService(name) } returns service
|
||||||
every { instance.getSystemService(klass) } returns service
|
every { instance.getSystemService(klass) } returns service
|
||||||
}
|
}
|
||||||
|
25
vector/src/test/java/im/vector/app/test/fakes/FakeHandler.kt
Normal file
25
vector/src/test/java/im/vector/app/test/fakes/FakeHandler.kt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeHandler {
|
||||||
|
|
||||||
|
val instance = mockk<Handler>()
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import android.location.LocationListener
|
||||||
|
import android.location.LocationManager
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
|
|
||||||
|
class FakeLocationManager {
|
||||||
|
val instance = mockk<LocationManager>()
|
||||||
|
|
||||||
|
fun givenActiveProviders(providers: List<String>) {
|
||||||
|
every { instance.allProviders } returns providers
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenRequestUpdatesForProvider(
|
||||||
|
provider: String,
|
||||||
|
listener: LocationListener
|
||||||
|
) {
|
||||||
|
every { instance.requestLocationUpdates(provider, any<Long>(), any(), listener) } just runs
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenRemoveUpdates(listener: LocationListener) {
|
||||||
|
every { instance.removeUpdates(listener) } just runs
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenLastLocationForProvider(provider: String, location: Location?) {
|
||||||
|
every { instance.getLastKnownLocation(provider) } returns location
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user