Skip to content

Replace OSMDroid with Mapbox#7245

Open
seadowg wants to merge 21 commits into
getodk:masterfrom
seadowg:osm
Open

Replace OSMDroid with Mapbox#7245
seadowg wants to merge 21 commits into
getodk:masterfrom
seadowg:osm

Conversation

@seadowg
Copy link
Copy Markdown
Member

@seadowg seadowg commented May 28, 2026

Closes #7092

Why is this the best possible solution? Were any other approaches considered?

The one big (potentially unexpected) change here is to simplify the MapConfigurator interface so that MapFragment implementations don't have to deal with them at all. This abstraction mostly worked, but the implementations are by definition implementation specific, so I'd argue it was only confusing to try and abstract details for settings away from them.

How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?

This is a pretty massive change: all the OSM maps (OSM, USGS and Carto) are now implemented using Mapbox, and so we'll need to verify that they behave as before. It's hard to put a boundary around how the "tile set" changes here can affect behaviour. I've manually verified that reference layers work as expected, but I have limited experience in that area so it'd be good to double-check.

Before submitting this PR, please make sure you have:

  • added or modified tests for any new or changed behavior
  • run ./gradlew connectedAndroidTest (or ./gradlew testLab) and confirmed all checks still pass
  • added a comment above any new strings describing it for translators
  • added any new strings with date formatting to DateFormatsTest
  • verified that any code or assets from external sources are properly credited in comments and/or in the about file.
  • verified that any new UI elements use theme colors. UI Components Style guidelines

}

private static class SourceOption {
public static class SourceOption {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think longer term, we really don't want MapConfiguratorProvider to be statically accessed, but that's a rabbit hole for another time.

@seadowg seadowg marked this pull request as ready for review June 1, 2026 09:41
@seadowg seadowg requested a review from grzesiek2010 June 1, 2026 09:41
object MapFragmentReferenceLayerUtils {

@JvmStatic
fun getReferenceLayerFile(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used in tests. We can remove it.

basemap == BASEMAP_SOURCE_USGS ||
basemap == BASEMAP_SOURCE_CARTO
private fun isMapbox(source: String?): Boolean {
return when (source) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified to:

when (source) {
    ProjectKeys.BASEMAP_SOURCE_MAPBOX,
    ProjectKeys.BASEMAP_SOURCE_OSM,
    ProjectKeys.BASEMAP_SOURCE_USGS,
    ProjectKeys.BASEMAP_SOURCE_CARTO -> true
    else -> false
}

val basemapSource = settings.getString(KEY_BASEMAP_SOURCE)
return if (isMapbox(basemapSource)) {
MapboxClassInstanceCreator.createMapboxMapFragment(
basemapSource ?: ProjectKeys.BASEMAP_SOURCE_MAPBOX
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basemapSource can't be null here - isMapbox(null) returns false, so the ?: BASEMAP_SOURCE_MAPBOX fallback is unreachable.

Comment thread collect_app/build.gradle
}
implementation project(':external-app')
implementation project(':maps')
implementation project(':osmdroid')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove osmdroid from libs.versions.toml.
Osmdroid is still mentioned in a couple of places, like: STATE.md, MapFragment (in the comment) etc.

@grzesiek2010
Copy link
Copy Markdown
Member

grzesiek2010 commented Jun 3, 2026

There is a crash when I try to change maps in settings:

FATAL EXCEPTION: main
Process: org.odk.collect.android, PID: 6809

java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
    at org.odk.collect.android.geo.MapConfiguratorProvider.getOption(MapConfiguratorProvider.java:123)
    at org.odk.collect.android.geo.MapConfiguratorProvider.getConfigurator(MapConfiguratorProvider.java:76)
    at org.odk.collect.android.preferences.screens.MapsPreferencesFragment.initBasemapSourcePref(MapsPreferencesFragment.kt:118)
    at org.odk.collect.android.preferences.screens.MapsPreferencesFragment.onCreatePreferences(MapsPreferencesFragment.kt:84)
    at androidx.preference.PreferenceFragmentCompat.onCreate(PreferenceFragmentCompat.java:161)
    at org.odk.collect.android.preferences.screens.MapsPreferencesFragment.onCreate(MapsPreferencesFragment.kt:64)
    at androidx.fragment.app.Fragment.performCreate(Fragment.java:3099)
    at androidx.fragment.app.FragmentStateManager.create(FragmentStateManager.java:524)
    at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2214)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2109)
    at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2052)
    at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:703)
    at android.os.Handler.handleCallback(Handler.java:958)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loopOnce(Looper.java:205)

It happened on a device without Google Play Services, this case seems to be handled when we open a map (there is an error message), but in settings, it crashes the app.

val uri = if (configuration.uri != null) {
configuration.uri
} else if (configuration.styleSetting != null) {
configuration.styleOptions.getValue(settings.getString(configuration.styleSetting)!!)
Copy link
Copy Markdown
Member

@grzesiek2010 grzesiek2010 Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getValue() returns a StyleOption, not a BasemapUri, so neither when branch matches and the style is never loaded.

val styleOptions: Map<String, StyleOption> = emptyMap()
)

class StyleOption(val name: Int, uri: BasemapUri)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uri here is never used.

import java.io.File
import kotlin.collections.toIntArray

class MapboxMapConfigurator(private val configuration: Configuration) : MapConfigurator {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few unused imports here.


override fun getDisplayName(file: File): String {
val name = MbtilesFile.readName(file)
return if (name != null) name else file.getName()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be just: return name ?: file.name

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this wrapper layout?

// This has to happen on the main thread but we might call `initialize` from tests
MapView(context).onCreate(null)
}
OsmDroidInitializer.initialize(userAgentProvider.userAgent)
Copy link
Copy Markdown
Member

@grzesiek2010 grzesiek2010 Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove userAgentProvider from the constructor.

styleOptions = mapOf(
"positron" to StyleOption(
name = R.string.carto_map_style_positron,
BasemapUri.Raster("http://1.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it work with https?

Copy link
Copy Markdown
Member

@lognaturel lognaturel Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing they're HTTP because they were added 15+ years ago! Let's use the currently-recommended ones with https: https://github.com/cartodb/basemap-styles#1-web-raster-basemaps

),
"dark_matter" to StyleOption(
name = R.string.carto_map_style_dark_matter,
BasemapUri.Raster("http://1.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it work with https?

getMapViewModel().getSettings(mapConfigurator.prefKeys).observe(viewLifecycleOwner) {
val newConfig = mapConfigurator.buildConfig(it)
onConfigChanged(newConfig)
getMapViewModel().getSettings(setOf(KEY_MAPBOX_MAP_STYLE)).observe(viewLifecycleOwner) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CollectionUtils from com.google.android.gms.common.util is GMS-internal (@KeepForSdk), so it can change or disappear in any Play Services release. Collections.singleton() would be safer here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace OSMDroid with Mapbox

3 participants