86 lines
2.9 KiB
Kotlin
86 lines
2.9 KiB
Kotlin
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()
|
||
|
|
}
|
||
|
|
}
|