Behörigheter: snabblänk till appinställningarna + robustare GPS-flöde
Some checks failed
release / build-release (push) Failing after 1h10m44s
Some checks failed
release / build-release (push) Failing after 1h10m44s
- 'Öppna appinställningarna'-knapp när plats- eller BT-behörighet saknas (spårning, vågparning, vägning) — systemets platslista visar bara appar som frågat, så det här är snabbaste vägen att slå på/av manuellt - 'Försök igen'-knapp som ställer behörighetsfrågan på nytt - POST_NOTIFICATIONS begärs bara på Android 13+ (fanns inte på 12) - Typladdningen i spårskärmen kan inte längre hänga i evig spinner — felmeddelande + försök igen vid nätfel. Version 0.7.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg
This commit is contained in:
@@ -29,7 +29,7 @@ android {
|
|||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = ciRunNumber ?: 1
|
versionCode = ciRunNumber ?: 1
|
||||||
versionName = "0.7.0" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
|
versionName = "0.7.1" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package eu.brassepc.fitnessdroid.ui.common
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öppna systemets inställningssida för appen — snabbvägen att slå på
|
||||||
|
* behörigheter (plats, Bluetooth, notiser) som nekats eller aldrig frågats.
|
||||||
|
*/
|
||||||
|
fun Context.openAppSettings() {
|
||||||
|
runCatching {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", packageName, null),
|
||||||
|
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ import eu.brassepc.fitnessdroid.data.ScaleManager
|
|||||||
import eu.brassepc.fitnessdroid.data.SettingsStore
|
import eu.brassepc.fitnessdroid.data.SettingsStore
|
||||||
import eu.brassepc.fitnessdroid.ui.appContainer
|
import eu.brassepc.fitnessdroid.ui.appContainer
|
||||||
import eu.brassepc.fitnessdroid.ui.common.compact
|
import eu.brassepc.fitnessdroid.ui.common.compact
|
||||||
|
import eu.brassepc.fitnessdroid.ui.common.openAppSettings
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -235,6 +236,10 @@ fun ScalePairScreen(
|
|||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { context.openAppSettings() },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { Text("Öppna appinställningarna") }
|
||||||
}
|
}
|
||||||
scanError?.let {
|
scanError?.let {
|
||||||
Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error)
|
Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error)
|
||||||
@@ -533,6 +538,10 @@ fun WeighScreen(
|
|||||||
"Bluetooth-behörighet nekades — kan inte nå vågen.",
|
"Bluetooth-behörighet nekades — kan inte nå vågen.",
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { context.openAppSettings() },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { Text("Öppna appinställningarna") }
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import eu.brassepc.fitnessdroid.data.encodePolyline
|
|||||||
import eu.brassepc.fitnessdroid.ui.activities.RPE_LABELS
|
import eu.brassepc.fitnessdroid.ui.activities.RPE_LABELS
|
||||||
import eu.brassepc.fitnessdroid.ui.appContainer
|
import eu.brassepc.fitnessdroid.ui.appContainer
|
||||||
import eu.brassepc.fitnessdroid.ui.common.compact
|
import eu.brassepc.fitnessdroid.ui.common.compact
|
||||||
|
import eu.brassepc.fitnessdroid.ui.common.openAppSettings
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.osmdroid.config.Configuration
|
import org.osmdroid.config.Configuration
|
||||||
@@ -82,21 +83,25 @@ class TrackViewModel(
|
|||||||
/** Sammanfattningen som visas efter stopp (null = spårning pågår/ej startad) */
|
/** Sammanfattningen som visas efter stopp (null = spårning pågår/ej startad) */
|
||||||
val summary = MutableStateFlow<TrackingState?>(null)
|
val summary = MutableStateFlow<TrackingState?>(null)
|
||||||
|
|
||||||
var pendingType: ActivityType? = null
|
val pendingType = MutableStateFlow<ActivityType?>(null)
|
||||||
private set
|
val typeLoadError = MutableStateFlow(false)
|
||||||
|
|
||||||
fun loadType(typeId: Int, onMissing: () -> Unit) {
|
fun loadType(typeId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
typeLoadError.value = false
|
||||||
if (TrackingService.state.value.isActive) return@launch
|
if (TrackingService.state.value.isActive) return@launch
|
||||||
runCatching { gymApi.activityTypes().find { it.id == typeId } }
|
runCatching { gymApi.activityTypes().find { it.id == typeId } }
|
||||||
.getOrNull()
|
.fold(
|
||||||
?.let { pendingType = it }
|
onSuccess = { t ->
|
||||||
?: onMissing()
|
if (t != null) pendingType.value = t else typeLoadError.value = true
|
||||||
|
},
|
||||||
|
onFailure = { typeLoadError.value = true },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(context: android.content.Context) {
|
fun start(context: android.content.Context) {
|
||||||
val type = pendingType ?: return
|
val type = pendingType.value ?: return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
TrackingService.start(context, type, repo.bodyWeightKg())
|
TrackingService.start(context, type, repo.bodyWeightKg())
|
||||||
}
|
}
|
||||||
@@ -154,6 +159,8 @@ fun TrackScreen(
|
|||||||
val saved by viewModel.saved.collectAsStateWithLifecycle()
|
val saved by viewModel.saved.collectAsStateWithLifecycle()
|
||||||
val message by viewModel.message.collectAsStateWithLifecycle()
|
val message by viewModel.message.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val pendingType by viewModel.pendingType.collectAsStateWithLifecycle()
|
||||||
|
val typeLoadError by viewModel.typeLoadError.collectAsStateWithLifecycle()
|
||||||
var permissionDenied by remember { mutableStateOf(false) }
|
var permissionDenied by remember { mutableStateOf(false) }
|
||||||
var started by remember { mutableStateOf(false) }
|
var started by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -162,7 +169,7 @@ fun TrackScreen(
|
|||||||
) { granted ->
|
) { granted ->
|
||||||
// Notis-permission är trevlig men inte nödvändig; GPS krävs för distans
|
// Notis-permission är trevlig men inte nödvändig; GPS krävs för distans
|
||||||
val fineOk = granted[Manifest.permission.ACCESS_FINE_LOCATION] ?: false
|
val fineOk = granted[Manifest.permission.ACCESS_FINE_LOCATION] ?: false
|
||||||
val needsGps = viewModel.pendingType?.isDistanceBased == true
|
val needsGps = viewModel.pendingType.value?.isDistanceBased == true
|
||||||
if (!needsGps || fineOk) {
|
if (!needsGps || fineOk) {
|
||||||
viewModel.start(context)
|
viewModel.start(context)
|
||||||
started = true
|
started = true
|
||||||
@@ -171,21 +178,19 @@ fun TrackScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ladda typen och starta (om inte en spårning redan pågår)
|
/** Begär saknade behörigheter (eller starta direkt) — körs även från "Försök igen". */
|
||||||
LaunchedEffect(typeId) {
|
fun tryStart() {
|
||||||
if (tracking.isActive) { started = true; return@LaunchedEffect }
|
val type = pendingType ?: return
|
||||||
viewModel.loadType(typeId, onMissing = onBack)
|
permissionDenied = false
|
||||||
// Vänta in typen
|
|
||||||
while (viewModel.pendingType == null && !tracking.isActive) {
|
|
||||||
kotlinx.coroutines.delay(100)
|
|
||||||
}
|
|
||||||
val type = viewModel.pendingType ?: return@LaunchedEffect
|
|
||||||
val needed = buildList {
|
val needed = buildList {
|
||||||
if (type.isDistanceBased) {
|
if (type.isDistanceBased) {
|
||||||
add(Manifest.permission.ACCESS_FINE_LOCATION)
|
add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
add(Manifest.permission.ACCESS_COARSE_LOCATION)
|
add(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||||
}
|
}
|
||||||
add(Manifest.permission.POST_NOTIFICATIONS)
|
// POST_NOTIFICATIONS finns först i Android 13 (API 33)
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 33) {
|
||||||
|
add(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}
|
||||||
}.filter {
|
}.filter {
|
||||||
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
|
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
@@ -197,6 +202,16 @@ fun TrackScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ladda typen (om inte en spårning redan pågår) …
|
||||||
|
LaunchedEffect(typeId) {
|
||||||
|
if (tracking.isActive) { started = true; return@LaunchedEffect }
|
||||||
|
viewModel.loadType(typeId)
|
||||||
|
}
|
||||||
|
// … och starta när den kommit
|
||||||
|
LaunchedEffect(pendingType) {
|
||||||
|
if (pendingType != null && !started && !tracking.isActive) tryStart()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -208,7 +223,7 @@ fun TrackScreen(
|
|||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
tracking.typeName.ifBlank { viewModel.pendingType?.nameSv ?: "Aktivitet" },
|
tracking.typeName.ifBlank { pendingType?.nameSv ?: "Aktivitet" },
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -234,12 +249,43 @@ fun TrackScreen(
|
|||||||
onSave = { rpe, overrideId -> viewModel.save(rpe, overrideId) },
|
onSave = { rpe, overrideId -> viewModel.save(rpe, overrideId) },
|
||||||
onDone = onBack,
|
onDone = onBack,
|
||||||
)
|
)
|
||||||
permissionDenied -> Column(modifier = Modifier.padding(16.dp)) {
|
permissionDenied -> Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Platsbehörighet nekades — GPS-spårning kräver den. " +
|
"Platsbehörighet saknas — GPS-spårning kräver den.",
|
||||||
"Ge appen platsåtkomst i systeminställningarna och försök igen.",
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Öppna appens inställningar och ge \"Plats\"-behörigheten " +
|
||||||
|
"(Tillåt endast när appen används räcker), kom sen tillbaka " +
|
||||||
|
"och tryck Försök igen.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { context.openAppSettings() },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { Text("Öppna appinställningarna") }
|
||||||
|
androidx.compose.material3.FilledTonalButton(
|
||||||
|
onClick = { tryStart() },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { Text("Försök igen") }
|
||||||
|
}
|
||||||
|
typeLoadError -> Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Kunde inte hämta aktivitetstypen — offline?",
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.loadType(typeId) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { Text("Försök igen") }
|
||||||
}
|
}
|
||||||
!tracking.isActive && !started -> Box(
|
!tracking.isActive && !started -> Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|||||||
Reference in New Issue
Block a user