feat: implement city selection flow with auto-detect
This commit is contained in:
@@ -7,17 +7,26 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.db3.airmq.features.entry.CityInitializer
|
||||
import org.db3.airmq.features.navigation.AirMQNavGraph
|
||||
import org.db3.airmq.sdk.city.CityService
|
||||
import org.db3.airmq.ui.theme.AirMQTheme
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var cityService: CityService
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
AirMQTheme {
|
||||
AirMQNavGraph(modifier = Modifier.fillMaxSize())
|
||||
CityInitializer(cityService = cityService) {
|
||||
AirMQNavGraph(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,409 @@
|
||||
package org.db3.airmq.features.city
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.compose.foundation.background
|
||||
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.Row
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import org.db3.airmq.R
|
||||
import org.db3.airmq.features.common.MockScreenScaffold
|
||||
import org.db3.airmq.features.common.ScreenAction
|
||||
import org.db3.airmq.features.city.CityScreenContract.Action
|
||||
import org.db3.airmq.features.city.CityScreenContract.Event
|
||||
import org.db3.airmq.features.city.CityScreenContract.State
|
||||
import org.db3.airmq.sdk.city.domain.City
|
||||
import org.db3.airmq.ui.theme.AirMQTheme
|
||||
|
||||
@Composable
|
||||
fun CityScreen(onBackToDashboard: () -> Unit) {
|
||||
MockScreenScaffold(
|
||||
title = stringResource(id = R.string.title_city),
|
||||
subtitle = stringResource(id = R.string.coming_soon),
|
||||
actions = listOf(ScreenAction(stringResource(id = R.string.back_to_dashboard), onBackToDashboard))
|
||||
fun CityScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CityViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val context = LocalContext.current
|
||||
val activity = context as? android.app.Activity
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { result ->
|
||||
val granted = result.values.any { it }
|
||||
scope.launch {
|
||||
val location = if (granted && activity != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
LocationServices.getFusedLocationProviderClient(activity).getLastLocation().await()
|
||||
}.getOrNull()
|
||||
}
|
||||
} else null
|
||||
viewModel.onEvent(Event.EnableDetectAutomaticallyResult(location))
|
||||
}
|
||||
}
|
||||
|
||||
val onDetectAutomaticallyChange: (Boolean) -> Unit = { enabled ->
|
||||
if (enabled) {
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
val allGranted = permissions.all {
|
||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
if (allGranted && activity != null) {
|
||||
scope.launch {
|
||||
val location = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
LocationServices.getFusedLocationProviderClient(activity).getLastLocation().await()
|
||||
}.getOrNull()
|
||||
}
|
||||
viewModel.onEvent(Event.EnableDetectAutomaticallyResult(location))
|
||||
}
|
||||
} else {
|
||||
permissionLauncher.launch(permissions)
|
||||
}
|
||||
} else {
|
||||
viewModel.onEvent(Event.DetectAutomaticallyChanged(false))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.actions.collect { action ->
|
||||
when (action) {
|
||||
Action.NavigateBack -> onNavigateBack()
|
||||
is Action.ShowToast -> Toast.makeText(context, action.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CityScreenScaffold(
|
||||
state = uiState,
|
||||
onEvent = viewModel::onEvent,
|
||||
onDetectAutomaticallyChange = onDetectAutomaticallyChange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun CityScreenScaffold(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
onDetectAutomaticallyChange: (Boolean) -> Unit
|
||||
) {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
TopBar(
|
||||
title = stringResource(R.string.title_city),
|
||||
onBackClick = { onEvent(Event.BackClicked) }
|
||||
)
|
||||
if (state.isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
CityScreenContent(
|
||||
uiState = state,
|
||||
onEvent = onEvent,
|
||||
onDetectAutomaticallyChange = onDetectAutomaticallyChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
title: String,
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_back),
|
||||
contentDescription = stringResource(R.string.content_back)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 48.dp),
|
||||
fontSize = 24.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CityScreenContent(
|
||||
uiState: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
onDetectAutomaticallyChange: (Boolean) -> Unit
|
||||
) {
|
||||
val expandedRegions = remember { mutableStateMapOf<Int, Boolean>() }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
WarningRow()
|
||||
DetectAutomaticallyRow(
|
||||
enabled = uiState.detectAutomatically,
|
||||
onCheckedChange = onDetectAutomaticallyChange
|
||||
)
|
||||
if (uiState.hasOnlyDefaultCity) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(36.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = uiState.selectedCity,
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFF6B6B6B)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||
) {
|
||||
uiState.regions.forEachIndexed { index, region ->
|
||||
val isExpanded = expandedRegions.getOrDefault(index, false)
|
||||
item(key = "region_$index") {
|
||||
RegionRow(
|
||||
countryName = region.countryName,
|
||||
isExpanded = isExpanded,
|
||||
onClick = {
|
||||
expandedRegions[index] = !isExpanded
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isExpanded) {
|
||||
region.cities.forEach { city ->
|
||||
item(key = "city_${city.id}") {
|
||||
CityRow(
|
||||
city = city,
|
||||
localeLanguage = uiState.localeLanguage,
|
||||
onClick = { onEvent(Event.CitySelected(city)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningRow() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_warning),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(36.dp),
|
||||
tint = Color(0x99333333)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_city_warning),
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0x99333333)
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(Color(0x1F000000))
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetectAutomaticallyRow(
|
||||
enabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_detect_automatically),
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF222222)
|
||||
)
|
||||
Switch(
|
||||
checked = enabled,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(Color(0x1F000000))
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RegionRow(
|
||||
countryName: String,
|
||||
isExpanded: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_down_dark),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.then(if (isExpanded) Modifier.rotate(180f) else Modifier),
|
||||
tint = Color(0xFF1F5DA5)
|
||||
)
|
||||
Text(
|
||||
text = countryName,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF1F5DA5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CityRow(
|
||||
city: City,
|
||||
localeLanguage: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = city.getLocalizedName(localeLanguage),
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFF222222)
|
||||
)
|
||||
Text(
|
||||
text = "${city.locationCount ?: "—"} x ",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0x99333333)
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_device_basic_active_10),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color(0x99333333)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, name = "City – list")
|
||||
@Composable
|
||||
private fun PreviewCityList() {
|
||||
AirMQTheme {
|
||||
CityScreenScaffold(
|
||||
state = CityScreenContract.previewState(),
|
||||
onEvent = {},
|
||||
onDetectAutomaticallyChange = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, name = "City – loading")
|
||||
@Composable
|
||||
private fun PreviewCityLoading() {
|
||||
AirMQTheme {
|
||||
CityScreenScaffold(
|
||||
state = CityScreenContract.previewState(isLoading = true),
|
||||
onEvent = {},
|
||||
onDetectAutomaticallyChange = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, name = "City – default only")
|
||||
@Composable
|
||||
private fun PreviewCityDefaultOnly() {
|
||||
AirMQTheme {
|
||||
CityScreenScaffold(
|
||||
state = CityScreenContract.previewState(
|
||||
regions = emptyList(),
|
||||
hasOnlyDefaultCity = true,
|
||||
selectedCity = "Minsk"
|
||||
),
|
||||
onEvent = {},
|
||||
onDetectAutomaticallyChange = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.db3.airmq.features.city
|
||||
|
||||
import android.location.Location
|
||||
import org.db3.airmq.sdk.city.domain.City
|
||||
import org.db3.airmq.sdk.city.domain.Region
|
||||
|
||||
object CityScreenContract {
|
||||
|
||||
data class State(
|
||||
val regions: List<Region> = emptyList(),
|
||||
val selectedCity: String = "",
|
||||
val isLoading: Boolean = true,
|
||||
val hasOnlyDefaultCity: Boolean = false,
|
||||
val localeLanguage: String = "en",
|
||||
val detectAutomatically: Boolean = false
|
||||
)
|
||||
|
||||
fun previewState(
|
||||
regions: List<Region> = listOf(
|
||||
Region(
|
||||
countryName = "Belarus",
|
||||
countryCode = "BY",
|
||||
cities = listOf(
|
||||
City("minsk", "BY", "Minsk", "Мінск", "Минск", 53.9, 27.5, 12),
|
||||
City("gomel", "BY", "Gomel", "Гомель", "Гомель", 52.4, 31.0, 5)
|
||||
)
|
||||
),
|
||||
Region(
|
||||
countryName = "Russia",
|
||||
countryCode = "RU",
|
||||
cities = listOf(
|
||||
City("moscow", "RU", "Moscow", null, "Москва", 55.7, 37.6, 45)
|
||||
)
|
||||
)
|
||||
),
|
||||
selectedCity: String = "Minsk",
|
||||
isLoading: Boolean = false,
|
||||
hasOnlyDefaultCity: Boolean = false,
|
||||
localeLanguage: String = "en",
|
||||
detectAutomatically: Boolean = false
|
||||
): State = State(
|
||||
regions = regions,
|
||||
selectedCity = selectedCity,
|
||||
isLoading = isLoading,
|
||||
hasOnlyDefaultCity = hasOnlyDefaultCity,
|
||||
localeLanguage = localeLanguage,
|
||||
detectAutomatically = detectAutomatically
|
||||
)
|
||||
|
||||
sealed interface Action {
|
||||
data object NavigateBack : Action
|
||||
data class ShowToast(val message: String) : Action
|
||||
}
|
||||
|
||||
sealed interface Event {
|
||||
data object BackClicked : Event
|
||||
data class CitySelected(val city: City) : Event
|
||||
data class DetectAutomaticallyChanged(val enabled: Boolean) : Event
|
||||
/** Called when permission flow completes for enabling auto-detect. Location is null if denied or unavailable. */
|
||||
data class EnableDetectAutomaticallyResult(val location: Location?) : Event
|
||||
}
|
||||
}
|
||||
109
app/src/main/kotlin/org/db3/airmq/features/city/CityViewModel.kt
Normal file
109
app/src/main/kotlin/org/db3/airmq/features/city/CityViewModel.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
package org.db3.airmq.features.city
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import android.location.Location
|
||||
import org.db3.airmq.R
|
||||
import org.db3.airmq.sdk.city.CityService
|
||||
import org.db3.airmq.sdk.city.domain.Region
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CityViewModel @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val cityService: CityService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(CityScreenContract.State())
|
||||
val uiState: StateFlow<CityScreenContract.State> = _uiState.asStateFlow()
|
||||
|
||||
private val _actions = MutableSharedFlow<CityScreenContract.Action>(extraBufferCapacity = 1)
|
||||
val actions: SharedFlow<CityScreenContract.Action> = _actions.asSharedFlow()
|
||||
|
||||
init {
|
||||
loadCities()
|
||||
}
|
||||
|
||||
fun onEvent(event: CityScreenContract.Event) {
|
||||
when (event) {
|
||||
CityScreenContract.Event.BackClicked -> _actions.tryEmit(CityScreenContract.Action.NavigateBack)
|
||||
is CityScreenContract.Event.CitySelected -> selectCity(event.city)
|
||||
is CityScreenContract.Event.DetectAutomaticallyChanged -> {
|
||||
if (event.enabled) return // Handled via EnableDetectAutomaticallyResult after permission flow
|
||||
setDetectAutomatically(false)
|
||||
}
|
||||
is CityScreenContract.Event.EnableDetectAutomaticallyResult -> enableDetectAutomaticallyWithLocation(event.location)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCities() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val localeLanguage = Locale.getDefault().language
|
||||
val regions = cityService.getCitiesGroupedByCountry(localeLanguage)
|
||||
val selectedCity = cityService.getSelectedCity()
|
||||
val hasOnlyDefaultCity = regions.isEmpty()
|
||||
val detectAutomatically = cityService.getDetectAutomatically()
|
||||
|
||||
_uiState.value = _uiState.value.copy(
|
||||
regions = regions,
|
||||
selectedCity = selectedCity,
|
||||
isLoading = false,
|
||||
hasOnlyDefaultCity = hasOnlyDefaultCity,
|
||||
detectAutomatically = detectAutomatically,
|
||||
localeLanguage = localeLanguage
|
||||
)
|
||||
|
||||
if (hasOnlyDefaultCity) {
|
||||
_actions.tryEmit(
|
||||
CityScreenContract.Action.ShowToast(
|
||||
appContext.getString(R.string.city_list_unavailable)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectCity(city: org.db3.airmq.sdk.city.domain.City) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
cityService.setSelectedCity(city.id)
|
||||
_actions.tryEmit(CityScreenContract.Action.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDetectAutomatically(enabled: Boolean) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
cityService.setDetectAutomatically(enabled)
|
||||
_uiState.value = _uiState.value.copy(detectAutomatically = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableDetectAutomaticallyWithLocation(location: Location?) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (location != null) {
|
||||
cityService.refreshCityFromLocation(location)
|
||||
cityService.setDetectAutomatically(true)
|
||||
val selectedCity = cityService.getSelectedCity()
|
||||
_uiState.value = _uiState.value.copy(
|
||||
detectAutomatically = true,
|
||||
selectedCity = selectedCity
|
||||
)
|
||||
} else {
|
||||
_actions.tryEmit(
|
||||
CityScreenContract.Action.ShowToast(appContext.getString(R.string.toast_error))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.db3.airmq.features.entry
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.db3.airmq.sdk.city.CityService
|
||||
|
||||
/**
|
||||
* Composable that runs the run-once city resolution flow when city_init is false.
|
||||
* Requests location permission, gets location or country, and calls CityService.initialize().
|
||||
*/
|
||||
@Composable
|
||||
fun CityInitializer(
|
||||
cityService: CityService,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as? android.app.Activity
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { result ->
|
||||
val granted = result.values.any { it }
|
||||
scope.launch {
|
||||
val location = if (granted && activity != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
LocationServices.getFusedLocationProviderClient(activity).getLastLocation().await()
|
||||
}.getOrNull()
|
||||
}
|
||||
} else null
|
||||
|
||||
val country = if (!granted) {
|
||||
withContext(Dispatchers.IO) {
|
||||
(context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager)
|
||||
?.networkCountryIso
|
||||
?.takeIf { !it.isNullOrBlank() }
|
||||
}
|
||||
} else null
|
||||
|
||||
cityService.initialize(granted, location, country)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(cityService) {
|
||||
if (!cityService.isCityInitComplete()) {
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
val allGranted = permissions.all {
|
||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
if (allGranted) {
|
||||
val location = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
LocationServices.getFusedLocationProviderClient(context).getLastLocation().await()
|
||||
}.getOrNull()
|
||||
}
|
||||
val country = withContext(Dispatchers.IO) {
|
||||
(context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager)
|
||||
?.networkCountryIso
|
||||
?.takeIf { !it.isNullOrBlank() }
|
||||
}
|
||||
cityService.initialize(true, location, country)
|
||||
} else {
|
||||
permissionLauncher.launch(permissions)
|
||||
}
|
||||
} else if (cityService.getDetectAutomatically()) {
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
val allGranted = permissions.all {
|
||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
if (allGranted && activity != null) {
|
||||
val location = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
LocationServices.getFusedLocationProviderClient(activity).getLastLocation().await()
|
||||
}.getOrNull()
|
||||
}
|
||||
cityService.refreshCityFromLocation(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content()
|
||||
}
|
||||
Reference in New Issue
Block a user