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 {