package app.grapheneos.geocoder import android.app.AppGlobals import android.content.Context import android.ext.settings.GeocoderSettings.GEOCODER_DISABLED import android.ext.settings.GeocoderSettings.GEOCODER_SERVER_GRAPHENEOS_PROXY import android.ext.settings.GeocoderSettings.GEOCODER_SERVER_NOMINATIM import android.ext.settings.GeocoderSettings.GEOCODER_SETTING import android.location.Address import android.os.Bundle import android.util.Log import androidx.core.os.toPersistableBundle import app.grapheneos.verboseLog import java.io.IOException import java.net.URL import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.util.Locale import javax.net.ssl.HttpsURLConnection import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import org.grapheneos.tls.ModernTLSSocketFactory private const val TAG = "NominatimGeocoder" private const val EXTRA_VERBOSE_TAG = "NominatimGeocoderVV" /** Version which should get incremented when request behavior changes */ private const val USER_AGENT_VERSION = 1 class NominatimGeocoder : Geocoder { private val tlsSocketFactory = ModernTLSSocketFactory() @Throws(IOException::class) override fun forwardGeocode( locationName: String, lowerLeftLatitude: Double, lowerLeftLongitude: Double, upperRightLatitude: Double, upperRightLongitude: Double, maxResults: Int, preferredLocale: Locale ): List
{ val urlSuffix = "/search?format=geocodejson&q=${ URLEncoder.encode( locationName, StandardCharsets.UTF_8 ) }&limit=$maxResults&addressdetails=1&extratags=1".let { // add bounding box parameter if it's set for this request if (lowerLeftLatitude == 0.0 && lowerLeftLongitude == 0.0 && upperRightLatitude == 0.0 && upperRightLongitude == 0.0) { it } else { "$it&viewbox=$lowerLeftLongitude,$lowerLeftLatitude,$upperRightLongitude,$upperRightLatitude&bounded=1" } } return fetch(urlSuffix, maxResults, preferredLocale) } @Throws(IOException::class) override fun reverseGeocode( latitude: Double, longitude: Double, maxResults: Int, preferredLocale: Locale, ): List { val urlSuffix = // Nominatim doesn't support returning more than 1 reverse geocoding result "/reverse?format=geocodejson&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1&extratags=1" return fetch(urlSuffix, maxResults, preferredLocale) } @Throws(IOException::class) private fun fetch( urlSuffix: String, expectedMaxResults: Int, preferredLocale: Locale ): List { val (baseUrl, enforceModernTls) = getServerBaseUrl() val url = URL("$baseUrl$urlSuffix") val connection = url.openConnection() as HttpsURLConnection val responseBytes = try { if (enforceModernTls) { connection.sslSocketFactory = tlsSocketFactory } connection.requestMethod = "GET" connection.setRequestProperty("Accept-Language", preferredLocale.toLanguageTag()) connection.setRequestProperty( "User-Agent", "GrapheneOS geocoder $USER_AGENT_VERSION" ) connection.connectTimeout = 10_000 connection.readTimeout = 10_000 val responseCode = connection.responseCode if (responseCode != HttpsURLConnection.HTTP_OK) { throw IOException("non-200 response code: $responseCode") } connection.inputStream.use { inputStream -> inputStream.readAllBytes() } } finally { connection.disconnect() } val responseString = responseBytes.decodeToString() // occurs when reverse geocode coordinates are in the middle of the ocean, presumably // because there were no results if (responseString == "{\"error\":\"Unable to geocode\"}") { return emptyList() } val response = try { val json = Json { ignoreUnknownKeys = true } json.decodeFromString