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.