Add multiBackend Android flavor for runtime OpenGL/Vulkan switching (#4288)

This commit is contained in:
Veniamin Vynohradov
2026-08-06 16:54:58 +03:00
committed by GitHub
parent ccb9af119e
commit b0388d186d
47 changed files with 896 additions and 163 deletions

View File

@@ -115,6 +115,15 @@ android {
}
}
}
create("multiBackend") {
dimension = "renderer"
externalNativeBuild {
cmake {
arguments("-DMLN_ANDROID_MULTI_BACKEND=ON")
targets("maplibre-opengl", "maplibre-vulkan")
}
}
}
}
buildFeatures {

View File

@@ -4,12 +4,14 @@ import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import android.text.TextUtils
import android.widget.Toast
import androidx.multidex.MultiDexApplication
import org.maplibre.android.MapStrictMode
import org.maplibre.android.MapLibre
import org.maplibre.android.WellKnownTileServer
import org.maplibre.android.log.Logger
import org.maplibre.android.testapp.utils.ApiKeyUtils
import org.maplibre.android.testapp.utils.RenderingEnginePreference
import org.maplibre.android.testapp.utils.TileLoadingMeasurementUtils
import org.maplibre.android.testapp.utils.TimberLogger
import timber.log.Timber
@@ -61,11 +63,37 @@ open class MapLibreApplication : MultiDexApplication() {
if (apiKey != null) {
validateApiKey(apiKey)
}
MapLibre.getInstance(applicationContext, apiKey, TILE_SERVER)
initializeMapLibre(apiKey)
TileLoadingMeasurementUtils.setUpTileLoadingMeasurement()
MapStrictMode.setStrictModeEnabled(true)
}
// The rendering engine is bound to the MapLibre instance at construction time, so a
// persisted choice has to be passed into getInstance() itself rather than set
// separately beforehand. A build installed over a different flavor (same
// applicationId across flavors) may have a persisted choice the compiled-in backend
// doesn't support, so an unsupported choice can't be allowed to crash startup — fall
// back to the default engine instead.
//
// If nothing is persisted yet (first launch, or preference cleared), MapLibre.getInstance()
// itself selects Vulkan automatically when this device's hardware supports it.
private fun initializeMapLibre(apiKey: String?) {
val persistedType = RenderingEnginePreference.load(this)
try {
MapLibre.getInstance(applicationContext, apiKey, TILE_SERVER, persistedType)
} catch (error: UnsupportedOperationException) {
RenderingEnginePreference.clear(this)
Timber.w(error, "Rendering engine %s is not supported by this build", persistedType)
Toast.makeText(
this,
getString(R.string.rendering_engine_unsupported, persistedType?.name),
Toast.LENGTH_LONG
).show()
MapLibre.getInstance(applicationContext, apiKey, TILE_SERVER)
}
}
companion object {
val TILE_SERVER = WellKnownTileServer.MapLibre
private const val DEFAULT_API_KEY = "YOUR_API_KEY_GOES_HERE"

View File

@@ -7,8 +7,10 @@ import android.content.pm.PackageManager
import android.content.res.Resources.NotFoundException
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
@@ -18,11 +20,14 @@ import androidx.recyclerview.widget.RecyclerView.SimpleOnItemTouchListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.maplibre.android.RenderingEngine
import org.maplibre.android.testapp.R
import org.maplibre.android.testapp.adapter.FeatureAdapter
import org.maplibre.android.testapp.adapter.FeatureSectionAdapter
import org.maplibre.android.testapp.model.activity.Feature
import org.maplibre.android.testapp.utils.AppRestarter
import org.maplibre.android.testapp.utils.ItemClickSupport
import org.maplibre.android.testapp.utils.RenderingEnginePreference
import timber.log.Timber
import java.util.*
@@ -184,6 +189,49 @@ class FeatureOverviewActivity : AppCompatActivity() {
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.action_rendering_engine) {
showRenderingEngineDialog()
return true
}
return super.onOptionsItemSelected(item)
}
private fun showRenderingEngineDialog() {
val engineTypes = RenderingEngine.Type.values()
val currentType = RenderingEngine.getCurrentType()
val labels = engineTypes.map { type ->
if (type == currentType) {
getString(R.string.rendering_engine_current_label, type.name)
} else {
type.name
}
}.toTypedArray()
val checkedIndex = engineTypes.indexOf(currentType)
AlertDialog.Builder(this)
.setTitle(R.string.rendering_engine_dialog_title)
.setSingleChoiceItems(labels, checkedIndex) { dialog, which ->
dialog.dismiss()
switchRenderingEngine(engineTypes[which])
}
.setNegativeButton(R.string.cancel, null)
.show()
}
// RenderingEngine.setCurrentType() throws once MapLibre.getInstance() has run
// (which already happened in Application#onCreate() by the time this activity is
// shown), so it can't be probed here. The choice is persisted and applied on the
// next launch instead, before MapLibre.getInstance() runs — see
// MapLibreApplication#applyPersistedRenderingEngine(), which also surfaces the
// unsupported-backend error if the persisted choice turns out not to be supported.
private fun switchRenderingEngine(type: RenderingEngine.Type) {
if (type == RenderingEngine.getCurrentType()) {
return
}
RenderingEnginePreference.save(this, type)
AppRestarter.restart(this)
}
// Filter the features based on the search query
private fun filterFeatures(query: String?) {
val filteredFeatures = if (query.isNullOrEmpty()) {

View File

@@ -0,0 +1,19 @@
package org.maplibre.android.testapp.utils
import android.content.Context
import android.content.Intent
/**
* Restarts the app: relaunches its launcher activity in a fresh task, then kills this
* process. Used after changing process-lifetime state (like the rendering engine) that
* only takes effect on the next launch.
*/
object AppRestarter {
fun restart(context: Context) {
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
requireNotNull(intent) { "No launch intent for package ${context.packageName}" }
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
Runtime.getRuntime().exit(0)
}
}

View File

@@ -0,0 +1,36 @@
package org.maplibre.android.testapp.utils
import android.content.Context
import org.maplibre.android.RenderingEngine
/**
* Persists the user's chosen [RenderingEngine.Type] across process restarts.
*
* Single-backend flavors lock the compiled-in backend and throw from
* [RenderingEngine.setCurrentType] for any other value; the multiBackend
* flavor accepts either value but only applies it on the next native library
* load. Either way the choice needs to survive an app restart, which is what
* this class is for.
*/
object RenderingEnginePreference {
private const val PREFS_NAME = "rendering_engine_prefs"
private const val KEY_ENGINE_TYPE = "engine_type"
/** Returns the persisted choice, or null if the user never picked one. */
fun load(context: Context): RenderingEngine.Type? {
val stored = prefs(context).getString(KEY_ENGINE_TYPE, null) ?: return null
return runCatching { RenderingEngine.Type.valueOf(stored) }.getOrNull()
}
fun save(context: Context, type: RenderingEngine.Type) {
prefs(context).edit().putString(KEY_ENGINE_TYPE, type.name).commit()
}
fun clear(context: Context) {
prefs(context).edit().clear().commit()
}
private fun prefs(context: Context) =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}

View File

@@ -7,4 +7,8 @@
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView" />
<item
android:id="@+id/action_rendering_engine"
android:title="@string/action_rendering_engine"
app:showAsAction="never" />
</menu>

View File

@@ -9,4 +9,9 @@
<string name="description_world_tour_benchmark">Writes out avg. fps and 1% fps after world tour with .flyTo()</string>
<string name="search">Search</string>
<string name="category_data">Data Sources</string>
<string name="rendering_engine_unsupported">%1$s is not supported by this build. Use the multiBackend flavor to switch backends at runtime.</string>
<string name="action_rendering_engine">Rendering Engine</string>
<string name="rendering_engine_dialog_title">Select Rendering Engine</string>
<string name="rendering_engine_current_label">%1$s (current)</string>
<string name="cancel">Cancel</string>
</resources>