Add LocationService interface

Combined Geofencing and LocationProvider
pull/1376/head
Alex Baker 4 years ago
parent 0e728152c9
commit 90bc28c91c

@ -16,8 +16,8 @@ import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class AndroidLocationProviderTest : InjectingTestCase() {
@Inject lateinit var provider: AndroidLocationProvider
class LocationServiceAndroidTest : InjectingTestCase() {
@Inject lateinit var service: LocationServiceAndroid
@Inject lateinit var locationManager: MockLocationManager
@Test
@ -25,7 +25,7 @@ class AndroidLocationProviderTest : InjectingTestCase() {
newLocation(NETWORK_PROVIDER, 45.0, 46.0, 50f, DateTime(2021, 2, 4, 13, 35, 45, 121))
newLocation(GPS_PROVIDER, 45.1, 46.1, 30f, DateTime(2021, 2, 4, 13, 33, 45, 121))
assertEquals(MapPosition(45.1, 46.1), provider.currentLocation())
assertEquals(MapPosition(45.1, 46.1), service.currentLocation())
}
@Test
@ -33,7 +33,7 @@ class AndroidLocationProviderTest : InjectingTestCase() {
newLocation(GPS_PROVIDER, 45.1, 46.1, 30f, DateTime(2021, 2, 4, 13, 33, 44, 121))
newLocation(NETWORK_PROVIDER, 45.0, 46.0, 50f, DateTime(2021, 2, 4, 13, 35, 45, 121))
assertEquals(MapPosition(45.0, 46.0), provider.currentLocation())
assertEquals(MapPosition(45.0, 46.0), service.currentLocation())
}
@Test
@ -41,23 +41,23 @@ class AndroidLocationProviderTest : InjectingTestCase() {
newLocation(GPS_PROVIDER, 45.1, 46.1, 50f, DateTime(2021, 2, 4, 13, 35, 45, 100))
newLocation(NETWORK_PROVIDER, 45.0, 46.0, 50f, DateTime(2021, 2, 4, 13, 35, 45, 121))
assertEquals(MapPosition(45.0, 46.0), provider.currentLocation())
assertEquals(MapPosition(45.0, 46.0), service.currentLocation())
}
@Test
fun returnCachedLocation() = runBlocking {
newLocation(GPS_PROVIDER, 45.1, 46.1, 50f, DateTime(2021, 2, 4, 13, 35, 45, 100))
provider.currentLocation()
service.currentLocation()
locationManager.clearLocations()
assertEquals(MapPosition(45.1, 46.1), provider.currentLocation())
assertEquals(MapPosition(45.1, 46.1), service.currentLocation())
}
@Test
fun nullWhenNoPosition() = runBlocking {
assertNull(provider.currentLocation())
assertNull(service.currentLocation())
}
private fun newLocation(

@ -1,5 +1,6 @@
package org.tasks.location
import android.app.PendingIntent
import android.location.Location
class MockLocationManager : LocationManager {
@ -13,4 +14,13 @@ class MockLocationManager : LocationManager {
override val lastKnownLocations: List<Location>
get() = mockLocations
override fun addProximityAlert(
latitude: Double,
longitude: Double,
radius: Float,
intent: PendingIntent
) {}
override fun removeProximityAlert(intent: PendingIntent) {}
}

@ -4,12 +4,12 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.tasks.location.AndroidGeofencing
import org.tasks.location.Geofencing
import org.tasks.location.LocationService
import org.tasks.location.LocationServiceAndroid
@Module
@InstallIn(SingletonComponent::class)
class FlavorModule {
@Provides
fun getGeofencing(geofencing: AndroidGeofencing): Geofencing = geofencing
fun getLocationService(service: LocationServiceAndroid): LocationService = service
}

@ -7,18 +7,12 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import org.tasks.location.AndroidLocationProvider
import org.tasks.location.LocationProvider
import org.tasks.location.MapFragment
import org.tasks.location.OsmMapFragment
@Module
@InstallIn(ActivityComponent::class)
class LocationModule {
@Provides
@ActivityScoped
fun getLocationProvider(provider: AndroidLocationProvider): LocationProvider = provider
@Provides
@ActivityScoped
fun getMapFragment(@ApplicationContext context: Context): MapFragment {

@ -4,12 +4,12 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.tasks.location.Geofencing
import org.tasks.location.GoogleGeofencing
import org.tasks.location.LocationService
import org.tasks.location.LocationServiceGooglePlay
@Module
@InstallIn(SingletonComponent::class)
class FlavorModule {
@Provides
fun getGeofencing(geofencing: GoogleGeofencing): Geofencing = geofencing
fun getLocationService(service: LocationServiceGooglePlay): LocationService = service
}

@ -8,17 +8,14 @@ import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import org.tasks.R
import org.tasks.location.*
import org.tasks.location.GoogleMapFragment
import org.tasks.location.MapFragment
import org.tasks.location.OsmMapFragment
import org.tasks.preferences.Preferences
@Module
@InstallIn(ActivityComponent::class)
internal class LocationModule {
@Provides
@ActivityScoped
fun getLocationProvider(@ApplicationContext context: Context): LocationProvider =
PlayLocationProvider(context)
@Provides
@ActivityScoped
fun getMapFragment(

@ -8,18 +8,34 @@ import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofencingRequest
import com.google.android.gms.location.LocationServices
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.data.MergedGeofence
import org.tasks.data.Place
import javax.inject.Inject
import kotlin.coroutines.suspendCoroutine
class GoogleGeofencing @Inject constructor(
class LocationServiceGooglePlay @Inject constructor(
@ApplicationContext private val context: Context
): Geofencing {
private val client = LocationServices.getGeofencingClient(context)
) : LocationService {
@SuppressLint("MissingPermission")
override suspend fun currentLocation(): MapPosition = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
LocationServices
.getFusedLocationProviderClient(context)
.lastLocation
.addOnSuccessListener {
cont.resumeWith(Result.success(MapPosition(it.latitude, it.longitude)))
}
.addOnFailureListener { cont.resumeWith(Result.failure(it)) }
}
}
@SuppressLint("MissingPermission")
override fun addGeofences(geofence: MergedGeofence) {
client.addGeofences(
LocationServices
.getGeofencingClient(context)
.addGeofences(
GeofencingRequest.Builder().addGeofence(toGoogleGeofence(geofence)).build(),
PendingIntent.getBroadcast(
context,
@ -29,7 +45,9 @@ class GoogleGeofencing @Inject constructor(
}
override fun removeGeofences(place: Place) {
client.removeGeofences(listOf(place.id.toString()))
LocationServices
.getGeofencingClient(context)
.removeGeofences(listOf(place.id.toString()))
}
private fun toGoogleGeofence(geofence: MergedGeofence): Geofence {

@ -1,21 +0,0 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.content.Context
import com.google.android.gms.location.LocationServices
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.suspendCoroutine
class PlayLocationProvider(private val context: Context) : LocationProvider {
@SuppressLint("MissingPermission")
override suspend fun currentLocation(): MapPosition = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
LocationServices.getFusedLocationProviderClient(context).lastLocation
.addOnSuccessListener {
cont.resumeWith(Result.success(MapPosition(it.latitude, it.longitude)))
}
.addOnFailureListener { cont.resumeWith(Result.failure(it)) }
}
}
}

@ -1,44 +0,0 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.location.LocationManager
import android.net.Uri
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.data.MergedGeofence
import org.tasks.data.Place
import javax.inject.Inject
@Suppress("unused")
class AndroidGeofencing @Inject constructor(
@ApplicationContext private val context: Context
): Geofencing {
private val client = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
@SuppressLint("MissingPermission")
override fun addGeofences(geofence: MergedGeofence) {
client.addProximityAlert(
geofence.latitude,
geofence.longitude,
geofence.radius.toFloat(),
-1,
createPendingIntent(geofence.place.id)
)
}
override fun removeGeofences(place: Place) {
client.removeProximityAlert(createPendingIntent(place.id))
}
private fun createPendingIntent(place: Long) =
PendingIntent.getBroadcast(
context,
0,
Intent(context, AndroidGeofenceTransitionIntentService.Broadcast::class.java)
.setData(Uri.parse("tasks://geofence/$place")),
PendingIntent.FLAG_UPDATE_CURRENT
)
}

@ -1,6 +1,7 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.location.Location
import dagger.hilt.android.qualifiers.ApplicationContext
@ -8,17 +9,28 @@ import timber.log.Timber
import javax.inject.Inject
class AndroidLocationManager @Inject constructor(
@ApplicationContext context: Context,
@ApplicationContext private val context: Context,
) : LocationManager {
private val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager
private val locationManager
get() = context.getSystemService(android.location.LocationManager::class.java)
override val lastKnownLocations: List<Location>
get() = locationManager.allProviders.mapNotNull {
locationManager.getLastKnownLocationOrNull(it)
}
@SuppressLint("MissingPermission")
override fun addProximityAlert(
latitude: Double,
longitude: Double,
radius: Float,
intent: PendingIntent
) = locationManager.addProximityAlert(latitude, longitude, radius, -1, intent)
override fun removeProximityAlert(intent: PendingIntent) =
locationManager.removeProximityAlert(intent)
companion object {
@SuppressLint("MissingPermission")
private fun android.location.LocationManager.getLastKnownLocationOrNull(provider: String) =

@ -9,7 +9,7 @@ import javax.inject.Inject
class GeofenceApi @Inject constructor(
private val permissionChecker: PermissionChecker,
private val locationDao: LocationDao,
private val client: Geofencing
private val locationService: LocationService
) {
suspend fun registerAll() = locationDao.getPlacesWithGeofences().forEach { update(it) }
@ -26,13 +26,13 @@ class GeofenceApi @Inject constructor(
locationDao
.getGeofencesByPlace(place.uid!!)?.let {
Timber.d("Adding geofence for %s", it)
client.addGeofences(it)
locationService.addGeofences(it)
}
?: cancel(place)
}
private fun cancel(place: Place?) = place?.let {
Timber.d("Removing geofence for %s", place)
client.removeGeofences(place)
locationService.removeGeofences(place)
}
}

@ -1,7 +1,17 @@
package org.tasks.location
import android.app.PendingIntent
import android.location.Location
interface LocationManager {
val lastKnownLocations: List<Location>
fun addProximityAlert(
latitude: Double,
longitude: Double,
radius: Float,
intent: PendingIntent
)
fun removeProximityAlert(intent: PendingIntent)
}

@ -94,7 +94,7 @@ class LocationPickerActivity : InjectingAppCompatActivity(), Toolbar.OnMenuItemC
@Inject lateinit var geocoder: Geocoder
@Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var locationProvider: LocationProvider
@Inject lateinit var locationService: LocationService
@Inject lateinit var firebase: Firebase
@Inject lateinit var preferences: Preferences
@ -265,7 +265,7 @@ class LocationPickerActivity : InjectingAppCompatActivity(), Toolbar.OnMenuItemC
}
lifecycleScope.launch {
try {
locationProvider.currentLocation()?.let { map.movePosition(it, animate) }
locationService.currentLocation()?.let { map.movePosition(it, animate) }
} catch (e: Exception) {
toaster.longToast(e.message)
}

@ -1,5 +0,0 @@
package org.tasks.location
interface LocationProvider {
suspend fun currentLocation(): MapPosition?
}

@ -3,7 +3,9 @@ package org.tasks.location
import org.tasks.data.MergedGeofence
import org.tasks.data.Place
interface Geofencing {
interface LocationService {
suspend fun currentLocation(): MapPosition?
fun addGeofences(geofence: MergedGeofence)
fun removeGeofences(place: Place)

@ -1,14 +1,23 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.location.Location
import android.net.Uri
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.data.MergedGeofence
import org.tasks.data.Place
import org.tasks.preferences.PermissionChecker
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class AndroidLocationProvider @Inject constructor(
class LocationServiceAndroid @Inject constructor(
@ApplicationContext private val context: Context,
private val locationManager: LocationManager,
private val permissionChecker: PermissionChecker,
) : LocationProvider {
) : LocationService {
private var cached: Location? = null
@ -27,6 +36,29 @@ class AndroidLocationProvider @Inject constructor(
null
}
@SuppressLint("MissingPermission")
override fun addGeofences(geofence: MergedGeofence) {
locationManager.addProximityAlert(
geofence.latitude,
geofence.longitude,
geofence.radius.toFloat(),
createPendingIntent(geofence.place.id)
)
}
override fun removeGeofences(place: Place) {
locationManager.removeProximityAlert(createPendingIntent(place.id))
}
private fun createPendingIntent(place: Long) =
PendingIntent.getBroadcast(
context,
0,
Intent(context, AndroidGeofenceTransitionIntentService.Broadcast::class.java)
.setData(Uri.parse("tasks://geofence/$place")),
PendingIntent.FLAG_UPDATE_CURRENT
)
companion object {
private val TWO_MINUTES = TimeUnit.MINUTES.toMillis(2)
Loading…
Cancel
Save