@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.
@@ -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
- // Mapbox Navigation SDK v3 for Android
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
- // Explicitly pin Mapbox Maps SDK to ≥ 11.11.0 to guarantee method compatibility
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
- // Lifecycle support
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 — the correct Expo Modules API for View events ───────
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
- // ── Map & Navigation ──────────────────────────────────────────────────────
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
- mapView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
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
- mapView.mapboxMap.loadStyle(mapStyle ?: Style.MAPBOX_STREETS) { _ ->
82
- routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
83
- routeLineView = MapboxRouteLineView(
84
- MapboxRouteLineViewOptions.Builder(context).build()
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
- // ── Issue #43: safe camera ────────────────────────────────────────────────
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 (issue #43): ${e.message}")
482
+ Log.e(TAG, "Camera transition error: ${e.message}")
155
483
  }
156
484
  }
157
485
 
158
- // ── Issue #31: voice units ────────────────────────────────────────────────
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
- // ── Route request ─────────────────────────────────────────────────────────
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}\n` +
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}" // Required for 16 KB page size (expo-mapbox-navigation)`
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}" // Forced by expo-mapbox-navigation for 16KB page size`
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 // 16 KB page size support\n }\n }`
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 — add access token as meta-data ─────────────
97
+ // ── Android: AndroidManifest ─────────────────────────────────────────────
110
98
  config = withAndroidManifest(config, (mod) => {
111
- const mainApp = mod.modResults.manifest.application?.[0];
112
- if (mainApp) {
113
- if (!mainApp['meta-data']) {
114
- mainApp['meta-data'] = [];
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: Write color overrides resource file ──────────────────────────
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
- colorsXml
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.8",
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",
@@ -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}\n` +
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}" // Required for 16 KB page size (expo-mapbox-navigation)`
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}" // Forced by expo-mapbox-navigation for 16KB page size`
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 // 16 KB page size support\n }\n }`
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 — add access token as meta-data ─────────────
97
+ // ── Android: AndroidManifest ─────────────────────────────────────────────
110
98
  config = withAndroidManifest(config, (mod) => {
111
- const mainApp = mod.modResults.manifest.application?.[0];
112
- if (mainApp) {
113
- if (!mainApp['meta-data']) {
114
- mainApp['meta-data'] = [];
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: Write color overrides resource file ──────────────────────────
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
- colorsXml
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