@jacques_gordon/expo-mapbox-navigation 2.0.8 → 2.1.0
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,19 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
apply plugin: 'kotlin-android'
|
|
3
|
-
apply plugin: 'maven-publish'
|
|
4
|
-
|
|
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)
|
|
16
|
-
def MAPBOX_MAPS_MIN_VERSION = "11.11.0"
|
|
17
3
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
def MAPBOX_NAV_VERSION = "3.8.1"
|
|
5
|
+
def MAPBOX_MAPS_MIN_VERSION = "11.11.0"
|
|
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
|
-
|
|
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
|
-
externalNativeBuild {
|
|
42
|
-
cmake {
|
|
43
|
-
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
16
|
ndk {
|
|
48
|
-
// Only build for 64-bit ABIs — 16 KB page size requirement is 64-bit only
|
|
49
17
|
abiFilters "arm64-v8a", "x86_64"
|
|
50
18
|
}
|
|
51
19
|
}
|
|
@@ -60,8 +28,6 @@ android {
|
|
|
60
28
|
}
|
|
61
29
|
|
|
62
30
|
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
31
|
jniLibs {
|
|
66
32
|
useLegacyPackaging = false
|
|
67
33
|
}
|
|
@@ -75,13 +41,9 @@ android {
|
|
|
75
41
|
repositories {
|
|
76
42
|
mavenCentral()
|
|
77
43
|
google()
|
|
78
|
-
|
|
79
|
-
// Mapbox Maven repository — requires a secret token (sk.*)
|
|
80
44
|
maven {
|
|
81
45
|
url 'https://api.mapbox.com/downloads/v2/releases/maven'
|
|
82
|
-
authentication {
|
|
83
|
-
basic(BasicAuthentication)
|
|
84
|
-
}
|
|
46
|
+
authentication { basic(BasicAuthentication) }
|
|
85
47
|
credentials {
|
|
86
48
|
username = 'mapbox'
|
|
87
49
|
password = project.hasProperty('MAPBOX_DOWNLOADS_TOKEN')
|
|
@@ -95,22 +57,30 @@ dependencies {
|
|
|
95
57
|
implementation project(':expo-modules-core')
|
|
96
58
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${safeExtGet('kotlinVersion', '1.9.0')}"
|
|
97
59
|
|
|
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}"
|
|
60
|
+
// Mapbox Navigation SDK v3
|
|
61
|
+
implementation "com.mapbox.navigationcore:android:${MAPBOX_NAV_VERSION}"
|
|
104
62
|
|
|
105
|
-
//
|
|
106
|
-
// Prevents gradle from resolving an older transitive version (issue #43 root cause)
|
|
63
|
+
// Mapbox Maps SDK
|
|
107
64
|
implementation("com.mapbox.maps:android:${MAPBOX_MAPS_MIN_VERSION}") {
|
|
108
65
|
transitive = true
|
|
109
66
|
}
|
|
110
67
|
|
|
111
|
-
//
|
|
68
|
+
// Navigation UI Components (ManeuverView, SpeedInfoView)
|
|
69
|
+
implementation "com.mapbox.navigationcore:ui-components:${MAPBOX_NAV_VERSION}"
|
|
70
|
+
|
|
71
|
+
// TripData (ManeuverApi, SpeedInfoApi, TripProgressApi)
|
|
72
|
+
implementation "com.mapbox.navigationcore:tripdata:${MAPBOX_NAV_VERSION}"
|
|
73
|
+
|
|
74
|
+
// UI Maps (NavigationStyles, NavigationLocationProvider, NavigationCamera)
|
|
75
|
+
implementation "com.mapbox.navigationcore:ui-maps:${MAPBOX_NAV_VERSION}"
|
|
76
|
+
|
|
77
|
+
// ConstraintLayout — required because MapboxManeuverView extends ConstraintLayout
|
|
78
|
+
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
79
|
+
|
|
80
|
+
// AndroidX
|
|
112
81
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
|
113
82
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
|
83
|
+
implementation 'androidx.core:core-ktx:1.13.1'
|
|
114
84
|
}
|
|
115
85
|
|
|
116
86
|
def safeExtGet(prop, fallback) {
|
|
@@ -2,13 +2,22 @@ package expo.modules.mapboxnavigation
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
+
import android.graphics.Color
|
|
5
6
|
import android.util.Log
|
|
7
|
+
import android.view.Gravity
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.widget.FrameLayout
|
|
10
|
+
import android.widget.LinearLayout
|
|
11
|
+
import android.widget.TextView
|
|
6
12
|
import com.mapbox.api.directions.v5.models.RouteOptions
|
|
13
|
+
import com.mapbox.common.location.Location
|
|
7
14
|
import com.mapbox.geojson.Point
|
|
8
15
|
import com.mapbox.maps.MapView
|
|
9
|
-
import com.mapbox.maps.Style
|
|
10
16
|
import com.mapbox.maps.plugin.animation.camera
|
|
17
|
+
import com.mapbox.maps.plugin.locationcomponent.location
|
|
18
|
+
import com.mapbox.navigation.base.TimeFormat
|
|
11
19
|
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
|
20
|
+
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
|
|
12
21
|
import com.mapbox.navigation.base.options.NavigationOptions
|
|
13
22
|
import com.mapbox.navigation.base.route.NavigationRoute
|
|
14
23
|
import com.mapbox.navigation.base.route.NavigationRouterCallback
|
|
@@ -18,10 +27,29 @@ import com.mapbox.navigation.core.MapboxNavigation
|
|
|
18
27
|
import com.mapbox.navigation.core.MapboxNavigationProvider
|
|
19
28
|
import com.mapbox.navigation.core.directions.session.RoutesObserver
|
|
20
29
|
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
|
|
30
|
+
import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
|
|
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
|
|
50
|
+
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi
|
|
51
|
+
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView
|
|
52
|
+
import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions
|
|
25
53
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
|
|
26
54
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
|
|
27
55
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
|
|
@@ -29,6 +57,7 @@ import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
|
|
|
29
57
|
import expo.modules.kotlin.AppContext
|
|
30
58
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
31
59
|
import expo.modules.kotlin.views.ExpoView
|
|
60
|
+
import java.util.Calendar
|
|
32
61
|
import java.util.Locale
|
|
33
62
|
|
|
34
63
|
private const val TAG = "ExpoMapboxNavigation"
|
|
@@ -36,8 +65,7 @@ private const val TAG = "ExpoMapboxNavigation"
|
|
|
36
65
|
class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
37
66
|
ExpoView(context, appContext) {
|
|
38
67
|
|
|
39
|
-
// ── EventDispatchers
|
|
40
|
-
// Each event declared here must match the Events(...) list in ExpoMapboxNavigationModule
|
|
68
|
+
// ── EventDispatchers (correct Expo Modules API for View events) ───────────
|
|
41
69
|
private val onRouteProgressChanged by EventDispatcher()
|
|
42
70
|
private val onRoutesReady by EventDispatcher()
|
|
43
71
|
private val onNavigationFinished by EventDispatcher()
|
|
@@ -45,14 +73,32 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
45
73
|
private val onRoutesFailed by EventDispatcher()
|
|
46
74
|
private val onArrival by EventDispatcher()
|
|
47
75
|
|
|
48
|
-
// ──
|
|
76
|
+
// ── Views ─────────────────────────────────────────────────────────────────
|
|
49
77
|
private val mapView: MapView = MapView(context)
|
|
50
|
-
private var
|
|
51
|
-
private var
|
|
52
|
-
private var
|
|
53
|
-
private var
|
|
78
|
+
private var maneuverView: MapboxManeuverView? = null
|
|
79
|
+
private var speedInfoView: MapboxSpeedInfoView? = null
|
|
80
|
+
private var tripProgressView: MapboxTripProgressView? = null
|
|
81
|
+
private var tvEtaTime: TextView? = null
|
|
82
|
+
private var tvDuration: TextView? = null
|
|
83
|
+
private var tvDistance: TextView? = null
|
|
84
|
+
private var etaBar: LinearLayout? = null
|
|
85
|
+
|
|
86
|
+
// ── Navigation APIs (from official TurnByTurnExperienceActivity) ──────────
|
|
87
|
+
private lateinit var navigationCamera: NavigationCamera
|
|
88
|
+
private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
|
|
89
|
+
private lateinit var routeLineApi: MapboxRouteLineApi
|
|
90
|
+
private lateinit var routeLineView: MapboxRouteLineView
|
|
91
|
+
private lateinit var maneuverApi: MapboxManeuverApi
|
|
92
|
+
private lateinit var tripProgressApi: MapboxTripProgressApi
|
|
93
|
+
private lateinit var speedInfoApi: MapboxSpeedInfoApi
|
|
94
|
+
private val routeArrowApi = MapboxRouteArrowApi()
|
|
95
|
+
private lateinit var routeArrowView: MapboxRouteArrowView
|
|
96
|
+
private val navigationLocationProvider = NavigationLocationProvider()
|
|
54
97
|
private var mapboxNavigation: MapboxNavigation? = null
|
|
55
98
|
|
|
99
|
+
// ── Night mode ────────────────────────────────────────────────────────────
|
|
100
|
+
private var isNightMode = false
|
|
101
|
+
|
|
56
102
|
// ── Props ─────────────────────────────────────────────────────────────────
|
|
57
103
|
private var coordinates: List<Map<String, Double>> = emptyList()
|
|
58
104
|
private var waypointIndices: List<Int>? = null
|
|
@@ -69,67 +115,250 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
69
115
|
private var customRasterAboveLayerId: String? = null
|
|
70
116
|
|
|
71
117
|
init {
|
|
72
|
-
|
|
73
|
-
|
|
118
|
+
buildUI()
|
|
119
|
+
initAPIs()
|
|
74
120
|
setupNavigation()
|
|
75
121
|
}
|
|
76
122
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
123
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
124
|
+
// 1. Build programmatic UI
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
126
|
+
private fun buildUI() {
|
|
127
|
+
val dp = context.resources.displayMetrics.density
|
|
128
|
+
val root = FrameLayout(context).apply {
|
|
129
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Full-screen MapView
|
|
133
|
+
root.addView(mapView, FrameLayout.LayoutParams(
|
|
134
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
135
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
136
|
+
))
|
|
80
137
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
138
|
+
// ManeuverView — top banner
|
|
139
|
+
// NOTE: MapboxManeuverView extends ConstraintLayout, cast to View for addView()
|
|
140
|
+
val mv = MapboxManeuverView(context)
|
|
141
|
+
val mvParams = FrameLayout.LayoutParams(
|
|
142
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
143
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
144
|
+
)
|
|
145
|
+
mvParams.gravity = Gravity.TOP
|
|
146
|
+
mv.visibility = View.INVISIBLE // matches official example (INVISIBLE not GONE)
|
|
147
|
+
root.addView(mv as View, mvParams)
|
|
148
|
+
maneuverView = mv
|
|
149
|
+
|
|
150
|
+
// SpeedInfoView — bottom-left
|
|
151
|
+
val siv = MapboxSpeedInfoView(context)
|
|
152
|
+
val sivParams = FrameLayout.LayoutParams(
|
|
153
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
154
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
155
|
+
)
|
|
156
|
+
sivParams.gravity = Gravity.BOTTOM or Gravity.START
|
|
157
|
+
sivParams.setMargins((16 * dp).toInt(), 0, 0, (100 * dp).toInt())
|
|
158
|
+
siv.visibility = View.GONE
|
|
159
|
+
root.addView(siv as View, sivParams)
|
|
160
|
+
speedInfoView = siv
|
|
161
|
+
|
|
162
|
+
// TripProgressView — embedded in bottom bar
|
|
163
|
+
val tpv = MapboxTripProgressView(context)
|
|
164
|
+
tripProgressView = tpv
|
|
165
|
+
|
|
166
|
+
// ETA bottom bar
|
|
167
|
+
val bar = LinearLayout(context).apply {
|
|
168
|
+
orientation = LinearLayout.HORIZONTAL
|
|
169
|
+
setBackgroundColor(Color.WHITE)
|
|
170
|
+
elevation = 8 * dp
|
|
171
|
+
visibility = View.INVISIBLE
|
|
172
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
173
|
+
setPadding(
|
|
174
|
+
(16 * dp).toInt(), (8 * dp).toInt(),
|
|
175
|
+
(16 * dp).toInt(), (8 * dp).toInt()
|
|
85
176
|
)
|
|
177
|
+
}
|
|
178
|
+
val barParams = FrameLayout.LayoutParams(
|
|
179
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
180
|
+
(80 * dp).toInt()
|
|
181
|
+
)
|
|
182
|
+
barParams.gravity = Gravity.BOTTOM
|
|
183
|
+
|
|
184
|
+
val etaTime = TextView(context).apply {
|
|
185
|
+
textSize = 22f
|
|
186
|
+
setTextColor(Color.BLACK)
|
|
187
|
+
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
188
|
+
}
|
|
189
|
+
bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
190
|
+
tvEtaTime = etaTime
|
|
191
|
+
|
|
192
|
+
val center = LinearLayout(context).apply {
|
|
193
|
+
orientation = LinearLayout.VERTICAL
|
|
194
|
+
gravity = Gravity.CENTER
|
|
195
|
+
}
|
|
196
|
+
val dur = TextView(context).apply { textSize = 16f; setTextColor(Color.DKGRAY); gravity = Gravity.CENTER }
|
|
197
|
+
val dist = TextView(context).apply { textSize = 13f; setTextColor(Color.GRAY); gravity = Gravity.CENTER }
|
|
198
|
+
center.addView(dur)
|
|
199
|
+
center.addView(dist)
|
|
200
|
+
bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
|
|
201
|
+
tvDuration = dur
|
|
202
|
+
tvDistance = dist
|
|
203
|
+
|
|
204
|
+
val cancelBtn = TextView(context).apply {
|
|
205
|
+
text = "✕"
|
|
206
|
+
textSize = 22f
|
|
207
|
+
setTextColor(Color.DKGRAY)
|
|
208
|
+
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
209
|
+
setOnClickListener { cancelNavigation() }
|
|
210
|
+
}
|
|
211
|
+
bar.addView(cancelBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
212
|
+
etaBar = bar
|
|
213
|
+
root.addView(bar, barParams)
|
|
214
|
+
|
|
215
|
+
addView(root)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
219
|
+
// 2. Init APIs (from official example — exact same pattern)
|
|
220
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
221
|
+
private fun initAPIs() {
|
|
222
|
+
val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
|
|
223
|
+
|
|
224
|
+
// EXACT same instantiation as TurnByTurnExperienceActivity
|
|
225
|
+
maneuverApi = MapboxManeuverApi(
|
|
226
|
+
MapboxDistanceFormatter(distanceFormatterOptions)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
tripProgressApi = MapboxTripProgressApi(
|
|
230
|
+
TripProgressUpdateFormatter.Builder(context)
|
|
231
|
+
.distanceRemainingFormatter(DistanceRemainingFormatter(distanceFormatterOptions))
|
|
232
|
+
.timeRemainingFormatter(TimeRemainingFormatter(context))
|
|
233
|
+
.percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
|
|
234
|
+
.estimatedTimeToArrivalFormatter(
|
|
235
|
+
EstimatedTimeToArrivalFormatter(context, TimeFormat.NONE_SPECIFIED)
|
|
236
|
+
)
|
|
237
|
+
.build()
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
speedInfoApi = MapboxSpeedInfoApi()
|
|
241
|
+
|
|
242
|
+
val routeLineViewOptions = MapboxRouteLineViewOptions.Builder(context)
|
|
243
|
+
.routeLineBelowLayerId("road-label-navigation")
|
|
244
|
+
.build()
|
|
245
|
+
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
|
|
246
|
+
routeLineView = MapboxRouteLineView(routeLineViewOptions)
|
|
247
|
+
|
|
248
|
+
val routeArrowOptions = RouteArrowOptions.Builder(context).build()
|
|
249
|
+
routeArrowView = MapboxRouteArrowView(routeArrowOptions)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
253
|
+
// 3. Setup Navigation
|
|
254
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
255
|
+
private fun setupNavigation() {
|
|
256
|
+
mapboxNavigation = MapboxNavigationProvider.create(
|
|
257
|
+
NavigationOptions.Builder(context).build()
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
val style = mapStyle ?: getAutoStyle()
|
|
261
|
+
mapView.mapboxMap.loadStyle(style) { mapStyle ->
|
|
262
|
+
|
|
263
|
+
// Route line layers must be initialized first
|
|
264
|
+
routeLineView.initializeLayers(mapStyle)
|
|
265
|
+
|
|
266
|
+
// Camera setup — EXACT same as official example
|
|
86
267
|
viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
|
|
87
268
|
navigationCamera = NavigationCamera(
|
|
88
269
|
mapView.mapboxMap,
|
|
89
270
|
mapView.camera,
|
|
90
|
-
viewportDataSource
|
|
271
|
+
viewportDataSource
|
|
91
272
|
)
|
|
273
|
+
|
|
274
|
+
// Location puck
|
|
275
|
+
mapView.location.apply {
|
|
276
|
+
setLocationProvider(navigationLocationProvider)
|
|
277
|
+
enabled = true
|
|
278
|
+
}
|
|
279
|
+
|
|
92
280
|
registerObservers()
|
|
93
281
|
}
|
|
94
282
|
}
|
|
95
283
|
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
285
|
+
// 4. Observers — based 100% on official TurnByTurnExperienceActivity
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
routeLineApi
|
|
104
|
-
mapView.mapboxMap.style?.
|
|
105
|
-
routeLineView
|
|
293
|
+
if (result.navigationRoutes.isNotEmpty()) {
|
|
294
|
+
// Draw route line
|
|
295
|
+
routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
|
|
296
|
+
mapView.mapboxMap.style?.apply {
|
|
297
|
+
routeLineView.renderRouteDrawData(this, value)
|
|
106
298
|
}
|
|
107
299
|
}
|
|
108
|
-
|
|
109
|
-
viewportDataSource
|
|
110
|
-
|
|
300
|
+
// Update camera viewport
|
|
301
|
+
viewportDataSource.onRouteChanged(result.navigationRoutes.first())
|
|
302
|
+
viewportDataSource.evaluate()
|
|
303
|
+
|
|
304
|
+
// Show navigation UI
|
|
305
|
+
maneuverView?.visibility = View.VISIBLE
|
|
306
|
+
etaBar?.visibility = View.VISIBLE
|
|
111
307
|
|
|
112
308
|
onRoutesReady(mapOf(
|
|
113
|
-
"routeCount" to
|
|
114
|
-
"distanceMeters" to (
|
|
115
|
-
"durationSeconds" to (
|
|
309
|
+
"routeCount" to result.navigationRoutes.size,
|
|
310
|
+
"distanceMeters" to (result.navigationRoutes.first().directionsRoute.distance() ?: 0.0),
|
|
311
|
+
"durationSeconds" to (result.navigationRoutes.first().directionsRoute.duration() ?: 0.0)
|
|
116
312
|
))
|
|
117
313
|
} else {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
314
|
+
// Clear route line
|
|
315
|
+
val style = mapView.mapboxMap.style
|
|
316
|
+
if (style != null) {
|
|
317
|
+
routeLineApi.clearRouteLine { value ->
|
|
318
|
+
routeLineView.renderClearRouteLineValue(style, value)
|
|
121
319
|
}
|
|
320
|
+
routeArrowView.render(style, routeArrowApi.clearArrows())
|
|
122
321
|
}
|
|
123
|
-
viewportDataSource
|
|
124
|
-
viewportDataSource
|
|
322
|
+
viewportDataSource.clearRouteData()
|
|
323
|
+
viewportDataSource.evaluate()
|
|
324
|
+
hideUI()
|
|
125
325
|
}
|
|
126
326
|
}
|
|
127
327
|
})
|
|
128
328
|
|
|
329
|
+
// ── Route progress observer ───────────────────────────────────────────
|
|
330
|
+
// EXACT same pattern as TurnByTurnExperienceActivity.routeProgressObserver
|
|
129
331
|
nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
|
|
130
|
-
|
|
131
|
-
viewportDataSource
|
|
332
|
+
// Update camera
|
|
333
|
+
viewportDataSource.onRouteProgressChanged(routeProgress)
|
|
334
|
+
viewportDataSource.evaluate()
|
|
132
335
|
|
|
336
|
+
// Draw maneuver arrow
|
|
337
|
+
val style = mapView.mapboxMap.style
|
|
338
|
+
if (style != null) {
|
|
339
|
+
val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
|
|
340
|
+
routeArrowView.renderManeuverUpdate(style, maneuverArrowResult)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Maneuver banner — EXACT same fold() pattern as official example
|
|
344
|
+
val maneuvers = maneuverApi.getManeuvers(routeProgress)
|
|
345
|
+
maneuvers.fold(
|
|
346
|
+
{ error -> Log.w(TAG, "Maneuver error: ${error.errorMessage}") },
|
|
347
|
+
{
|
|
348
|
+
maneuverView?.visibility = View.VISIBLE
|
|
349
|
+
maneuverView?.renderManeuvers(maneuvers)
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
// Trip progress — EXACT same as official example
|
|
354
|
+
// getTripProgress returns TripProgressUpdateValue directly (not nullable)
|
|
355
|
+
val tripProgress = tripProgressApi.getTripProgress(routeProgress)
|
|
356
|
+
tripProgressView?.render(tripProgress)
|
|
357
|
+
|
|
358
|
+
// Update our custom ETA bar from tripProgress fields
|
|
359
|
+
updateEtaBar(tripProgress)
|
|
360
|
+
|
|
361
|
+
// Emit event
|
|
133
362
|
onRouteProgressChanged(mapOf(
|
|
134
363
|
"distanceRemaining" to routeProgress.distanceRemaining,
|
|
135
364
|
"durationRemaining" to routeProgress.durationRemaining,
|
|
@@ -140,22 +369,145 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
140
369
|
?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
141
370
|
))
|
|
142
371
|
})
|
|
372
|
+
|
|
373
|
+
// ── Location observer ─────────────────────────────────────────────────
|
|
374
|
+
// EXACT same as official example
|
|
375
|
+
nav.registerLocationObserver(object : LocationObserver {
|
|
376
|
+
override fun onNewRawLocation(rawLocation: Location) {}
|
|
377
|
+
|
|
378
|
+
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
|
|
379
|
+
val enhancedLocation = locationMatcherResult.enhancedLocation
|
|
380
|
+
|
|
381
|
+
// Update location puck
|
|
382
|
+
navigationLocationProvider.changePosition(
|
|
383
|
+
location = enhancedLocation,
|
|
384
|
+
keyPoints = locationMatcherResult.keyPoints,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
// Update camera
|
|
388
|
+
viewportDataSource.onLocationChanged(enhancedLocation)
|
|
389
|
+
viewportDataSource.evaluate()
|
|
390
|
+
|
|
391
|
+
// Speed limit display
|
|
392
|
+
val formatterOptions = DistanceFormatterOptions.Builder(context).build()
|
|
393
|
+
val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(
|
|
394
|
+
locationMatcherResult,
|
|
395
|
+
formatterOptions
|
|
396
|
+
)
|
|
397
|
+
if (speedInfo != null) {
|
|
398
|
+
speedInfoView?.visibility = View.VISIBLE
|
|
399
|
+
speedInfoView?.render(speedInfo)
|
|
400
|
+
} else {
|
|
401
|
+
speedInfoView?.visibility = View.GONE
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Auto day/night switch
|
|
405
|
+
checkAndSwitchDayNight()
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
411
|
+
// 5. ETA bar update
|
|
412
|
+
// TripProgressUpdateValue fields confirmed from official tripdata API
|
|
413
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
414
|
+
private fun updateEtaBar(tripProgress: com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue) {
|
|
415
|
+
// ETA arrival time
|
|
416
|
+
val etaSec = tripProgress.formatter.timeRemainingFormatter
|
|
417
|
+
.formatTime(tripProgress).toString()
|
|
418
|
+
// Calculate arrival clock time
|
|
419
|
+
val totalSec = tripProgress.totalTimeRemaining.toLong()
|
|
420
|
+
val cal = Calendar.getInstance().apply { add(Calendar.SECOND, totalSec.toInt()) }
|
|
421
|
+
tvEtaTime?.text = String.format("%02d:%02d",
|
|
422
|
+
cal.get(Calendar.HOUR_OF_DAY),
|
|
423
|
+
cal.get(Calendar.MINUTE)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
// Duration remaining
|
|
427
|
+
val totalMin = (tripProgress.totalTimeRemaining / 60).toInt()
|
|
428
|
+
tvDuration?.text = if (totalMin >= 60)
|
|
429
|
+
"${totalMin / 60}h ${totalMin % 60}min"
|
|
430
|
+
else
|
|
431
|
+
"${totalMin} min"
|
|
432
|
+
|
|
433
|
+
// Distance remaining
|
|
434
|
+
val distM = tripProgress.distanceRemaining
|
|
435
|
+
tvDistance?.text = if (distM >= 1000)
|
|
436
|
+
String.format("%.1f km", distM / 1000.0)
|
|
437
|
+
else
|
|
438
|
+
"${distM.toInt()} m"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
442
|
+
// 6. Day / Night auto switch
|
|
443
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
444
|
+
private fun getAutoStyle(): String {
|
|
445
|
+
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
446
|
+
return if (hour in 6..20) NavigationStyles.NAVIGATION_DAY_STYLE
|
|
447
|
+
else NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private fun checkAndSwitchDayNight() {
|
|
451
|
+
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
452
|
+
val shouldBeNight = hour !in 6..20
|
|
453
|
+
if (shouldBeNight == isNightMode) return
|
|
454
|
+
isNightMode = shouldBeNight
|
|
455
|
+
|
|
456
|
+
val newStyle = if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
457
|
+
else NavigationStyles.NAVIGATION_DAY_STYLE
|
|
458
|
+
|
|
459
|
+
mapView.mapboxMap.loadStyle(newStyle) { style ->
|
|
460
|
+
routeLineView.initializeLayers(style)
|
|
461
|
+
mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
|
|
462
|
+
routeLineApi.setNavigationRoutes(routes) { value ->
|
|
463
|
+
routeLineView.renderRouteDrawData(style, value)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
etaBar?.setBackgroundColor(
|
|
469
|
+
if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE
|
|
470
|
+
)
|
|
471
|
+
val tc = if (shouldBeNight) Color.WHITE else Color.BLACK
|
|
472
|
+
tvEtaTime?.setTextColor(tc)
|
|
473
|
+
tvDuration?.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
|
|
474
|
+
tvDistance?.setTextColor(Color.GRAY)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
478
|
+
// 7. Cancel navigation
|
|
479
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
480
|
+
private fun cancelNavigation() {
|
|
481
|
+
mapboxNavigation?.setNavigationRoutes(listOf())
|
|
482
|
+
mapboxNavigation?.stopTripSession()
|
|
483
|
+
hideUI()
|
|
484
|
+
onNavigationCancelled(mapOf<String, Any>())
|
|
143
485
|
}
|
|
144
486
|
|
|
145
|
-
|
|
487
|
+
private fun hideUI() {
|
|
488
|
+
maneuverView?.visibility = View.INVISIBLE
|
|
489
|
+
speedInfoView?.visibility = View.GONE
|
|
490
|
+
etaBar?.visibility = View.INVISIBLE
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
494
|
+
// 8. Issue #43 — safe camera following
|
|
495
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
146
496
|
private fun safeCameraFollowing() {
|
|
147
497
|
try {
|
|
148
|
-
navigationCamera
|
|
498
|
+
navigationCamera.requestNavigationCameraToFollowing(
|
|
149
499
|
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
|
|
150
500
|
.maxDuration(0)
|
|
151
501
|
.build()
|
|
152
502
|
)
|
|
153
503
|
} catch (e: Exception) {
|
|
154
|
-
Log.e(TAG, "Camera transition error
|
|
504
|
+
Log.e(TAG, "Camera transition error: ${e.message}")
|
|
155
505
|
}
|
|
156
506
|
}
|
|
157
507
|
|
|
158
|
-
//
|
|
508
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
509
|
+
// 9. Issue #31 — Voice units
|
|
510
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
159
511
|
private fun resolveVoiceUnits(): String {
|
|
160
512
|
return when (voiceUnits?.lowercase()) {
|
|
161
513
|
"metric" -> "metric"
|
|
@@ -167,7 +519,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
167
519
|
}
|
|
168
520
|
}
|
|
169
521
|
|
|
170
|
-
//
|
|
522
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
523
|
+
// 10. Route request
|
|
524
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
171
525
|
@SuppressLint("MissingPermission")
|
|
172
526
|
private fun fetchRoutes() {
|
|
173
527
|
val nav = mapboxNavigation ?: return
|
|
@@ -183,14 +537,13 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
183
537
|
.language(locale.toLanguageTag())
|
|
184
538
|
.voiceUnits(resolveVoiceUnits())
|
|
185
539
|
.coordinatesList(points)
|
|
540
|
+
.annotations("maxspeed,congestion,duration")
|
|
186
541
|
|
|
187
542
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
188
543
|
navigationProfile?.let {
|
|
189
544
|
builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
|
|
190
545
|
}
|
|
191
|
-
excludeTypes?.takeIf { it.isNotEmpty() }?.let {
|
|
192
|
-
builder.exclude(it.joinToString(","))
|
|
193
|
-
}
|
|
546
|
+
excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
|
|
194
547
|
maxHeight?.let { builder.maxHeight(it) }
|
|
195
548
|
maxWidth?.let { builder.maxWidth(it) }
|
|
196
549
|
|
|
@@ -207,12 +560,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
207
560
|
}
|
|
208
561
|
nav.setNavigationRoutes(routes)
|
|
209
562
|
nav.startTripSession()
|
|
563
|
+
safeCameraFollowing()
|
|
210
564
|
}
|
|
211
565
|
|
|
212
|
-
override fun onFailure(
|
|
213
|
-
reasons: List<RouterFailure>,
|
|
214
|
-
routeOptions: RouteOptions
|
|
215
|
-
) {
|
|
566
|
+
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
216
567
|
val msg = reasons.firstOrNull()?.message ?: "Unknown error"
|
|
217
568
|
Log.e(TAG, "Route request failed: $msg")
|
|
218
569
|
onRoutesFailed(mapOf("message" to msg))
|
|
@@ -249,8 +600,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
249
600
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
250
601
|
override fun onDetachedFromWindow() {
|
|
251
602
|
super.onDetachedFromWindow()
|
|
252
|
-
|
|
253
|
-
|
|
603
|
+
maneuverApi.cancel()
|
|
604
|
+
routeLineApi.cancel()
|
|
605
|
+
routeLineView.cancel()
|
|
254
606
|
mapboxNavigation?.stopTripSession()
|
|
255
607
|
MapboxNavigationProvider.destroy()
|
|
256
608
|
mapView.onStop()
|
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.1.0",
|
|
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",
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
],
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation.git"
|
|
27
27
|
},
|
|
28
28
|
"bugs": {
|
|
29
|
-
"url": "https://github.com/
|
|
29
|
+
"url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation/issues"
|
|
30
30
|
},
|
|
31
31
|
"author": "Jacques Gordon",
|
|
32
32
|
"license": "MIT",
|
|
33
|
-
"homepage": "https://github.com/
|
|
33
|
+
"homepage": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation#readme",
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"expo": ">=53.0.0",
|
|
36
36
|
"react": "*",
|
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
|
|