This commit is contained in:
Dimitris
2026-03-26 07:13:31 +01:00
parent 5098dad9d6
commit a3370e42a8
18 changed files with 590 additions and 101 deletions

View File

@@ -6,6 +6,10 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>--> <!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>-->
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="MockLocation" /> tools:ignore="MockLocation" />
@@ -36,6 +40,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name="com.kouros.navigation.car.navigation.NavigationService"
android:enabled="true"
android:foregroundServiceType="location"
android:exported="true">
</service>
</application> </application>
</manifest> </manifest>

View File

@@ -1,11 +1,13 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui
import android.Manifest import android.Manifest
import android.app.AppOpsManager import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.IBinder
import android.widget.Toast import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@@ -41,19 +43,16 @@ import com.google.android.gms.location.LocationServices
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.car.TextToSpeechManager import com.kouros.navigation.car.TextToSpeechManager
import com.kouros.navigation.car.navigation.NavigationService
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.BaseStyleModel import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SimulationType import com.kouros.navigation.model.SimulationType
import com.kouros.navigation.model.simulate
import com.kouros.navigation.model.simulationJob import com.kouros.navigation.model.simulationJob
import com.kouros.navigation.model.test
import com.kouros.navigation.model.testSingle
import com.kouros.navigation.ui.app.AppViewModel import com.kouros.navigation.ui.app.AppViewModel
import com.kouros.navigation.ui.app.appViewModel import com.kouros.navigation.ui.app.appViewModel
import com.kouros.navigation.ui.navigation.AppNavGraph import com.kouros.navigation.ui.navigation.AppNavGraph
@@ -80,10 +79,13 @@ import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
var navigationService: NavigationService? = null
var isBound: Boolean = false
val routeData = MutableLiveData("") val routeData = MutableLiveData("")
val routeModel = RouteModel() val routeModel = RouteModel()
var tilt = TILT var tilt = TILT
val useMock = false
val type = SimulationType.SIMULATE val type = SimulationType.SIMULATE
val stepData: MutableLiveData<StepData> by lazy { val stepData: MutableLiveData<StepData> by lazy {
@@ -96,26 +98,21 @@ class MainActivity : ComponentActivity() {
var lastLocation = location(0.0, 0.0) var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute -> val observer = Observer<String> { newRoute ->
if (newRoute.isNotEmpty()) { if (newRoute.isNotEmpty()) {
val repository = getSettingsRepository(applicationContext) startNavigation(newRoute)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(newRoute)
routeData.value = routeModel.curRoute.routeGeoJson
// checkMock()
} }
} }
// Monitors the state of the connection to the navigation service.
private fun checkMock() { private val serviceConnection: ServiceConnection = object : ServiceConnection {
if (useMock) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
when (type) { val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder
SimulationType.SIMULATE -> simulate(routeModel, mock) navigationService = binder.service
SimulationType.TEST -> test(applicationContext, routeModel) isBound = true
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
else -> {}
} }
override fun onServiceDisconnected(name: ComponentName?) {
navigationService = null
isBound = false
} }
} }
@@ -127,9 +124,7 @@ class MainActivity : ComponentActivity() {
private lateinit var locationManager: LocationManager private lateinit var locationManager: LocationManager
private lateinit var fusedLocationClient: FusedLocationProviderClient private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var mock: MockLocation
private var loadRecentPlaces = false private var loadRecentPlaces = false
lateinit var textToSpeechManager: TextToSpeechManager lateinit var textToSpeechManager: TextToSpeechManager
var guidanceAudio = 0 var guidanceAudio = 0
@@ -152,20 +147,10 @@ class MainActivity : ComponentActivity() {
repository.guidanceAudioFlow.asLiveData().observe(this, Observer { repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it guidanceAudio = it
}) })
if (useMock) {
checkMockLocationEnabled()
}
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? -> fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? ->
navigationViewModel.route.observe(this, observer) navigationViewModel.route.observe(this, observer)
if (useMock) {
mock = MockLocation(locationManager)
mock.setMockLocation(
homeVogelhart.latitude, homeVogelhart.longitude, 0F
)
}
} }
lifecycleScope.launch { lifecycleScope.launch {
getSettingsViewModel(applicationContext).routingEngine.first() getSettingsViewModel(applicationContext).routingEngine.first()
@@ -183,6 +168,27 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onStart() {
super.onStart()
Log.i(TAG, "In onStart()")
bindService(
Intent(this, NavigationService::class.java),
serviceConnection,
BIND_AUTO_CREATE
)
requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)
}
override fun onStop() {
Log.i(TAG, "In onStop(). bound $isBound")
if (isBound) {
unbindService(serviceConnection)
isBound = false
navigationService = null
}
super.onStop()
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun StartScreen( fun StartScreen(
@@ -206,10 +212,8 @@ class MainActivity : ComponentActivity() {
// navigationViewModel.route.value = lastRoute // navigationViewModel.route.value = lastRoute
//} //}
val userLocationState = rememberUserLocationState(locationProvider) val userLocationState = rememberUserLocationState(locationProvider)
if (!useMock) {
val locationState = locationProvider.location.collectAsState() val locationState = locationProvider.location.collectAsState()
updateLocation(locationState.value) updateLocation(locationState.value)
}
val step: StepData? by stepData.observeAsState() val step: StepData? by stepData.observeAsState()
val nextStep: StepData? by nextStepData.observeAsState() val nextStep: StepData? by nextStepData.observeAsState()
@@ -284,7 +288,7 @@ class MainActivity : ComponentActivity() {
step, step,
nextStep, nextStep,
{ stopNavigation {} }, { stopNavigation {} },
{ simulateNavigation() }) { })
} }
} }
} }
@@ -346,18 +350,21 @@ class MainActivity : ComponentActivity() {
} }
} }
fun startNavigation(newRoute: String) {
val repository = getSettingsRepository(applicationContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(newRoute)
routeData.value = routeModel.curRoute.routeGeoJson
navigationService?.startNavigation()
}
fun stopNavigation(closeSheet: () -> Unit) { fun stopNavigation(closeSheet: () -> Unit) {
val latitude = routeModel.curRoute.waypoints[0][1]
val longitude = routeModel.curRoute.waypoints[0][0]
closeSheet() closeSheet()
routeModel.stopNavigation() routeModel.stopNavigation()
getSettingsViewModel(applicationContext).onLastRouteChanged("") getSettingsViewModel(applicationContext).onLastRouteChanged("")
if (useMock) {
simulationJob?.cancel()
mock.setMockLocation(latitude, longitude, 0F)
}
routeData.value = "" routeData.value = ""
stepData.value = StepData("", "", 0.0, 0, 0, 0, 0.0) stepData.value = StepData("", "", 0.0, 0, 0, 0, 0.0)
navigationService?.stopNavigation()
} }
fun textToSpeech() { fun textToSpeech() {
@@ -368,37 +375,5 @@ class MainActivity : ComponentActivity() {
lastStepIndex = currentStep.index lastStepIndex = currentStep.index
} }
} }
fun simulateNavigation() {
simulate(
routeModel = routeModel, mock = mock
)
}
private fun checkMockLocationEnabled() {
try {
// Check if mock location is enabled for this app
val appOpsManager = getSystemService(APP_OPS_SERVICE) as AppOpsManager
val mode = appOpsManager.checkOp(
AppOpsManager.OPSTR_MOCK_LOCATION, Process.myUid(), packageName
)
if (mode != AppOpsManager.MODE_ALLOWED) {
Toast.makeText(
this,
"Please select this app as mock location app in Developer Options",
Toast.LENGTH_LONG
).show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
enum class ExpandedType {
HALF, FULL, COLLAPSED
}
} }

View File

@@ -70,7 +70,12 @@
android:name="distractionOptimized" android:name="distractionOptimized"
android:value="true" /> android:value="true" />
</activity> </activity>
<service
android:name="com.kouros.navigation.car.navigation.NavigationService"
android:enabled="true"
android:foregroundServiceType="location"
android:exported="true">
</service>
</application> </application>
</manifest> </manifest>

View File

@@ -29,7 +29,6 @@ android {
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
} }

View File

@@ -137,7 +137,7 @@ class CarSensorManager(
carCompassListener carCompassListener
) )
carSensors.addCarHardwareLocationListener( carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_NORMAL, CarSensors.UPDATE_RATE_FASTEST,
carContext.mainExecutor, carContext.mainExecutor,
carLocationListener carLocationListener
) )

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kouros.navigation.car
import android.content.Intent
import android.content.res.Configuration
import android.util.Log
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.Session
import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon
import androidx.car.app.model.OnClickListener
import androidx.car.app.navigation.model.Trip
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationService
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
/** Session class for the Navigation sample app. */
internal class ClusterSession : Session(), NavigationListener {
var mNavigationScreen: NavigationScreen? = null
var mNavigationCarSurface: SurfaceRenderer? = null
// A reference to the navigation service used to get location updates and routing.
var service: NavigationService? = null
var mSettingsAction: Action? = null
var routeModel = RouteCarModel()
lateinit var viewModelStoreOwner: ViewModelStoreOwner
override fun onCreateScreen(intent: Intent): Screen {
Log.i(TAG, "In onCreateScreen()")
setupViewModelStore()
mSettingsAction =
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.alt_route_48px
)
)
.build()
)
.setOnClickListener(
OnClickListener {})
.build()
mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
// mNavigationScreen =
// new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface);
val action = intent.action
if (CarContext.ACTION_NAVIGATE == action) {
CarToast.makeText(
carContext,
"Navigation intent: " + intent.dataString,
CarToast.LENGTH_LONG
)
.show()
}
return mNavigationScreen!!
}
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
// mNavigationCarSurface.onCarConfigurationChanged();
}
override fun stopNavigation() {
if (service != null) {
service!!.stopNavigation()
}
}
override fun startNavigation() {
}
override fun updateTrip(trip: Trip) {
}
companion object {
val TAG: String = ClusterSession::class.java.getSimpleName()
}
private fun setupViewModelStore() {
viewModelStoreOwner = object : ViewModelStoreOwner {
override val viewModelStore = ViewModelStore()
}
lifecycleScope.launch {
try {
awaitCancellation()
} finally {
viewModelStoreOwner.viewModelStore.clear()
}
}
}
}

View File

@@ -1,11 +1,15 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.car.app.CarAppService import androidx.car.app.CarAppService
import androidx.car.app.Session import androidx.car.app.Session
import androidx.car.app.SessionInfo import androidx.car.app.SessionInfo
import androidx.car.app.validation.HostValidator import androidx.car.app.validation.HostValidator
import com.kouros.navigation.data.Constants.TAG
class NavigationCarAppService : CarAppService() { class NavigationCarAppService : CarAppService() {
@@ -13,6 +17,7 @@ class NavigationCarAppService : CarAppService() {
val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP = val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP =
"com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP" "com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP"
val channelId: String = "NavigationSessionChannel"
fun createDeepLinkUri(deepLinkAction: String): Uri { fun createDeepLinkUri(deepLinkAction: String): Uri {
return Uri.fromParts(NavigationSession.uriScheme, NavigationSession.uriHost, deepLinkAction) return Uri.fromParts(NavigationSession.uriScheme, NavigationSession.uriHost, deepLinkAction)
@@ -26,8 +31,27 @@ class NavigationCarAppService : CarAppService() {
} }
override fun onCreateSession(sessionInfo: SessionInfo): Session { override fun onCreateSession(sessionInfo: SessionInfo): Session {
Log.d(TAG, "Display Type: ${sessionInfo.displayType}")
if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) {
return ClusterSession()
} else {
createNotificationChannel()
return NavigationSession() return NavigationSession()
} }
} }
private fun createNotificationChannel() {
val notificationManager =
getSystemService(NotificationManager::class.java)
val name: CharSequence = "Car App Service"
val serviceChannel =
NotificationChannel(
channelId,
name,
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(serviceChannel)
}
}

View File

@@ -1,17 +1,27 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.Manifest.permission import android.Manifest.permission
import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.Session import androidx.car.app.Session
import androidx.car.app.connection.CarConnection import androidx.car.app.connection.CarConnection
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.navigation.NavigationManager import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
@@ -22,6 +32,7 @@ import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.navigation.car.navigation.NavigationService
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.navigation.Simulation import com.kouros.navigation.car.navigation.Simulation
import com.kouros.navigation.car.screen.NavigationListener import com.kouros.navigation.car.screen.NavigationListener
@@ -36,7 +47,6 @@ import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
@@ -47,9 +57,7 @@ import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -90,11 +98,64 @@ class NavigationSession : Session(), NavigationListener {
var navigationManagerStarted = false var navigationManagerStarted = false
var navigationService : NavigationService? = null
val serviceListener : NavigationService.Listener = object : NavigationService.Listener {
override fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination?>?,
steps: MutableList<Step>?,
nextDestinationTravelEstimate: TravelEstimate?,
nextStepRemainingDistance: Distance?,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?
) {
//navigationScreen.updateTrip()
}
}
// Monitors the state of the connection to the Navigation service.
val serviceConnection: ServiceConnection = object : ServiceConnection {
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. * Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed. * Cleans up resources when the session is destroyed.
*/ */
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { 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 onStop(owner: LifecycleOwner) {
Log.i(TAG, "In onStop()")
carContext.unbindService(serviceConnection)
navigationService = null
}
override fun onDestroy(owner: LifecycleOwner) { override fun onDestroy(owner: LifecycleOwner) {
if (::navigationManager.isInitialized) { if (::navigationManager.isInitialized) {
navigationManager.clearNavigationManagerCallback() navigationManager.clearNavigationManagerCallback()
@@ -220,12 +281,9 @@ class NavigationSession : Session(), NavigationListener {
// Called when the app should simulate navigation (e.g., for testing) // Called when the app should simulate navigation (e.g., for testing)
deviceLocationManager.stopLocationUpdates() deviceLocationManager.stopLocationUpdates()
autoDriveEnabled = true autoDriveEnabled = true
surfaceRenderer.viewStyle = ViewStyle.VIEW startNavigation()
simulation.startSimulation( CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
routeModel, lifecycle.coroutineScope .show()
) { location ->
updateLocation(location)
}
} }
override fun onStopNavigation() { override fun onStopNavigation() {
@@ -427,6 +485,7 @@ class NavigationSession : Session(), NavigationListener {
surfaceRenderer.routeData.value = "" surfaceRenderer.routeData.value = ""
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW navigationScreen.navigationType = NavigationType.VIEW
navigationService!!.stopNavigation()
} }
/** /**

View File

@@ -64,7 +64,8 @@ import java.time.LocalDateTime
* Manages camera position, zoom, tilt, and navigation state for the map view. * Manages camera position, zoom, tilt, and navigation state for the map view.
*/ */
class SurfaceRenderer( class SurfaceRenderer(
private var carContext: CarContext, lifecycle: Lifecycle, private var carContext: CarContext,
private var lifecycle: Lifecycle,
private var routeModel: RouteCarModel, private var routeModel: RouteCarModel,
private var viewModelStoreOwner: ViewModelStoreOwner private var viewModelStoreOwner: ViewModelStoreOwner
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {

View File

@@ -0,0 +1,258 @@
package com.kouros.navigation.car.navigation
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.ComponentName
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Binder
import android.os.IBinder
import android.util.Log
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.car.app.notification.CarAppExtender
import androidx.car.app.notification.CarPendingIntent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.kouros.data.R
import com.kouros.navigation.car.NavigationCarAppService
import com.kouros.navigation.data.Constants.TAG
import androidx.core.graphics.toColorInt
class NavigationService : Service() {
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()
private lateinit var navigationManager: NavigationManager
private var navigationManagerInitialized = false
var binder: IBinder = LocalBinder()
override fun onBind(p0: Intent?): IBinder {
return binder
}
override fun onUnbind(intent: Intent): Boolean {
return true
}
/** 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<Destination?>?,
steps: MutableList<Step>?,
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 onCreate() {
Log.i(TAG, "In onCreate()");
createNotificationChannel();
}
/** Sets the [CarContext] to use while the service is connected. */
fun setCarContext(
carContext: CarContext,
listener: Listener
) {
Log.d(TAG, "in setCarContext")
this.carContext = carContext
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() {
carContext = null;
// navigationManager.clearNavigationManagerCallback();
}
/** Starts navigation. */
fun startNavigation() {
Log.i(TAG, "Starting Navigation")
startService(Intent(applicationContext, NavigationService::class.java))
Log.i(TAG, "Starting foreground service")
startForeground(
NAV_NOTIFICATION_ID,
getNotification(
true,
showInCar = false,
navigatingDisplayTitle = getString(R.string.navigation_settings),
navigatingDisplayContent = null,
notificationIcon = R.drawable.navigation_48px
)
)
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()
}
private fun createNotificationChannel() {
notificationManager =
getSystemService(NotificationManager::class.java)
val name: CharSequence = getString(R.string.navigation_settings)
val serviceChannel =
NotificationChannel(
channelId,
name,
NotificationManager.IMPORTANCE_HIGH
)
notificationManager!!.createNotificationChannel(serviceChannel)
}
/** Returns the [NotificationCompat] used as part of the foreground service. */
private fun getNotification(
shouldNotify: Boolean,
showInCar: Boolean,
navigatingDisplayTitle: CharSequence?,
navigatingDisplayContent: CharSequence?,
notificationIcon: Int
): Notification {
val builder: NotificationCompat.Builder =
NotificationCompat.Builder(this, channelId)
//.setContentIntent(createMainActivityPendingIntent())
.setContentTitle(navigatingDisplayTitle)
.setContentText(navigatingDisplayContent)
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_NAVIGATION)
.setOnlyAlertOnce(!shouldNotify) // Set the notification's background color on the car screen.
.setColor(
"#003000".toColorInt()
)
.setColorized(true)
.setSmallIcon(R.drawable.ic_pan_24)
.setLargeIcon(
BitmapFactory.decodeResource(resources, notificationIcon)
)
.setTicker(navigatingDisplayTitle)
.setWhen(System.currentTimeMillis())
builder.setChannelId(channelId)
builder.setPriority(NotificationManager.IMPORTANCE_HIGH)
if (showInCar) {
val intent = Intent(Intent.ACTION_VIEW)
.setComponent(ComponentName(this, NavigationCarAppService::class.java))
.setData(NavigationCarAppService().createDeepLinkUri(Intent.ACTION_VIEW))
builder.extend(
CarAppExtender.Builder()
.setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
.setContentIntent(
CarPendingIntent.getCarApp(
this, intent.hashCode(),
intent,
0
)
)
.build()
)
}
return builder.build()
}
// 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)
// }
}

View File

@@ -3,7 +3,6 @@ package com.kouros.navigation.car.navigation
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.util.Log
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
@@ -92,8 +91,17 @@ class RouteCarModel : RouteModel() {
return step return step
} }
fun travelEstimate(carContext: CarContext, distanceMode: Int): TravelEstimate { fun travelEstimateTrip(carContext: CarContext, distanceMode: Int): TravelEstimate {
val timeLeft = routeCalculator.travelLeftTime()
return travelEstimate(carContext, routeCalculator.travelLeftTime(), distanceMode)
}
fun travelEstimateStep(carContext: CarContext, distanceMode: Int): TravelEstimate {
return travelEstimate(carContext, routeCalculator.travelStepLeftTime(), distanceMode)
}
fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate {
val timeToDestinationMillis = val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong()) TimeUnit.SECONDS.toMillis(timeLeft.toLong())
val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance()) val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance())

View File

@@ -4,7 +4,7 @@ import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.SystemClock import android.os.SystemClock
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.kouros.android.cars.carappservice.BuildConfig import com.kouros.data.BuildConfig
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import io.ticofab.androidgpxparser.parser.GPXParser import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx import io.ticofab.androidgpxparser.parser.domain.Gpx

View File

@@ -169,7 +169,7 @@ open class NavigationScreen(
.setNavigationInfo( .setNavigationInfo(
getRoutingInfo() getRoutingInfo()
) )
.setDestinationTravelEstimate(routeModel.travelEstimate(carContext, distanceMode)) .setDestinationTravelEstimate(routeModel.travelEstimateTrip(carContext, distanceMode))
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(
@@ -627,9 +627,11 @@ open class NavigationScreen(
.build() .build()
tripBuilder.addDestination( tripBuilder.addDestination(
destination, destination,
routeModel.travelEstimate(carContext, distanceMode) routeModel.travelEstimateTrip(carContext, distanceMode)
) )
tripBuilder.setLoading(false) tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(routeModel.currentStep.street)
tripBuilder.addStep(routeModel.currentStep(carContext), routeModel.travelEstimateStep(carContext, distanceMode ))
listener.updateTrip(tripBuilder.build()) listener.updateTrip(tripBuilder.build())
} }
} }

View File

@@ -3,9 +3,9 @@ package com.kouros.navigation.car.screen
import android.net.Uri import android.net.Uri
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.OnScreenResultListener
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon

View File

@@ -18,6 +18,7 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
buildTypes { buildTypes {

View File

@@ -10,15 +10,24 @@ data class NavigationState (
val iconMapper: IconMapper = IconMapper(), val iconMapper: IconMapper = IconMapper(),
val navigating: Boolean = false, val navigating: Boolean = false,
val arrived: Boolean = false, val arrived: Boolean = false,
// travel message
val travelMessage: String = "", val travelMessage: String = "",
// maneuver type
val maneuverType: Int = 0, val maneuverType: Int = 0,
// last location
val lastLocation: Location = location(0.0, 0.0), val lastLocation: Location = location(0.0, 0.0),
// current location
val currentLocation: Location = location(0.0, 0.0), val currentLocation: Location = location(0.0, 0.0),
// bearing of the route
val routeBearing: Float = 0F, val routeBearing: Float = 0F,
// index of current route in the list of routes // index of current route in the list of routes
val currentRouteIndex: Int = 0, val currentRouteIndex: Int = 0,
// destination name
val destination: Place = Place(), val destination: Place = Place(),
// car connection used
val carConnection: Int = 0, val carConnection: Int = 0,
// routing engine used
val routingEngine: Int = 0, val routingEngine: Int = 0,
// show next step information
val nextStep: Boolean = false, val nextStep: Boolean = false,
) )

View File

@@ -2,6 +2,7 @@ package com.kouros.navigation.data.tomtom
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import com.kouros.data.BuildConfig
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.EngineType import com.kouros.navigation.data.EngineType
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
@@ -20,9 +21,9 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident
private const val tomtomFields = private const val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}" "{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
const val useLocal = false val useLocal = BuildConfig.DEBUG
const val useLocalTraffic = false val useLocalTraffic = BuildConfig.DEBUG
class TomTomRepository : NavigationRepository() { class TomTomRepository : NavigationRepository() {

View File

@@ -54,6 +54,18 @@ class RouteCalculator(var routeModel: RouteModel) {
return timeLeft return timeLeft
} }
fun travelStepLeftTime(): Double {
var timeLeft = 0.0
// time for current step
val step = routeModel.route.currentStep()
val curTime = step.duration
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
val time = curTime * percent / 100
timeLeft += time
return timeLeft
}
/** Returns the current [Step] left distance in m. */ /** Returns the current [Step] left distance in m. */
fun leftStepDistance(): Double { fun leftStepDistance(): Double {
val step = routeModel.route.currentStep() val step = routeModel.route.currentStep()