Skip to content

Commit 6fa4585

Browse files
committed
Fix marker bitmap lifecycle crash, add cache regression tests, and bump OpenMapView docs/version to 0.13.1
1 parent b852cb9 commit 6fa4585

7 files changed

Lines changed: 71 additions & 11 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Add to your `build.gradle.kts`:
1414

1515
```kotlin
1616
dependencies {
17-
implementation("de.afarber:openmapview:0.13.0")
17+
implementation("de.afarber:openmapview:0.13.1")
1818
}
1919
```
2020

docs/PERFORMANCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ HttpClient(Android) {
269269
All requests include a user-agent header as required by OSM tile usage policy:
270270

271271
```kotlin
272-
header("User-Agent", "OpenMapView/0.13.0 (https://github.com/afarber/OpenMapView)")
272+
header("User-Agent", "OpenMapView/0.13.1 (https://github.com/afarber/OpenMapView)")
273273
```
274274

275275
### Coroutine-Based Downloads

docs/REPLACING_GOOGLE_MAPS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ dependencies {
107107
```kotlin
108108
// Add to build.gradle.kts
109109
dependencies {
110-
implementation("de.afarber:openmapview:0.13.0")
110+
implementation("de.afarber:openmapview:0.13.1")
111111
}
112112
```
113113

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ POM_DESCRIPTION=A modern, Kotlin-first MapView replacement for Android powered b
1010
POM_URL=https://github.com/afarber/OpenMapView
1111
GROUP=de.afarber
1212
# Version is automatically derived from the latest Git tag (vX.Y.Z)
13-
VERSION_NAME=0.1.0
13+
VERSION_NAME=0.13.1
1414

openmapview/src/main/kotlin/de/afarber/openmapview/MapController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ class MapController(
235235
MarkerIconFactory.getDefaultIcon(descriptor.hue)
236236
}
237237
is BitmapDescriptor.BitmapMarker -> {
238-
descriptor.bitmap
238+
descriptor.bitmap.takeUnless { it.isRecycled } ?: defaultMarkerIcon
239239
}
240240
is BitmapDescriptor.ResourceMarker -> {
241241
try {

openmapview/src/main/kotlin/de/afarber/openmapview/MarkerIconFactory.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,23 @@ internal object MarkerIconFactory {
3434
// Normalize hue to 0-360 range
3535
val normalizedHue = hue % 360f
3636

37-
// Return cached icon if available
38-
iconCache[normalizedHue]?.let { return it }
37+
// Return cached icon if available. If a recycled bitmap somehow ended up in cache,
38+
// drop it and regenerate below.
39+
iconCache[normalizedHue]?.let { cached ->
40+
if (!cached.isRecycled) {
41+
return cached
42+
}
43+
iconCache.remove(normalizedHue)
44+
}
3945

4046
// Create new icon
4147
val bitmap = createMarkerIcon(normalizedHue)
4248

43-
// Add to cache with LRU eviction
49+
// Add to cache with LRU eviction.
50+
// Do not call recycle() here: returned icons may still be used by active map instances.
4451
if (iconCache.size >= MAX_CACHE_SIZE) {
4552
val firstKey = iconCache.keys.first()
46-
iconCache.remove(firstKey)?.recycle()
53+
iconCache.remove(firstKey)
4754
}
4855
iconCache[normalizedHue] = bitmap
4956

@@ -109,10 +116,12 @@ internal object MarkerIconFactory {
109116
}
110117

111118
/**
112-
* Clears all cached icons to free memory.
119+
* Clears all cached icons by dropping references only.
120+
*
121+
* We intentionally avoid recycling cached bitmaps because references can still be held
122+
* by active map instances and recycled bitmaps would crash canvas drawing.
113123
*/
114124
fun clearCache() {
115-
iconCache.values.forEach { it.recycle() }
116125
iconCache.clear()
117126
}
118127
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview
9+
10+
import org.junit.Assert.assertFalse
11+
import org.junit.Assert.assertNotSame
12+
import org.junit.Assert.assertSame
13+
import org.junit.Test
14+
import org.junit.runner.RunWith
15+
import org.robolectric.RobolectricTestRunner
16+
17+
@RunWith(RobolectricTestRunner::class)
18+
class MarkerIconFactoryTest {
19+
@Test
20+
fun getDefaultIcon_SameHue_ReturnsCachedBitmap() {
21+
MarkerIconFactory.clearCache()
22+
23+
val icon1 = MarkerIconFactory.getDefaultIcon(0f)
24+
val icon2 = MarkerIconFactory.getDefaultIcon(0f)
25+
26+
assertSame(icon1, icon2)
27+
assertFalse(icon1.isRecycled)
28+
}
29+
30+
@Test
31+
fun clearCache_DoesNotRecycleBitmapInUse() {
32+
MarkerIconFactory.clearCache()
33+
34+
val icon = MarkerIconFactory.getDefaultIcon(120f)
35+
MarkerIconFactory.clearCache()
36+
37+
assertFalse(icon.isRecycled)
38+
}
39+
40+
@Test
41+
fun getDefaultIcon_AfterClearCache_ReturnsFreshBitmap() {
42+
MarkerIconFactory.clearCache()
43+
44+
val first = MarkerIconFactory.getDefaultIcon(240f)
45+
MarkerIconFactory.clearCache()
46+
val second = MarkerIconFactory.getDefaultIcon(240f)
47+
48+
assertNotSame(first, second)
49+
assertFalse(second.isRecycled)
50+
}
51+
}

0 commit comments

Comments
 (0)