Behörigheter: snabblänk till appinställningarna + robustare GPS-flöde
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:
2026-08-24 08:27:36 +02:00
parent 444028c91e
commit cf91020aae
4 changed files with 99 additions and 23 deletions

View File

@@ -29,7 +29,7 @@ android {
minSdk = 31
targetSdk = 35
versionCode = ciRunNumber ?: 1
versionName = "0.7.0" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
versionName = "0.7.1" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
}
signingConfigs {

View File

@@ -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)
)
}
}

View File

@@ -63,6 +63,7 @@ import eu.brassepc.fitnessdroid.data.ScaleManager
import eu.brassepc.fitnessdroid.data.SettingsStore
import eu.brassepc.fitnessdroid.ui.appContainer
import eu.brassepc.fitnessdroid.ui.common.compact
import eu.brassepc.fitnessdroid.ui.common.openAppSettings
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
@@ -235,6 +236,10 @@ fun ScalePairScreen(
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
FilledTonalButton(
onClick = { context.openAppSettings() },
modifier = Modifier.fillMaxWidth(),
) { Text("Öppna appinställningarna") }
}
scanError?.let {
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.",
color = MaterialTheme.colorScheme.error,
)
FilledTonalButton(
onClick = { context.openAppSettings() },
modifier = Modifier.fillMaxWidth(),
) { Text("Öppna appinställningarna") }
}
when (val s = state) {

View File

@@ -62,6 +62,7 @@ import eu.brassepc.fitnessdroid.data.encodePolyline
import eu.brassepc.fitnessdroid.ui.activities.RPE_LABELS
import eu.brassepc.fitnessdroid.ui.appContainer
import eu.brassepc.fitnessdroid.ui.common.compact
import eu.brassepc.fitnessdroid.ui.common.openAppSettings
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.osmdroid.config.Configuration
@@ -82,21 +83,25 @@ class TrackViewModel(
/** Sammanfattningen som visas efter stopp (null = spårning pågår/ej startad) */
val summary = MutableStateFlow<TrackingState?>(null)
var pendingType: ActivityType? = null
private set
val pendingType = MutableStateFlow<ActivityType?>(null)
val typeLoadError = MutableStateFlow(false)
fun loadType(typeId: Int, onMissing: () -> Unit) {
fun loadType(typeId: Int) {
viewModelScope.launch {
typeLoadError.value = false
if (TrackingService.state.value.isActive) return@launch
runCatching { gymApi.activityTypes().find { it.id == typeId } }
.getOrNull()
?.let { pendingType = it }
?: onMissing()
.fold(
onSuccess = { t ->
if (t != null) pendingType.value = t else typeLoadError.value = true
},
onFailure = { typeLoadError.value = true },
)
}
}
fun start(context: android.content.Context) {
val type = pendingType ?: return
val type = pendingType.value ?: return
viewModelScope.launch {
TrackingService.start(context, type, repo.bodyWeightKg())
}
@@ -154,6 +159,8 @@ fun TrackScreen(
val saved by viewModel.saved.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 started by remember { mutableStateOf(false) }
@@ -162,7 +169,7 @@ fun TrackScreen(
) { granted ->
// 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 needsGps = viewModel.pendingType?.isDistanceBased == true
val needsGps = viewModel.pendingType.value?.isDistanceBased == true
if (!needsGps || fineOk) {
viewModel.start(context)
started = true
@@ -171,21 +178,19 @@ fun TrackScreen(
}
}
// Ladda typen och starta (om inte en spårning redan pågår)
LaunchedEffect(typeId) {
if (tracking.isActive) { started = true; return@LaunchedEffect }
viewModel.loadType(typeId, onMissing = onBack)
// Vänta in typen
while (viewModel.pendingType == null && !tracking.isActive) {
kotlinx.coroutines.delay(100)
}
val type = viewModel.pendingType ?: return@LaunchedEffect
/** Begär saknade behörigheter (eller starta direkt) — körs även från "Försök igen". */
fun tryStart() {
val type = pendingType ?: return
permissionDenied = false
val needed = buildList {
if (type.isDistanceBased) {
add(Manifest.permission.ACCESS_FINE_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 {
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(
topBar = {
TopAppBar(
@@ -208,7 +223,7 @@ fun TrackScreen(
tint = MaterialTheme.colorScheme.primary,
)
Text(
tracking.typeName.ifBlank { viewModel.pendingType?.nameSv ?: "Aktivitet" },
tracking.typeName.ifBlank { pendingType?.nameSv ?: "Aktivitet" },
modifier = Modifier.padding(start = 8.dp),
)
}
@@ -234,12 +249,43 @@ fun TrackScreen(
onSave = { rpe, overrideId -> viewModel.save(rpe, overrideId) },
onDone = onBack,
)
permissionDenied -> Column(modifier = Modifier.padding(16.dp)) {
permissionDenied -> Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
"Platsbehörighet nekades — GPS-spårning kräver den. " +
"Ge appen platsåtkomst i systeminställningarna och försök igen.",
"Platsbehörighet saknas — GPS-spårning kräver den.",
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,
)
Button(
onClick = { viewModel.loadType(typeId) },
modifier = Modifier.fillMaxWidth(),
) { Text("Försök igen") }
}
!tracking.isActive && !started -> Box(
modifier = Modifier.fillMaxSize(),