Categories

This commit is contained in:
Dimitris
2026-03-09 18:55:13 +01:00
parent 8c103a1f96
commit 61ce09f393
38 changed files with 864 additions and 338 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 61
versionName = "0.2.0.61"
versionCode = 64
versionName = "0.2.0.64"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -6,39 +6,43 @@ import android.app.AppOpsManager
import android.location.LocationManager
import android.os.Bundle
import android.os.Process
import android.speech.tts.TextToSpeech
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavHostController
@@ -46,7 +50,9 @@ import com.google.android.gms.location.FusedLocationProviderClient
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.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.StepData
@@ -77,7 +83,6 @@ import org.maplibre.compose.location.Location
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.spatialk.geojson.Position
import java.util.Locale
import kotlin.time.Duration.Companion.seconds
@@ -94,7 +99,6 @@ class MainActivity : ComponentActivity() {
val nextStepData: MutableLiveData<StepData> by lazy {
MutableLiveData()
}
var lastStepIndex = -1
var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute ->
@@ -108,7 +112,6 @@ class MainActivity : ComponentActivity() {
}
}
lateinit var textToSpeech: TextToSpeech
private fun checkMock() {
if (useMock) {
@@ -118,7 +121,6 @@ class MainActivity : ComponentActivity() {
SimulationType.GPX -> gpx(
context = applicationContext, mock
)
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
}
}
@@ -135,6 +137,10 @@ class MainActivity : ComponentActivity() {
private lateinit var mock: MockLocation
private var loadRecentPlaces = false
lateinit var textToSpeechManager: TextToSpeechManager
var guidanceAudio = 0
override fun onDestroy() {
if (simulationJob != null) {
simulationJob?.cancel()
@@ -146,11 +152,11 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textToSpeech = TextToSpeech(applicationContext) { status ->
if (status == TextToSpeech.SUCCESS) {
textToSpeech.language = Locale.getDefault()
}
}
textToSpeechManager = TextToSpeechManager(applicationContext)
val repository = getSettingsRepository(applicationContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it
})
if (useMock) {
checkMockLocationEnabled()
@@ -158,12 +164,12 @@ class MainActivity : ComponentActivity() {
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
)
navigationViewModel.route.observe(this, observer)
}
}
lifecycleScope.launch {
@@ -195,19 +201,11 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StartScreen(
navController: NavHostController
) {
navController: NavHostController) {
val appViewModel: AppViewModel = appViewModel()
val darkMode by appViewModel.darkMode.collectAsState()
val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val sheetPeekHeight = 180.dp
val sheetPeekHeightState = remember { mutableStateOf(sheetPeekHeight) }
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
)
@@ -218,40 +216,98 @@ class MainActivity : ComponentActivity() {
}
val step: StepData? by stepData.observeAsState()
val nextStep: StepData? by nextStepData.observeAsState()
var collapsedHeight by remember {
mutableIntStateOf(300)
}
val scope = rememberCoroutineScope()
fun closeSheet() {
scope.launch {
scaffoldState.bottomSheetState.partialExpand()
sheetPeekHeightState.value = 50.dp
collapsedHeight = if (routeModel.isNavigating()) {
150
} else {
300
}
}
}
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp
var expandedType by remember {
mutableStateOf(ExpandedType.COLLAPSED)
}
val height by animateIntAsState(
when (expandedType) {
ExpandedType.HALF -> screenHeight / 2
ExpandedType.FULL -> screenHeight
ExpandedType.COLLAPSED -> collapsedHeight
}
)
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState()
NavigationTheme(useDarkTheme = darkMode == 1) {
BottomSheetScaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
scaffoldState = scaffoldState,
sheetPeekHeight = sheetPeekHeightState.value,
scaffoldState = bottomSheetScaffoldState,
sheetShape = RoundedCornerShape(
bottomStart = 0.dp,
bottomEnd = 0.dp,
topStart = 12.dp,
topEnd = 12.dp
),
sheetContent = {
SheetContent(step, nextStep) { closeSheet() }
var isUpdated = false
Box(
Modifier
.fillMaxWidth()
.height(height.dp)
.pointerInput(Unit) {
detectVerticalDragGestures(
onVerticalDrag = { change, dragAmount ->
change.consume()
if (!isUpdated) {
expandedType = when {
dragAmount < 0 && expandedType == ExpandedType.COLLAPSED -> {
ExpandedType.HALF
}
dragAmount < 0 && expandedType == ExpandedType.HALF -> {
ExpandedType.FULL
}
dragAmount > 0 && expandedType == ExpandedType.FULL -> {
ExpandedType.HALF
}
dragAmount > 0 && expandedType == ExpandedType.HALF -> {
ExpandedType.COLLAPSED
}
else -> {
ExpandedType.FULL
}
}
isUpdated = true
}
},
onDragEnd = {
isUpdated = false
}
)
}
) {
SheetContent(step, nextStep) { closeSheet() }
}
},
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center,
) {
MapView(
applicationContext,
userLocationState,
step,
nextStep,
cameraPosition,
routeData,
tilt,
baseStyle,
)
}
sheetPeekHeight = height.dp
) {
MapView(
applicationContext,
userLocationState,
step,
nextStep,
cameraPosition,
routeData,
tilt,
baseStyle,
)
if (!routeModel.isNavigating()) {
Settings(navController, modifier = Modifier.fillMaxWidth())
}
@@ -338,7 +394,9 @@ class MainActivity : ComponentActivity() {
if (isNavigating()) {
updateLocation(currentLocation, navigationViewModel)
stepData.value = currentStep()
//textToSpeech(stepData.value!!.instruction)
if (guidanceAudio == 1) {
textToSpeech()
}
if (navState.nextStep) {
nextStepData.value = nextStep()
}
@@ -377,19 +435,11 @@ class MainActivity : ComponentActivity() {
stepData.value = StepData("", "", 0.0, 0, 0, 0, 0.0)
}
fun textToSpeech(text: String) {
fun textToSpeech() {
val currentStep = routeModel.route.currentStep()
val stepData = routeModel.currentStep()
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < 50) {
lifecycleScope.launch {
try {
val cs: CharSequence = stepData.instruction
textToSpeech.speak(cs, TextToSpeech.QUEUE_FLUSH, null, "1233455")
Log.d("TTS", "speak $cs")
} catch (e: Throwable) {
Log.d("TTS", "speak error", e)
}
}
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
textToSpeechManager.speak(stepData.message)
lastStepIndex = currentStep.index
}
}
@@ -420,5 +470,11 @@ class MainActivity : ComponentActivity() {
e.printStackTrace()
}
}
enum class ExpandedType {
HALF, FULL, COLLAPSED
}
}

View File

@@ -4,11 +4,14 @@ import android.content.Context
import android.location.Location
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.captionBar
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
@@ -42,6 +45,7 @@ import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.location
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchSheet(
applicationContext: Context,
@@ -61,7 +65,6 @@ fun SearchSheet(
.fillMaxWidth()
.wrapContentHeight()
) {
// Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
SearchBar(
textFieldState = textFieldState,
searchPlaces = emptyList(),
@@ -72,7 +75,7 @@ fun SearchSheet(
closeSheet = { closeSheet() }
)
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
if (recentPlaces.value != null) {
val items = listOf(recentPlaces)
if (items.isNotEmpty()) {
@@ -121,29 +124,33 @@ fun Home(
}
}
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
textFieldState: TextFieldState,
searchPlaces: List<Place>,
searchResults: List<SearchResult>,
modifier: Modifier = Modifier,
viewModel: NavigationViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit
) {
var expanded by rememberSaveable { mutableStateOf(false) }
SearchBar(
windowInsets = WindowInsets.captionBar,
colors = SearchBarDefaults.colors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
modifier = modifier,
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.speed_camera_24px),
painter = painterResource(id = R.drawable.search_48px),
"Search",
modifier = Modifier.size(24.dp, 24.dp),
)
@@ -160,23 +167,19 @@ fun SearchBar(
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
onExpandedChange = { },
) {
if (searchPlaces.isNotEmpty()) {
Text(context.getString(R.string.recent_destinations))
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
}
if (searchResults.isNotEmpty()) {
Text("Search places")
SearchPlaces( searchResults, viewModel, context, location, closeSheet)
Text("Search places")
SearchPlaces( searchResults, viewModel, context, location, closeSheet)
}
}
}
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@Composable
private fun SearchPlaces(
searchResults: List<SearchResult>,