From 55cfa3a5b3b811cfc1259d1693cea981f191fa4d Mon Sep 17 00:00:00 2001 From: alejandrocalles Date: Fri, 31 May 2024 11:26:58 +0200 Subject: [PATCH 1/4] Add GPS Service. --- app/build.gradle.kts | 5 +- .../echo/connectivity/GPSServiceImplTest.kt | 30 ++++++++++ .../swent/echo/connectivity/GPSService.kt | 11 ++++ .../swent/echo/connectivity/GPSServiceImpl.kt | 55 +++++++++++++++++++ .../github/swent/echo/di/GPSServiceModule.kt | 22 ++++++++ gradle/libs.versions.toml | 2 + 6 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt create mode 100644 app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt create mode 100644 app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt create mode 100644 app/src/main/java/com/github/swent/echo/di/GPSServiceModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c4dc507c..b94946061 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -110,7 +110,10 @@ dependencies { // Map Libre implementation(libs.android.sdk) implementation(libs.ramani.maplibre) - + + // Location + implementation(libs.play.services.location) + // Hilt implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) diff --git a/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt b/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt new file mode 100644 index 000000000..7682ca153 --- /dev/null +++ b/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt @@ -0,0 +1,30 @@ +package com.github.swent.echo.connectivity + +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.mapbox.mapboxsdk.geometry.LatLng +import org.junit.Assert.assertNotNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class GPSServiceImplTest { + private lateinit var gpsService: GPSService + + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun shouldRefreshLocation() { + lateinit var location: State + composeTestRule.setContent { + gpsService = GPSServiceImpl(LocalContext.current) + location = gpsService.userLocation.collectAsState() + } + composeTestRule.waitForIdle() + assertNotNull(gpsService.userLocation) + } +} diff --git a/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt b/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt new file mode 100644 index 000000000..7df59fb6b --- /dev/null +++ b/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt @@ -0,0 +1,11 @@ +package com.github.swent.echo.connectivity + +import com.mapbox.mapboxsdk.geometry.LatLng +import kotlinx.coroutines.flow.StateFlow + +/** A GPS service. Allows the location to be accessed either as */ +interface GPSService { + val userLocation: StateFlow + + fun currentUserLocation(): LatLng? +} diff --git a/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt b/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt new file mode 100644 index 000000000..a1410f994 --- /dev/null +++ b/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt @@ -0,0 +1,55 @@ +package com.github.swent.echo.connectivity + +import android.content.Context +import android.content.pm.PackageManager +import android.location.LocationManager +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat.getSystemService +import com.github.swent.echo.compose.navigation.LOCATION_PERMISSIONS +import com.google.android.gms.location.LocationServices +import com.mapbox.mapboxsdk.geometry.LatLng +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class GPSServiceImpl(context: Context) : GPSService { + private val locationProviderClient = LocationServices.getFusedLocationProviderClient(context) + private val locationManager: LocationManager? = + getSystemService(context, LocationManager::class.java) + private var _location = MutableStateFlow(null) + + private fun getLocation(context: Context) { + if ( + LOCATION_PERMISSIONS.any { + ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED + } + ) { + locationProviderClient.lastLocation.addOnSuccessListener { + it?.apply { + val old = _location.value + _location.compareAndSet( + old, + this.let { location -> LatLng(location.latitude, location.longitude) } + ) + } + // Request again if null, unless location is turned off in settings + locationManager?.apply { + // TODO maybe also check for NETWORK_PROVIDER + if (isProviderEnabled(LocationManager.GPS_PROVIDER)) { + it ?: getLocation(context) + } + } + } + } + } + + init { + getLocation(context) + } + + override val userLocation: StateFlow = _location.asStateFlow() + + override fun currentUserLocation(): LatLng? = _location.value + + fun refreshUserLocation(context: Context) = getLocation(context) +} diff --git a/app/src/main/java/com/github/swent/echo/di/GPSServiceModule.kt b/app/src/main/java/com/github/swent/echo/di/GPSServiceModule.kt new file mode 100644 index 000000000..a998484b2 --- /dev/null +++ b/app/src/main/java/com/github/swent/echo/di/GPSServiceModule.kt @@ -0,0 +1,22 @@ +package com.github.swent.echo.di + +import android.app.Application +import com.github.swent.echo.connectivity.GPSService +import com.github.swent.echo.connectivity.GPSServiceImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object GPSServiceModule { + + @Singleton + @Provides + fun provideGPSService(application: Application): GPSService { + val context = application.applicationContext + return GPSServiceImpl(context) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d3e34d77..f27c24ea7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ androidSdk = "10.2.0" ##################### kotlinxSerializationJson = "1.6.3" ktorClientAndroid = "2.3.9" +playServicesLocation = "21.3.0" ramaniMaplibre = "0.3.0" robolectric = "4.12" roomRuntime = "2.6.1" @@ -47,6 +48,7 @@ hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", vers kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktorClientAndroid" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } +play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } ramani-maplibre = { module = "org.ramani-maps:ramani-maplibre", version.ref = "ramaniMaplibre" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4jSimple" } From 625ae733c847a251bee2949feb440934d936ed13 Mon Sep 17 00:00:00 2001 From: alejandrocalles Date: Sat, 1 Jun 2024 19:53:13 +0200 Subject: [PATCH 2/4] Write tests, add refactor internal functions. --- .../swent/echo/connectivity/GPSServiceImplTest.kt | 13 +++++++++++-- .../github/swent/echo/connectivity/GPSService.kt | 2 ++ .../swent/echo/connectivity/GPSServiceImpl.kt | 10 +++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt b/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt index 7682ca153..34399f808 100644 --- a/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt +++ b/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt @@ -6,6 +6,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.mapbox.mapboxsdk.geometry.LatLng +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertNotNull import org.junit.Rule import org.junit.Test @@ -13,6 +17,11 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class GPSServiceImplTest { + + companion object { + const val LOCATION_DELAY_MILLIS = 500 + } + private lateinit var gpsService: GPSService @get:Rule val composeTestRule = createComposeRule() @@ -24,7 +33,7 @@ class GPSServiceImplTest { gpsService = GPSServiceImpl(LocalContext.current) location = gpsService.userLocation.collectAsState() } - composeTestRule.waitForIdle() - assertNotNull(gpsService.userLocation) + runBlocking { delay(LOCATION_DELAY_MILLIS.toDuration(DurationUnit.MILLISECONDS)) } + assertNotNull(location.value) } } diff --git a/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt b/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt index 7df59fb6b..c99b277f0 100644 --- a/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt +++ b/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt @@ -8,4 +8,6 @@ interface GPSService { val userLocation: StateFlow fun currentUserLocation(): LatLng? + + fun refreshUserLocation() } diff --git a/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt b/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt index a1410f994..439ea93fe 100644 --- a/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt +++ b/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -class GPSServiceImpl(context: Context) : GPSService { +class GPSServiceImpl(val context: Context) : GPSService { private val locationProviderClient = LocationServices.getFusedLocationProviderClient(context) private val locationManager: LocationManager? = getSystemService(context, LocationManager::class.java) private var _location = MutableStateFlow(null) - private fun getLocation(context: Context) { + private fun getLocation() { if ( LOCATION_PERMISSIONS.any { ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED @@ -36,7 +36,7 @@ class GPSServiceImpl(context: Context) : GPSService { locationManager?.apply { // TODO maybe also check for NETWORK_PROVIDER if (isProviderEnabled(LocationManager.GPS_PROVIDER)) { - it ?: getLocation(context) + it ?: getLocation() } } } @@ -44,12 +44,12 @@ class GPSServiceImpl(context: Context) : GPSService { } init { - getLocation(context) + getLocation() } override val userLocation: StateFlow = _location.asStateFlow() override fun currentUserLocation(): LatLng? = _location.value - fun refreshUserLocation(context: Context) = getLocation(context) + override fun refreshUserLocation() = getLocation() } From 3afe5d3daeb20f87b0ae091fa3be4cd47c2536ec Mon Sep 17 00:00:00 2001 From: alejandrocalles Date: Sat, 1 Jun 2024 20:11:36 +0200 Subject: [PATCH 3/4] Add documentation. --- .../github/swent/echo/connectivity/GPSService.kt | 6 +++++- .../swent/echo/connectivity/GPSServiceImpl.kt | 14 +++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt b/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt index c99b277f0..9cb1bc169 100644 --- a/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt +++ b/app/src/main/java/com/github/swent/echo/connectivity/GPSService.kt @@ -3,11 +3,15 @@ package com.github.swent.echo.connectivity import com.mapbox.mapboxsdk.geometry.LatLng import kotlinx.coroutines.flow.StateFlow -/** A GPS service. Allows the location to be accessed either as */ +/** A service that provides the user's current location if it's accessible. */ interface GPSService { + + /** The user location as a state flow. */ val userLocation: StateFlow + /** The current user location. */ fun currentUserLocation(): LatLng? + /** Force the service to refresh the user location */ fun refreshUserLocation() } diff --git a/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt b/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt index 439ea93fe..8e6c77b96 100644 --- a/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt +++ b/app/src/main/java/com/github/swent/echo/connectivity/GPSServiceImpl.kt @@ -12,12 +12,22 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +/** + * A simple implementation of a [GPSService] + * + * @param context The context of the application that's using this service. + */ class GPSServiceImpl(val context: Context) : GPSService { private val locationProviderClient = LocationServices.getFusedLocationProviderClient(context) + + // This manager allows to check whether the location is disabled in settings. private val locationManager: LocationManager? = getSystemService(context, LocationManager::class.java) + + // User's last known location private var _location = MutableStateFlow(null) + /** Updates the last known location. */ private fun getLocation() { if ( LOCATION_PERMISSIONS.any { @@ -32,7 +42,9 @@ class GPSServiceImpl(val context: Context) : GPSService { this.let { location -> LatLng(location.latitude, location.longitude) } ) } - // Request again if null, unless location is turned off in settings + // Request again if null, unless location is turned off in settings. + // Even if this process has permissions to access the location, as + // long as it's disabled in settings, the listener will return null. locationManager?.apply { // TODO maybe also check for NETWORK_PROVIDER if (isProviderEnabled(LocationManager.GPS_PROVIDER)) { From 42967bd138728b817a8aea6f7e40f37cc164d8e5 Mon Sep 17 00:00:00 2001 From: alejandrocalles Date: Sun, 2 Jun 2024 01:24:06 +0200 Subject: [PATCH 4/4] Increase delay in tests to correctly get the location. --- .../com/github/swent/echo/connectivity/GPSServiceImplTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt b/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt index 34399f808..52f284319 100644 --- a/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt +++ b/app/src/androidTest/java/com/github/swent/echo/connectivity/GPSServiceImplTest.kt @@ -19,7 +19,7 @@ import org.junit.runner.RunWith class GPSServiceImplTest { companion object { - const val LOCATION_DELAY_MILLIS = 500 + const val LOCATION_DELAY_MILLIS = 3000 } private lateinit var gpsService: GPSService