Add LocationService interface

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

@ -16,8 +16,8 @@ import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class AndroidLocationProviderTest : InjectingTestCase() { class LocationServiceAndroidTest : InjectingTestCase() {
@Inject lateinit var provider: AndroidLocationProvider @Inject lateinit var service: LocationServiceAndroid
@Inject lateinit var locationManager: MockLocationManager @Inject lateinit var locationManager: MockLocationManager
@Test @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(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)) 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 @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(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)) 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 @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(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)) 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 @Test
fun returnCachedLocation() = runBlocking { fun returnCachedLocation() = runBlocking {
newLocation(GPS_PROVIDER, 45.1, 46.1, 50f, DateTime(2021, 2, 4, 13, 35, 45, 100)) newLocation(GPS_PROVIDER, 45.1, 46.1, 50f, DateTime(2021, 2, 4, 13, 35, 45, 100))
provider.currentLocation() service.currentLocation()
locationManager.clearLocations() locationManager.clearLocations()
assertEquals(MapPosition(45.1, 46.1), provider.currentLocation()) assertEquals(MapPosition(45.1, 46.1), service.currentLocation())
} }
@Test @Test
fun nullWhenNoPosition() = runBlocking { fun nullWhenNoPosition() = runBlocking {
assertNull(provider.currentLocation()) assertNull(service.currentLocation())
} }
private fun newLocation( private fun newLocation(

@ -1,5 +1,6 @@
package org.tasks.location package org.tasks.location
import android.app.PendingIntent
import android.location.Location import android.location.Location
class MockLocationManager : LocationManager { class MockLocationManager : LocationManager {
@ -13,4 +14,13 @@ class MockLocationManager : LocationManager {
override val lastKnownLocations: List<Location> override val lastKnownLocations: List<Location>
get() = mockLocations 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.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.tasks.location.AndroidGeofencing import org.tasks.location.LocationService
import org.tasks.location.Geofencing import org.tasks.location.LocationServiceAndroid
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class FlavorModule { class FlavorModule {
@Provides @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.components.ActivityComponent
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped 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.MapFragment
import org.tasks.location.OsmMapFragment import org.tasks.location.OsmMapFragment
@Module @Module
@InstallIn(ActivityComponent::class) @InstallIn(ActivityComponent::class)
class LocationModule { class LocationModule {
@Provides
@ActivityScoped
fun getLocationProvider(provider: AndroidLocationProvider): LocationProvider = provider
@Provides @Provides
@ActivityScoped @ActivityScoped
fun getMapFragment(@ApplicationContext context: Context): MapFragment { fun getMapFragment(@ApplicationContext context: Context): MapFragment {

@ -4,12 +4,12 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.tasks.location.Geofencing import org.tasks.location.LocationService
import org.tasks.location.GoogleGeofencing import org.tasks.location.LocationServiceGooglePlay
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class FlavorModule { class FlavorModule {
@Provides @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.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.android.scopes.ActivityScoped
import org.tasks.R 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 import org.tasks.preferences.Preferences
@Module @Module
@InstallIn(ActivityComponent::class) @InstallIn(ActivityComponent::class)
internal class LocationModule { internal class LocationModule {
@Provides
@ActivityScoped
fun getLocationProvider(@ApplicationContext context: Context): LocationProvider =
PlayLocationProvider(context)
@Provides @Provides
@ActivityScoped @ActivityScoped
fun getMapFragment( 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.GeofencingRequest
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.data.MergedGeofence import org.tasks.data.MergedGeofence
import org.tasks.data.Place import org.tasks.data.Place
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.suspendCoroutine
class GoogleGeofencing @Inject constructor( class LocationServiceGooglePlay @Inject constructor(
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
): Geofencing { ) : LocationService {
private val client = LocationServices.getGeofencingClient(context) @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") @SuppressLint("MissingPermission")
override fun addGeofences(geofence: MergedGeofence) { override fun addGeofences(geofence: MergedGeofence) {
client.addGeofences( LocationServices
.getGeofencingClient(context)
.addGeofences(
GeofencingRequest.Builder().addGeofence(toGoogleGeofence(geofence)).build(), GeofencingRequest.Builder().addGeofence(toGoogleGeofence(geofence)).build(),
PendingIntent.getBroadcast( PendingIntent.getBroadcast(
context, context,
@ -29,7 +45,9 @@ class GoogleGeofencing @Inject constructor(
} }
override fun removeGeofences(place: Place) { 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 { 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 package org.tasks.location
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -8,17 +9,28 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class AndroidLocationManager @Inject constructor( class AndroidLocationManager @Inject constructor(
@ApplicationContext context: Context, @ApplicationContext private val context: Context,
) : LocationManager { ) : LocationManager {
private val locationManager = private val locationManager
context.getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager get() = context.getSystemService(android.location.LocationManager::class.java)
override val lastKnownLocations: List<Location> override val lastKnownLocations: List<Location>
get() = locationManager.allProviders.mapNotNull { get() = locationManager.allProviders.mapNotNull {
locationManager.getLastKnownLocationOrNull(it) 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 { companion object {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private fun android.location.LocationManager.getLastKnownLocationOrNull(provider: String) = private fun android.location.LocationManager.getLastKnownLocationOrNull(provider: String) =

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

@ -1,7 +1,17 @@
package org.tasks.location package org.tasks.location
import android.app.PendingIntent
import android.location.Location import android.location.Location
interface LocationManager { interface LocationManager {
val lastKnownLocations: List<Location> 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 geocoder: Geocoder
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var locationProvider: LocationProvider @Inject lateinit var locationService: LocationService
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@ -265,7 +265,7 @@ class LocationPickerActivity : InjectingAppCompatActivity(), Toolbar.OnMenuItemC
} }
lifecycleScope.launch { lifecycleScope.launch {
try { try {
locationProvider.currentLocation()?.let { map.movePosition(it, animate) } locationService.currentLocation()?.let { map.movePosition(it, animate) }
} catch (e: Exception) { } catch (e: Exception) {
toaster.longToast(e.message) 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.MergedGeofence
import org.tasks.data.Place import org.tasks.data.Place
interface Geofencing { interface LocationService {
suspend fun currentLocation(): MapPosition?
fun addGeofences(geofence: MergedGeofence) fun addGeofences(geofence: MergedGeofence)
fun removeGeofences(place: Place) fun removeGeofences(place: Place)

@ -1,14 +1,23 @@
package org.tasks.location 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.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 org.tasks.preferences.PermissionChecker
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
class AndroidLocationProvider @Inject constructor( class LocationServiceAndroid @Inject constructor(
@ApplicationContext private val context: Context,
private val locationManager: LocationManager, private val locationManager: LocationManager,
private val permissionChecker: PermissionChecker, private val permissionChecker: PermissionChecker,
) : LocationProvider { ) : LocationService {
private var cached: Location? = null private var cached: Location? = null
@ -27,6 +36,29 @@ class AndroidLocationProvider @Inject constructor(
null 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 { companion object {
private val TWO_MINUTES = TimeUnit.MINUTES.toMillis(2) private val TWO_MINUTES = TimeUnit.MINUTES.toMillis(2)
Loading…
Cancel
Save