Notification
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user