Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,15 @@ HttpClient(Android) {

### User-Agent Header

All requests include a user-agent header as required by OSM tile usage policy:
All requests include a user-agent header that identifies both OpenMapView and the app, as required by the OSM tile usage policy:

```kotlin
header("User-Agent", USER_AGENT) // "OpenMapView/<version> (https://github.com/afarber/OpenMapView)"
header("User-Agent", UserAgent.value) // "OpenMapView/<version> (<app package name>; https://github.com/afarber/OpenMapView)"
```

`USER_AGENT` is generated at build time by the `generateLibraryVersion` Gradle task, using the same Git-tag-derived version as the published artifact.
The package name comes from the `Context` of the map, so if one app overuses the tile servers, a block only affects that app. The version is `LIBRARY_VERSION`, generated at build time by the `generateLibraryVersion` Gradle task from the same Git tag as the published artifact.

Tile overlays send the same header by default. A `UrlTileProvider` subclass can override `getUserAgent()` to send a different one.

### Coroutine-Based Downloads

Expand Down
2 changes: 1 addition & 1 deletion docs/REPLACING_GOOGLE_MAPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ Start using maps immediately without:
- No monthly billing
- No credit card required

Note: If using public OpenStreetMap tile servers, follow their [Tile Usage Policy](https://operations.osmfoundation.org/policies/tiles/). For high-volume applications, consider self-hosting tiles or using commercial OSM providers.
Note: If using public OpenStreetMap tile servers, follow their [Tile Usage Policy](https://operations.osmfoundation.org/policies/tiles/). OpenMapView sends a User-Agent containing your app package name, so the tile servers can tell apps apart. For high-volume applications, consider self-hosting tiles or using commercial OSM providers.

### Complete Data Control

Expand Down
2 changes: 0 additions & 2 deletions openmapview/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ abstract class GenerateLibraryVersionTask : DefaultTask() {
|
|internal const val LIBRARY_VERSION = "${libraryVersion.get()}"
|
|internal const val USER_AGENT = "OpenMapView/${'$'}LIBRARY_VERSION (https://github.com/afarber/OpenMapView)"
|
""".trimMargin(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ class MapController(
private val scope = CoroutineScope(Dispatchers.Main + Job())
private val tileDownloader = TileDownloader()

init {
UserAgent.init(context)
}

// Replaced on every tile source change, so downloads still running for the old
// source can't put their tiles into the cache of the new one
@Volatile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class TileDownloader {
try {
val response =
client.get(url) {
header("User-Agent", USER_AGENT)
header("User-Agent", UserAgent.value)
}
if (!response.status.isSuccess()) {
Log.w(TAG, "Failed to download tile from $url: HTTP ${response.status.value}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,14 @@ abstract class UrlTileProvider(

/**
* Returns the User-Agent header to use for HTTP requests.
*
* The default identifies both OpenMapView and the app, e.g.
* `OpenMapView/0.17.1 (com.example.app; https://github.com/afarber/OpenMapView)`.
* Subclasses can override to provide a custom User-Agent.
*
* @return User-Agent string
*/
protected open fun getUserAgent(): String = USER_AGENT
protected open fun getUserAgent(): String = UserAgent.value

/**
* Builds the URL for the specified tile coordinates by replacing placeholders
Expand Down
36 changes: 36 additions & 0 deletions openmapview/src/main/kotlin/de/afarber/openmapview/UserAgent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Alexander Farber
* SPDX-License-Identifier: MIT
*
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
*/

package de.afarber.openmapview

import android.content.Context

/**
* User-Agent sent with tile requests, e.g.
* `OpenMapView/0.17.1 (com.example.app; https://github.com/afarber/OpenMapView)`.
*
* The OSM tile usage policy asks for a User-Agent that identifies the application, so the
* app's package name is included. Otherwise a block caused by one app would hit every app
* using OpenMapView. The package name is process-wide, because [UrlTileProvider] has no
* [Context]; it is set by every [MapController].
*/
internal object UserAgent {
@Volatile
private var packageName: String? = null

fun init(context: Context) {
packageName = context.packageName
}

val value: String
get() = build(packageName)

fun build(packageName: String?): String {
val app = packageName?.let { "$it; " } ?: ""
return "OpenMapView/$LIBRARY_VERSION (${app}https://github.com/afarber/OpenMapView)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

package de.afarber.openmapview

import android.content.Context
import android.graphics.Bitmap
import androidx.test.core.app.ApplicationProvider
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
Expand Down Expand Up @@ -58,8 +60,10 @@ class TileDownloaderTest {
}

@Test
fun testDownloadTile_SendsVersionedUserAgent() =
fun testDownloadTile_SendsAppUserAgent() =
runTest {
val context: Context = ApplicationProvider.getApplicationContext()
UserAgent.init(context)
var userAgent: String? = null
val mockEngine =
MockEngine { request ->
Expand All @@ -79,7 +83,10 @@ class TileDownloaderTest {
downloader.downloadTile("https://example.com/tile.png")

assertTrue("Library version should not be blank", LIBRARY_VERSION.isNotBlank())
assertEquals("OpenMapView/$LIBRARY_VERSION (https://github.com/afarber/OpenMapView)", userAgent)
assertEquals(
"OpenMapView/$LIBRARY_VERSION (${context.packageName}; https://github.com/afarber/OpenMapView)",
userAgent,
)
downloader.close()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

package de.afarber.openmapview

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.isActive
Expand Down Expand Up @@ -44,6 +47,25 @@ class UrlTileProviderTest {
assertEquals("https://example.com/3/1/2.png", requestedUrl)
}

@Test
fun testGetTile_SendsAppUserAgent() =
runTest {
val context: Context = ApplicationProvider.getApplicationContext()
UserAgent.init(context)
var userAgent: String? = null
val provider =
createProvider(
MockEngine { request ->
userAgent = request.headers[HttpHeaders.UserAgent]
respond(ByteReadChannel(byteArrayOf(1, 2, 3)), HttpStatusCode.OK)
},
)

provider.getTile(1, 2, 3)

assertEquals(UserAgent.build(context.packageName), userAgent)
}

@Test
fun testGetTile_NotFoundReturnsNull() =
runTest {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 Alexander Farber
* SPDX-License-Identifier: MIT
*
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
*/

package de.afarber.openmapview

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class UserAgentTest {
@Test
fun testBuild_WithPackageName_IdentifiesApp() {
assertEquals(
"OpenMapView/$LIBRARY_VERSION (com.example.app; https://github.com/afarber/OpenMapView)",
UserAgent.build("com.example.app"),
)
}

@Test
fun testBuild_WithoutPackageName_IdentifiesLibraryOnly() {
assertEquals("OpenMapView/$LIBRARY_VERSION (https://github.com/afarber/OpenMapView)", UserAgent.build(null))
}

@Test
fun testMapController_SetsPackageName() {
val context: Context = ApplicationProvider.getApplicationContext()

MapController(context)

assertEquals(UserAgent.build(context.packageName), UserAgent.value)
}
}
Loading