@jacques_gordon/expo-mapbox-navigation 2.0.8 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/android/build.gradle
CHANGED
|
@@ -1,51 +1,25 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
apply plugin: 'kotlin-android'
|
|
3
|
-
apply plugin: 'maven-publish'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
// Mapbox Navigation SDK version used by this module.
|
|
7
|
-
// Must be aligned with the Mapbox Maps SDK version used in @rnmapbox/maps.
|
|
8
|
-
// Minimum required: 3.x compatible with mapbox-maps-android 11.11.0
|
|
9
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
-
// Navigation SDK v3 uses com.mapbox.navigationcore (NOT com.mapbox.navigation)
|
|
11
|
-
// v3.8.1 is the stable release that ships with Maps SDK 11.11.0
|
|
12
|
-
def MAPBOX_NAVIGATION_VERSION = "3.8.1"
|
|
13
|
-
|
|
14
|
-
// Forces minimum Mapbox Maps Android version to 11.11.0 to ensure
|
|
15
|
-
// CameraAnimationsUtils.calculateCameraAnimationHint(...) is available (fixes #43)
|
|
4
|
+
def MAPBOX_NAV_VERSION = "3.8.1"
|
|
16
5
|
def MAPBOX_MAPS_MIN_VERSION = "11.11.0"
|
|
17
|
-
|
|
18
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
// NDK 27 — required for 16 KB page size compatibility (Android 15+)
|
|
20
|
-
// See: https://developer.android.com/guide/practices/page-sizes
|
|
21
|
-
// NDK 27 added full support for 16 KB page alignment via -DALIGN_16KB flag
|
|
22
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
6
|
def REQUIRED_NDK_VERSION = "27.0.12077973"
|
|
24
7
|
|
|
25
8
|
android {
|
|
26
9
|
compileSdkVersion safeExtGet("compileSdkVersion", 35)
|
|
27
10
|
namespace "expo.modules.mapboxnavigation"
|
|
28
|
-
|
|
29
|
-
// Force NDK 27 for 16 KB page size compatibility
|
|
30
11
|
ndkVersion REQUIRED_NDK_VERSION
|
|
31
12
|
|
|
32
13
|
defaultConfig {
|
|
33
14
|
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
34
15
|
targetSdkVersion safeExtGet("targetSdkVersion", 35)
|
|
35
16
|
|
|
36
|
-
// ─────────────────────────────────────────────────────────────────
|
|
37
|
-
// 16 KB PAGE SIZE SUPPORT (Android 15 / API 35+)
|
|
38
|
-
// Required for Google Play compliance starting Feb 2025 (64-bit apps)
|
|
39
|
-
// This ensures native .so libraries are aligned to 16 KB boundaries.
|
|
40
|
-
// ─────────────────────────────────────────────────────────────────
|
|
41
17
|
externalNativeBuild {
|
|
42
18
|
cmake {
|
|
43
19
|
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
44
20
|
}
|
|
45
21
|
}
|
|
46
|
-
|
|
47
22
|
ndk {
|
|
48
|
-
// Only build for 64-bit ABIs — 16 KB page size requirement is 64-bit only
|
|
49
23
|
abiFilters "arm64-v8a", "x86_64"
|
|
50
24
|
}
|
|
51
25
|
}
|
|
@@ -60,8 +34,6 @@ android {
|
|
|
60
34
|
}
|
|
61
35
|
|
|
62
36
|
packagingOptions {
|
|
63
|
-
// Ensure .so files are NOT compressed — required for 16 KB page size support
|
|
64
|
-
// Compressed .so files cannot be aligned in the APK
|
|
65
37
|
jniLibs {
|
|
66
38
|
useLegacyPackaging = false
|
|
67
39
|
}
|
|
@@ -75,13 +47,9 @@ android {
|
|
|
75
47
|
repositories {
|
|
76
48
|
mavenCentral()
|
|
77
49
|
google()
|
|
78
|
-
|
|
79
|
-
// Mapbox Maven repository — requires a secret token (sk.*)
|
|
80
50
|
maven {
|
|
81
51
|
url 'https://api.mapbox.com/downloads/v2/releases/maven'
|
|
82
|
-
authentication {
|
|
83
|
-
basic(BasicAuthentication)
|
|
84
|
-
}
|
|
52
|
+
authentication { basic(BasicAuthentication) }
|
|
85
53
|
credentials {
|
|
86
54
|
username = 'mapbox'
|
|
87
55
|
password = project.hasProperty('MAPBOX_DOWNLOADS_TOKEN')
|
|
@@ -95,22 +63,26 @@ dependencies {
|
|
|
95
63
|
implementation project(':expo-modules-core')
|
|
96
64
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${safeExtGet('kotlinVersion', '1.9.0')}"
|
|
97
65
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
// Group changed from com.mapbox.navigation to com.mapbox.navigationcore in v3
|
|
101
|
-
// v3.8.1 ships with mapbox-maps-android 11.11.0 (fixes issue #43)
|
|
102
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
103
|
-
implementation "com.mapbox.navigationcore:android:${MAPBOX_NAVIGATION_VERSION}"
|
|
66
|
+
// Mapbox Navigation SDK v3 core
|
|
67
|
+
implementation "com.mapbox.navigationcore:android:${MAPBOX_NAV_VERSION}"
|
|
104
68
|
|
|
105
|
-
//
|
|
106
|
-
// Prevents gradle from resolving an older transitive version (issue #43 root cause)
|
|
69
|
+
// Mapbox Maps SDK (pinned to minimum version)
|
|
107
70
|
implementation("com.mapbox.maps:android:${MAPBOX_MAPS_MIN_VERSION}") {
|
|
108
71
|
transitive = true
|
|
109
72
|
}
|
|
110
73
|
|
|
111
|
-
//
|
|
74
|
+
// ── Navigation UI Components (Waze-style UI) ─────────────────────────────
|
|
75
|
+
// MapboxManeuverView — turn-by-turn banner
|
|
76
|
+
implementation "com.mapbox.navigationcore:ui-components:${MAPBOX_NAV_VERSION}"
|
|
77
|
+
// MapboxSpeedInfoView — speed limit + current speed
|
|
78
|
+
implementation "com.mapbox.navigationcore:tripdata:${MAPBOX_NAV_VERSION}"
|
|
79
|
+
// Trip progress API — ETA, distance, duration
|
|
80
|
+
implementation "com.mapbox.navigationcore:ui-maps:${MAPBOX_NAV_VERSION}"
|
|
81
|
+
|
|
82
|
+
// AndroidX
|
|
112
83
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
|
113
84
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
|
85
|
+
implementation 'androidx.core:core-ktx:1.13.1'
|
|
114
86
|
}
|
|
115
87
|
|
|
116
88
|
def safeExtGet(prop, fallback) {
|
|
@@ -2,13 +2,23 @@ package expo.modules.mapboxnavigation
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.os.Build
|
|
5
7
|
import android.util.Log
|
|
8
|
+
import android.view.Gravity
|
|
9
|
+
import android.view.View
|
|
10
|
+
import android.widget.FrameLayout
|
|
11
|
+
import android.widget.LinearLayout
|
|
12
|
+
import android.widget.TextView
|
|
13
|
+
import androidx.core.content.ContextCompat
|
|
6
14
|
import com.mapbox.api.directions.v5.models.RouteOptions
|
|
7
15
|
import com.mapbox.geojson.Point
|
|
8
16
|
import com.mapbox.maps.MapView
|
|
9
17
|
import com.mapbox.maps.Style
|
|
10
18
|
import com.mapbox.maps.plugin.animation.camera
|
|
19
|
+
import com.mapbox.maps.plugin.locationcomponent.location
|
|
11
20
|
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
|
21
|
+
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
|
|
12
22
|
import com.mapbox.navigation.base.options.NavigationOptions
|
|
13
23
|
import com.mapbox.navigation.base.route.NavigationRoute
|
|
14
24
|
import com.mapbox.navigation.base.route.NavigationRouterCallback
|
|
@@ -18,10 +28,25 @@ import com.mapbox.navigation.core.MapboxNavigation
|
|
|
18
28
|
import com.mapbox.navigation.core.MapboxNavigationProvider
|
|
19
29
|
import com.mapbox.navigation.core.directions.session.RoutesObserver
|
|
20
30
|
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
|
|
31
|
+
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
|
|
32
|
+
import com.mapbox.navigation.core.trip.session.LocationObserver
|
|
21
33
|
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
|
|
34
|
+
import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi
|
|
35
|
+
import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi
|
|
36
|
+
import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter
|
|
37
|
+
import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalFormatter
|
|
38
|
+
import com.mapbox.navigation.tripdata.progress.model.PercentDistanceTraveledFormatter
|
|
39
|
+
import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter
|
|
40
|
+
import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter
|
|
41
|
+
import com.mapbox.navigation.tripdata.speedlimit.api.MapboxSpeedInfoApi
|
|
42
|
+
import com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
|
|
43
|
+
import com.mapbox.navigation.ui.components.speedlimit.view.MapboxSpeedInfoView
|
|
44
|
+
import com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView
|
|
45
|
+
import com.mapbox.navigation.ui.maps.NavigationStyles
|
|
22
46
|
import com.mapbox.navigation.ui.maps.camera.NavigationCamera
|
|
23
47
|
import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
|
|
24
48
|
import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
|
|
49
|
+
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
|
|
25
50
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
|
|
26
51
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
|
|
27
52
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
|
|
@@ -29,6 +54,7 @@ import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
|
|
|
29
54
|
import expo.modules.kotlin.AppContext
|
|
30
55
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
31
56
|
import expo.modules.kotlin.views.ExpoView
|
|
57
|
+
import java.util.Calendar
|
|
32
58
|
import java.util.Locale
|
|
33
59
|
|
|
34
60
|
private const val TAG = "ExpoMapboxNavigation"
|
|
@@ -36,8 +62,7 @@ private const val TAG = "ExpoMapboxNavigation"
|
|
|
36
62
|
class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
37
63
|
ExpoView(context, appContext) {
|
|
38
64
|
|
|
39
|
-
// ── EventDispatchers
|
|
40
|
-
// Each event declared here must match the Events(...) list in ExpoMapboxNavigationModule
|
|
65
|
+
// ── EventDispatchers ──────────────────────────────────────────────────────
|
|
41
66
|
private val onRouteProgressChanged by EventDispatcher()
|
|
42
67
|
private val onRoutesReady by EventDispatcher()
|
|
43
68
|
private val onNavigationFinished by EventDispatcher()
|
|
@@ -45,13 +70,32 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
45
70
|
private val onRoutesFailed by EventDispatcher()
|
|
46
71
|
private val onArrival by EventDispatcher()
|
|
47
72
|
|
|
48
|
-
// ──
|
|
73
|
+
// ── Core views ────────────────────────────────────────────────────────────
|
|
49
74
|
private val mapView: MapView = MapView(context)
|
|
75
|
+
|
|
76
|
+
// ── Overlay UI views (Waze-style) ─────────────────────────────────────────
|
|
77
|
+
private lateinit var maneuverView: MapboxManeuverView
|
|
78
|
+
private lateinit var speedInfoView: MapboxSpeedInfoView
|
|
79
|
+
private lateinit var tripProgressView: MapboxTripProgressView
|
|
80
|
+
private lateinit var etaBottomBar: LinearLayout
|
|
81
|
+
private lateinit var tvEtaTime: TextView
|
|
82
|
+
private lateinit var tvDuration: TextView
|
|
83
|
+
private lateinit var tvDistance: TextView
|
|
84
|
+
private lateinit var btnCancel: TextView
|
|
85
|
+
|
|
86
|
+
// ── Navigation APIs ───────────────────────────────────────────────────────
|
|
50
87
|
private var navigationCamera: NavigationCamera? = null
|
|
51
88
|
private var viewportDataSource: MapboxNavigationViewportDataSource? = null
|
|
52
89
|
private var routeLineApi: MapboxRouteLineApi? = null
|
|
53
90
|
private var routeLineView: MapboxRouteLineView? = null
|
|
54
91
|
private var mapboxNavigation: MapboxNavigation? = null
|
|
92
|
+
private var maneuverApi: MapboxManeuverApi? = null
|
|
93
|
+
private var speedInfoApi: MapboxSpeedInfoApi? = null
|
|
94
|
+
private var tripProgressApi: MapboxTripProgressApi? = null
|
|
95
|
+
private val navigationLocationProvider = NavigationLocationProvider()
|
|
96
|
+
|
|
97
|
+
// ── Night mode state ──────────────────────────────────────────────────────
|
|
98
|
+
private var isNightMode = false
|
|
55
99
|
|
|
56
100
|
// ── Props ─────────────────────────────────────────────────────────────────
|
|
57
101
|
private var coordinates: List<Map<String, Double>> = emptyList()
|
|
@@ -69,33 +113,181 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
69
113
|
private var customRasterAboveLayerId: String? = null
|
|
70
114
|
|
|
71
115
|
init {
|
|
72
|
-
|
|
73
|
-
addView(mapView)
|
|
116
|
+
buildOverlayUI()
|
|
74
117
|
setupNavigation()
|
|
75
118
|
}
|
|
76
119
|
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
121
|
+
// Build the Waze-style overlay UI programmatically
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
123
|
+
private fun buildOverlayUI() {
|
|
124
|
+
val root = FrameLayout(context).apply {
|
|
125
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MapView full screen
|
|
129
|
+
mapView.layoutParams = FrameLayout.LayoutParams(
|
|
130
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
131
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
132
|
+
)
|
|
133
|
+
root.addView(mapView)
|
|
134
|
+
|
|
135
|
+
val dp = context.resources.displayMetrics.density
|
|
136
|
+
|
|
137
|
+
// ── Maneuver banner (top) ─────────────────────────────────────────────
|
|
138
|
+
maneuverView = MapboxManeuverView(context).apply {
|
|
139
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
140
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
141
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
142
|
+
).also { it.gravity = Gravity.TOP }
|
|
143
|
+
visibility = View.GONE
|
|
144
|
+
}
|
|
145
|
+
root.addView(maneuverView)
|
|
146
|
+
|
|
147
|
+
// ── Speed info (bottom-left, Waze style) ──────────────────────────────
|
|
148
|
+
speedInfoView = MapboxSpeedInfoView(context).apply {
|
|
149
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
150
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
151
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
152
|
+
).also {
|
|
153
|
+
it.gravity = Gravity.BOTTOM or Gravity.START
|
|
154
|
+
it.setMargins(
|
|
155
|
+
(16 * dp).toInt(), 0,
|
|
156
|
+
0, (120 * dp).toInt()
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
visibility = View.GONE
|
|
160
|
+
}
|
|
161
|
+
root.addView(speedInfoView)
|
|
162
|
+
|
|
163
|
+
// ── ETA bottom bar ────────────────────────────────────────────────────
|
|
164
|
+
etaBottomBar = LinearLayout(context).apply {
|
|
165
|
+
orientation = LinearLayout.HORIZONTAL
|
|
166
|
+
setBackgroundColor(Color.WHITE)
|
|
167
|
+
elevation = 8 * dp
|
|
168
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
169
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
170
|
+
(80 * dp).toInt()
|
|
171
|
+
).also { it.gravity = Gravity.BOTTOM }
|
|
172
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
173
|
+
setPadding(
|
|
174
|
+
(16 * dp).toInt(), (8 * dp).toInt(),
|
|
175
|
+
(16 * dp).toInt(), (8 * dp).toInt()
|
|
176
|
+
)
|
|
177
|
+
visibility = View.GONE
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ETA time
|
|
181
|
+
tvEtaTime = TextView(context).apply {
|
|
182
|
+
textSize = 22f
|
|
183
|
+
setTextColor(Color.BLACK)
|
|
184
|
+
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
185
|
+
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
|
186
|
+
}
|
|
187
|
+
etaBottomBar.addView(tvEtaTime)
|
|
188
|
+
|
|
189
|
+
// Duration + Distance (center)
|
|
190
|
+
val centerLayout = LinearLayout(context).apply {
|
|
191
|
+
orientation = LinearLayout.VERTICAL
|
|
192
|
+
gravity = Gravity.CENTER
|
|
193
|
+
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
|
|
194
|
+
}
|
|
195
|
+
tvDuration = TextView(context).apply {
|
|
196
|
+
textSize = 16f
|
|
197
|
+
setTextColor(Color.DKGRAY)
|
|
198
|
+
gravity = Gravity.CENTER
|
|
199
|
+
}
|
|
200
|
+
tvDistance = TextView(context).apply {
|
|
201
|
+
textSize = 13f
|
|
202
|
+
setTextColor(Color.GRAY)
|
|
203
|
+
gravity = Gravity.CENTER
|
|
204
|
+
}
|
|
205
|
+
centerLayout.addView(tvDuration)
|
|
206
|
+
centerLayout.addView(tvDistance)
|
|
207
|
+
etaBottomBar.addView(centerLayout)
|
|
208
|
+
|
|
209
|
+
// Cancel button
|
|
210
|
+
btnCancel = TextView(context).apply {
|
|
211
|
+
text = "✕"
|
|
212
|
+
textSize = 22f
|
|
213
|
+
setTextColor(Color.DKGRAY)
|
|
214
|
+
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
215
|
+
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
|
216
|
+
setOnClickListener { cancelNavigation() }
|
|
217
|
+
}
|
|
218
|
+
etaBottomBar.addView(btnCancel)
|
|
219
|
+
root.addView(etaBottomBar)
|
|
220
|
+
|
|
221
|
+
addView(root)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
225
|
+
// Setup Navigation
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
77
227
|
private fun setupNavigation() {
|
|
78
228
|
val navigationOptions = NavigationOptions.Builder(context).build()
|
|
79
229
|
mapboxNavigation = MapboxNavigationProvider.create(navigationOptions)
|
|
80
230
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
|
|
87
|
-
navigationCamera = NavigationCamera(
|
|
88
|
-
mapView.mapboxMap,
|
|
89
|
-
mapView.camera,
|
|
90
|
-
viewportDataSource!!
|
|
91
|
-
)
|
|
231
|
+
// Auto day/night style
|
|
232
|
+
val style = mapStyle ?: getAutoStyle()
|
|
233
|
+
|
|
234
|
+
mapView.mapboxMap.loadStyle(style) { _ ->
|
|
235
|
+
setupComponents()
|
|
92
236
|
registerObservers()
|
|
93
237
|
}
|
|
94
238
|
}
|
|
95
239
|
|
|
240
|
+
private fun setupComponents() {
|
|
241
|
+
val mapboxMap = mapView.mapboxMap
|
|
242
|
+
|
|
243
|
+
// Route line
|
|
244
|
+
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
|
|
245
|
+
routeLineView = MapboxRouteLineView(
|
|
246
|
+
MapboxRouteLineViewOptions.Builder(context)
|
|
247
|
+
.routeLineBelowLayerId("road-label-navigation")
|
|
248
|
+
.build()
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
// Camera
|
|
252
|
+
viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap)
|
|
253
|
+
navigationCamera = NavigationCamera(
|
|
254
|
+
mapboxMap,
|
|
255
|
+
mapView.camera,
|
|
256
|
+
viewportDataSource!!
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
// Speed info API
|
|
260
|
+
speedInfoApi = MapboxSpeedInfoApi()
|
|
261
|
+
|
|
262
|
+
// Maneuver API
|
|
263
|
+
val formatter = DistanceFormatterOptions.Builder(context).build()
|
|
264
|
+
maneuverApi = MapboxManeuverApi(
|
|
265
|
+
com.mapbox.navigation.base.formatter.MapboxDistanceFormatter(formatter)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
// Trip progress API
|
|
269
|
+
val progressFormatter = TripProgressUpdateFormatter.Builder(context)
|
|
270
|
+
.distanceRemainingFormatter(DistanceRemainingFormatter(formatter))
|
|
271
|
+
.timeRemainingFormatter(TimeRemainingFormatter(context))
|
|
272
|
+
.percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
|
|
273
|
+
.estimatedTimeToArrivalFormatter(EstimatedTimeToArrivalFormatter(context))
|
|
274
|
+
.build()
|
|
275
|
+
tripProgressApi = MapboxTripProgressApi(progressFormatter)
|
|
276
|
+
|
|
277
|
+
// Location puck
|
|
278
|
+
mapView.location.apply {
|
|
279
|
+
setLocationProvider(navigationLocationProvider)
|
|
280
|
+
enabled = true
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
285
|
+
// Register all observers
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
96
287
|
private fun registerObservers() {
|
|
97
288
|
val nav = mapboxNavigation ?: return
|
|
98
289
|
|
|
290
|
+
// Routes observer
|
|
99
291
|
nav.registerRoutesObserver(object : RoutesObserver {
|
|
100
292
|
override fun onRoutesChanged(result: RoutesUpdatedResult) {
|
|
101
293
|
val routes = result.navigationRoutes
|
|
@@ -109,6 +301,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
109
301
|
viewportDataSource?.evaluate()
|
|
110
302
|
safeCameraFollowing()
|
|
111
303
|
|
|
304
|
+
// Show UI overlays
|
|
305
|
+
maneuverView.visibility = View.VISIBLE
|
|
306
|
+
etaBottomBar.visibility = View.VISIBLE
|
|
307
|
+
|
|
112
308
|
onRoutesReady(mapOf(
|
|
113
309
|
"routeCount" to routes.size,
|
|
114
310
|
"distanceMeters" to (routes.first().directionsRoute.distance() ?: 0.0),
|
|
@@ -122,14 +318,45 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
122
318
|
}
|
|
123
319
|
viewportDataSource?.clearRouteData()
|
|
124
320
|
viewportDataSource?.evaluate()
|
|
321
|
+
hideNavigationUI()
|
|
125
322
|
}
|
|
126
323
|
}
|
|
127
324
|
})
|
|
128
325
|
|
|
326
|
+
// Route progress observer — maneuvers + ETA + speed limit
|
|
129
327
|
nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
|
|
130
328
|
viewportDataSource?.onRouteProgressChanged(routeProgress)
|
|
131
329
|
viewportDataSource?.evaluate()
|
|
132
330
|
|
|
331
|
+
// ── Maneuver instructions ──────────────────────────────────────
|
|
332
|
+
maneuverApi?.getManeuvers(routeProgress) { maneuverState ->
|
|
333
|
+
maneuverView.renderManeuvers(maneuverState)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Trip progress (ETA, duration, distance) ────────────────────
|
|
337
|
+
tripProgressApi?.getTripProgress(routeProgress)?.let { tripProgress ->
|
|
338
|
+
val eta = tripProgress.currentLegTimeRemaining
|
|
339
|
+
val cal = Calendar.getInstance().apply {
|
|
340
|
+
add(Calendar.SECOND, eta.toInt())
|
|
341
|
+
}
|
|
342
|
+
val h = cal.get(Calendar.HOUR_OF_DAY)
|
|
343
|
+
val m = cal.get(Calendar.MINUTE)
|
|
344
|
+
tvEtaTime.text = String.format("%02d:%02d", h, m)
|
|
345
|
+
|
|
346
|
+
val totalMin = (tripProgress.totalTimeRemaining / 60).toInt()
|
|
347
|
+
tvDuration.text = if (totalMin >= 60)
|
|
348
|
+
"${totalMin / 60}h ${totalMin % 60}min"
|
|
349
|
+
else
|
|
350
|
+
"${totalMin} min"
|
|
351
|
+
|
|
352
|
+
val distKm = tripProgress.distanceRemaining / 1000.0
|
|
353
|
+
tvDistance.text = if (distKm >= 1.0)
|
|
354
|
+
String.format("%.1f km", distKm)
|
|
355
|
+
else
|
|
356
|
+
"${tripProgress.distanceRemaining.toInt()} m"
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Emit progress event ───────────────────────────────────────
|
|
133
360
|
onRouteProgressChanged(mapOf(
|
|
134
361
|
"distanceRemaining" to routeProgress.distanceRemaining,
|
|
135
362
|
"durationRemaining" to routeProgress.durationRemaining,
|
|
@@ -140,9 +367,110 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
140
367
|
?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
141
368
|
))
|
|
142
369
|
})
|
|
370
|
+
|
|
371
|
+
// Location observer — speed limit + location puck
|
|
372
|
+
nav.registerLocationObserver(object : LocationObserver {
|
|
373
|
+
override fun onNewRawLocation(rawLocation: com.mapbox.common.location.Location) {}
|
|
374
|
+
|
|
375
|
+
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
|
|
376
|
+
val enhancedLocation = locationMatcherResult.enhancedLocation
|
|
377
|
+
|
|
378
|
+
// Update location puck
|
|
379
|
+
navigationLocationProvider.changePosition(
|
|
380
|
+
location = enhancedLocation,
|
|
381
|
+
keyPoints = locationMatcherResult.keyPoints,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
// Update camera
|
|
385
|
+
viewportDataSource?.onLocationChanged(enhancedLocation)
|
|
386
|
+
viewportDataSource?.evaluate()
|
|
387
|
+
|
|
388
|
+
// ── Speed limit (issue — show posted + current speed) ──────
|
|
389
|
+
val formatter = DistanceFormatterOptions.Builder(context).build()
|
|
390
|
+
val info = speedInfoApi?.updatePostedAndCurrentSpeed(
|
|
391
|
+
locationMatcherResult,
|
|
392
|
+
formatter
|
|
393
|
+
)
|
|
394
|
+
if (info != null) {
|
|
395
|
+
speedInfoView.visibility = View.VISIBLE
|
|
396
|
+
speedInfoView.render(info)
|
|
397
|
+
} else {
|
|
398
|
+
speedInfoView.visibility = View.GONE
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Auto day/night switch ──────────────────────────────────
|
|
402
|
+
checkAndSwitchDayNight()
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
408
|
+
// Auto day/night mode
|
|
409
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
410
|
+
private fun getAutoStyle(): String {
|
|
411
|
+
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
412
|
+
return if (hour in 6..20)
|
|
413
|
+
NavigationStyles.NAVIGATION_DAY_STYLE
|
|
414
|
+
else
|
|
415
|
+
NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private fun checkAndSwitchDayNight() {
|
|
419
|
+
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
420
|
+
val shouldBeNight = hour !in 6..20
|
|
421
|
+
if (shouldBeNight != isNightMode) {
|
|
422
|
+
isNightMode = shouldBeNight
|
|
423
|
+
val newStyle = if (shouldBeNight)
|
|
424
|
+
NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
425
|
+
else
|
|
426
|
+
NavigationStyles.NAVIGATION_DAY_STYLE
|
|
427
|
+
|
|
428
|
+
mapView.mapboxMap.loadStyle(newStyle) { _ ->
|
|
429
|
+
// Re-render route line on new style
|
|
430
|
+
mapView.mapboxMap.style?.let { style ->
|
|
431
|
+
routeLineApi?.let { api ->
|
|
432
|
+
mapboxNavigation?.getNavigationRoutes()?.let { routes ->
|
|
433
|
+
if (routes.isNotEmpty()) {
|
|
434
|
+
api.setNavigationRoutes(routes) { value ->
|
|
435
|
+
routeLineView?.renderRouteDrawData(style, value)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Switch bottom bar color
|
|
444
|
+
etaBottomBar.setBackgroundColor(
|
|
445
|
+
if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE
|
|
446
|
+
)
|
|
447
|
+
val textColor = if (shouldBeNight) Color.WHITE else Color.BLACK
|
|
448
|
+
tvEtaTime.setTextColor(textColor)
|
|
449
|
+
tvDuration.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
|
|
450
|
+
tvDistance.setTextColor(if (shouldBeNight) Color.GRAY else Color.GRAY)
|
|
451
|
+
btnCancel.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
456
|
+
// Cancel navigation
|
|
457
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
458
|
+
private fun cancelNavigation() {
|
|
459
|
+
mapboxNavigation?.setNavigationRoutes(emptyList())
|
|
460
|
+
mapboxNavigation?.stopTripSession()
|
|
461
|
+
hideNavigationUI()
|
|
462
|
+
onNavigationCancelled(mapOf<String, Any>())
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private fun hideNavigationUI() {
|
|
466
|
+
maneuverView.visibility = View.GONE
|
|
467
|
+
speedInfoView.visibility = View.GONE
|
|
468
|
+
etaBottomBar.visibility = View.GONE
|
|
143
469
|
}
|
|
144
470
|
|
|
145
|
-
//
|
|
471
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
472
|
+
// Issue #43: safe camera
|
|
473
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
146
474
|
private fun safeCameraFollowing() {
|
|
147
475
|
try {
|
|
148
476
|
navigationCamera?.requestNavigationCameraToFollowing(
|
|
@@ -151,11 +479,13 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
151
479
|
.build()
|
|
152
480
|
)
|
|
153
481
|
} catch (e: Exception) {
|
|
154
|
-
Log.e(TAG, "Camera transition error
|
|
482
|
+
Log.e(TAG, "Camera transition error: ${e.message}")
|
|
155
483
|
}
|
|
156
484
|
}
|
|
157
485
|
|
|
158
|
-
//
|
|
486
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
487
|
+
// Issue #31: voice units
|
|
488
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
159
489
|
private fun resolveVoiceUnits(): String {
|
|
160
490
|
return when (voiceUnits?.lowercase()) {
|
|
161
491
|
"metric" -> "metric"
|
|
@@ -167,7 +497,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
167
497
|
}
|
|
168
498
|
}
|
|
169
499
|
|
|
170
|
-
//
|
|
500
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
501
|
+
// Route request
|
|
502
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
171
503
|
@SuppressLint("MissingPermission")
|
|
172
504
|
private fun fetchRoutes() {
|
|
173
505
|
val nav = mapboxNavigation ?: return
|
|
@@ -183,6 +515,8 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
183
515
|
.language(locale.toLanguageTag())
|
|
184
516
|
.voiceUnits(resolveVoiceUnits())
|
|
185
517
|
.coordinatesList(points)
|
|
518
|
+
// maxspeed annotation required for speed limit display
|
|
519
|
+
.annotations("maxspeed,congestion,duration")
|
|
186
520
|
|
|
187
521
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
188
522
|
navigationProfile?.let {
|
|
@@ -249,6 +583,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
249
583
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
250
584
|
override fun onDetachedFromWindow() {
|
|
251
585
|
super.onDetachedFromWindow()
|
|
586
|
+
maneuverApi?.cancel()
|
|
252
587
|
routeLineApi?.cancel()
|
|
253
588
|
routeLineView?.cancel()
|
|
254
589
|
mapboxNavigation?.stopTripSession()
|
package/app.plugin.js
CHANGED
|
@@ -4,8 +4,6 @@ const path = require('path');
|
|
|
4
4
|
|
|
5
5
|
const NDK_VERSION = '27.0.12077973';
|
|
6
6
|
const MAPBOX_MAPS_MIN_VERSION = '11.11.0';
|
|
7
|
-
// Navigation SDK v3 uses com.mapbox.navigationcore group
|
|
8
|
-
const MAPBOX_NAV_VERSION = '3.8.1';
|
|
9
7
|
|
|
10
8
|
const withMapboxNavigation = (config, options = {}) => {
|
|
11
9
|
const {
|
|
@@ -22,13 +20,11 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
22
20
|
);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
// Validate mapboxMapsVersion >= 11.11.0
|
|
26
23
|
const [major, minor] = mapboxMapsVersion.split('.').map(Number);
|
|
27
24
|
if (major < 11 || (major === 11 && minor < 11)) {
|
|
28
25
|
throw new Error(
|
|
29
26
|
`[@jacques_gordon/expo-mapbox-navigation] mapboxMapsVersion must be >= 11.11.0.\n` +
|
|
30
|
-
`Provided: ${mapboxMapsVersion}
|
|
31
|
-
`This minimum version is required to fix the CameraAnimationsUtils crash (issue #43).`
|
|
27
|
+
`Provided: ${mapboxMapsVersion}`
|
|
32
28
|
);
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -36,7 +32,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
36
32
|
config = withProjectBuildGradle(config, (mod) => {
|
|
37
33
|
let contents = mod.modResults.contents;
|
|
38
34
|
|
|
39
|
-
// Add Mapbox Maven repo if not already present
|
|
40
35
|
if (!contents.includes('api.mapbox.com/downloads/v2/releases/maven')) {
|
|
41
36
|
const mavenBlock = `
|
|
42
37
|
maven {
|
|
@@ -49,7 +44,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
49
44
|
: System.getenv('MAPBOX_DOWNLOADS_TOKEN') ?: ""
|
|
50
45
|
}
|
|
51
46
|
}`;
|
|
52
|
-
|
|
53
47
|
if (contents.includes('allprojects') && contents.includes('repositories')) {
|
|
54
48
|
contents = contents.replace(
|
|
55
49
|
/allprojects\s*\{[\s\S]*?repositories\s*\{/,
|
|
@@ -58,7 +52,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
|
|
62
55
|
mod.modResults.contents = contents;
|
|
63
56
|
return mod;
|
|
64
57
|
});
|
|
@@ -67,37 +60,32 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
67
60
|
config = withAppBuildGradle(config, (mod) => {
|
|
68
61
|
let contents = mod.modResults.contents;
|
|
69
62
|
|
|
70
|
-
// Force NDK 27 for 16 KB page size
|
|
71
63
|
if (!contents.includes('ndkVersion')) {
|
|
72
64
|
contents = contents.replace(
|
|
73
65
|
/android\s*\{/,
|
|
74
|
-
`android {\n ndkVersion "${NDK_VERSION}"
|
|
66
|
+
`android {\n ndkVersion "${NDK_VERSION}"`
|
|
75
67
|
);
|
|
76
68
|
} else if (!contents.includes(NDK_VERSION)) {
|
|
77
69
|
contents = contents.replace(
|
|
78
70
|
/ndkVersion\s+["'][^"']*["']/,
|
|
79
|
-
`ndkVersion "${NDK_VERSION}"
|
|
71
|
+
`ndkVersion "${NDK_VERSION}"`
|
|
80
72
|
);
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
// Enable useLegacyPackaging = false for 16 KB page alignment
|
|
84
75
|
if (!contents.includes('useLegacyPackaging')) {
|
|
85
76
|
contents = contents.replace(
|
|
86
77
|
/android\s*\{/,
|
|
87
|
-
`android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false
|
|
78
|
+
`android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false\n }\n }`
|
|
88
79
|
);
|
|
89
80
|
}
|
|
90
81
|
|
|
91
|
-
// Pin Mapbox Maps + Navigation versions via resolutionStrategy
|
|
92
82
|
const resolutionBlock = `
|
|
93
83
|
configurations.all {
|
|
94
84
|
resolutionStrategy {
|
|
95
|
-
// Force Mapbox Maps SDK >= ${mapboxMapsVersion} — required for NavigationCamera compatibility (issue #43)
|
|
96
85
|
force "com.mapbox.maps:android:${mapboxMapsVersion}"
|
|
97
86
|
force "com.mapbox.maps:android-ndk27:${mapboxMapsVersion}"
|
|
98
87
|
}
|
|
99
88
|
}`;
|
|
100
|
-
|
|
101
89
|
if (!contents.includes('com.mapbox.maps:android:')) {
|
|
102
90
|
contents = contents + '\n' + resolutionBlock;
|
|
103
91
|
}
|
|
@@ -106,31 +94,57 @@ configurations.all {
|
|
|
106
94
|
return mod;
|
|
107
95
|
});
|
|
108
96
|
|
|
109
|
-
// ── Android: AndroidManifest
|
|
97
|
+
// ── Android: AndroidManifest ─────────────────────────────────────────────
|
|
110
98
|
config = withAndroidManifest(config, (mod) => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
const manifest = mod.modResults.manifest;
|
|
100
|
+
|
|
101
|
+
// ── Permissions required by Mapbox Navigation SDK (targetSdk 35) ────────
|
|
102
|
+
//
|
|
103
|
+
// SecurityException: Starting FGS with type location requires:
|
|
104
|
+
// - android.permission.FOREGROUND_SERVICE_LOCATION (mandatory API 34+)
|
|
105
|
+
// - android.permission.FOREGROUND_SERVICE (mandatory)
|
|
106
|
+
// - android.permission.ACCESS_FINE_LOCATION (at least one of coarse/fine)
|
|
107
|
+
// - android.permission.ACCESS_COARSE_LOCATION
|
|
108
|
+
//
|
|
109
|
+
const requiredPermissions = [
|
|
110
|
+
'android.permission.ACCESS_FINE_LOCATION',
|
|
111
|
+
'android.permission.ACCESS_COARSE_LOCATION',
|
|
112
|
+
'android.permission.FOREGROUND_SERVICE',
|
|
113
|
+
'android.permission.FOREGROUND_SERVICE_LOCATION', // ← fixes the crash
|
|
114
|
+
'android.permission.POST_NOTIFICATIONS', // needed for nav notification (API 33+)
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (!manifest['uses-permission']) {
|
|
118
|
+
manifest['uses-permission'] = [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const permission of requiredPermissions) {
|
|
122
|
+
const already = manifest['uses-permission'].some(
|
|
123
|
+
(p) => p.$?.['android:name'] === permission
|
|
124
|
+
);
|
|
125
|
+
if (!already) {
|
|
126
|
+
manifest['uses-permission'].push({ $: { 'android:name': permission } });
|
|
115
127
|
}
|
|
128
|
+
}
|
|
116
129
|
|
|
130
|
+
// ── Mapbox access token meta-data ────────────────────────────────────────
|
|
131
|
+
const mainApp = manifest.application?.[0];
|
|
132
|
+
if (mainApp) {
|
|
133
|
+
if (!mainApp['meta-data']) mainApp['meta-data'] = [];
|
|
117
134
|
const tokenMeta = mainApp['meta-data'].find(
|
|
118
135
|
(m) => m.$?.['android:name'] === 'com.mapbox.token'
|
|
119
136
|
);
|
|
120
|
-
|
|
121
137
|
if (!tokenMeta) {
|
|
122
138
|
mainApp['meta-data'].push({
|
|
123
|
-
$: {
|
|
124
|
-
'android:name': 'com.mapbox.token',
|
|
125
|
-
'android:value': accessToken,
|
|
126
|
-
},
|
|
139
|
+
$: { 'android:name': 'com.mapbox.token', 'android:value': accessToken },
|
|
127
140
|
});
|
|
128
141
|
}
|
|
129
142
|
}
|
|
143
|
+
|
|
130
144
|
return mod;
|
|
131
145
|
});
|
|
132
146
|
|
|
133
|
-
// ── Android:
|
|
147
|
+
// ── Android: color overrides ─────────────────────────────────────────────
|
|
134
148
|
if (Object.keys(androidColorOverrides).length > 0) {
|
|
135
149
|
config = withDangerousMod(config, [
|
|
136
150
|
'android',
|
|
@@ -139,27 +153,16 @@ configurations.all {
|
|
|
139
153
|
mod.modRequest.platformProjectRoot,
|
|
140
154
|
'app/src/main/res/values'
|
|
141
155
|
);
|
|
142
|
-
|
|
143
|
-
if (!fs.existsSync(colorsDir)) {
|
|
144
|
-
fs.mkdirSync(colorsDir, { recursive: true });
|
|
145
|
-
}
|
|
156
|
+
if (!fs.existsSync(colorsDir)) fs.mkdirSync(colorsDir, { recursive: true });
|
|
146
157
|
|
|
147
158
|
const colorEntries = Object.entries(androidColorOverrides)
|
|
148
159
|
.map(([name, value]) => ` <color name="${name}">${value}</color>`)
|
|
149
160
|
.join('\n');
|
|
150
161
|
|
|
151
|
-
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
152
|
-
<!-- Generated by @jacques_gordon/expo-mapbox-navigation config plugin -->
|
|
153
|
-
<resources>
|
|
154
|
-
${colorEntries}
|
|
155
|
-
</resources>
|
|
156
|
-
`;
|
|
157
|
-
|
|
158
162
|
fs.writeFileSync(
|
|
159
163
|
path.join(colorsDir, 'mapbox_navigation_colors.xml'),
|
|
160
|
-
|
|
164
|
+
`<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${colorEntries}\n</resources>\n`
|
|
161
165
|
);
|
|
162
|
-
|
|
163
166
|
return mod;
|
|
164
167
|
},
|
|
165
168
|
]);
|
|
@@ -177,16 +180,12 @@ ${colorEntries}
|
|
|
177
180
|
mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription =
|
|
178
181
|
'Your location is used for turn-by-turn navigation even when the app is in the background.';
|
|
179
182
|
}
|
|
180
|
-
|
|
181
|
-
if (!mod.modResults.UIBackgroundModes) {
|
|
182
|
-
mod.modResults.UIBackgroundModes = [];
|
|
183
|
-
}
|
|
183
|
+
if (!mod.modResults.UIBackgroundModes) mod.modResults.UIBackgroundModes = [];
|
|
184
184
|
for (const mode of ['audio', 'location']) {
|
|
185
185
|
if (!mod.modResults.UIBackgroundModes.includes(mode)) {
|
|
186
186
|
mod.modResults.UIBackgroundModes.push(mode);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
|
|
190
189
|
return mod;
|
|
191
190
|
});
|
|
192
191
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacques_gordon/expo-mapbox-navigation",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "Expo module for Mapbox Navigation SDK with 16KB page size support, NDK27, and Mapbox Maps v11.11.0+",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
package/plugin/src/index.js
CHANGED
|
@@ -4,8 +4,6 @@ const path = require('path');
|
|
|
4
4
|
|
|
5
5
|
const NDK_VERSION = '27.0.12077973';
|
|
6
6
|
const MAPBOX_MAPS_MIN_VERSION = '11.11.0';
|
|
7
|
-
// Navigation SDK v3 uses com.mapbox.navigationcore group
|
|
8
|
-
const MAPBOX_NAV_VERSION = '3.8.1';
|
|
9
7
|
|
|
10
8
|
const withMapboxNavigation = (config, options = {}) => {
|
|
11
9
|
const {
|
|
@@ -22,13 +20,11 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
22
20
|
);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
// Validate mapboxMapsVersion >= 11.11.0
|
|
26
23
|
const [major, minor] = mapboxMapsVersion.split('.').map(Number);
|
|
27
24
|
if (major < 11 || (major === 11 && minor < 11)) {
|
|
28
25
|
throw new Error(
|
|
29
26
|
`[@jacques_gordon/expo-mapbox-navigation] mapboxMapsVersion must be >= 11.11.0.\n` +
|
|
30
|
-
`Provided: ${mapboxMapsVersion}
|
|
31
|
-
`This minimum version is required to fix the CameraAnimationsUtils crash (issue #43).`
|
|
27
|
+
`Provided: ${mapboxMapsVersion}`
|
|
32
28
|
);
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -36,7 +32,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
36
32
|
config = withProjectBuildGradle(config, (mod) => {
|
|
37
33
|
let contents = mod.modResults.contents;
|
|
38
34
|
|
|
39
|
-
// Add Mapbox Maven repo if not already present
|
|
40
35
|
if (!contents.includes('api.mapbox.com/downloads/v2/releases/maven')) {
|
|
41
36
|
const mavenBlock = `
|
|
42
37
|
maven {
|
|
@@ -49,7 +44,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
49
44
|
: System.getenv('MAPBOX_DOWNLOADS_TOKEN') ?: ""
|
|
50
45
|
}
|
|
51
46
|
}`;
|
|
52
|
-
|
|
53
47
|
if (contents.includes('allprojects') && contents.includes('repositories')) {
|
|
54
48
|
contents = contents.replace(
|
|
55
49
|
/allprojects\s*\{[\s\S]*?repositories\s*\{/,
|
|
@@ -58,7 +52,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
|
|
62
55
|
mod.modResults.contents = contents;
|
|
63
56
|
return mod;
|
|
64
57
|
});
|
|
@@ -67,37 +60,32 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
67
60
|
config = withAppBuildGradle(config, (mod) => {
|
|
68
61
|
let contents = mod.modResults.contents;
|
|
69
62
|
|
|
70
|
-
// Force NDK 27 for 16 KB page size
|
|
71
63
|
if (!contents.includes('ndkVersion')) {
|
|
72
64
|
contents = contents.replace(
|
|
73
65
|
/android\s*\{/,
|
|
74
|
-
`android {\n ndkVersion "${NDK_VERSION}"
|
|
66
|
+
`android {\n ndkVersion "${NDK_VERSION}"`
|
|
75
67
|
);
|
|
76
68
|
} else if (!contents.includes(NDK_VERSION)) {
|
|
77
69
|
contents = contents.replace(
|
|
78
70
|
/ndkVersion\s+["'][^"']*["']/,
|
|
79
|
-
`ndkVersion "${NDK_VERSION}"
|
|
71
|
+
`ndkVersion "${NDK_VERSION}"`
|
|
80
72
|
);
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
// Enable useLegacyPackaging = false for 16 KB page alignment
|
|
84
75
|
if (!contents.includes('useLegacyPackaging')) {
|
|
85
76
|
contents = contents.replace(
|
|
86
77
|
/android\s*\{/,
|
|
87
|
-
`android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false
|
|
78
|
+
`android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false\n }\n }`
|
|
88
79
|
);
|
|
89
80
|
}
|
|
90
81
|
|
|
91
|
-
// Pin Mapbox Maps + Navigation versions via resolutionStrategy
|
|
92
82
|
const resolutionBlock = `
|
|
93
83
|
configurations.all {
|
|
94
84
|
resolutionStrategy {
|
|
95
|
-
// Force Mapbox Maps SDK >= ${mapboxMapsVersion} — required for NavigationCamera compatibility (issue #43)
|
|
96
85
|
force "com.mapbox.maps:android:${mapboxMapsVersion}"
|
|
97
86
|
force "com.mapbox.maps:android-ndk27:${mapboxMapsVersion}"
|
|
98
87
|
}
|
|
99
88
|
}`;
|
|
100
|
-
|
|
101
89
|
if (!contents.includes('com.mapbox.maps:android:')) {
|
|
102
90
|
contents = contents + '\n' + resolutionBlock;
|
|
103
91
|
}
|
|
@@ -106,31 +94,57 @@ configurations.all {
|
|
|
106
94
|
return mod;
|
|
107
95
|
});
|
|
108
96
|
|
|
109
|
-
// ── Android: AndroidManifest
|
|
97
|
+
// ── Android: AndroidManifest ─────────────────────────────────────────────
|
|
110
98
|
config = withAndroidManifest(config, (mod) => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
const manifest = mod.modResults.manifest;
|
|
100
|
+
|
|
101
|
+
// ── Permissions required by Mapbox Navigation SDK (targetSdk 35) ────────
|
|
102
|
+
//
|
|
103
|
+
// SecurityException: Starting FGS with type location requires:
|
|
104
|
+
// - android.permission.FOREGROUND_SERVICE_LOCATION (mandatory API 34+)
|
|
105
|
+
// - android.permission.FOREGROUND_SERVICE (mandatory)
|
|
106
|
+
// - android.permission.ACCESS_FINE_LOCATION (at least one of coarse/fine)
|
|
107
|
+
// - android.permission.ACCESS_COARSE_LOCATION
|
|
108
|
+
//
|
|
109
|
+
const requiredPermissions = [
|
|
110
|
+
'android.permission.ACCESS_FINE_LOCATION',
|
|
111
|
+
'android.permission.ACCESS_COARSE_LOCATION',
|
|
112
|
+
'android.permission.FOREGROUND_SERVICE',
|
|
113
|
+
'android.permission.FOREGROUND_SERVICE_LOCATION', // ← fixes the crash
|
|
114
|
+
'android.permission.POST_NOTIFICATIONS', // needed for nav notification (API 33+)
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (!manifest['uses-permission']) {
|
|
118
|
+
manifest['uses-permission'] = [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const permission of requiredPermissions) {
|
|
122
|
+
const already = manifest['uses-permission'].some(
|
|
123
|
+
(p) => p.$?.['android:name'] === permission
|
|
124
|
+
);
|
|
125
|
+
if (!already) {
|
|
126
|
+
manifest['uses-permission'].push({ $: { 'android:name': permission } });
|
|
115
127
|
}
|
|
128
|
+
}
|
|
116
129
|
|
|
130
|
+
// ── Mapbox access token meta-data ────────────────────────────────────────
|
|
131
|
+
const mainApp = manifest.application?.[0];
|
|
132
|
+
if (mainApp) {
|
|
133
|
+
if (!mainApp['meta-data']) mainApp['meta-data'] = [];
|
|
117
134
|
const tokenMeta = mainApp['meta-data'].find(
|
|
118
135
|
(m) => m.$?.['android:name'] === 'com.mapbox.token'
|
|
119
136
|
);
|
|
120
|
-
|
|
121
137
|
if (!tokenMeta) {
|
|
122
138
|
mainApp['meta-data'].push({
|
|
123
|
-
$: {
|
|
124
|
-
'android:name': 'com.mapbox.token',
|
|
125
|
-
'android:value': accessToken,
|
|
126
|
-
},
|
|
139
|
+
$: { 'android:name': 'com.mapbox.token', 'android:value': accessToken },
|
|
127
140
|
});
|
|
128
141
|
}
|
|
129
142
|
}
|
|
143
|
+
|
|
130
144
|
return mod;
|
|
131
145
|
});
|
|
132
146
|
|
|
133
|
-
// ── Android:
|
|
147
|
+
// ── Android: color overrides ─────────────────────────────────────────────
|
|
134
148
|
if (Object.keys(androidColorOverrides).length > 0) {
|
|
135
149
|
config = withDangerousMod(config, [
|
|
136
150
|
'android',
|
|
@@ -139,27 +153,16 @@ configurations.all {
|
|
|
139
153
|
mod.modRequest.platformProjectRoot,
|
|
140
154
|
'app/src/main/res/values'
|
|
141
155
|
);
|
|
142
|
-
|
|
143
|
-
if (!fs.existsSync(colorsDir)) {
|
|
144
|
-
fs.mkdirSync(colorsDir, { recursive: true });
|
|
145
|
-
}
|
|
156
|
+
if (!fs.existsSync(colorsDir)) fs.mkdirSync(colorsDir, { recursive: true });
|
|
146
157
|
|
|
147
158
|
const colorEntries = Object.entries(androidColorOverrides)
|
|
148
159
|
.map(([name, value]) => ` <color name="${name}">${value}</color>`)
|
|
149
160
|
.join('\n');
|
|
150
161
|
|
|
151
|
-
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
152
|
-
<!-- Generated by @jacques_gordon/expo-mapbox-navigation config plugin -->
|
|
153
|
-
<resources>
|
|
154
|
-
${colorEntries}
|
|
155
|
-
</resources>
|
|
156
|
-
`;
|
|
157
|
-
|
|
158
162
|
fs.writeFileSync(
|
|
159
163
|
path.join(colorsDir, 'mapbox_navigation_colors.xml'),
|
|
160
|
-
|
|
164
|
+
`<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${colorEntries}\n</resources>\n`
|
|
161
165
|
);
|
|
162
|
-
|
|
163
166
|
return mod;
|
|
164
167
|
},
|
|
165
168
|
]);
|
|
@@ -177,16 +180,12 @@ ${colorEntries}
|
|
|
177
180
|
mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription =
|
|
178
181
|
'Your location is used for turn-by-turn navigation even when the app is in the background.';
|
|
179
182
|
}
|
|
180
|
-
|
|
181
|
-
if (!mod.modResults.UIBackgroundModes) {
|
|
182
|
-
mod.modResults.UIBackgroundModes = [];
|
|
183
|
-
}
|
|
183
|
+
if (!mod.modResults.UIBackgroundModes) mod.modResults.UIBackgroundModes = [];
|
|
184
184
|
for (const mode of ['audio', 'location']) {
|
|
185
185
|
if (!mod.modResults.UIBackgroundModes.includes(mode)) {
|
|
186
186
|
mod.modResults.UIBackgroundModes.push(mode);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
|
|
190
189
|
return mod;
|
|
191
190
|
});
|
|
192
191
|
|