Add multiBackend Android flavor for runtime OpenGL/Vulkan switching (#4288)
This commit is contained in:
committed by
GitHub
parent
ccb9af119e
commit
b0388d186d
@@ -115,6 +115,15 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
create("multiBackend") {
|
||||
dimension = "renderer"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments("-DMLN_ANDROID_MULTI_BACKEND=ON")
|
||||
targets("maplibre-opengl", "maplibre-vulkan")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user