diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bc046f5d5..dd6400036 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -49,6 +49,13 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
+
+
+
diff --git a/app/src/main/java/org/maplibre/navigation/android/example/CustomNavigationNotification.java b/app/src/main/java/org/maplibre/navigation/android/example/CustomNavigationNotification.java
index 15dd9f55b..8b57270e8 100644
--- a/app/src/main/java/org/maplibre/navigation/android/example/CustomNavigationNotification.java
+++ b/app/src/main/java/org/maplibre/navigation/android/example/CustomNavigationNotification.java
@@ -1,94 +1,96 @@
package org.maplibre.navigation.android.example;
-//import android.app.Notification;
-//import android.app.NotificationChannel;
-//import android.app.NotificationManager;
-//import android.app.PendingIntent;
-//import android.content.BroadcastReceiver;
-//import android.content.Context;
-//import android.content.Intent;
-//import android.content.IntentFilter;
-//import android.graphics.Color;
-//import android.os.Build;
-//
-//import androidx.annotation.RequiresApi;
-//import androidx.core.app.NotificationCompat;
-//
-//import org.maplibre.navigation.core.navigation.notification.NavigationNotification;
-//import org.maplibre.navigation.core.routeprogress.RouteProgress;
-//
-//import static org.maplibre.navigation.core.navigation.NavigationConstants.NAVIGATION_NOTIFICATION_CHANNEL;
-//
-//public class CustomNavigationNotification implements NavigationNotification {
-//
-// private static final int CUSTOM_NOTIFICATION_ID = 91234821;
-// private static final String STOP_NAVIGATION_ACTION = "stop_navigation_action";
-//
-// private final Notification customNotification;
-// private final NotificationCompat.Builder customNotificationBuilder;
-// private final NotificationManager notificationManager;
-// private BroadcastReceiver stopNavigationReceiver;
-// private int numberOfUpdates;
-//
-// public CustomNavigationNotification(Context applicationContext) {
-// notificationManager = (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE);
-//
-// customNotificationBuilder = new NotificationCompat.Builder(applicationContext, NAVIGATION_NOTIFICATION_CHANNEL)
-// .setSmallIcon(org.maplibre.navigation.android.navigation.ui.v5.R.drawable.ic_navigation)
-// .setContentTitle("Custom Navigation Notification")
-// .setContentText("Display your own content here!")
-// .setContentIntent(createPendingStopIntent(applicationContext));
-//
-// customNotification = customNotificationBuilder.build();
-// }
-//
-// @Override
-// public Notification getNotification() {
-// return customNotification;
-// }
-//
-// @Override
-// public int getNotificationId() {
-// return CUSTOM_NOTIFICATION_ID;
-// }
-//
-// @Override
-// public void updateNotification(RouteProgress routeProgress) {
-// // Update the builder with a new number of updates
-// customNotificationBuilder.setContentText("Number of updates: " + numberOfUpdates++);
-//
-// notificationManager.notify(CUSTOM_NOTIFICATION_ID, customNotificationBuilder.build());
-// }
-//
-// @Override
-// public void onNavigationStopped(Context context) {
-// context.unregisterReceiver(stopNavigationReceiver);
-// notificationManager.cancel(CUSTOM_NOTIFICATION_ID);
-// }
-//
-// public void register(BroadcastReceiver stopNavigationReceiver, Context applicationContext) {
-// this.stopNavigationReceiver = stopNavigationReceiver;
-//
-// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-// applicationContext.registerReceiver(stopNavigationReceiver, new IntentFilter(STOP_NAVIGATION_ACTION), Context.RECEIVER_NOT_EXPORTED);
-// } else {
-// applicationContext.registerReceiver(stopNavigationReceiver, new IntentFilter(STOP_NAVIGATION_ACTION));
-// }
-// }
-//
-// private PendingIntent createPendingStopIntent(Context context) {
-// Intent stopNavigationIntent = new Intent(STOP_NAVIGATION_ACTION);
-// return PendingIntent.getBroadcast(context, 0, stopNavigationIntent, PendingIntent.FLAG_IMMUTABLE);
-// }
-//
-// @RequiresApi(Build.VERSION_CODES.O)
-// public void createNotificationChannel(Context context) {
-// NotificationChannel chan = new NotificationChannel(NAVIGATION_NOTIFICATION_CHANNEL, "CustomNavigationNotification", NotificationManager.IMPORTANCE_NONE);
-// chan.setLightColor(Color.BLUE);
-// chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
-// NotificationManager service = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
-// if (service != null) {
-// service.createNotificationChannel(chan);
-// }
-// }
-//}
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Color;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+
+import org.maplibre.navigation.android.navigation.ui.v5.notification.NavigationNotification;
+import org.maplibre.navigation.core.routeprogress.RouteProgress;
+
+import static org.maplibre.navigation.android.navigation.ui.v5.notification.MapLibreNavigationNotification.NAVIGATION_NOTIFICATION_CHANNEL;
+
+public class CustomNavigationNotification implements NavigationNotification {
+
+ private static final int CUSTOM_NOTIFICATION_ID = 91234821;
+ private static final String STOP_NAVIGATION_ACTION = "stop_navigation_action";
+
+ private final Notification customNotification;
+ private final NotificationCompat.Builder customNotificationBuilder;
+ private final NotificationManager notificationManager;
+ private BroadcastReceiver stopNavigationReceiver;
+ private int numberOfUpdates;
+
+ public CustomNavigationNotification(Context applicationContext) {
+ notificationManager = (NotificationManager) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ customNotificationBuilder = new NotificationCompat.Builder(applicationContext, NAVIGATION_NOTIFICATION_CHANNEL)
+ .setSmallIcon(org.maplibre.navigation.android.navigation.ui.v5.R.drawable.ic_navigation)
+ .setContentTitle("Custom Navigation Notification")
+ .setContentText("Display your own content here!")
+ .setContentIntent(createPendingStopIntent(applicationContext));
+
+ customNotification = customNotificationBuilder.build();
+ }
+
+ @Override
+ public Notification getNotification() {
+ return customNotification;
+ }
+
+ @Override
+ public int getNotificationId() {
+ return CUSTOM_NOTIFICATION_ID;
+ }
+
+ @Override
+ public void updateNotification(RouteProgress routeProgress) {
+ // Update the builder with a new number of updates
+ customNotificationBuilder.setContentText("Number of updates: " + numberOfUpdates++);
+
+ notificationManager.notify(CUSTOM_NOTIFICATION_ID, customNotificationBuilder.build());
+ }
+
+ @Override
+ public void onNavigationStopped(Context context) {
+ context.unregisterReceiver(stopNavigationReceiver);
+ notificationManager.cancel(CUSTOM_NOTIFICATION_ID);
+ }
+
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
+ public void register(BroadcastReceiver stopNavigationReceiver, Context applicationContext) {
+ this.stopNavigationReceiver = stopNavigationReceiver;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ applicationContext.registerReceiver(stopNavigationReceiver, new IntentFilter(STOP_NAVIGATION_ACTION), Context.RECEIVER_NOT_EXPORTED);
+ } else {
+ applicationContext.registerReceiver(stopNavigationReceiver, new IntentFilter(STOP_NAVIGATION_ACTION));
+ }
+ }
+
+ private PendingIntent createPendingStopIntent(Context context) {
+ Intent stopNavigationIntent = new Intent(STOP_NAVIGATION_ACTION);
+ return PendingIntent.getBroadcast(context, 0, stopNavigationIntent, PendingIntent.FLAG_IMMUTABLE);
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ public void createNotificationChannel(Context context) {
+ NotificationChannel chan = new NotificationChannel(NAVIGATION_NOTIFICATION_CHANNEL, "CustomNavigationNotification", NotificationManager.IMPORTANCE_NONE);
+ chan.setLightColor(Color.BLUE);
+ chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
+ NotificationManager service = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (service != null) {
+ service.createNotificationChannel(chan);
+ }
+ }
+}
diff --git a/app/src/main/java/org/maplibre/navigation/android/example/MainActivity.java b/app/src/main/java/org/maplibre/navigation/android/example/MainActivity.java
index d2c6ef0cb..db4d8147a 100644
--- a/app/src/main/java/org/maplibre/navigation/android/example/MainActivity.java
+++ b/app/src/main/java/org/maplibre/navigation/android/example/MainActivity.java
@@ -63,6 +63,11 @@ protected void onCreate(Bundle savedInstanceState) {
getString(R.string.description_foreground_notification),
NavigationWithForegroundNotificationActivity.class
));
+ list.add(new SampleItem(
+ getString(R.string.title_custom_foreground_notification),
+ getString(R.string.description_custom_foreground_notification),
+ NavigationWithForegroundNotificationActivity.class
+ ));
RecyclerView.Adapter adapter = new MainAdapter(list);
recyclerView.setAdapter(adapter);
diff --git a/app/src/main/java/org/maplibre/navigation/android/example/MockNavigationActivity.kt b/app/src/main/java/org/maplibre/navigation/android/example/MockNavigationActivity.kt
index 8ccbb4b53..1aecf2cb4 100644
--- a/app/src/main/java/org/maplibre/navigation/android/example/MockNavigationActivity.kt
+++ b/app/src/main/java/org/maplibre/navigation/android/example/MockNavigationActivity.kt
@@ -85,20 +85,7 @@ class MockNavigationActivity :
getMapAsync(this@MockNavigationActivity)
}
- val context = applicationContext
-// val customNotification =
-// CustomNavigationNotification(
-// context
-// )
-// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-// customNotification.createNotificationChannel(this)
-// }
- val options = MapLibreNavigationOptions(
-//// navigationNotification = customNotification
- )
-//
- navigation = AndroidMapLibreNavigation(context, options)
-
+ navigation = AndroidMapLibreNavigation(applicationContext)
navigation.addMilestone(
RouteMilestone(
identifier = BEGIN_ROUTE_MILESTONE,
@@ -116,8 +103,7 @@ class MockNavigationActivity :
),
)
)
-// customNotification.register(MyBroadcastReceiver(navigation), context)
-//
+
binding.startRouteButton.setOnClickListener {
route?.let { route ->
binding.startRouteButton.visibility = View.INVISIBLE
diff --git a/app/src/main/java/org/maplibre/navigation/android/example/NavigationWithCustomForegroundNotificationActivity.kt b/app/src/main/java/org/maplibre/navigation/android/example/NavigationWithCustomForegroundNotificationActivity.kt
new file mode 100644
index 000000000..03146e514
--- /dev/null
+++ b/app/src/main/java/org/maplibre/navigation/android/example/NavigationWithCustomForegroundNotificationActivity.kt
@@ -0,0 +1,229 @@
+package org.maplibre.navigation.android.example
+
+import android.location.Location as AndroidLocation
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import org.maplibre.navigation.core.models.DirectionsResponse
+import org.maplibre.geojson.Point
+import org.maplibre.android.location.LocationComponent
+import org.maplibre.android.location.LocationComponentActivationOptions
+import org.maplibre.android.location.OnLocationCameraTransitionListener
+import org.maplibre.android.location.modes.CameraMode
+import org.maplibre.android.location.modes.RenderMode
+import org.maplibre.android.maps.MapLibreMap
+import org.maplibre.android.maps.OnMapReadyCallback
+import org.maplibre.android.maps.Style
+import org.maplibre.navigation.android.navigation.ui.v5.route.NavigationRoute
+import org.maplibre.navigation.core.location.replay.ReplayRouteLocationEngine
+import org.maplibre.navigation.core.models.DirectionsRoute
+import org.maplibre.navigation.core.routeprogress.ProgressChangeListener
+import org.maplibre.navigation.core.routeprogress.RouteProgress
+import okhttp3.Request
+import org.maplibre.navigation.android.example.databinding.ActivitySnapToRouteNavigationBinding
+import org.maplibre.navigation.android.navigation.ui.v5.notification.NavigationNotification
+import org.maplibre.navigation.android.navigation.ui.v5.notification.service.NavigationNotificationService
+import org.maplibre.navigation.android.navigation.ui.v5.notification.service.NavigationNotificationServiceConnection
+import org.maplibre.navigation.android.navigation.ui.v5.route.NavigationMapRoute
+import org.maplibre.navigation.core.location.Location
+import org.maplibre.navigation.core.models.UnitType
+import org.maplibre.navigation.core.navigation.AndroidMapLibreNavigation
+import org.maplibre.navigation.core.navigation.MapLibreNavigation
+import org.maplibre.navigation.core.navigation.MapLibreNavigationOptions
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import timber.log.Timber
+
+/**
+ * This activity shows you how navigation with active foreground notification is setup.
+ *
+ * You need to do the following steps to enable foreground notification:
+ * 1. When [MapLibreNavigation] is ready, create instance of [NavigationNotificationServiceConnection]
+ * 2. Start the service with [NavigationNotificationServiceConnection.start]
+ * 3. Don't forget to disconnect the service when the activity gets destroyed
+ *
+ * You can also handle the lifecycle of [NavigationNotificationService] by yourself. Using your own
+ * custom [NavigationNotification] can done by injecting in [NavigationNotificationServiceConnection] constructor.
+ */
+class NavigationWithCustomForegroundNotificationActivity : AppCompatActivity(), OnMapReadyCallback,
+ ProgressChangeListener {
+
+ private lateinit var binding: ActivitySnapToRouteNavigationBinding
+ private lateinit var mapLibreMap: MapLibreMap
+ private var locationEngine: ReplayRouteLocationEngine =
+ ReplayRouteLocationEngine()
+ private lateinit var navigation: MapLibreNavigation
+ private var route: DirectionsRoute? = null
+ private var navigationMapRoute: NavigationMapRoute? = null
+ private var notificationServiceConnection: NavigationNotificationServiceConnection? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivitySnapToRouteNavigationBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ navigation = AndroidMapLibreNavigation(
+ this,
+ MapLibreNavigationOptions(snapToRoute = true)
+ ).apply {
+ snapEngine
+ addProgressChangeListener(this@NavigationWithCustomForegroundNotificationActivity)
+ }
+
+ binding.mapView.apply {
+ onCreate(savedInstanceState)
+ getMapAsync(this@NavigationWithCustomForegroundNotificationActivity)
+ }
+
+ binding.btnFollow.setOnClickListener {
+ followLocation()
+ }
+ }
+
+ private var locationComponent: LocationComponent? = null
+
+ override fun onMapReady(mapLibreMap: MapLibreMap) {
+ this.mapLibreMap = mapLibreMap
+ mapLibreMap.setStyle(
+ Style.Builder().fromUri(getString(R.string.map_style_light))
+ ) { style ->
+ enableLocationComponent(style)
+ navigationMapRoute = NavigationMapRoute(navigation, binding.mapView, mapLibreMap)
+ startNavigationService(navigation)
+ calculateRouteAndStartNavigation()
+ }
+ }
+
+ @SuppressWarnings("MissingPermission")
+ private fun enableLocationComponent(style: Style) {
+ locationComponent = mapLibreMap.locationComponent
+ mapLibreMap.locationComponent.activateLocationComponent(
+ LocationComponentActivationOptions.builder(
+ this,
+ style,
+ )
+ .useDefaultLocationEngine(false)
+ .build()
+ )
+
+ followLocation()
+
+ mapLibreMap.locationComponent.isLocationComponentEnabled = true
+ }
+
+ private fun followLocation() {
+ if (!mapLibreMap.locationComponent.isLocationComponentActivated) {
+ return
+ }
+
+ mapLibreMap.locationComponent.renderMode = RenderMode.GPS
+ mapLibreMap.locationComponent.setCameraMode(
+ CameraMode.TRACKING_GPS,
+ object :
+ OnLocationCameraTransitionListener {
+ override fun onLocationCameraTransitionFinished(cameraMode: Int) {
+ mapLibreMap.locationComponent.zoomWhileTracking(17.0)
+ mapLibreMap.locationComponent.tiltWhileTracking(60.0)
+ }
+
+ override fun onLocationCameraTransitionCanceled(cameraMode: Int) {}
+ }
+ )
+ }
+
+ private fun startNavigationService(mapLibreNavigation: MapLibreNavigation) {
+ notificationServiceConnection = NavigationNotificationServiceConnection(mapLibreNavigation, CustomNavigationNotification(this))
+ notificationServiceConnection?.start(this)
+ }
+
+ private fun calculateRouteAndStartNavigation() {
+ val navigationRouteBuilder = NavigationRoute.builder(this).apply {
+ this.accessToken(getString(R.string.mapbox_access_token))
+ this.origin(Point.fromLngLat(9.7536318, 52.3717979))
+ this.addWaypoint(Point.fromLngLat(9.741052, 52.360496))
+ this.destination(Point.fromLngLat(9.756259, 52.342620))
+ this.voiceUnits(UnitType.METRIC)
+ this.alternatives(true)
+ this.baseUrl(getString(R.string.base_url))
+ }
+
+ navigationRouteBuilder.build().getRoute(object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ Timber.d("Url: %s", (call.request() as Request).url.toString())
+ response.body()?.let { responseBody ->
+ if (responseBody.routes.isNotEmpty()) {
+ val maplibreResponse = DirectionsResponse.fromJson(responseBody.toJson());
+ val directionsRoute = maplibreResponse.routes.first()
+ this@NavigationWithCustomForegroundNotificationActivity.route = directionsRoute
+ navigationMapRoute?.addRoutes(maplibreResponse.routes)
+
+ startNavigation()
+ }
+ }
+ }
+
+ override fun onFailure(call: Call, throwable: Throwable) {
+ Timber.e(throwable, "onFailure: navigation.getRoute()")
+ }
+ })
+ }
+
+ fun startNavigation() {
+ route?.let { route ->
+ locationEngine.also { locationEngine ->
+ locationEngine.assign(route)
+ navigation.locationEngine = locationEngine
+ navigation.startNavigation(route)
+ }
+ }
+ }
+
+ override fun onProgressChange(location: Location, routeProgress: RouteProgress) {
+ // Update own location with the snapped location
+ locationComponent?.forceLocationUpdate(
+ AndroidLocation(location.provider).apply {
+ latitude = location.latitude
+ longitude = location.longitude
+ bearing = location.bearing ?: 0f
+ accuracy = location.accuracyMeters ?: 0.0f
+ }
+ )
+ }
+
+
+ override fun onResume() {
+ super.onResume()
+ binding.mapView.onResume()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ binding.mapView.onPause()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ binding.mapView.onStart()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ binding.mapView.onStop()
+ }
+
+ override fun onLowMemory() {
+ super.onLowMemory()
+ binding.mapView.onLowMemory()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ notificationServiceConnection?.stop(this)
+ navigation.onDestroy()
+ binding.mapView.onDestroy()
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 324f0e304..303130c27 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -13,6 +13,9 @@
Foreground notification
Show a foreground notification that allows continue navigation when app is minimized.
+ Custom foreground notification
+ Show a custom foreground notification instead of the default one.
+
Valhalla Navigation
Use Valhalla routing server to generate directions.