diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 75f3814..18fefb2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -41,8 +41,7 @@
diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml
index c5e7047..3a5eba8 100644
--- a/automotive/src/main/AndroidManifest.xml
+++ b/automotive/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
+
diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationNotificationService.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationNotificationService.kt
index ac2437d..190e1a4 100644
--- a/common/car/src/main/java/com/kouros/navigation/car/NavigationNotificationService.kt
+++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationNotificationService.kt
@@ -9,6 +9,7 @@ import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
+import android.util.Log
import androidx.car.app.notification.CarAppExtender
import androidx.car.app.notification.CarNotificationManager
import androidx.car.app.notification.CarPendingIntent
@@ -16,6 +17,7 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.kouros.data.R
+import com.kouros.navigation.data.Constants.TAG
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.concurrent.TimeUnit
@@ -44,17 +46,12 @@ class NavigationNotificationService : Service() {
Handler(Looper.getMainLooper(), HandlerCallback())
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ val message = intent.getStringExtra("EXTRA_MESSAGE") ?: "Navigating..."
initNotifications(this)
- startForeground(
- NAV_NOTIFICATION_ID,
- getNavigationNotification(this, mNotificationCount).build()
- )
-
- // Start updating the notification continuously.
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_SEND_NOTIFICATION), NAV_NOTIFICATION_DELAY_IN_MILLIS
- )
-
+ val notification = getNavigationNotification(this, message)
+ // This updates the existing notification if the service is already running
+ CarNotificationManager.from(this).notify(NAV_NOTIFICATION_ID, notification)
+ startForeground(NAV_NOTIFICATION_ID, notification.build())
return START_NOT_STICKY
}
@@ -71,11 +68,12 @@ class NavigationNotificationService : Service() {
*/
internal inner class HandlerCallback : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
+ Log.d(TAG, "Notification handleMessage: $msg")
if (msg.what == MSG_SEND_NOTIFICATION) {
val context: Context = this@NavigationNotificationService
CarNotificationManager.from(context).notify(
NAV_NOTIFICATION_ID,
- getNavigationNotification(context, mNotificationCount)
+ getNavigationNotification(context, "Nachricht")
)
mNotificationCount++
mHandler.sendMessageDelayed(
@@ -96,6 +94,12 @@ class NavigationNotificationService : Service() {
val mOnlyAlertOnce: Boolean
)
+ fun startForeground(message: String) {
+ startForeground(
+ NAV_NOTIFICATION_ID,
+ getNavigationNotification(this, message).build()
+ )
+ }
companion object {
private const val MSG_SEND_NOTIFICATION = 1
private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00"
@@ -124,13 +128,32 @@ class NavigationNotificationService : Service() {
CarNotificationManager.from(context).createNotificationChannel(navChannel)
}
+ /**
+ * Returns a [DirectionInfo] that corresponds to the given notification count.
+ *
+ *
+ * There are 5 directions, repeating in order. For each direction, the alert will only show
+ * once, but the distance will update on every count on the rail widget.
+ */
+ private fun getDirectionInfo(context: Context, message: String): DirectionInfo {
+ val formatter = DecimalFormat("#.##")
+ formatter.setRoundingMode(RoundingMode.DOWN)
+ val distance = formatter.format((10) * 0.1) + "km"
+ return DirectionInfo(
+ message,
+ distance,
+ R.drawable.navigation_48px,
+ false
+ )
+ }
+
/** Returns the navigation notification that corresponds to the given notification count. */
fun getNavigationNotification(
- context: Context, notificationCount: Int
+ context: Context
): NotificationCompat.Builder {
val builder =
NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
- val directionInfo = getDirectionInfo(context, notificationCount)
+ val directionInfo = getDirectionInfo(context, "Test")
// Set an intent to open the car app. The app receives this intent when the user taps the
// heads-up notification or the rail widget.
@@ -179,54 +202,59 @@ class NavigationNotificationService : Service() {
)
}
- /**
- * Returns a [DirectionInfo] that corresponds to the given notification count.
- *
- *
- * There are 5 directions, repeating in order. For each direction, the alert will only show
- * once, but the distance will update on every count on the rail widget.
- */
- private fun getDirectionInfo(context: Context, notificationCount: Int): DirectionInfo {
- val formatter = DecimalFormat("#.##")
- formatter.setRoundingMode(RoundingMode.DOWN)
- val repeatingCount = notificationCount % 35
- if (repeatingCount in 0..<10) {
- // Distance decreases from 1km to 0.1km
- val distance = formatter.format((10 - repeatingCount) * 0.1) + "km"
- return DirectionInfo(
- context.getString(R.string.stop_action_title),
- distance,
- R.drawable.arrow_back_24px,
- repeatingCount > 0
+ fun getNavigationNotification(
+ context: Context, message: String
+ ): NotificationCompat.Builder {
+ val builder =
+ NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
+ val directionInfo = getDirectionInfo(context, message)
+
+ // Set an intent to open the car app. The app receives this intent when the user taps the
+ // heads-up notification or the rail widget.
+ val pendingIntent = CarPendingIntent.getCarApp(
+ context,
+ NavigationCarAppService().intentActionNavNotificationOpenApp.hashCode(),
+ Intent(
+ NavigationCarAppService().intentActionNavNotificationOpenApp
+ ).setComponent(
+ ComponentName(
+ context,
+ NavigationCarAppService()::class.java
+ )
+ ).setData(
+ NavigationCarAppService().createDeepLinkUri(
+ NavigationCarAppService().intentActionNavNotificationOpenApp
+ )
+ ),
+ 0
+ )
+
+ return builder
+ // This title, text, and icon will be shown in both phone and car screen. These
+ // values can
+ // be overridden in the extender below, to customize notifications in the car
+ // screen.
+ .setContentTitle(directionInfo.mTitle)
+ .setContentText(directionInfo.mDistance)
+ .setSmallIcon(directionInfo.mIcon) // The notification must be set to 'ongoing' and its category must be set to
+ // CATEGORY_NAVIGATION in order to show it in the rail widget when the app is
+ // navigating on
+ // the background.
+ // These values cannot be overridden in the extender.
+
+ .setOngoing(true)
+ .setCategory(NotificationCompat.CATEGORY_NAVIGATION) // If set to true, the notification will only show the alert once in both phone and
+ // car screen. This value cannot be overridden in the extender.
+
+ .setOnlyAlertOnce(directionInfo.mOnlyAlertOnce) // This extender must be set in order to display the notification in the car screen.
+ // The extender also allows various customizations, such as showing different title
+ // or icon on the car screen.
+
+ .extend(
+ CarAppExtender.Builder()
+ .setContentIntent(pendingIntent)
+ .build()
)
- } else if (repeatingCount in 10..<20) {
- // Distance decreases from 5km to 0.5km
- val distance = formatter.format((20 - repeatingCount) * 0.5) + "km"
- return DirectionInfo(
- context.getString(R.string.route_preview),
- distance,
- R.drawable.ic_turn_normal_right, /* onlyAlertOnce= */
- repeatingCount > 10
- )
- } else if (repeatingCount in 20..<25) {
- // Distance decreases from 200m to 40m
- val distance = formatter.format(((25 - repeatingCount) * 40).toLong()) + "m"
- return DirectionInfo(
- context.getString(R.string.route_preview),
- distance,
- R.drawable.navigation_48px, /* onlyAlertOnce= */
- repeatingCount > 20
- )
- } else {
- // Distance decreases from 1km to 0.1km
- val distance = formatter.format((35 - repeatingCount) * 0.1) + "km"
- return DirectionInfo(
- context.getString(R.string.charging_station),
- distance,
- R.drawable.local_gas_station_24,
- repeatingCount > 25
- )
- }
}
}
}
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 c8de4ad..7c15b58 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
@@ -100,6 +100,9 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
lateinit var textToSpeechManager: TextToSpeechManager
+ lateinit var notificationManager: NotificationManager
+
+
var autoDriveEnabled = false
val simulation = Simulation()
@@ -117,12 +120,24 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
var navigationManagerStarted = false
+ var notificationActive = false
+
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
*/
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
+ override fun onPause(owner: LifecycleOwner) {
+ Log.d(TAG, "NavigationSession paused")
+ super.onPause(owner)
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ Log.d(TAG, "NavigationSession resumed")
+ super.onResume(owner)
+ }
+
override fun onDestroy(owner: LifecycleOwner) {
if (::navigationManager.isInitialized) {
navigationManager.clearNavigationManagerCallback()
@@ -136,6 +151,13 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup()
}
+ carContext
+ .stopService(
+ Intent(
+ carContext,
+ NavigationNotificationService::class.java
+ )
+ )
Log.i(TAG, "NavigationSession destroyed")
}
}
@@ -314,6 +336,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it
})
+ notificationManager = NotificationManager(carContext, this)
}
/**
@@ -548,23 +571,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
}
}
- /**
- * Stops active navigation and clears route state.
- * Called when user exits navigation or arrives at destination.
- */
- override fun stopNavigation() {
- routeModel.stopNavigation()
- navigationManager.navigationEnded()
- if (autoDriveEnabled) {
- simulation.stopSimulation()
- autoDriveEnabled = false
- }
- surfaceRenderer.routeData.value = ""
- lastCameraSearch = 0
- surfaceRenderer.viewStyle = ViewStyle.VIEW
- navigationScreen.navigationType = NavigationType.VIEW
- }
-
/**
* Start navigation process.
* Called when user starts navigation
@@ -580,6 +586,27 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
updateLocation(location)
}
}
+ if (notificationActive)
+ notificationManager.startNotificationService()
+ }
+
+ /**
+ * Stops active navigation and clears route state.
+ * Called when user exits navigation or arrives at destination.
+ */
+ override fun stopNavigation() {
+ routeModel.stopNavigation()
+ navigationManager.navigationEnded()
+ if (autoDriveEnabled) {
+ simulation.stopSimulation()
+ autoDriveEnabled = false
+ }
+ surfaceRenderer.routeData.value = ""
+ lastCameraSearch = 0
+ surfaceRenderer.viewStyle = ViewStyle.VIEW
+ navigationScreen.navigationType = NavigationType.VIEW
+ if (notificationActive)
+ notificationManager.stopNotificationService()
}
override fun updateTrip(trip: Trip) {
@@ -598,6 +625,9 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
textToSpeechManager.speak(stepData.message)
lastStepIndex = currentStep.index
+ if (notificationActive) {
+ notificationManager.sendMessage(stepData.message)
+ }
}
}
@@ -691,11 +721,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
* Loads a route to the specified place and sets it as the destination.
*/
override fun navigateToPlace(place: Place) {
- val preview = place.route //navigationViewModel.previewRoute.value
+ val preview = place.route
navigationViewModel.previewRoute.value = ""
val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place)
- //currentNavigationLocation = location
+ routeModel.navState = routeModel.navState.copy(destination = place)
if (preview.isEmpty()) {
navigationViewModel.loadRoute(
carContext,
@@ -707,7 +737,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
onRouteReceived(preview)
}
- routeModel.navState = routeModel.navState.copy(destination = place)
surfaceRenderer.activateNavigationView()
}
diff --git a/common/car/src/main/java/com/kouros/navigation/car/NotificationManager.kt b/common/car/src/main/java/com/kouros/navigation/car/NotificationManager.kt
new file mode 100644
index 0000000..59c53e7
--- /dev/null
+++ b/common/car/src/main/java/com/kouros/navigation/car/NotificationManager.kt
@@ -0,0 +1,62 @@
+package com.kouros.navigation.car
+
+import android.content.Intent
+import android.location.Location
+import android.os.Message
+import android.util.Log
+import androidx.car.app.CarContext
+import androidx.car.app.hardware.CarHardwareManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.kouros.navigation.data.Constants.TAG
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+class NotificationManager(
+ private val carContext: CarContext,
+ private val lifecycleOwner: LifecycleOwner,
+) {
+
+ private var notificationServiceStarted = false
+
+ private var serviceStarted = false
+
+ init {
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+
+ }
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.DESTROYED) {
+ if (notificationServiceStarted) {
+ stopNotificationService()
+ }
+ }
+ }
+ }
+
+ fun startNotificationService() {
+ val intent = Intent(carContext, NavigationNotificationService::class.java)
+ carContext.startForegroundService(intent)
+ notificationServiceStarted = true
+ }
+
+ fun stopNotificationService() {
+ carContext
+ .stopService(
+ Intent(
+ carContext,
+ NavigationNotificationService::class.java
+ )
+ )
+ notificationServiceStarted = false
+ }
+
+ fun sendMessage(message: String) {
+ val intent = Intent(carContext, NavigationNotificationService::class.java).apply {
+ putExtra("EXTRA_MESSAGE", message)
+ }
+ carContext.startForegroundService(intent)
+ }
+}
\ No newline at end of file
diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt
index 6cc8475..9f2fbb2 100644
--- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt
+++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt
@@ -1,7 +1,11 @@
package com.kouros.navigation.car.screen
+import android.content.Context
+import android.content.Intent
import android.location.Location
import android.location.LocationManager
+import android.os.Build
+import android.os.Build.VERSION_CODES
import android.os.CountDownTimer
import android.os.Handler
import androidx.car.app.CarContext
@@ -31,6 +35,7 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
+import com.kouros.navigation.car.NavigationNotificationService
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants
@@ -54,8 +59,6 @@ open class NavigationScreen(
private val navigationViewModel: NavigationViewModel
) : Screen(carContext) {
- var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
-
var recentPlaces = mutableListOf()
var recentPlace: Place = Place()
@@ -269,7 +272,7 @@ open class NavigationScreen(
val listBuilder = ItemList.Builder()
recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach {
val row = Row.Builder()
- .setTitle(it.name!!)
+ .setTitle(it.name)
.addAction(
createNavigateAction(it)
)
@@ -443,7 +446,7 @@ open class NavigationScreen(
if (place.longitude == 0.0) {
navigationViewModel.findAddress(
"${obj.city} ${obj.street}},",
- currentNavigationLocation
+ surfaceRenderer.lastLocation
)
// result see observer
} else {