From 796d570e58b2f3ae7a280392a004283cfc4bf2f9 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 14:56:27 +0100 Subject: [PATCH 01/26] feat: add addressFromCoordinates function to LocationModel --- .../model/location/LocationModel.kt | 16 +++ .../model/location/LocationModelNominatim.kt | 130 +++++++++++++----- 2 files changed, 110 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt index d3514264c..260464678 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt @@ -19,4 +19,20 @@ interface LocationModel { * search. It takes an [Exception] as its parameter. */ fun search(query: String, onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit) + + /** + * Performs a "reverse location search". + * + * In other words, the function returns the closest address to the latitude and longitude of the + * [Location] parameter. + * + * @param gpsCoordinates The location object containing the latitude and longitude. + * @param onSuccess A callback function to handle the succesful retrieval of the address. + * @param onFailure A callback function to handle any errors or exceptions encountered during the search. + */ + fun addressFromCoordinates( + gpsCoordinates: Location, + onSuccess: (String) -> Unit, + onFailure: (Exception) -> Unit + ) } diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt index 04ec99b06..b51c5399f 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt @@ -1,7 +1,6 @@ package com.android.periodpals.model.location import android.util.Log -import java.io.IOException import okhttp3.Call import okhttp3.Callback import okhttp3.HttpUrl @@ -9,9 +8,17 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.json.JSONArray +import java.io.IOException private const val TAG = "LocationModelNominatim" +private const val SCHEME = "https" +private const val HOST = "nominatim.openstreetmap.org" +private const val FORMAT_NAME = "format" +private const val FORMAT_VAL = "json" +private const val HEADER_NAME = "User-Agent" +private const val HEADER_VALUE = "PeriodPals" + /** * A concrete implementation of the [LocationModel] interface that uses the Nominatim API from * OpenStreetMap to search for locations. @@ -55,52 +62,103 @@ class LocationModelNominatim(val client: OkHttpClient) : LocationModel { * its parameter. */ override fun search( - query: String, - onSuccess: (List) -> Unit, - onFailure: (Exception) -> Unit + query: String, + onSuccess: (List) -> Unit, + onFailure: (Exception) -> Unit, ) { // Using HttpUrl.Builder to properly construct the URL with query parameters. val url = - HttpUrl.Builder() - .scheme("https") - .host("nominatim.openstreetmap.org") - .addPathSegment("search") - .addQueryParameter("q", query) - .addQueryParameter("format", "json") - .build() + HttpUrl.Builder() + .scheme(SCHEME) + .host(HOST) + .addPathSegment("search") + .addQueryParameter("q", query) + .addQueryParameter(FORMAT_NAME, FORMAT_VAL) + .build() // Log the URL to Logcat for inspection Log.d(TAG, "Request URL: $url") // Create the request with a custom User-Agent and optional Referer - val request = Request.Builder().url(url).header("User-Agent", "PeriodPals").build() + val request = Request.Builder().url(url).header(HEADER_NAME, HEADER_VALUE).build() client - .newCall(request) - .enqueue( - object : Callback { - override fun onFailure(call: Call, e: IOException) { - Log.e(TAG, "Failed to execute request", e) - onFailure(e) + .newCall(request) + .enqueue( + object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "Failed to execute request", e) + onFailure(e) + } + + override fun onResponse(call: Call, response: Response) { + response.use { + if (!response.isSuccessful) { + onFailure(Exception("Unexpected code $response")) + Log.d(TAG, "Unexpected code $response") + return } - override fun onResponse(call: Call, response: Response) { - response.use { - if (!response.isSuccessful) { - onFailure(Exception("Unexpected code $response")) - Log.d(TAG, "Unexpected code $response") - return - } - - val body = response.body?.string() - if (body != null) { - onSuccess(parseBody(body)) - Log.d(TAG, "Body: $body") - } else { - Log.d(TAG, "Empty body") - onSuccess(emptyList()) - } - } + val body = response.body?.string() + if (body != null) { + onSuccess(parseBody(body)) + Log.d(TAG, "Body: $body") + } else { + Log.d(TAG, "Empty body") + onSuccess(emptyList()) + } + } + } + } + ) + } + + override fun addressFromCoordinates( + gpsCoordinates: Location, + onSuccess: (String) -> Unit, + onFailure: (Exception) -> Unit + ){ + val lat = gpsCoordinates.latitude.toString() + val lon = gpsCoordinates.longitude.toString() + + val url = + HttpUrl.Builder() + .scheme(SCHEME) + .host(HOST) + .addPathSegment("reverse") + .addQueryParameter("lat", lat) + .addQueryParameter("lon", lon) + .addQueryParameter(FORMAT_NAME, FORMAT_VAL) + .build() + val request = Request.Builder().url(url).header(HEADER_NAME, HEADER_VALUE).build() + Log.d(TAG, "Request url: $url") + + client + .newCall(request) + .enqueue( + object : Callback { + + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "Failed to execute request", e) + onFailure(e) + } + + override fun onResponse(call: Call, response: Response) { + response.use { + if(!response.isSuccessful){ + Log.e(TAG, "Unexpected code: $response") + onFailure( Exception("Unexpected code: $response")) + return } - }) + + val responseBody = response.body?.string() + responseBody?.let { + val jsonObject = org.json.JSONObject(it) + jsonObject.getString("display_name") + onSuccess(it) + } ?: onSuccess("Unknown address") + } + } + } + ) } } From 68c1641da7070286ec9fb60975178663dc15fa2f Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 15:10:39 +0100 Subject: [PATCH 02/26] refactor: update function name to reverseSearch --- .../android/periodpals/model/location/LocationModelNominatim.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt index b51c5399f..80185db0f 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt @@ -112,7 +112,7 @@ class LocationModelNominatim(val client: OkHttpClient) : LocationModel { ) } - override fun addressFromCoordinates( + override fun reverseSearch( gpsCoordinates: Location, onSuccess: (String) -> Unit, onFailure: (Exception) -> Unit From ed3070e101dffe78fa3b62ca3f02473b5a2e3da1 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 15:10:59 +0100 Subject: [PATCH 03/26] feat: add getAddressFromCoordinates function --- .../model/location/LocationViewModel.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index 3b0d15f0d..547a2eaa8 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -3,10 +3,14 @@ package com.android.periodpals.model.location import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import okhttp3.OkHttpClient +private const val TAG = "LocationViewModel" + /** * ViewModel responsible for managing and providing location data for UI components. * @@ -24,6 +28,9 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { private var _locationSuggestions = MutableStateFlow(emptyList()) val locationSuggestions: StateFlow> = _locationSuggestions + private val _address = MutableStateFlow("") + val address: StateFlow get() = _address + // create factory companion object { val Factory: ViewModelProvider.Factory = @@ -54,4 +61,19 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { { Log.d("SearchError", "Failed to fetch location suggestions for query: $query") }) } } + + fun getAddressFromCoordinates(location: Location) { + viewModelScope.launch { + val result = repository.reverseSearch( + gpsCoordinates = location, + onSuccess = { resultAddress -> + Log.d(TAG, "Successfully fetched address related to coordinates: (${location.latitude}, ${location.longitude}") + _address.value = resultAddress + }, + onFailure = { + Log.d(TAG, "Failed to fetch address related to the coordinates: (${location.latitude}, ${location.longitude})") + } + ) + } + } } From 989c5c56bf3e33f2a05e8ebba1102acce5de659f Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 15:11:19 +0100 Subject: [PATCH 04/26] feat: rename function name to reverseSearch --- .../java/com/android/periodpals/model/location/LocationModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt index 260464678..bb84c8e7e 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt @@ -30,7 +30,7 @@ interface LocationModel { * @param onSuccess A callback function to handle the succesful retrieval of the address. * @param onFailure A callback function to handle any errors or exceptions encountered during the search. */ - fun addressFromCoordinates( + fun reverseSearch( gpsCoordinates: Location, onSuccess: (String) -> Unit, onFailure: (Exception) -> Unit From 1622add87ef307f11a071f6a934896c44bfacc57 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 15:12:15 +0100 Subject: [PATCH 05/26] fix: make _locationSuggestions read-only --- .../com/android/periodpals/model/location/LocationViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index 547a2eaa8..1fe5d80b9 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -26,7 +26,7 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { val query: StateFlow = _query private var _locationSuggestions = MutableStateFlow(emptyList()) - val locationSuggestions: StateFlow> = _locationSuggestions + val locationSuggestions: StateFlow> get() = _locationSuggestions private val _address = MutableStateFlow("") val address: StateFlow get() = _address From 39188255c0555a6f420defda24226e067354bad2 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 15:12:38 +0100 Subject: [PATCH 06/26] feat: add TAG for debugging --- .../android/periodpals/model/location/LocationViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index 1fe5d80b9..d66731ce2 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -56,9 +56,9 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { query, { _locationSuggestions.value = it - Log.d("SearchSuccess", "Successfully fetched location suggestions for query: $query") + Log.d(TAG, "Successfully fetched location suggestions for query: $query") }, - { Log.d("SearchError", "Failed to fetch location suggestions for query: $query") }) + { Log.d(TAG, "Failed to fetch location suggestions for query: $query") }) } } From 2ec06c0756293fae84bd7b3a0f0aa47f671dce5d Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 16:52:42 +0100 Subject: [PATCH 07/26] feat: add doc for getAddressFromCoordinates --- .../android/periodpals/model/location/LocationViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index d66731ce2..7893a0551 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -62,6 +62,11 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { } } + /** + * Finds the address closest to the latitude and longitude of the [location]. + * + * @param location Location to find the address closest to. + */ fun getAddressFromCoordinates(location: Location) { viewModelScope.launch { val result = repository.reverseSearch( From b23443323853ca0bf37b0a65961c4320545c07c2 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 19:09:20 +0100 Subject: [PATCH 08/26] fix: correct onSuccess call in reverseSearch method --- .../periodpals/model/location/LocationModelNominatim.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt index 80185db0f..65d76af62 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt @@ -129,6 +129,7 @@ class LocationModelNominatim(val client: OkHttpClient) : LocationModel { .addQueryParameter("lon", lon) .addQueryParameter(FORMAT_NAME, FORMAT_VAL) .build() + val request = Request.Builder().url(url).header(HEADER_NAME, HEADER_VALUE).build() Log.d(TAG, "Request url: $url") @@ -153,8 +154,8 @@ class LocationModelNominatim(val client: OkHttpClient) : LocationModel { val responseBody = response.body?.string() responseBody?.let { val jsonObject = org.json.JSONObject(it) - jsonObject.getString("display_name") - onSuccess(it) + val displayName = jsonObject.getString("display_name") + onSuccess(displayName) } ?: onSuccess("Unknown address") } } From c28e4616ec7aaabc29da31e08ae9f60e83e635ee Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 19:10:02 +0100 Subject: [PATCH 09/26] fix: remove unnecessary viewModelScope.launch --- .../model/location/LocationViewModel.kt | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index 7893a0551..bdc215c1d 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -68,17 +68,15 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { * @param location Location to find the address closest to. */ fun getAddressFromCoordinates(location: Location) { - viewModelScope.launch { - val result = repository.reverseSearch( - gpsCoordinates = location, - onSuccess = { resultAddress -> - Log.d(TAG, "Successfully fetched address related to coordinates: (${location.latitude}, ${location.longitude}") - _address.value = resultAddress - }, - onFailure = { - Log.d(TAG, "Failed to fetch address related to the coordinates: (${location.latitude}, ${location.longitude})") - } - ) - } + repository.reverseSearch( + gpsCoordinates = location, + onSuccess = { resultAddress -> + Log.d(TAG, "Successfully fetched address related to coordinates: (${location.latitude}, ${location.longitude}") + _address.value = resultAddress + }, + onFailure = { + Log.d(TAG, "Failed to fetch address related to the coordinates: (${location.latitude}, ${location.longitude})") + } + ) } } From fd6ee58b0df9a755d64be0916240976222eb774c Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 19:10:19 +0100 Subject: [PATCH 10/26] feat: add tests for reverseSearch --- .../NominatimLocationRepositoryTest.kt | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt b/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt index edef889ba..68e480b19 100644 --- a/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt +++ b/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt @@ -103,4 +103,82 @@ class LocationModelNominatimTest { onSuccess = { assert(false) { "Expected failure, but got success" } }, onFailure = { exception -> assert(exception.message?.contains("Server Error") == true) }) } + + @Test + fun reverse_search_successful_response() { + val jsonResponse = + """ + { + "place_id": 83566034, + "lat": "46.5098584", + "lon": "6.4857429", + "display_name": "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland" + } + """.trimIndent() + + // Create a basic Request object + val mockRequest = Request.Builder().url("https://mockurl.com").build() + + val response = + Response.Builder() + .request(mockRequest) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(jsonResponse.toResponseBody("application/json".toMediaType())) + .build() + + whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) + doAnswer { invocation -> + val callback = invocation.getArgument(0) + callback.onResponse(mockCall, response) + } + .whenever(mockCall) + .enqueue(any()) + + locationRepository.reverseSearch( + Location( + latitude = 46.509858, + longitude = 6.485742, + name = "some place" + ), + onSuccess = {address -> + assert(address.isNotEmpty()) + assert(address == "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland") + }, + onFailure = { assert(false) { "Expected success, but got failure" } } + ) + } + + @Test + fun reverse_search_unsuccessful_response() { + val mockRequest = Request.Builder().url("https://mockurl.com").build() + + val response = + Response.Builder() + .request(mockRequest) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(500) + .message("Server Error") + .body("Internal Server Error".toResponseBody("text/plain".toMediaType())) + .build() + + whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) + doAnswer { invocation -> + val callback = invocation.getArgument(0) + callback.onResponse(mockCall, response) + } + .whenever(mockCall) + .enqueue(any()) + + locationRepository.reverseSearch( + Location( + latitude = 0.0, + longitude = 0.0, + name = "some place" + ), + onSuccess = { assert(false) { "Expected failure, but got success" } }, + onFailure = { exception -> assert(exception.message?.contains("Server Error") == true) } + ) + } } From f253857dfdf458125ddb41fb87df16d9a1cfb3d2 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 19:10:41 +0100 Subject: [PATCH 11/26] feat: add test for getAddressFromCoordinates --- .../model/location/LocationViewModelTest.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt index 030efa0a1..0915ea28f 100644 --- a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt +++ b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt @@ -26,6 +26,16 @@ class LocationViewModelTest { val testQuery = "EPFL" private val mockLocations = listOf(Location(46.5197, 6.5662, "EPFL")) + private val mockAddressName = + "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland" + + private val mockLocation = + Location( + latitude = 46.509858, + longitude = 6.485742, + name = "some place" + ) + private val testDispatcher = StandardTestDispatcher() @Before @@ -46,10 +56,18 @@ class LocationViewModelTest { verify(locationRepository).search(eq(testQuery), any(), any()) } + @Test + fun getAddressFromCoordinatesCallsRepository() = runTest { + locationViewModel.getAddressFromCoordinates(mockLocation) + verify(locationRepository) + .reverseSearch(eq(mockLocation), any<(String) -> Unit>(), any<(Exception) -> Unit>()) + } + @Test fun initialStateIsCorrect() { assertThat(locationViewModel.query.value, `is`("")) assertThat(locationViewModel.locationSuggestions.value, `is`(emptyList())) + assertThat(locationViewModel.address.value, `is`("")) } @Test @@ -89,4 +107,36 @@ class LocationViewModelTest { assertThat(locationViewModel.locationSuggestions.value, `is`(emptyList())) } + + @Test + fun reverseSearchSuccessfulUpdatesAddress() = runTest { + // Simulate successful repository call + doAnswer { + val successCallback = it.getArgument<(String) -> Unit>(1) + successCallback(mockAddressName) + } + .whenever(locationRepository) + .reverseSearch(any(), any(), any()) + + locationViewModel.getAddressFromCoordinates(mockLocation) + testDispatcher.scheduler.advanceUntilIdle() // Ensure all coroutines complete + + assertThat(locationViewModel.address.value, `is`(mockAddressName)) + } + + @Test + fun reverseSearchFailureDoesNotCrash() = runTest { + // Simulate failure in the repository call + doAnswer { + val failureCallback = it.getArgument<(Exception) -> Unit>(2) + failureCallback(RuntimeException("Network error")) + } + .whenever(locationRepository) + .reverseSearch(any(), any(), any()) + + locationViewModel.getAddressFromCoordinates(mockLocation) + testDispatcher.scheduler.advanceUntilIdle() // Ensure all coroutines complete + + assertThat(locationViewModel.address.value, `is`("")) + } } From e842681a33ccb88465febb7ff2abb7f67235bcf3 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 20:27:40 +0100 Subject: [PATCH 12/26] feat: integrate reverse search in AlertComponents.kt --- .../periodpals/ui/components/AlertComponents.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt b/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt index a0fc31e52..05fc975d5 100644 --- a/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt +++ b/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt @@ -142,6 +142,14 @@ fun LocationField( var name by remember { mutableStateOf(location?.name ?: "") } val gpsLocation by gpsService.location.collectAsState() + LaunchedEffect(gpsLocation) { + locationViewModel + .getAddressFromCoordinates( + lat = gpsLocation.latitude, + lon = gpsLocation.longitude + ) + } + // State for dropdown visibility var showDropdown by remember { mutableStateOf(false) } @@ -189,7 +197,13 @@ fun LocationField( "Selected current location: ${gpsLocation.name} at (${gpsLocation.latitude}, ${gpsLocation.longitude})", ) name = Location.CURRENT_LOCATION_NAME - onLocationSelected(gpsLocation) + onLocationSelected( + Location( + latitude = gpsLocation.latitude, + longitude = gpsLocation.longitude, + name = locationViewModel.address.value + ) + ) showDropdown = false }, modifier = From 4703898d16f7fdcf984da87e86397c52a1a16e57 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 20:28:14 +0100 Subject: [PATCH 13/26] refactor: remove unused imports --- .../java/com/android/periodpals/services/GPSServiceImpl.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/services/GPSServiceImpl.kt b/app/src/main/java/com/android/periodpals/services/GPSServiceImpl.kt index 682e416ae..dae2780f3 100644 --- a/app/src/main/java/com/android/periodpals/services/GPSServiceImpl.kt +++ b/app/src/main/java/com/android/periodpals/services/GPSServiceImpl.kt @@ -9,7 +9,9 @@ import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityCompat +import androidx.lifecycle.viewmodel.compose.viewModel import com.android.periodpals.model.location.Location +import com.android.periodpals.model.location.LocationViewModel import com.android.periodpals.model.location.parseLocationGIS import com.android.periodpals.model.user.UserViewModel import com.google.android.gms.location.FusedLocationProviderClient @@ -244,13 +246,14 @@ class GPSServiceImpl( result.lastLocation?.let { location -> val lat = location.latitude val long = location.longitude + + _location.value = Location( lat, long, Location.CURRENT_LOCATION_NAME, - ) // TODO change CURRENT_LOCATION_NAME to actual - // location based on the coordinates + ) _accuracy.value = location.accuracy From 3358fad18e5fe6452aac972ed49302e3ea16f947 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 20:28:29 +0100 Subject: [PATCH 14/26] refactor: remove unused imports --- .../main/java/com/android/periodpals/model/location/Location.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/android/periodpals/model/location/Location.kt b/app/src/main/java/com/android/periodpals/model/location/Location.kt index 6ef27e299..aa0ec86ba 100644 --- a/app/src/main/java/com/android/periodpals/model/location/Location.kt +++ b/app/src/main/java/com/android/periodpals/model/location/Location.kt @@ -1,5 +1,6 @@ package com.android.periodpals.model.location +import android.util.Log import org.osmdroid.util.GeoPoint private const val PARTS_ERROR_MESSAGE = "Invalid format. Expected 'latitude,longitude,name'." From 15ca03c1bca22d966652c3c08745959ab1cf857a Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 20:29:20 +0100 Subject: [PATCH 15/26] refactor: change params in reverse search to use lat and lan instead of Location --- .../com/android/periodpals/model/location/LocationModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt index bb84c8e7e..b8969e668 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt @@ -26,12 +26,14 @@ interface LocationModel { * In other words, the function returns the closest address to the latitude and longitude of the * [Location] parameter. * - * @param gpsCoordinates The location object containing the latitude and longitude. + * @param lat The latitude of the location. + * @param lon The longitude of the location. * @param onSuccess A callback function to handle the succesful retrieval of the address. * @param onFailure A callback function to handle any errors or exceptions encountered during the search. */ fun reverseSearch( - gpsCoordinates: Location, + lat: Double, + lon: Double, onSuccess: (String) -> Unit, onFailure: (Exception) -> Unit ) From 40a7e5bd66925ec453037fb4ce6b0d3330d14e97 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 20:29:33 +0100 Subject: [PATCH 16/26] refactor: change params in reverse search to use lat and lan instead of Location --- .../periodpals/model/location/LocationModelNominatim.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt index 65d76af62..99cf11151 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt @@ -113,20 +113,19 @@ class LocationModelNominatim(val client: OkHttpClient) : LocationModel { } override fun reverseSearch( - gpsCoordinates: Location, + lat: Double, + lon: Double, onSuccess: (String) -> Unit, onFailure: (Exception) -> Unit ){ - val lat = gpsCoordinates.latitude.toString() - val lon = gpsCoordinates.longitude.toString() val url = HttpUrl.Builder() .scheme(SCHEME) .host(HOST) .addPathSegment("reverse") - .addQueryParameter("lat", lat) - .addQueryParameter("lon", lon) + .addQueryParameter("lat", lat.toString()) + .addQueryParameter("lon", lon.toString()) .addQueryParameter(FORMAT_NAME, FORMAT_VAL) .build() From 2d54573b7133c7d4383f82692082bc8c637bf005 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 20:29:46 +0100 Subject: [PATCH 17/26] refactor: change params in reverse search to use lat and lan instead of Location --- .../periodpals/model/location/LocationViewModel.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index bdc215c1d..c404a221e 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -3,10 +3,8 @@ package com.android.periodpals.model.location import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch import okhttp3.OkHttpClient private const val TAG = "LocationViewModel" @@ -65,17 +63,19 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { /** * Finds the address closest to the latitude and longitude of the [location]. * - * @param location Location to find the address closest to. + * @param lat + * @param lon */ - fun getAddressFromCoordinates(location: Location) { + fun getAddressFromCoordinates(lat: Double, lon: Double) { repository.reverseSearch( - gpsCoordinates = location, + lat = lat, + lon = lon, onSuccess = { resultAddress -> - Log.d(TAG, "Successfully fetched address related to coordinates: (${location.latitude}, ${location.longitude}") + Log.d(TAG, "Successfully fetched address related to coordinates: ($lat, $lon") _address.value = resultAddress }, onFailure = { - Log.d(TAG, "Failed to fetch address related to the coordinates: (${location.latitude}, ${location.longitude})") + Log.d(TAG, "Failed to fetch address related to the coordinates: ($lat, $lon)") } ) } From 0096e5b95c2587c44e869a04ed478dcdfd31bc3c Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Tue, 17 Dec 2024 20:29:52 +0100 Subject: [PATCH 18/26] refactor: change params in reverse search to use lat and lan instead of Location --- .../model/location/LocationViewModelTest.kt | 20 ++++++++----------- .../NominatimLocationRepositoryTest.kt | 14 ++++--------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt index 0915ea28f..e7fc57d09 100644 --- a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt +++ b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt @@ -29,12 +29,8 @@ class LocationViewModelTest { private val mockAddressName = "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland" - private val mockLocation = - Location( - latitude = 46.509858, - longitude = 6.485742, - name = "some place" - ) + private val mockLat = 46.509858 + private val mockLon = 6.485742 private val testDispatcher = StandardTestDispatcher() @@ -58,9 +54,9 @@ class LocationViewModelTest { @Test fun getAddressFromCoordinatesCallsRepository() = runTest { - locationViewModel.getAddressFromCoordinates(mockLocation) + locationViewModel.getAddressFromCoordinates(lat = mockLat, lon = mockLon) verify(locationRepository) - .reverseSearch(eq(mockLocation), any<(String) -> Unit>(), any<(Exception) -> Unit>()) + .reverseSearch(eq(mockLat), eq(mockLon), any<(String) -> Unit>(), any<(Exception) -> Unit>()) } @Test @@ -116,9 +112,9 @@ class LocationViewModelTest { successCallback(mockAddressName) } .whenever(locationRepository) - .reverseSearch(any(), any(), any()) + .reverseSearch(any(),any(), any(), any()) - locationViewModel.getAddressFromCoordinates(mockLocation) + locationViewModel.getAddressFromCoordinates(mockLat, mockLon) testDispatcher.scheduler.advanceUntilIdle() // Ensure all coroutines complete assertThat(locationViewModel.address.value, `is`(mockAddressName)) @@ -132,9 +128,9 @@ class LocationViewModelTest { failureCallback(RuntimeException("Network error")) } .whenever(locationRepository) - .reverseSearch(any(), any(), any()) + .reverseSearch(any(), any(), any(), any()) - locationViewModel.getAddressFromCoordinates(mockLocation) + locationViewModel.getAddressFromCoordinates(mockLat, mockLon) testDispatcher.scheduler.advanceUntilIdle() // Ensure all coroutines complete assertThat(locationViewModel.address.value, `is`("")) diff --git a/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt b/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt index 68e480b19..fece69ceb 100644 --- a/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt +++ b/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt @@ -137,11 +137,8 @@ class LocationModelNominatimTest { .enqueue(any()) locationRepository.reverseSearch( - Location( - latitude = 46.509858, - longitude = 6.485742, - name = "some place" - ), + lat = 46.509858, + lon = 6.485742, onSuccess = {address -> assert(address.isNotEmpty()) assert(address == "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland") @@ -172,11 +169,8 @@ class LocationModelNominatimTest { .enqueue(any()) locationRepository.reverseSearch( - Location( - latitude = 0.0, - longitude = 0.0, - name = "some place" - ), + lon = 0.0, + lat = 0.0, onSuccess = { assert(false) { "Expected failure, but got success" } }, onFailure = { exception -> assert(exception.message?.contains("Server Error") == true) } ) From ffd0780cd3f42ed28178b37e3e991682986ee4a5 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Wed, 18 Dec 2024 12:09:35 +0100 Subject: [PATCH 19/26] feat: update doc --- .../periodpals/model/location/LocationModel.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt index b8969e668..1baa8562d 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt @@ -23,13 +23,12 @@ interface LocationModel { /** * Performs a "reverse location search". * - * In other words, the function returns the closest address to the latitude and longitude of the - * [Location] parameter. + * In other words, the function returns the closest address to the specified latitude and longitude. * - * @param lat The latitude of the location. - * @param lon The longitude of the location. - * @param onSuccess A callback function to handle the succesful retrieval of the address. - * @param onFailure A callback function to handle any errors or exceptions encountered during the search. + * @param lat Latitude of the location. + * @param lon Longitude of the location. + * @param onSuccess Callback function to handle the succesful retrieval of the address. + * @param onFailure Callback function to handle any errors or exceptions encountered during the search. */ fun reverseSearch( lat: Double, From 69210b4fecb373223d4f893ed2348764afc76b3d Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Wed, 18 Dec 2024 12:12:27 +0100 Subject: [PATCH 20/26] feat: update doc --- .../android/periodpals/model/location/LocationViewModel.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index c404a221e..8eaff407f 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -61,10 +61,11 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { } /** - * Finds the address closest to the latitude and longitude of the [location]. + * Finds the address closest to the specified latitude and longitude and assigns it to the [address] + * state flow. * - * @param lat - * @param lon + * @param lat Latitude of the location. + * @param lon Longitude of the location. */ fun getAddressFromCoordinates(lat: Double, lon: Double) { repository.reverseSearch( From c05bd362ca8a97ca9237d3194ff013f527c3f4ae Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Wed, 18 Dec 2024 12:16:57 +0100 Subject: [PATCH 21/26] feat: resolve small issue --- .../android/periodpals/model/location/LocationViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt index e7fc57d09..ab125b2ca 100644 --- a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt +++ b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt @@ -23,7 +23,7 @@ class LocationViewModelTest { private lateinit var locationRepository: LocationModel private lateinit var locationViewModel: LocationViewModel - val testQuery = "EPFL" + private val testQuery = "EPFL" private val mockLocations = listOf(Location(46.5197, 6.5662, "EPFL")) private val mockAddressName = From 2004434b217cac880df4414baf72d64711f4ee64 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Wed, 18 Dec 2024 12:26:13 +0100 Subject: [PATCH 22/26] feat: reverse search works and is integrated to UI --- .../com/android/periodpals/ui/components/AlertComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt b/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt index 05fc975d5..f664d82f6 100644 --- a/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt +++ b/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt @@ -142,7 +142,7 @@ fun LocationField( var name by remember { mutableStateOf(location?.name ?: "") } val gpsLocation by gpsService.location.collectAsState() - LaunchedEffect(gpsLocation) { + LaunchedEffect(Unit) { locationViewModel .getAddressFromCoordinates( lat = gpsLocation.latitude, From 2423369379eb1fdfe06712ddad5b65d4ec7caed9 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Wed, 18 Dec 2024 12:34:26 +0100 Subject: [PATCH 23/26] style: ktfmt --- .../periodpals/model/location/Location.kt | 1 - .../model/location/LocationModel.kt | 14 +- .../model/location/LocationModelNominatim.kt | 146 +++++++++--------- .../model/location/LocationViewModel.kt | 29 ++-- .../periodpals/services/GPSServiceImpl.kt | 3 - .../ui/components/AlertComponents.kt | 17 +- .../model/location/LocationViewModelTest.kt | 25 +-- .../NominatimLocationRepositoryTest.kt | 79 +++++----- 8 files changed, 154 insertions(+), 160 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/Location.kt b/app/src/main/java/com/android/periodpals/model/location/Location.kt index aa0ec86ba..6ef27e299 100644 --- a/app/src/main/java/com/android/periodpals/model/location/Location.kt +++ b/app/src/main/java/com/android/periodpals/model/location/Location.kt @@ -1,6 +1,5 @@ package com.android.periodpals.model.location -import android.util.Log import org.osmdroid.util.GeoPoint private const val PARTS_ERROR_MESSAGE = "Invalid format. Expected 'latitude,longitude,name'." diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt index 1baa8562d..71161e8f3 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModel.kt @@ -23,17 +23,19 @@ interface LocationModel { /** * Performs a "reverse location search". * - * In other words, the function returns the closest address to the specified latitude and longitude. + * In other words, the function returns the closest address to the specified latitude and + * longitude. * * @param lat Latitude of the location. * @param lon Longitude of the location. * @param onSuccess Callback function to handle the succesful retrieval of the address. - * @param onFailure Callback function to handle any errors or exceptions encountered during the search. + * @param onFailure Callback function to handle any errors or exceptions encountered during the + * search. */ fun reverseSearch( - lat: Double, - lon: Double, - onSuccess: (String) -> Unit, - onFailure: (Exception) -> Unit + lat: Double, + lon: Double, + onSuccess: (String) -> Unit, + onFailure: (Exception) -> Unit ) } diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt index 99cf11151..577e695ad 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationModelNominatim.kt @@ -1,6 +1,7 @@ package com.android.periodpals.model.location import android.util.Log +import java.io.IOException import okhttp3.Call import okhttp3.Callback import okhttp3.HttpUrl @@ -8,7 +9,6 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.json.JSONArray -import java.io.IOException private const val TAG = "LocationModelNominatim" @@ -62,19 +62,19 @@ class LocationModelNominatim(val client: OkHttpClient) : LocationModel { * its parameter. */ override fun search( - query: String, - onSuccess: (List) -> Unit, - onFailure: (Exception) -> Unit, + query: String, + onSuccess: (List) -> Unit, + onFailure: (Exception) -> Unit, ) { // Using HttpUrl.Builder to properly construct the URL with query parameters. val url = - HttpUrl.Builder() - .scheme(SCHEME) - .host(HOST) - .addPathSegment("search") - .addQueryParameter("q", query) - .addQueryParameter(FORMAT_NAME, FORMAT_VAL) - .build() + HttpUrl.Builder() + .scheme(SCHEME) + .host(HOST) + .addPathSegment("search") + .addQueryParameter("q", query) + .addQueryParameter(FORMAT_NAME, FORMAT_VAL) + .build() // Log the URL to Logcat for inspection Log.d(TAG, "Request URL: $url") @@ -82,83 +82,81 @@ class LocationModelNominatim(val client: OkHttpClient) : LocationModel { // Create the request with a custom User-Agent and optional Referer val request = Request.Builder().url(url).header(HEADER_NAME, HEADER_VALUE).build() client - .newCall(request) - .enqueue( - object : Callback { - override fun onFailure(call: Call, e: IOException) { - Log.e(TAG, "Failed to execute request", e) - onFailure(e) - } - - override fun onResponse(call: Call, response: Response) { - response.use { - if (!response.isSuccessful) { - onFailure(Exception("Unexpected code $response")) - Log.d(TAG, "Unexpected code $response") - return + .newCall(request) + .enqueue( + object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "Failed to execute request", e) + onFailure(e) } - val body = response.body?.string() - if (body != null) { - onSuccess(parseBody(body)) - Log.d(TAG, "Body: $body") - } else { - Log.d(TAG, "Empty body") - onSuccess(emptyList()) + override fun onResponse(call: Call, response: Response) { + response.use { + if (!response.isSuccessful) { + onFailure(Exception("Unexpected code $response")) + Log.d(TAG, "Unexpected code $response") + return + } + + val body = response.body?.string() + if (body != null) { + onSuccess(parseBody(body)) + Log.d(TAG, "Body: $body") + } else { + Log.d(TAG, "Empty body") + onSuccess(emptyList()) + } + } } - } - } - } - ) + }) } override fun reverseSearch( - lat: Double, - lon: Double, - onSuccess: (String) -> Unit, - onFailure: (Exception) -> Unit - ){ + lat: Double, + lon: Double, + onSuccess: (String) -> Unit, + onFailure: (Exception) -> Unit + ) { val url = - HttpUrl.Builder() - .scheme(SCHEME) - .host(HOST) - .addPathSegment("reverse") - .addQueryParameter("lat", lat.toString()) - .addQueryParameter("lon", lon.toString()) - .addQueryParameter(FORMAT_NAME, FORMAT_VAL) - .build() + HttpUrl.Builder() + .scheme(SCHEME) + .host(HOST) + .addPathSegment("reverse") + .addQueryParameter("lat", lat.toString()) + .addQueryParameter("lon", lon.toString()) + .addQueryParameter(FORMAT_NAME, FORMAT_VAL) + .build() val request = Request.Builder().url(url).header(HEADER_NAME, HEADER_VALUE).build() Log.d(TAG, "Request url: $url") client - .newCall(request) - .enqueue( - object : Callback { - - override fun onFailure(call: Call, e: IOException) { - Log.e(TAG, "Failed to execute request", e) - onFailure(e) - } - - override fun onResponse(call: Call, response: Response) { - response.use { - if(!response.isSuccessful){ - Log.e(TAG, "Unexpected code: $response") - onFailure( Exception("Unexpected code: $response")) - return + .newCall(request) + .enqueue( + object : Callback { + + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "Failed to execute request", e) + onFailure(e) } - val responseBody = response.body?.string() - responseBody?.let { - val jsonObject = org.json.JSONObject(it) - val displayName = jsonObject.getString("display_name") - onSuccess(displayName) - } ?: onSuccess("Unknown address") - } - } - } - ) + override fun onResponse(call: Call, response: Response) { + response.use { + if (!response.isSuccessful) { + Log.e(TAG, "Unexpected code: $response") + onFailure(Exception("Unexpected code: $response")) + return + } + + val responseBody = response.body?.string() + responseBody?.let { + val jsonObject = org.json.JSONObject(it) + val displayName = jsonObject.getString("display_name") + onSuccess(displayName) + } ?: onSuccess("Unknown address") + } + } + }) } } diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index 8eaff407f..24cef4e98 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -24,10 +24,12 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { val query: StateFlow = _query private var _locationSuggestions = MutableStateFlow(emptyList()) - val locationSuggestions: StateFlow> get() = _locationSuggestions + val locationSuggestions: StateFlow> + get() = _locationSuggestions private val _address = MutableStateFlow("") - val address: StateFlow get() = _address + val address: StateFlow + get() = _address // create factory companion object { @@ -61,23 +63,22 @@ class LocationViewModel(val repository: LocationModel) : ViewModel() { } /** - * Finds the address closest to the specified latitude and longitude and assigns it to the [address] - * state flow. + * Finds the address closest to the specified latitude and longitude and assigns it to the + * [address] state flow. * * @param lat Latitude of the location. * @param lon Longitude of the location. */ fun getAddressFromCoordinates(lat: Double, lon: Double) { repository.reverseSearch( - lat = lat, - lon = lon, - onSuccess = { resultAddress -> - Log.d(TAG, "Successfully fetched address related to coordinates: ($lat, $lon") - _address.value = resultAddress - }, - onFailure = { - Log.d(TAG, "Failed to fetch address related to the coordinates: ($lat, $lon)") - } - ) + lat = lat, + lon = lon, + onSuccess = { resultAddress -> + Log.d(TAG, "Successfully fetched address related to coordinates: ($lat, $lon") + _address.value = resultAddress + }, + onFailure = { + Log.d(TAG, "Failed to fetch address related to the coordinates: ($lat, $lon)") + }) } } diff --git a/app/src/main/java/com/android/periodpals/services/GPSServiceImpl.kt b/app/src/main/java/com/android/periodpals/services/GPSServiceImpl.kt index dae2780f3..9cfb6ba78 100644 --- a/app/src/main/java/com/android/periodpals/services/GPSServiceImpl.kt +++ b/app/src/main/java/com/android/periodpals/services/GPSServiceImpl.kt @@ -9,9 +9,7 @@ import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityCompat -import androidx.lifecycle.viewmodel.compose.viewModel import com.android.periodpals.model.location.Location -import com.android.periodpals.model.location.LocationViewModel import com.android.periodpals.model.location.parseLocationGIS import com.android.periodpals.model.user.UserViewModel import com.google.android.gms.location.FusedLocationProviderClient @@ -247,7 +245,6 @@ class GPSServiceImpl( val lat = location.latitude val long = location.longitude - _location.value = Location( lat, diff --git a/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt b/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt index f664d82f6..0770e13ee 100644 --- a/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt +++ b/app/src/main/java/com/android/periodpals/ui/components/AlertComponents.kt @@ -143,11 +143,8 @@ fun LocationField( val gpsLocation by gpsService.location.collectAsState() LaunchedEffect(Unit) { - locationViewModel - .getAddressFromCoordinates( - lat = gpsLocation.latitude, - lon = gpsLocation.longitude - ) + locationViewModel.getAddressFromCoordinates( + lat = gpsLocation.latitude, lon = gpsLocation.longitude) } // State for dropdown visibility @@ -198,12 +195,10 @@ fun LocationField( ) name = Location.CURRENT_LOCATION_NAME onLocationSelected( - Location( - latitude = gpsLocation.latitude, - longitude = gpsLocation.longitude, - name = locationViewModel.address.value - ) - ) + Location( + latitude = gpsLocation.latitude, + longitude = gpsLocation.longitude, + name = locationViewModel.address.value)) showDropdown = false }, modifier = diff --git a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt index ab125b2ca..18a10969f 100644 --- a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt +++ b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt @@ -27,7 +27,7 @@ class LocationViewModelTest { private val mockLocations = listOf(Location(46.5197, 6.5662, "EPFL")) private val mockAddressName = - "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland" + "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland" private val mockLat = 46.509858 private val mockLon = 6.485742 @@ -56,7 +56,8 @@ class LocationViewModelTest { fun getAddressFromCoordinatesCallsRepository() = runTest { locationViewModel.getAddressFromCoordinates(lat = mockLat, lon = mockLon) verify(locationRepository) - .reverseSearch(eq(mockLat), eq(mockLon), any<(String) -> Unit>(), any<(Exception) -> Unit>()) + .reverseSearch( + eq(mockLat), eq(mockLon), any<(String) -> Unit>(), any<(Exception) -> Unit>()) } @Test @@ -108,11 +109,11 @@ class LocationViewModelTest { fun reverseSearchSuccessfulUpdatesAddress() = runTest { // Simulate successful repository call doAnswer { - val successCallback = it.getArgument<(String) -> Unit>(1) - successCallback(mockAddressName) - } - .whenever(locationRepository) - .reverseSearch(any(),any(), any(), any()) + val successCallback = it.getArgument<(String) -> Unit>(1) + successCallback(mockAddressName) + } + .whenever(locationRepository) + .reverseSearch(any(), any(), any(), any()) locationViewModel.getAddressFromCoordinates(mockLat, mockLon) testDispatcher.scheduler.advanceUntilIdle() // Ensure all coroutines complete @@ -124,11 +125,11 @@ class LocationViewModelTest { fun reverseSearchFailureDoesNotCrash() = runTest { // Simulate failure in the repository call doAnswer { - val failureCallback = it.getArgument<(Exception) -> Unit>(2) - failureCallback(RuntimeException("Network error")) - } - .whenever(locationRepository) - .reverseSearch(any(), any(), any(), any()) + val failureCallback = it.getArgument<(Exception) -> Unit>(2) + failureCallback(RuntimeException("Network error")) + } + .whenever(locationRepository) + .reverseSearch(any(), any(), any(), any()) locationViewModel.getAddressFromCoordinates(mockLat, mockLon) testDispatcher.scheduler.advanceUntilIdle() // Ensure all coroutines complete diff --git a/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt b/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt index fece69ceb..f954ca787 100644 --- a/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt +++ b/app/src/test/java/com/android/periodpals/model/location/NominatimLocationRepositoryTest.kt @@ -107,44 +107,46 @@ class LocationModelNominatimTest { @Test fun reverse_search_successful_response() { val jsonResponse = - """ + """ { "place_id": 83566034, "lat": "46.5098584", "lon": "6.4857429", "display_name": "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland" } - """.trimIndent() + """ + .trimIndent() // Create a basic Request object val mockRequest = Request.Builder().url("https://mockurl.com").build() val response = - Response.Builder() - .request(mockRequest) - .protocol(okhttp3.Protocol.HTTP_1_1) - .code(200) - .message("OK") - .body(jsonResponse.toResponseBody("application/json".toMediaType())) - .build() + Response.Builder() + .request(mockRequest) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(jsonResponse.toResponseBody("application/json".toMediaType())) + .build() whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) doAnswer { invocation -> - val callback = invocation.getArgument(0) - callback.onResponse(mockCall, response) - } - .whenever(mockCall) - .enqueue(any()) + val callback = invocation.getArgument(0) + callback.onResponse(mockCall, response) + } + .whenever(mockCall) + .enqueue(any()) locationRepository.reverseSearch( - lat = 46.509858, - lon = 6.485742, - onSuccess = {address -> - assert(address.isNotEmpty()) - assert(address == "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland") - }, - onFailure = { assert(false) { "Expected success, but got failure" } } - ) + lat = 46.509858, + lon = 6.485742, + onSuccess = { address -> + assert(address.isNotEmpty()) + assert( + address == + "1, Avenue de Praz-Rodet, Morges, District de Morges, Vaud, 1110, Switzerland") + }, + onFailure = { assert(false) { "Expected success, but got failure" } }) } @Test @@ -152,27 +154,26 @@ class LocationModelNominatimTest { val mockRequest = Request.Builder().url("https://mockurl.com").build() val response = - Response.Builder() - .request(mockRequest) - .protocol(okhttp3.Protocol.HTTP_1_1) - .code(500) - .message("Server Error") - .body("Internal Server Error".toResponseBody("text/plain".toMediaType())) - .build() + Response.Builder() + .request(mockRequest) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(500) + .message("Server Error") + .body("Internal Server Error".toResponseBody("text/plain".toMediaType())) + .build() whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) doAnswer { invocation -> - val callback = invocation.getArgument(0) - callback.onResponse(mockCall, response) - } - .whenever(mockCall) - .enqueue(any()) + val callback = invocation.getArgument(0) + callback.onResponse(mockCall, response) + } + .whenever(mockCall) + .enqueue(any()) locationRepository.reverseSearch( - lon = 0.0, - lat = 0.0, - onSuccess = { assert(false) { "Expected failure, but got success" } }, - onFailure = { exception -> assert(exception.message?.contains("Server Error") == true) } - ) + lon = 0.0, + lat = 0.0, + onSuccess = { assert(false) { "Expected failure, but got success" } }, + onFailure = { exception -> assert(exception.message?.contains("Server Error") == true) }) } } From 839f6afeeee14df106f16598333aed1ef78a6903 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Wed, 18 Dec 2024 12:58:42 +0100 Subject: [PATCH 24/26] fix: capture correct argument in reverseSearch tests --- .../periodpals/model/location/LocationViewModelTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt index 18a10969f..6f829d46a 100644 --- a/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt +++ b/app/src/test/java/com/android/periodpals/model/location/LocationViewModelTest.kt @@ -109,7 +109,7 @@ class LocationViewModelTest { fun reverseSearchSuccessfulUpdatesAddress() = runTest { // Simulate successful repository call doAnswer { - val successCallback = it.getArgument<(String) -> Unit>(1) + val successCallback = it.getArgument<(String) -> Unit>(2) successCallback(mockAddressName) } .whenever(locationRepository) @@ -125,7 +125,7 @@ class LocationViewModelTest { fun reverseSearchFailureDoesNotCrash() = runTest { // Simulate failure in the repository call doAnswer { - val failureCallback = it.getArgument<(Exception) -> Unit>(2) + val failureCallback = it.getArgument<(Exception) -> Unit>(3) failureCallback(RuntimeException("Network error")) } .whenever(locationRepository) From 54055b1ce00f899f8d73a1ca0eb57b27ca5c5aae Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Wed, 18 Dec 2024 13:24:15 +0100 Subject: [PATCH 25/26] fix: adapt CreateAlertScreenTest and EditAlertScreenTest to account for changes --- .../com/android/periodpals/ui/alert/CreateAlertScreenTest.kt | 2 ++ .../com/android/periodpals/ui/alert/EditAlertScreenTest.kt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/src/test/java/com/android/periodpals/ui/alert/CreateAlertScreenTest.kt b/app/src/test/java/com/android/periodpals/ui/alert/CreateAlertScreenTest.kt index fd85cb8bc..4eae0138f 100644 --- a/app/src/test/java/com/android/periodpals/ui/alert/CreateAlertScreenTest.kt +++ b/app/src/test/java/com/android/periodpals/ui/alert/CreateAlertScreenTest.kt @@ -58,6 +58,7 @@ class CreateAlertScreenTest { private lateinit var navigationActions: NavigationActions private lateinit var locationViewModel: LocationViewModel + private val mockAddress = MutableStateFlow("Some address") private lateinit var gpsService: GPSServiceImpl private val mockLocationFLow = MutableStateFlow(Location.DEFAULT_LOCATION) private lateinit var authenticationViewModel: AuthenticationViewModel @@ -162,6 +163,7 @@ class CreateAlertScreenTest { `when`(userViewModel.user).thenReturn(userState) `when`(authenticationViewModel.authUserData).thenReturn(authUserData) `when`(navigationActions.currentRoute()).thenReturn(Route.ALERT) + `when`(locationViewModel.address).thenReturn(mockAddress) } @Test diff --git a/app/src/test/java/com/android/periodpals/ui/alert/EditAlertScreenTest.kt b/app/src/test/java/com/android/periodpals/ui/alert/EditAlertScreenTest.kt index 3247d282f..a77a6737d 100644 --- a/app/src/test/java/com/android/periodpals/ui/alert/EditAlertScreenTest.kt +++ b/app/src/test/java/com/android/periodpals/ui/alert/EditAlertScreenTest.kt @@ -57,6 +57,7 @@ import org.robolectric.RobolectricTestRunner class EditAlertScreenTest { private lateinit var navigationActions: NavigationActions private lateinit var locationViewModel: LocationViewModel + private val mockAddress = MutableStateFlow("Some address") private lateinit var gpsService: GPSServiceImpl private val mockLocationFLow = MutableStateFlow(Location.DEFAULT_LOCATION) private lateinit var authenticationViewModel: AuthenticationViewModel @@ -179,6 +180,8 @@ class EditAlertScreenTest { MutableStateFlow( listOf(LOCATION_SUGGESTION1, LOCATION_SUGGESTION2, LOCATION_SUGGESTION3))) `when`(locationViewModel.query).thenReturn(MutableStateFlow(LOCATION_SUGGESTION1.name)) + + `when`(locationViewModel.address).thenReturn(mockAddress) } @Test From 44e3ce90719846262c8002c592bcaddc1b928368 Mon Sep 17 00:00:00 2001 From: Bruno Lazarini Sigg Date: Thu, 19 Dec 2024 16:54:40 +0100 Subject: [PATCH 26/26] fix: make repository private in LocationViewModel --- .../com/android/periodpals/model/location/LocationViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt index 24cef4e98..5e8b70ecd 100644 --- a/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt +++ b/app/src/main/java/com/android/periodpals/model/location/LocationViewModel.kt @@ -18,7 +18,7 @@ private const val TAG = "LocationViewModel" * query input. It exposes StateFlows to observe query changes and location suggestions in a * reactive way. */ -class LocationViewModel(val repository: LocationModel) : ViewModel() { +class LocationViewModel(private val repository: LocationModel) : ViewModel() { private val _query = MutableStateFlow("") val query: StateFlow = _query