App DrawItem and Search
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) //
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt
Executable file
53
app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt
Executable 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
198
app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt
Normal file
198
app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt
Normal 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")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
217
app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt
Normal file
217
app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user