Notification

This commit is contained in:
Dimitris
2026-03-27 13:40:14 +01:00
parent 2348d3b633
commit 90010d91b7
6 changed files with 209 additions and 88 deletions

View File

@@ -41,8 +41,7 @@
</intent-filter>
</activity>
<service
android:name="com.kouros.navigation.car.navigation.NavigationService"
android:enabled="true"
android:name=".car.NavigationNotificationService"
android:foregroundServiceType="location"
android:exported="true">
</service>

View File

@@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.car.permission.CAR_SPEED"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature
android:name="android.hardware.type.automotive"
@@ -71,8 +72,7 @@
android:value="true" />
</activity>
<service
android:name="com.kouros.navigation.car.navigation.NavigationService"
android:enabled="true"
android:name=".car.NavigationNotificationService"
android:foregroundServiceType="location"
android:exported="true">
</service>

View File

@@ -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
)
}
}
}
}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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<Place>()
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 {