Initial commit

This commit is contained in:
2026-08-11 19:46:54 +08:00
commit 8d4535ff00
60 changed files with 2794 additions and 0 deletions
@@ -0,0 +1,85 @@
package com.example.jnicpp.bowling
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
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.isActive
import kotlinx.coroutines.launch
/**
* Holds camera/recording UI state so it survives configuration changes and
* so the state machine lives outside the Activity. This class knows nothing
* about CameraX or ML Kit APIs directly -- [BowlingCameraActivity] and
* [CameraXController] report events into it, and the UI observes it back
* out. That keeps this class trivially unit-testable (no Android camera
* framework involved).
*/
class CameraViewModel : ViewModel() {
sealed interface RecordingState {
data object Idle : RecordingState
data object Starting : RecordingState
data class Recording(val elapsedSeconds: Long) : RecordingState
}
private val _recordingState = MutableStateFlow<RecordingState>(RecordingState.Idle)
val recordingState: StateFlow<RecordingState> = _recordingState.asStateFlow()
private val _permissionsGranted = MutableStateFlow(false)
val permissionsGranted: StateFlow<Boolean> = _permissionsGranted.asStateFlow()
// One-shot user-facing error messages (camera unavailable, detector
// failure, storage failure, ...). SharedFlow, not StateFlow, so the same
// error doesn't get replayed and re-shown after a config change.
private val _errorEvents = MutableSharedFlow<String>(extraBufferCapacity = 4)
val errorEvents: SharedFlow<String> = _errorEvents.asSharedFlow()
private var timerJob: Job? = null
fun onPermissionsResult(granted: Boolean) {
_permissionsGranted.value = granted
}
fun onRecordingStarting() {
_recordingState.value = RecordingState.Starting
}
fun onRecordingStarted() {
timerJob?.cancel()
timerJob = viewModelScope.launch {
var seconds = 0L
while (isActive) {
_recordingState.value = RecordingState.Recording(seconds)
delay(1000)
seconds++
}
}
}
fun onRecordingStopped() {
timerJob?.cancel()
timerJob = null
_recordingState.value = RecordingState.Idle
}
fun postError(message: String) {
_errorEvents.tryEmit(message)
// A failed start/stop shouldn't leave the UI stuck showing a
// recording indicator that no longer reflects reality.
if (_recordingState.value != RecordingState.Idle) {
onRecordingStopped()
}
}
override fun onCleared() {
super.onCleared()
timerJob?.cancel()
}
}