diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d3b54c..75f3814 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,10 @@ + + + + @@ -36,6 +40,12 @@ + + \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index 8e91eae..e64a985 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -1,11 +1,13 @@ package com.kouros.navigation.ui 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.os.Bundle -import android.os.Process -import android.widget.Toast +import android.os.IBinder +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -41,19 +43,16 @@ import com.google.android.gms.location.LocationServices import com.kouros.data.R import com.kouros.navigation.MainApplication.Companion.navigationViewModel 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.INSTRUCTION_DISTANCE +import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TILT -import com.kouros.navigation.data.Constants.homeVogelhart import com.kouros.navigation.data.StepData import com.kouros.navigation.model.BaseStyleModel -import com.kouros.navigation.model.MockLocation import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.SimulationType -import com.kouros.navigation.model.simulate 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.navigation.AppNavGraph @@ -80,10 +79,13 @@ import kotlin.time.Duration.Companion.seconds class MainActivity : ComponentActivity() { + + var navigationService: NavigationService? = null + + var isBound: Boolean = false val routeData = MutableLiveData("") val routeModel = RouteModel() var tilt = TILT - val useMock = false val type = SimulationType.SIMULATE val stepData: MutableLiveData by lazy { @@ -96,26 +98,21 @@ class MainActivity : ComponentActivity() { var lastLocation = location(0.0, 0.0) val observer = Observer { newRoute -> if (newRoute.isNotEmpty()) { - 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 - // checkMock() + startNavigation(newRoute) } } + // Monitors the state of the connection to the navigation service. + private val serviceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder + navigationService = binder.service + isBound = true + } - private fun checkMock() { - if (useMock) { - when (type) { - SimulationType.SIMULATE -> simulate(routeModel, mock) - SimulationType.TEST -> test(applicationContext, routeModel) - - - 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 fusedLocationClient: FusedLocationProviderClient - private lateinit var mock: MockLocation private var loadRecentPlaces = false - lateinit var textToSpeechManager: TextToSpeechManager var guidanceAudio = 0 @@ -152,20 +147,10 @@ class MainActivity : ComponentActivity() { repository.guidanceAudioFlow.asLiveData().observe(this, Observer { guidanceAudio = it }) - - if (useMock) { - checkMockLocationEnabled() - } locationManager = getSystemService(LOCATION_SERVICE) as LocationManager fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? -> navigationViewModel.route.observe(this, observer) - if (useMock) { - mock = MockLocation(locationManager) - mock.setMockLocation( - homeVogelhart.latitude, homeVogelhart.longitude, 0F - ) - } } lifecycleScope.launch { 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) @Composable fun StartScreen( @@ -206,10 +212,8 @@ class MainActivity : ComponentActivity() { // navigationViewModel.route.value = lastRoute //} val userLocationState = rememberUserLocationState(locationProvider) - if (!useMock) { val locationState = locationProvider.location.collectAsState() updateLocation(locationState.value) - } val step: StepData? by stepData.observeAsState() val nextStep: StepData? by nextStepData.observeAsState() @@ -284,7 +288,7 @@ class MainActivity : ComponentActivity() { step, nextStep, { 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) { - val latitude = routeModel.curRoute.waypoints[0][1] - val longitude = routeModel.curRoute.waypoints[0][0] closeSheet() routeModel.stopNavigation() getSettingsViewModel(applicationContext).onLastRouteChanged("") - if (useMock) { - simulationJob?.cancel() - mock.setMockLocation(latitude, longitude, 0F) - } routeData.value = "" stepData.value = StepData("", "", 0.0, 0, 0, 0, 0.0) + navigationService?.stopNavigation() } fun textToSpeech() { @@ -368,37 +375,5 @@ class MainActivity : ComponentActivity() { 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 - } - } diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml index cf7c1e5..c5e7047 100644 --- a/automotive/src/main/AndroidManifest.xml +++ b/automotive/src/main/AndroidManifest.xml @@ -70,7 +70,12 @@ android:name="distractionOptimized" android:value="true" /> - + + \ No newline at end of file diff --git a/common/car/build.gradle.kts b/common/car/build.gradle.kts index b064eb1..29b90ca 100644 --- a/common/car/build.gradle.kts +++ b/common/car/build.gradle.kts @@ -29,7 +29,6 @@ android { } buildFeatures { compose = true - buildConfig = true } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/CarSensorManager.kt b/common/car/src/main/java/com/kouros/navigation/car/CarSensorManager.kt index f7a6adf..3b56910 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/CarSensorManager.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/CarSensorManager.kt @@ -137,7 +137,7 @@ class CarSensorManager( carCompassListener ) carSensors.addCarHardwareLocationListener( - CarSensors.UPDATE_RATE_NORMAL, + CarSensors.UPDATE_RATE_FASTEST, carContext.mainExecutor, carLocationListener ) diff --git a/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt b/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt new file mode 100644 index 0000000..a837570 --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt @@ -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() + } + } + } +} diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt index c103e48..0378d3e 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt @@ -1,11 +1,15 @@ package com.kouros.navigation.car import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager import android.net.Uri +import android.util.Log import androidx.car.app.CarAppService import androidx.car.app.Session import androidx.car.app.SessionInfo import androidx.car.app.validation.HostValidator +import com.kouros.navigation.data.Constants.TAG class NavigationCarAppService : CarAppService() { @@ -13,6 +17,7 @@ class NavigationCarAppService : CarAppService() { val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP = "com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP" + val channelId: String = "NavigationSessionChannel" fun createDeepLinkUri(deepLinkAction: String): Uri { return Uri.fromParts(NavigationSession.uriScheme, NavigationSession.uriHost, deepLinkAction) @@ -26,7 +31,26 @@ class NavigationCarAppService : CarAppService() { } override fun onCreateSession(sessionInfo: SessionInfo): Session { - return NavigationSession() + Log.d(TAG, "Display Type: ${sessionInfo.displayType}") + if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) { + return ClusterSession() + } else { + createNotificationChannel() + 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) } } 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 98950f4..7cb4383 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 @@ -1,17 +1,27 @@ package com.kouros.navigation.car import android.Manifest.permission +import android.content.ComponentName +import android.content.Context import android.content.Intent +import android.content.ServiceConnection import android.content.pm.PackageManager import android.location.Location +import android.os.IBinder import android.util.Log import androidx.car.app.CarContext +import androidx.car.app.CarToast import androidx.car.app.Screen import androidx.car.app.ScreenManager import androidx.car.app.Session 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.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.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleObserver @@ -22,6 +32,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.asLiveData import androidx.lifecycle.coroutineScope import androidx.lifecycle.lifecycleScope +import com.kouros.navigation.car.navigation.NavigationService import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.Simulation 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_SNAP_CORRECTION import com.kouros.navigation.data.Constants.TAG -import com.kouros.navigation.data.Place import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.ViewStyle 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.getSettingsRepository import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.time.LocalDateTime import java.time.ZoneOffset @@ -90,11 +98,64 @@ class NavigationSession : Session(), NavigationListener { 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?, + steps: MutableList?, + 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. * Cleans up resources when the session is destroyed. */ 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) { if (::navigationManager.isInitialized) { navigationManager.clearNavigationManagerCallback() @@ -220,12 +281,9 @@ class NavigationSession : Session(), NavigationListener { // Called when the app should simulate navigation (e.g., for testing) deviceLocationManager.stopLocationUpdates() autoDriveEnabled = true - surfaceRenderer.viewStyle = ViewStyle.VIEW - simulation.startSimulation( - routeModel, lifecycle.coroutineScope - ) { location -> - updateLocation(location) - } + startNavigation() + CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG) + .show() } override fun onStopNavigation() { @@ -427,6 +485,7 @@ class NavigationSession : Session(), NavigationListener { surfaceRenderer.routeData.value = "" surfaceRenderer.viewStyle = ViewStyle.VIEW navigationScreen.navigationType = NavigationType.VIEW + navigationService!!.stopNavigation() } /** diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index 8f1ecf7..ee8b1eb 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -64,7 +64,8 @@ import java.time.LocalDateTime * Manages camera position, zoom, tilt, and navigation state for the map view. */ class SurfaceRenderer( - private var carContext: CarContext, lifecycle: Lifecycle, + private var carContext: CarContext, + private var lifecycle: Lifecycle, private var routeModel: RouteCarModel, private var viewModelStoreOwner: ViewModelStoreOwner ) : DefaultLifecycleObserver { diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt new file mode 100644 index 0000000..9027d3f --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt @@ -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?, + steps: MutableList?, + 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) +// } +} diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt index 9e8cd12..18aba18 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt @@ -3,7 +3,6 @@ package com.kouros.navigation.car.navigation import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned -import android.util.Log import androidx.annotation.StringRes import androidx.car.app.AppManager import androidx.car.app.CarContext @@ -92,8 +91,17 @@ class RouteCarModel : RouteModel() { return step } - fun travelEstimate(carContext: CarContext, distanceMode: Int): TravelEstimate { - val timeLeft = routeCalculator.travelLeftTime() + fun travelEstimateTrip(carContext: CarContext, distanceMode: Int): TravelEstimate { + + 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 = TimeUnit.SECONDS.toMillis(timeLeft.toLong()) val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance()) diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt index c4e18b1..e0ce33e 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt @@ -4,7 +4,7 @@ import android.location.Location import android.location.LocationManager import android.os.SystemClock import androidx.lifecycle.LifecycleCoroutineScope -import com.kouros.android.cars.carappservice.BuildConfig +import com.kouros.data.BuildConfig import com.kouros.navigation.data.tomtom.TomTomRepository import io.ticofab.androidgpxparser.parser.GPXParser import io.ticofab.androidgpxparser.parser.domain.Gpx 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 9372964..0864432 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 @@ -169,7 +169,7 @@ open class NavigationScreen( .setNavigationInfo( getRoutingInfo() ) - .setDestinationTravelEstimate(routeModel.travelEstimate(carContext, distanceMode)) + .setDestinationTravelEstimate(routeModel.travelEstimateTrip(carContext, distanceMode)) .setActionStrip(actionStripBuilder.build()) .setMapActionStrip( mapActionStrip( @@ -627,9 +627,11 @@ open class NavigationScreen( .build() tripBuilder.addDestination( destination, - routeModel.travelEstimate(carContext, distanceMode) + routeModel.travelEstimateTrip(carContext, distanceMode) ) tripBuilder.setLoading(false) + tripBuilder.setCurrentRoad(routeModel.currentStep.street) + tripBuilder.addStep(routeModel.currentStep(carContext), routeModel.travelEstimateStep(carContext, distanceMode )) listener.updateTrip(tripBuilder.build()) } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt index 51a8aa3..912a9be 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt @@ -3,9 +3,9 @@ package com.kouros.navigation.car.screen import android.net.Uri import android.text.Spannable import android.text.SpannableString -import android.util.Log import androidx.car.app.CarContext import androidx.car.app.CarToast +import androidx.car.app.OnScreenResultListener import androidx.car.app.Screen import androidx.car.app.model.Action import androidx.car.app.model.CarIcon diff --git a/common/data/build.gradle.kts b/common/data/build.gradle.kts index 6d81d16..4a7fc2d 100644 --- a/common/data/build.gradle.kts +++ b/common/data/build.gradle.kts @@ -18,6 +18,7 @@ android { buildFeatures { compose = true + buildConfig = true } buildTypes { diff --git a/common/data/src/main/java/com/kouros/navigation/data/NavigationState.kt b/common/data/src/main/java/com/kouros/navigation/data/NavigationState.kt index d86e805..8c78128 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/NavigationState.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/NavigationState.kt @@ -10,15 +10,24 @@ data class NavigationState ( val iconMapper: IconMapper = IconMapper(), val navigating: Boolean = false, val arrived: Boolean = false, + // travel message val travelMessage: String = "", + // maneuver type val maneuverType: Int = 0, + // last location val lastLocation: Location = location(0.0, 0.0), + // current location val currentLocation: Location = location(0.0, 0.0), + // bearing of the route val routeBearing: Float = 0F, // index of current route in the list of routes val currentRouteIndex: Int = 0, + // destination name val destination: Place = Place(), + // car connection used val carConnection: Int = 0, + // routing engine used val routingEngine: Int = 0, + // show next step information val nextStep: Boolean = false, ) diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt index 7c5f5e8..56b0c43 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt @@ -2,6 +2,7 @@ package com.kouros.navigation.data.tomtom import android.content.Context import android.location.Location +import com.kouros.data.BuildConfig import com.kouros.data.R import com.kouros.navigation.data.EngineType 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 = "{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() { diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt index c5eb723..defe253 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt @@ -54,6 +54,18 @@ class RouteCalculator(var routeModel: RouteModel) { 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. */ fun leftStepDistance(): Double { val step = routeModel.route.currentStep()