feat: collect gyro and gps

This commit is contained in:
2026-05-24 07:17:51 +02:00
parent 4315944733
commit c11ad041d7
11 changed files with 616 additions and 0 deletions

View File

@@ -1,5 +1,9 @@
package at.lockstep.player.ui
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -22,9 +26,11 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -34,8 +40,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import at.lockstep.player.LockstepViewModel
import at.lockstep.player.playback.PlaybackService
import at.lockstep.player.R
import at.lockstep.player.util.RunDataCollector
import at.lockstep.player.util.RunDataStorage
data class NowPlayingUiState(
val title: String,
@@ -153,10 +164,38 @@ fun NowPlayingScreen(
fun NowPlayingRoute(
playlistId: String,
playback: PlaybackService?,
viewModel: LockstepViewModel,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val collectRunData by viewModel.collectRunData.collectAsStateWithLifecycle()
val collector = remember { RunDataCollector(context) }
val runSessionFolder = remember { RunDataStorage.newRunSessionFolderName() }
val locationPermissionLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { granted ->
if (collectRunData) {
collector.start(enableLocation = granted)
}
},
)
fun startRunDataCollection() {
val hasLocation =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (!hasLocation) {
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
collector.start(enableLocation = hasLocation)
}
var playlistDisplayName by remember { mutableStateOf("playlist") }
var currentTrackId by remember { mutableStateOf<String?>(null) }
var currentQueueIndex by remember { mutableIntStateOf(0) }
var ui by remember {
mutableStateOf(
NowPlayingUiState(
@@ -170,9 +209,15 @@ fun NowPlayingRoute(
)
}
LaunchedEffect(playlistId) {
playlistDisplayName = viewModel.getPlaylistDisplayName(playlistId)
}
LaunchedEffect(playback) {
val service = playback ?: return@LaunchedEffect
service.uiState.collect { p ->
currentTrackId = p.currentTrackId
currentQueueIndex = p.currentQueueIndex
ui =
NowPlayingUiState(
title = p.title,
@@ -185,6 +230,58 @@ fun NowPlayingRoute(
}
}
LaunchedEffect(collectRunData, playback) {
if (!collectRunData) {
collector.setAcceptSamples(false)
return@LaunchedEffect
}
val service = playback ?: return@LaunchedEffect
var lastTrackId: String? = null
service.uiState.collect { state ->
collector.setAcceptSamples(state.isPlaying)
val trackId = state.currentTrackId
if (trackId != null && trackId != lastTrackId) {
collector.markSongStart()
lastTrackId = trackId
}
}
}
LaunchedEffect(collectRunData, playback, playlistId, playlistDisplayName) {
if (!collectRunData) return@LaunchedEffect
val service = playback ?: return@LaunchedEffect
service.trackBoundaryEvents.collect { event ->
val snapshot = collector.snapshotAndClear()
viewModel.persistRunData(playlistId, playlistDisplayName, runSessionFolder, event, snapshot)
}
}
DisposableEffect(collectRunData) {
if (collectRunData) {
startRunDataCollection()
} else {
collector.stop()
collector.snapshotAndClear()
}
onDispose {
if (collectRunData) {
val snapshot = collector.snapshotAndClear()
val trackId = currentTrackId
if (!snapshot.isEmpty() && trackId != null) {
viewModel.persistRunDataForCurrentTrack(
playlistId = playlistId,
playlistDisplayName = playlistDisplayName,
runSessionFolder = runSessionFolder,
trackId = trackId,
queueIndex = currentQueueIndex,
snapshot = snapshot,
)
}
}
collector.release()
}
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {