diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2bac046..dc9a9d1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/common/AppSettings.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/common/AppSettings.kt new file mode 100644 index 0000000..825d096 --- /dev/null +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/common/AppSettings.kt @@ -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) + ) + } +} diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt index 7bc8aeb..f7f8ece 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt @@ -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) { diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/track/TrackScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/track/TrackScreen.kt index 3cd4519..cd9f45c 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/track/TrackScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/track/TrackScreen.kt @@ -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(null) - var pendingType: ActivityType? = null - private set + val pendingType = MutableStateFlow(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(),