App DrawItem and Search

This commit is contained in:
Dimitris
2025-12-09 15:52:27 +01:00
parent f70ca6e8fe
commit aeca6ff237
25 changed files with 868 additions and 654 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 4
versionName = "0.1.3.3"
versionCode = 6
versionName = "0.1.3.6"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -91,6 +91,8 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3.window.size.class1)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.window)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)

View File

@@ -6,6 +6,7 @@ import android.os.SystemClock
class MockLocation (private var locationManager: LocationManager) {
var curSpeed = 0F
fun setMockLocation(latitude: Double, longitude: Double) {
try {
// Set mock location for all providers
@@ -48,7 +49,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 0F
this.speed = 0f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -56,7 +57,6 @@ class MockLocation (private var locationManager: LocationManager) {
this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f
}
// Set the mock location
locationManager.setTestProviderLocation(provider, mockLocation)
@@ -72,6 +72,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 0f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -79,7 +80,6 @@ class MockLocation (private var locationManager: LocationManager) {
this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f
}
locationManager.setTestProviderLocation(provider, mockLocation)
} catch (ex: Exception) {
ex.printStackTrace()

View File

@@ -1,10 +1,9 @@
package com.kouros.navigation.ui
import NavigationSheet
import android.Manifest
import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.Bundle
import android.os.Process
@@ -12,100 +11,93 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Card
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
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.mutableStateOf
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import androidx.window.layout.WindowMetricsCalculator
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.MainApplication
import com.kouros.navigation.car.BuildingLayer
import com.kouros.navigation.car.PuckState
import com.kouros.navigation.car.RouteLayer
import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.car.map.RouteLayer
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.location
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() {
private val LOCATION_PERMISSION_REQUEST_CODE = 1001
private val CONTACTS_PERMISSION_REQUEST_CODE = 1002
val routeData = MutableLiveData("")
val vieModel = ViewModel(NavigationRepository())
val viewModel = ViewModel(NavigationRepository())
val routeModel = RouteModel()
var tilt = 50.0
val useMock = true
val instruction: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>()
}
var lastLocation = Location(LocationManager.GPS_PROVIDER)
var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute ->
routeModel.startNavigation(newRoute)
routeData.value = routeModel.route.routeGeoJson
println("Start simulating $newRoute")
simulate()
}
val cameraPosition = MutableLiveData(
CameraPosition(
zoom = 15.0,
@@ -113,153 +105,93 @@ class MainActivity : ComponentActivity() {
)
)
private lateinit var locationManager: LocationManager
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var mock: MockLocation
private var loadRecentPlaces = false
init {
vieModel.route.observe(this, observer)
viewModel.route.observe(this, observer)
}
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkLocationPermissions()
if (MainApplication.useContacts) {
checkContactsPermissions()
if (useMock) {
checkMockLocationEnabled()
}
// checkMockLocationEnabled()
enableEdgeToEdge()
setContent {
if ((checkPermissionForLocation() && !MainApplication.useContacts)
|| (checkPermissionForLocation() && MainApplication.useContacts && checkPermissionForContact())) {
Content()
} else {
}
}
}
@Composable
fun Content() {
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
mock = MockLocation(locationManager)
mock.setMockLocation(
Constants.homeLocation.latitude,
Constants.homeLocation.longitude
)
if (useMock) {
mock = MockLocation(locationManager)
mock.setMockLocation(
Constants.homeLocation.latitude,
Constants.homeLocation.longitude
)
}
enableEdgeToEdge()
setContent {
CheckPermissionScreen()
}
}
val scope = rememberCoroutineScope()
@SuppressLint("AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Content() {
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
var simulationText by remember { mutableStateOf("Start Simulation") }
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
updateLocation(locationState.value)
var latitude by remember { mutableDoubleStateOf(0.0) }
if (locationState.value != null) {
latitude = locationState.value!!.position.latitude
}
val step: StepData? by instruction.observeAsState()
NavigationTheme {
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet {
Text("Drawer title", modifier = Modifier.Companion.padding(16.dp))
HorizontalDivider()
NavigationDrawerItem(
label = { Text(text = "Drawer Item") },
selected = false,
onClick = { /*TODO*/ }
)
}
BottomSheetScaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
gesturesEnabled = false
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = {
Text(simulationText)
},
icon = { SegmentedButtonDefaults.Icon(true) },
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Starte Navigation")
}
if (!routeModel.isNavigating()) {
tilt = 60.0
vieModel.loadRoute(
applicationContext,
lastLocation,
Constants.home2Location
)
simulationText = "Stop Simulation"
} else {
tilt = 0.0
routeModel.stopNavigation()
routeData.value = ""
println("stopNavigation")
simulationText = "Start Simulation"
}
}
)
}
) { innerPadding ->
Column(modifier = Modifier.Companion.padding(innerPadding)) {
//CheckPermission()
Map()
}
scaffoldState = scaffoldState,
sheetPeekHeight = 128.dp,
sheetContent = {
SheetContent(latitude, step)
},
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center,
) {
Map(userLocationState, step)
}
}
}
}
@SuppressLint("PermissionLaunchedDuringComposition")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CheckPermission() {
var rationaleState by remember {
mutableStateOf<RationaleState?>(null)
}
@OptIn(ExperimentalPermissionsApi::class)
val fineLocationPermissionState = rememberMultiplePermissionsState(
listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
//Manifest.permission.READ_CONTACTS,
),
)
if (fineLocationPermissionState.allPermissionsGranted) {
Map()
}
// Show rationale dialog when needed
rationaleState?.run { PermissionRationaleDialog(rationaleState = this) }
PermissionRequestButton(
isGranted = fineLocationPermissionState.allPermissionsGranted,
title = "Precise location access",
) {
if (fineLocationPermissionState.shouldShowRationale) {
rationaleState = RationaleState(
"Request Precise Location",
"In order to use this feature please grant access by accepting " + "the location permission dialog." + "\n\nWould you like to continue?",
) { proceed ->
if (proceed) {
fineLocationPermissionState.launchMultiplePermissionRequest()
}
rationaleState = null
}
} else {
fineLocationPermissionState.launchMultiplePermissionRequest()
}
fun SheetContent(locationState: Double, step: StepData?) {
if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, viewModel, lastLocation)
} else {
NavigationSheet( routeModel, step, { simulate() })
}
// to recomposite SheetContent !
Text("State $locationState")
}
@Composable
fun NavigationInfo(step: StepData?) {
Card {
@@ -269,7 +201,6 @@ class MainActivity : ComponentActivity() {
contentDescription = stringResource(id = R.string.accept_action_title)
)
if (step != null) {
Text(text = step.bearing.toString(), fontSize = 25.sp)
Text(text = step.instruction, fontSize = 25.sp)
}
}
@@ -277,32 +208,24 @@ class MainActivity : ComponentActivity() {
}
@Composable
fun Map() {
val step: StepData? by instruction.observeAsState()
fun Map(userLocationState: UserLocationState, step: StepData?) {
Column {
//SimpleSearchBar()
if (step != null) {
NavigationInfo(step)
}
MapView()
MapView(userLocationState)
}
}
@Composable
fun MapView() {
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
updateLocation(locationState.value)
if (locationState.value != null && lastLocation.latitude == 0.0) {
lastLocation.latitude = locationState.value?.position!!.latitude
lastLocation.longitude = locationState.value?.position!!.longitude
}
val position: CameraPosition? by cameraPosition.observeAsState()
fun MapView(userLocationState: UserLocationState) {
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val cameraState =
rememberCameraState(
@@ -315,124 +238,71 @@ class MainActivity : ComponentActivity() {
zoom = 15.0,
)
)
MaplibreMap(
cameraState = cameraState,
baseStyle = BaseStyle.Uri(Constants.STYLE),
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "")
}
val location = Location(LocationManager.GPS_PROVIDER)
if (userLocationState.location != null) {
location.longitude = userLocationState.location!!.position.longitude
location.latitude = userLocationState.location!!.position.latitude
PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt,
padding = PaddingValues(start = 0.dp, top = 350.dp)
Box (contentAlignment = Alignment.Center) {
MaplibreMap(
options = MapOptions(
ornamentOptions =
OrnamentOptions(isScaleBarEnabled = false)
),
duration = 1.seconds
)
cameraState = cameraState,
baseStyle = BaseStyle.Uri(Constants.STYLE),
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "", position!!.zoom)
}
if (userLocationState.location != null) {
///PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt,
padding = paddingValues
),
duration = 1.seconds
)
}
NavigationImage(paddingValues, width, height /6, "")
}
}
fun updateLocation(location: org.maplibre.compose.location.Location?) {
if (1 == 1)
return
if (location != null) {
if (location != null
&& lastLocation.latitude != location.position.latitude
&& lastLocation.longitude != location.position.longitude) {
if (routeModel.isNavigating()) {
routeModel.updateLocation(lastLocation)
instruction.value = routeModel.currentStep()
}
val currentLocation = location(location.position.longitude, location.position.latitude)
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
val zoom = calculateZoom(location.speed)
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = zoom,
target = location.position
target = location.position,
bearing = bearing
),
)
}
}
private fun checkLocationPermissions() {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (MainApplication.useContacts) {
permissions.add(Manifest.permission.READ_CONTACTS)
}
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
LOCATION_PERMISSION_REQUEST_CODE
)
}
}
fun checkPermissionForLocation(): Boolean {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (MainApplication.useContacts) {
permissions.add(Manifest.permission.READ_CONTACTS)
}
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
return permissionsToRequest.isEmpty()
}
fun checkPermissionForContact(): Boolean {
val permissions = arrayOf(
Manifest.permission.READ_CONTACTS,
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
return permissionsToRequest.isEmpty()
}
private fun checkContactsPermissions() {
val permissions = arrayOf(
Manifest.permission.READ_CONTACTS,
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
CONTACTS_PERMISSION_REQUEST_CODE
)
lastLocation = currentLocation
if (!loadRecentPlaces) {
viewModel.loadRecentPlaces(applicationContext, lastLocation)
loadRecentPlaces = true
}
}
}
@@ -460,18 +330,27 @@ class MainActivity : ComponentActivity() {
}
}
@SuppressLint("MissingPermission")
@Composable
fun CheckPermissionScreen() {
val permissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
)
PermissionScreen(
permissions = permissions,
requiredPermissions = listOf(permissions.first()),
onGranted = {
Content()
},
)
}
@OptIn(DelicateCoroutinesApi::class)
fun simulate() = GlobalScope.async {
for ((i, loc) in routeModel.route.waypoints.withIndex()) {
if (routeModel.isNavigating()) {
lastLocation.longitude = loc[0]
lastLocation.latitude = loc[1]
if (i == 20) {
mock.setMockLocation(loc[1] , loc[0])
} else {
mock.setMockLocation(loc[1], loc[0])
}
delay(500L) //
mock.setMockLocation(loc[1], loc[0])
delay(1000L) //
}
}
}

View File

@@ -0,0 +1,53 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel
import kotlinx.coroutines.Deferred
@Composable
fun NavigationSheet(
routeModel: RouteModel,
step: StepData?,
simulate: () -> Unit
) {
Column {
//Text("${routeModel.travelLeftTime()}")
if (step != null)
Text("${step.leftDistance / 1000} km")
HorizontalDivider()
Row() {
if (routeModel.isNavigating()) {
Button(onClick = {
routeModel.stopNavigation()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_close_white_24dp),
"Stop",
modifier = Modifier.size(24.dp, 24.dp),
)
}
Button(onClick = {
simulate()
}) {
Icon(
painter = painterResource(id = R.drawable.assistant_navigation_48px),
"Simulate",
modifier = Modifier.size(24.dp, 24.dp),
)
}
}
}
}
}

View File

@@ -0,0 +1,198 @@
package com.kouros.navigation.ui
import android.content.Intent
import android.provider.Settings
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import androidx.core.net.toUri
/**
* [PermissionScreen] that takes a list of permissions and only calls [onGranted] when
* all the [requiredPermissions] are granted.
*
* By default it assumes that all [permissions] are required.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionScreen(
modifier: Modifier = Modifier,
permissions: List<String>,
requiredPermissions: List<String> = permissions,
description: String? = null,
contentAlignment: Alignment = Alignment.TopStart,
onGranted: @Composable BoxScope.(List<String>) -> Unit,
) {
val context = LocalContext.current
var errorText by remember {
mutableStateOf("")
}
val permissionState = rememberMultiplePermissionsState(permissions = permissions) { map ->
val rejectedPermissions = map.filterValues { !it }.keys
errorText = if (rejectedPermissions.none { it in requiredPermissions }) {
""
} else {
"${rejectedPermissions.joinToString()} required for the sample"
}
}
val allRequiredPermissionsGranted =
permissionState.revokedPermissions.none { it.permission in requiredPermissions }
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = if (allRequiredPermissionsGranted) {
contentAlignment
} else {
Alignment.Center
},
) {
if (allRequiredPermissionsGranted) {
onGranted(
permissionState.permissions
.filter { it.status.isGranted }
.map { it.permission },
)
} else {
PermissionScreen(
permissionState,
description,
errorText,
)
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
data = "package:${context.packageName}".toUri()
}
context.startActivity(intent)
},
) {
Text("App settings")
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun PermissionScreen(
state: MultiplePermissionsState,
description: String?,
errorText: String,
) {
var showRationale by remember(state) {
mutableStateOf(false)
}
val permissions = remember(state.revokedPermissions) {
state.revokedPermissions.joinToString("\n") {
" - " + it.permission.removePrefix("android.permission.")
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Sample requires permission/s:",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp),
)
Text(
text = permissions,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
if (description != null) {
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
}
Button(
onClick = {
if (state.shouldShowRationale) {
showRationale = true
} else {
state.launchMultiplePermissionRequest()
}
},
) {
Text(text = "Grant permissions")
}
if (errorText.isNotBlank()) {
Text(
text = errorText,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(16.dp),
)
}
}
if (showRationale) {
AlertDialog(
onDismissRequest = {
showRationale = false
},
title = {
Text(text = "Permissions required by the navigation app")
},
text = {
Text(text = "The navigation app requires the following permissions to work:\n $permissions")
},
confirmButton = {
TextButton(
onClick = {
showRationale = false
state.launchMultiplePermissionRequest()
},
) {
Text("Continue")
}
},
dismissButton = {
TextButton(
onClick = {
showRationale = false
},
) {
Text("Dismiss")
}
},
)
}
}

View File

@@ -1,130 +0,0 @@
package com.kouros.navigation.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
/**
* Simple screen that manages the location permission state
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Permissions(text: String, rationale: String, locationState: PermissionState) {
Permissions(
text = text,
rationale = rationale,
locationState = rememberMultiplePermissionsState(
permissions = listOf(
locationState.permission
)
)
)
}
/**
* Simple screen that manages the location permission state
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Permissions(text: String, rationale: String, locationState: MultiplePermissionsState) {
var showRationale by remember(locationState) {
mutableStateOf(false)
}
if (showRationale) {
PermissionRationaleDialog(rationaleState = RationaleState(
title = "Permission Access",
rationale = rationale,
onRationaleReply = { proceed ->
if (proceed) {
locationState.launchMultiplePermissionRequest()
}
showRationale = false
}
))
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PermissionRequestButton(isGranted = false, title = text) {
if (locationState.shouldShowRationale) {
showRationale = true
} else {
locationState.launchMultiplePermissionRequest()
}
}
}
}
/**
* A button that shows the title or the request permission action.
*/
@Composable
fun PermissionRequestButton(isGranted: Boolean, title: String, onClick: () -> Unit) {
if (isGranted) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon(Icons.Outlined.CheckCircle, title, modifier = Modifier.size(48.dp))
Spacer(Modifier.size(10.dp))
Text(text = title, modifier = Modifier.background(Color.Transparent))
}
} else {
Button(onClick = onClick) {
Text("Request $title")
}
}
}
/**
* Simple AlertDialog that displays the given rationale state
*/
@Composable
fun PermissionRationaleDialog(rationaleState: RationaleState) {
AlertDialog(onDismissRequest = { rationaleState.onRationaleReply(false) }, title = {
Text(text = rationaleState.title)
}, text = {
Text(text = rationaleState.rationale)
}, confirmButton = {
TextButton(onClick = {
rationaleState.onRationaleReply(true)
}) {
Text("Continue")
}
}, dismissButton = {
TextButton(onClick = {
rationaleState.onRationaleReply(false)
}) {
Text("Dismiss")
}
})
}
data class RationaleState(
val title: String,
val rationale: String,
val onRationaleReply: (proceed: Boolean) -> Unit,
)

View File

@@ -1,79 +0,0 @@
package com.kouros.navigation.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleSearchBar(
textFieldState: TextFieldState,
onSearch: (String) -> Unit,
searchResults: List<String>,
modifier: Modifier = Modifier
) {
// Controls expansion state of the search bar
var expanded by rememberSaveable { mutableStateOf(false) }
Box(
modifier
.fillMaxSize()
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
query = textFieldState.text.toString(),
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
onSearch = {
onSearch(textFieldState.text.toString())
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Search") }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
// Display search results in a scrollable column
Column(Modifier.verticalScroll(rememberScrollState())) {
searchResults.forEach { result ->
ListItem(
headlineContent = { Text(result) },
modifier = Modifier
.clickable {
textFieldState.edit { replace(0, length, result) }
expanded = false
}
.fillMaxWidth()
)
}
}
}
}
}

View File

@@ -0,0 +1,217 @@
package com.kouros.navigation.ui
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.PlaceColor
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location
@Composable
fun SearchSheet(applicationContext: Context, viewModel: ViewModel, location: Location) {
val searchResults = mutableListOf<SearchResult>()
val recentPlaces = viewModel.places.observeAsState()
val search = viewModel.searchPlaces.observeAsState()
if (search.value != null) {
searchResults.addAll(search.value!!)
}
if (searchResults.isNotEmpty()) {
val textFieldState = rememberTextFieldState()
val items = listOf(searchResults)
if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location
)
}
}
if (recentPlaces.value != null) {
val textFieldState = rememberTextFieldState()
val items = listOf(recentPlaces)
if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
textFieldState: TextFieldState,
searchPlaces: List<Place>,
searchResults: List<SearchResult>,
modifier: Modifier = Modifier,
viewModel: ViewModel,
context: Context,
location: Location,
) {
var expanded by rememberSaveable { mutableStateOf(true) }
Box(
modifier
.fillMaxSize()
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_search_black36dp),
"Search",
modifier = Modifier.size(24.dp, 24.dp),
)
},
query = textFieldState.text.toString(),
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
onSearch = {
searchPlaces(viewModel, location, it)
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Suchen") }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
if (searchPlaces.isNotEmpty()) {
Text("Recent places")
RecentPlaces(searchPlaces, viewModel, context, location)
}
if (searchResults.isNotEmpty()) {
Text("Search places")
SearchPlaces(searchResults, viewModel, context, location)
}
}
}
}
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@Composable
private fun SearchPlaces(
searchResults: List<SearchResult>,
viewModel: ViewModel,
context: Context,
location: Location,
) {
val color = remember { PlaceColor }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (searchResults.isNotEmpty()) {
items(searchResults, key = { it.placeId }) { place ->
Row {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Navigation",
tint = color.copy(alpha = 1f),
modifier = Modifier.size(24.dp, 24.dp),
)
ListItem(
headlineContent = { Text("${place.address.road} ${place.address.postcode}") },
modifier = Modifier
.clickable {
val toLocation =
location(place.lon.toDouble(), place.lat.toDouble())
viewModel.loadRoute(context, location, toLocation)
}
.fillMaxWidth()
)
HorizontalDivider(color = Color.Gray)
}
}
}
}
}
@Composable
private fun RecentPlaces(
recentPlaces: List<Place>,
viewModel: ViewModel,
context: Context,
location: Location
) {
val color = remember { PlaceColor }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(recentPlaces, key = { it.id }) { place ->
Row {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Navigation",
tint = color.copy(alpha = 1f),
modifier = Modifier.size(24.dp, 24.dp),
)
ListItem(
headlineContent = { Text("${place.street!!} ${place.postalCode}") },
modifier = Modifier
.clickable {
val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation)
}
.fillMaxWidth()
)
HorizontalDivider(color = Color.Gray)
}
}
}
}

View File

@@ -40,7 +40,7 @@ fun NavigationTheme(
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
dynamicColor -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}