Initial commit
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user