Add AndroidLocationProvider

pull/1369/head
Alex Baker 4 years ago
parent a6ff285a43
commit beeb6b0250

@ -207,7 +207,6 @@ dependencies {
implementation("com.github.QuadFlask:colorpicker:0.0.15") implementation("com.github.QuadFlask:colorpicker:0.0.15")
implementation("com.github.openid:AppAuth-Android:0.8.0") implementation("com.github.openid:AppAuth-Android:0.8.0")
genericImplementation("com.mapbox.mapboxsdk:mapbox-android-core:3.1.1")
genericImplementation("org.osmdroid:osmdroid-android:6.1.10@aar") genericImplementation("org.osmdroid:osmdroid-android:6.1.10@aar")
googleplayImplementation("com.google.firebase:firebase-crashlytics:${Versions.crashlytics}") googleplayImplementation("com.google.firebase:firebase-crashlytics:${Versions.crashlytics}")

@ -6,18 +6,20 @@ import com.todoroo.astrid.dao.Database
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.tasks.TestUtilities import org.tasks.TestUtilities
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.location.LocationManager
import org.tasks.location.MockLocationManager
import org.tasks.preferences.PermissionChecker import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.PermissivePermissionChecker import org.tasks.preferences.PermissivePermissionChecker
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(ApplicationComponent::class) @InstallIn(SingletonComponent::class)
class TestModule { class TestModule {
@Provides @Provides
@Singleton @Singleton
@ -37,6 +39,13 @@ class TestModule {
return TestUtilities.newPreferences(context) return TestUtilities.newPreferences(context)
} }
@Provides
@Singleton
fun getMockLocationManager(): MockLocationManager = MockLocationManager()
@Provides
fun getLocationManager(locationManager: MockLocationManager): LocationManager = locationManager
@Provides @Provides
fun getWorkManager(): WorkManager = mock(WorkManager::class.java) fun getWorkManager(): WorkManager = mock(WorkManager::class.java)
} }

@ -0,0 +1,16 @@
package org.tasks.location
import android.location.Location
class MockLocationManager : LocationManager {
private val mockLocations = ArrayList<Location>()
fun addLocations(vararg locations: Location) {
mockLocations.addAll(locations)
}
fun clearLocations() = mockLocations.clear()
override val lastKnownLocations: List<Location>
get() = mockLocations
}

@ -0,0 +1,76 @@
package org.tasks.location
import android.location.Location
import android.location.LocationManager.GPS_PROVIDER
import android.location.LocationManager.NETWORK_PROVIDER
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTime
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class AndroidLocationProviderTest : InjectingTestCase() {
@Inject lateinit var provider: AndroidLocationProvider
@Inject lateinit var locationManager: MockLocationManager
@Test
fun sortByAccuracy() = runBlocking {
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())
}
@Test
fun sortWithStaleLocation() = runBlocking {
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())
}
@Test
fun useNewerUpdateWhenAccuracySame() = runBlocking {
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())
}
@Test
fun returnCachedLocation() = runBlocking {
newLocation(GPS_PROVIDER, 45.1, 46.1, 50f, DateTime(2021, 2, 4, 13, 35, 45, 100))
provider.currentLocation()
locationManager.clearLocations()
assertEquals(MapPosition(45.1, 46.1), provider.currentLocation())
}
@Test
fun nullWhenNoPosition() = runBlocking {
assertNull(provider.currentLocation())
}
private fun newLocation(
provider: String,
latitude: Double,
longitude: Double,
accuracy: Float,
time: DateTime) {
locationManager.addLocations(Location(provider).apply {
this.latitude = latitude
this.longitude = longitude
this.accuracy = accuracy
this.time = time.millis
})
}
}

@ -22,9 +22,7 @@ class LocationModule {
@Provides @Provides
@ActivityScoped @ActivityScoped
fun getLocationProvider(@ApplicationContext context: Context): LocationProvider { fun getLocationProvider(provider: AndroidLocationProvider): LocationProvider = provider
return MapboxLocationProvider(context)
}
@Provides @Provides
@ActivityScoped @ActivityScoped

@ -0,0 +1,47 @@
package org.tasks.location
import android.location.Location
import org.tasks.preferences.PermissionChecker
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class AndroidLocationProvider @Inject constructor(
internal val locationManager: LocationManager,
private val permissionChecker: PermissionChecker,
) : LocationProvider {
private var cached: Location? = null
override suspend fun currentLocation() = if (permissionChecker.canAccessForegroundLocation()) {
locationManager
.lastKnownLocations
.plus(cached)
.filterNotNull()
.sortedWith(COMPARATOR)
.firstOrNull()
?.let {
cached = it
MapPosition(it.latitude, it.longitude)
}
} else {
null
}
companion object {
private val TWO_MINUTES = TimeUnit.MINUTES.toMillis(2)
internal val COMPARATOR = Comparator<Location> { l1, l2 ->
val timeDelta = l1.time - l2.time
val accuracyDelta = l1.accuracy - l2.accuracy
when {
timeDelta > TWO_MINUTES -> -1
timeDelta < -TWO_MINUTES -> 1
accuracyDelta < 0 -> -1
accuracyDelta > 0 -> 1
timeDelta > 0 -> -1
timeDelta < 0 -> 1
else -> 0
}
}
}
}

@ -1,30 +0,0 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.content.Context
import com.mapbox.android.core.location.LocationEngineCallback
import com.mapbox.android.core.location.LocationEngineProvider
import com.mapbox.android.core.location.LocationEngineResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.suspendCoroutine
class MapboxLocationProvider(private val context: Context) : LocationProvider {
@SuppressLint("MissingPermission")
override suspend fun currentLocation(): MapPosition = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
LocationEngineProvider.getBestLocationEngine(context)
.getLastLocation(
object : LocationEngineCallback<LocationEngineResult> {
override fun onSuccess(result: LocationEngineResult) {
val location = result.lastLocation!!
cont.resumeWith(Result.success(MapPosition(location.latitude, location.longitude)))
}
override fun onFailure(exception: Exception) {
cont.resumeWith(Result.failure(exception))
}
})
}
}
}

@ -16,6 +16,8 @@ import org.tasks.data.OpenTaskDao
import org.tasks.db.Migrations import org.tasks.db.Migrations
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.jobs.WorkManagerImpl import org.tasks.jobs.WorkManagerImpl
import org.tasks.location.AndroidLocationManager
import org.tasks.location.LocationManager
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Singleton import javax.inject.Singleton
@ -36,6 +38,9 @@ internal class ProductionModule {
@Provides @Provides
fun getPreferences(@ApplicationContext context: Context): Preferences = Preferences(context) fun getPreferences(@ApplicationContext context: Context): Preferences = Preferences(context)
@Provides
fun locationManager(locationManager: AndroidLocationManager): LocationManager = locationManager
@Provides @Provides
@Singleton @Singleton
fun getWorkManager( fun getWorkManager(

@ -0,0 +1,32 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
import javax.inject.Inject
class AndroidLocationManager @Inject constructor(
@ApplicationContext context: Context,
) : LocationManager {
private val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager
override val lastKnownLocations: List<Location>
get() = locationManager.allProviders.mapNotNull {
locationManager.getLastKnownLocationOrNull(it)
}
companion object {
@SuppressLint("MissingPermission")
private fun android.location.LocationManager.getLastKnownLocationOrNull(provider: String) =
try {
getLastKnownLocation(provider)
} catch (e: Exception) {
Timber.e(e)
null
}
}
}

@ -0,0 +1,7 @@
package org.tasks.location
import android.location.Location
interface LocationManager {
val lastKnownLocations: List<Location>
}

@ -254,7 +254,7 @@ class LocationPickerActivity : InjectingAppCompatActivity(), Toolbar.OnMenuItemC
} }
lifecycleScope.launch { lifecycleScope.launch {
try { try {
map.movePosition(locationProvider.currentLocation(), animate) locationProvider.currentLocation()?.let { map.movePosition(it, animate) }
} catch (e: Exception) { } catch (e: Exception) {
toaster.longToast(e.message) toaster.longToast(e.message)
} }

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

@ -15,7 +15,6 @@
++--- androidx.databinding:databinding-adapters:4.1.2 ++--- androidx.databinding:databinding-adapters:4.1.2
+| +--- androidx.databinding:databinding-common:4.1.2 +| +--- androidx.databinding:databinding-common:4.1.2
+| \--- androidx.databinding:databinding-runtime:4.1.2 (*) +| \--- androidx.databinding:databinding-runtime:4.1.2 (*)
++--- com.mapbox.mapboxsdk:mapbox-android-core:3.1.1
++--- org.osmdroid:osmdroid-android:6.1.10 ++--- org.osmdroid:osmdroid-android:6.1.10
++--- com.gitlab.bitfireAT:dav4jvm:2.1.1 ++--- com.gitlab.bitfireAT:dav4jvm:2.1.1
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.30 +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.30

Loading…
Cancel
Save