From 757c4c8d8daa66f13aa0001c6030643b1f2477e5 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Sat, 28 Mar 2026 10:40:44 +0100 Subject: [PATCH] NavigationService --- app/src/main/AndroidManifest.xml | 6 + .../navigation/car/NavigationSession.kt | 59 ++++- .../car/navigation/NavigationService.kt | 234 ++++++++++++++++++ 3 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 18fefb2..8210e1e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,6 +45,12 @@ android:foregroundServiceType="location" android:exported="true"> + + \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index 6c5ebff..0998a1e 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -1,6 +1,5 @@ package com.kouros.navigation.car -import android.Manifest import android.Manifest.permission import android.content.Intent import android.content.pm.PackageManager @@ -54,8 +53,6 @@ import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.RouteModel -import com.kouros.navigation.model.SettingsViewModel -import com.kouros.navigation.repository.SettingsRepository import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.NavigationUtils.getViewModel @@ -68,7 +65,6 @@ import kotlinx.coroutines.launch import java.time.Duration import java.time.LocalDateTime import java.time.ZoneOffset -import kotlin.collections.listOf import kotlin.math.absoluteValue @@ -86,7 +82,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa // Model for managing route state and navigation logic for Android Auto lateinit var routeModel: RouteCarModel - var route = "" + var route = "" // Main navigation screen displayed to the user lateinit var navigationScreen: NavigationScreen @@ -126,12 +122,59 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa var notificationActive = false + var navigationService: NavigationService? = null + + val serviceListener: NavigationService.Listener = object : NavigationService.Listener { + + override fun navigationStateChanged( + isNavigating: Boolean, + isRerouting: Boolean, + hasArrived: Boolean, + destinations: MutableList?, + steps: MutableList?, + nextDestinationTravelEstimate: TravelEstimate?, + nextStepRemainingDistance: Distance?, + shouldShowNextStep: Boolean, + shouldShowLanes: Boolean, + junctionImage: CarIcon? + ) { + TODO("Not yet implemented") + } + } + + // Monitors the state of the connection to the Navigation service. + val serviceConnection: ServiceConnection = object : ServiceConnection { + @RequiresPermission(allOf = [permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION]) + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.i(TAG, "In onServiceConnected() Session component:$service") + val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder + navigationService = binder.service + navigationService!!.setCarContext(carContext, serviceListener) + } + + override fun onServiceDisconnected(name: ComponentName?) { + Log.i(TAG, "In onServiceDisconnected() component: $name") + // Unhook map models here + navigationService!!.clearCarContext() + navigationService = null + } + } /** * Lifecycle observer for managing session lifecycle events. * Cleans up resources when the session is destroyed. */ private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + Log.i(TAG, "In onStart() Session") + carContext + .bindService( + Intent(carContext, NavigationService::class.java), + serviceConnection, + Context.BIND_AUTO_CREATE + ) + } + override fun onPause(owner: LifecycleOwner) { Log.d(TAG, "NavigationSession paused") super.onPause(owner) @@ -142,6 +185,12 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa super.onResume(owner) } + override fun onStop(owner: LifecycleOwner) { + Log.i(TAG, "In onStop()") + carContext.unbindService(serviceConnection) + navigationService = null + } + override fun onDestroy(owner: LifecycleOwner) { if (::navigationManager.isInitialized) { navigationManager.clearNavigationManagerCallback() diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt new file mode 100644 index 0000000..14e05de --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt @@ -0,0 +1,234 @@ +package com.kouros.navigation.car.navigation + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.location.Location +import android.location.LocationManager +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.annotation.RequiresPermission +import androidx.car.app.CarContext +import androidx.car.app.model.CarIcon +import androidx.car.app.model.Distance +import androidx.car.app.navigation.NavigationManager +import androidx.car.app.navigation.model.Destination +import androidx.car.app.navigation.model.Step +import androidx.car.app.navigation.model.TravelEstimate +import androidx.core.location.LocationListenerCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.kouros.navigation.car.DeviceLocationManager +import com.kouros.navigation.car.DeviceLocationManagerService +import com.kouros.navigation.car.NavigationNotificationService +import com.kouros.navigation.data.Constants.TAG + + +class NavigationService : Service() { + + val TAG: String = "NavigationService" + + val DEEP_LINK_ACTION: String = ("com.kouros.navigation.car.navigation" + + ".NavigationDeepLinkAction") + + val channelId: String = "NavigationServiceChannel" + + /** The identifier for the navigation notification displayed for the foreground service. */ + + val NAV_NOTIFICATION_ID: Int = 87356325 + + /** The identifier for the non-navigation notifications, such as a traffic accident warning. */ + + val NOTIFICATION_ID: Int = 71653346 + + // Constants for location broadcast + val PACKAGE_NAME: String = + "androidx.car.app.sample.navigation.common.nav.navigationservice" + + val EXTRA_STARTED_FROM_NOTIFICATION: String = PACKAGE_NAME + ".started_from_notification" + + val CANCEL_ACTION: String = "CANCEL" + + private var notificationManager: NotificationManager? = null + private var carContext: CarContext? = null + + private lateinit var listener: Listener + + // Model for managing route state and navigation logic for Android Auto + var routeModel = RouteCarModel() + + // Manages device GPS location updates + lateinit var deviceLocationManager: DeviceLocationManagerService + + private lateinit var navigationManager: NavigationManager + private var navigationManagerInitialized = false + var binder: IBinder = LocalBinder() + + + /** A listener for the navigation state changes. */ + interface Listener { + /** Callback called when the navigation state changes. */ + fun navigationStateChanged( + isNavigating: Boolean, + isRerouting: Boolean, + hasArrived: Boolean, + destinations: MutableList?, + steps: MutableList?, + nextDestinationTravelEstimate: TravelEstimate?, + nextStepRemainingDistance: Distance?, + shouldShowNextStep: Boolean, + shouldShowLanes: Boolean, + junctionImage: CarIcon? + ) + } + + /** + * Class used for the client Binder. Since this service runs in the same process as its clients, + * we don't need to deal with IPC. + */ + inner class LocalBinder : Binder() { + val service: NavigationService + get() = this@NavigationService + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return START_STICKY + } + + + override fun onCreate() { + Log.i(TAG, "In onCreate()"); + createNotificationChannel(); + } + + override fun onBind(p0: Intent?): IBinder { + return binder + } + + override fun onUnbind(intent: Intent): Boolean { + if (::deviceLocationManager.isInitialized) { + deviceLocationManager.stopLocationUpdates() + } + return true + } + + override fun onDestroy() { + Log.i(TAG, "In onDestroy()"); + + } + + private fun createNotificationChannel() { + val serviceChannel = NotificationChannel( + "CHANNEL_ID", + "Location Service Channel", + NotificationManager.IMPORTANCE_HIGH + ) + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(serviceChannel) + } + + + + + /** Sets the [CarContext] to use while the service is connected. */ + @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]) + fun setCarContext( + carContext: CarContext, + listener: Listener + ) { + Log.d(TAG, "in setCarContext") + this.carContext = carContext + + deviceLocationManager = DeviceLocationManagerService( + carContext = carContext, + onLocationUpdate = ::updateLocation, + onInitialLocation = { location -> + Log.d(TAG, "Initial location: $location") + } + ) + deviceLocationManager.startLocationUpdates() + + navigationManagerInitialized = true + // navigationManager = + // carContext.getCarService(NavigationManager::class.java) + // navigationManager.setNavigationManagerCallback( + // object : NavigationManagerCallback { + // override fun onStopNavigation() { + // this@NavigationService.stopNavigation() + // } + // + // override fun onAutoDriveEnabled() { + // Log.d(TAG, "onAutoDriveEnabled called") + // CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG) + // .show() + // } + // }) + this.listener = listener + + // Uncomment if navigating + // mNavigationManager.navigationStarted(); + } + + /** Clears the currently used {@link CarContext}. */ + fun clearCarContext() { + Log.i(TAG, "clearContext"); + carContext = null; + deviceLocationManager.stopLocationUpdates() + // navigationManager.clearNavigationManagerCallback(); + } + + /** Starts navigation. */ + fun startNavigation() { + Log.i(TAG, "Starting Navigation") + startService(Intent(applicationContext, NavigationService::class.java)) + Log.i(TAG, "Starting foreground service") + + listener.navigationStateChanged( + isNavigating = false, + isRerouting = true, + hasArrived = false, + destinations = null, + steps = null, + nextDestinationTravelEstimate = null, + nextStepRemainingDistance = null, + shouldShowNextStep = false, + shouldShowLanes = false, + junctionImage = null + ) + + } + + /** Starts navigation. */ + fun stopNavigation() { + // if (navigationManagerInitialized) + // navigationManager.navigationEnded() + listener.navigationStateChanged( + false, + isRerouting = false, + hasArrived = false, + destinations = null, + steps = null, + nextDestinationTravelEstimate = null, + nextStepRemainingDistance = null, + shouldShowNextStep = false, + shouldShowLanes = false, + junctionImage = null, + ) + //stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } + + fun updateLocation(location: Location) { + Log.d(TAG, "Update location: $location") + } + + // private fun createMainActivityPendingIntent(): PendingIntent? { + // val intent: Intent = Intent(this, MainActivity::class.java) + // intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true) + // return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) + // } +}