current stand

This commit is contained in:
Myzel394 2023-12-15 18:44:47 +01:00
parent de30f681e8
commit 3d355df522
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
8 changed files with 107 additions and 80 deletions

View File

@ -1,7 +1,10 @@
package app.myzel394.alibi.enums
enum class RecorderState {
IDLE,
STOPPED,
RECORDING,
PAUSED,
// Only used by the model to indicate that the service is not running
IDLE
}

View File

@ -1,6 +1,7 @@
package app.myzel394.alibi.services
import android.content.Context
import android.content.pm.ServiceInfo
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
@ -9,6 +10,8 @@ import android.media.MediaRecorder.OnErrorListener
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.core.app.ServiceCompat
import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.AudioRecorderSettings
import app.myzel394.alibi.db.RecordingInformation
@ -71,11 +74,10 @@ class AudioRecorderService :
}
override suspend fun stop() {
super.stop()
resetRecorder()
selectedMicrophone = null
unregisterMicrophoneListener()
super.stop()
}
override fun resume() {
@ -83,6 +85,19 @@ class AudioRecorderService :
createAmplitudesTimer()
}
override fun startForegroundService() {
ServiceCompat.startForeground(
this,
NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID,
getNotificationHelper().buildStartingNotification(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
} else {
0
},
)
}
// ==== Amplitude related ====
private fun getAmplitudeAmount(): Int = amplitudesAmount

View File

@ -57,6 +57,7 @@ abstract class IntervalRecorderService<S : IntervalRecorderService.Settings, I>
override suspend fun stop() {
cycleTimer.shutdown()
super.stop()
}
fun clearAllRecordings() {

View File

@ -112,7 +112,7 @@ data class RecorderNotificationHelper(
.addAction(
R.drawable.ic_cancel,
context.getString(R.string.ui_audioRecorder_action_delete_label),
getNotificationChangeStateIntent(RecorderState.IDLE, 1),
getNotificationChangeStateIntent(RecorderState.STOPPED, 1),
)
.addAction(
R.drawable.ic_pause,

View File

@ -2,7 +2,6 @@ package app.myzel394.alibi.services
import android.annotation.SuppressLint
import android.app.Notification
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Binder
@ -25,29 +24,60 @@ abstract class RecorderService : LifecycleService() {
private val binder = RecorderBinder()
private var isPaused: Boolean = false
lateinit var recordingStart: LocalDateTime
private set
private lateinit var recordingTimeTimer: ScheduledExecutorService
private var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
var state = RecorderState.IDLE
private set
protected var _newState = RecorderState.IDLE
var state = RecorderState.STOPPED
private set
var onStateChange: ((RecorderState) -> Unit)? = null
var onError: () -> Unit = {}
var onRecordingTimeChange: ((Long) -> Unit)? = null
var recordingTime = 0L
private set
private lateinit var recordingTimeTimer: ScheduledExecutorService
var onRecordingTimeChange: ((Long) -> Unit)? = null
var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
protected abstract fun start()
protected abstract fun pause()
// TODO: Move pause / recording here
protected abstract fun resume()
protected abstract suspend fun stop()
protected open suspend fun stop() {
}
override fun onDestroy() {
NotificationManagerCompat.from(this)
.cancel(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID)
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
super.onDestroy()
}
protected abstract fun startForegroundService()
fun startRecording() {
recordingStart = LocalDateTime.now()
startForegroundService()
changeState(RecorderState.RECORDING)
start()
}
suspend fun stopRecording() {
changeState(RecorderState.STOPPED)
stop()
}
fun pauseRecording() {
changeState(RecorderState.PAUSED)
}
fun resumeRecording() {
changeState(RecorderState.RECORDING)
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
@ -68,7 +98,7 @@ abstract class RecorderService : LifecycleService() {
"changeState" -> {
val newState = intent.getStringExtra("newState")?.let {
RecorderState.valueOf(it)
} ?: RecorderState.IDLE
} ?: RecorderState.STOPPED
changeState(newState)
}
}
@ -94,14 +124,9 @@ abstract class RecorderService : LifecycleService() {
}
}
protected fun _changeStateValue(newState: RecorderState) {
state = newState
onStateChange?.invoke(newState)
}
// Used to change the state of the service
// will internally call start() / pause() / resume() / stop()
// Immediately after creating the service make sure to call `changeState(RecorderState.RECORDING)`
@SuppressLint("MissingPermission")
fun changeState(newState: RecorderState) {
if (state == newState) {
@ -114,23 +139,24 @@ abstract class RecorderService : LifecycleService() {
if (isPaused) {
resume()
isPaused = false
} else {
start()
}
// `start` is handled by `startRecording`
createRecordingTimeTimer()
}
RecorderState.PAUSED -> {
pause()
isPaused = true
recordingTimeTimer.shutdown()
pause()
}
RecorderState.IDLE -> {
RecorderState.STOPPED -> {
recordingTimeTimer.shutdown()
}
else -> {}
}
// Update notification
@ -148,48 +174,13 @@ abstract class RecorderService : LifecycleService() {
)
}
_changeStateValue(newState)
onStateChange?.invoke(newState)
}
// Must be immediately called after creating the service!
fun startRecording() {
recordingStart = LocalDateTime.now()
ServiceCompat.startForeground(
this,
NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID,
getNotificationHelper().buildStartingNotification(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
} else {
0
},
)
// Start
changeState(RecorderState.RECORDING)
}
suspend fun stopRecording() {
_newState = RecorderState.IDLE
stop()
changeState(RecorderState.IDLE)
}
override fun onDestroy() {
super.onDestroy()
stopForeground(STOP_FOREGROUND_REMOVE)
NotificationManagerCompat.from(this)
.cancel(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID)
stopSelf()
}
private fun getNotificationHelper(): RecorderNotificationHelper {
protected fun getNotificationHelper(): RecorderNotificationHelper {
return RecorderNotificationHelper(this, notificationDetails)
}
private fun buildNotification(): Notification {
val notificationHelper = getNotificationHelper()

View File

@ -2,9 +2,10 @@ package app.myzel394.alibi.services
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Range
import androidx.camera.core.Camera
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector
import androidx.camera.core.TorchState
import androidx.camera.lifecycle.ProcessCameraProvider
@ -15,7 +16,9 @@ import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.enums.RecorderState
@ -25,7 +28,6 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
@ -100,6 +102,19 @@ class VideoRecorderService :
stopActiveRecording()
}
override fun startForegroundService() {
ServiceCompat.startForeground(
this,
NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID,
getNotificationHelper().buildStartingNotification(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
} else {
0
},
)
}
@SuppressLint("MissingPermission")
override fun startNewCycle() {
super.startNewCycle()
@ -109,7 +124,7 @@ class VideoRecorderService :
val newRecording = prepareVideoRecording()
activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event ->
if (event is VideoRecordEvent.Finalize && this@VideoRecorderService._newState == RecorderState.IDLE) {
if (event is VideoRecordEvent.Finalize && this@VideoRecorderService.state == RecorderState.STOPPED) {
_videoFinalizerListener.complete(Unit)
}
}

View File

@ -4,22 +4,19 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.services.IntervalRecorderService
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.services.RecorderService
import kotlinx.coroutines.delay
import kotlinx.serialization.json.Json
abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : IntervalRecorderService<S, I>, B : BatchesFolder?> :
@ -28,19 +25,19 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
var recorderState by mutableStateOf(RecorderState.IDLE)
protected set
var recordingTime by mutableStateOf<Long?>(null)
var recordingTime by mutableLongStateOf(0)
protected set
open val isInRecording: Boolean
get() = recorderState !== RecorderState.IDLE && recordingTime != null && recorderService != null
get() = recorderService != null
val isPaused: Boolean
get() = recorderState === RecorderState.PAUSED
val progress: Float
get() = (recordingTime!! / recorderService!!.settings.maxDuration).toFloat()
get() = (recordingTime / recorderService!!.settings.maxDuration).toFloat()
var recorderService: T? = null
var recorderService by mutableStateOf<T?>(null)
protected set
var onRecordingSave: () -> Unit = {}
@ -80,14 +77,14 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
}
override fun onServiceDisconnected(arg0: ComponentName) {
recorderService = null
reset()
}
}
open fun reset() {
recorderService = null
recorderState = RecorderState.IDLE
recordingTime = null
recordingTime = 0
}
protected open fun handleIntent(intent: Intent) = intent
@ -99,6 +96,7 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
) {
this.settings = settings
// Clean up
runCatching {
recorderService?.clearAllRecordings()
context.unbindService(connection)
@ -132,22 +130,25 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
}
suspend fun stopRecording(context: Context) {
// TODO: Make modal on video only appear on long press and by default use back camera
// TODO: Also show what camera is in use while recording
recorderService!!.stopRecording()
val intent = Intent(context, intentClass)
unbindFromService(context)
context.unbindService(connection)
context.stopService(intent)
}
fun pauseRecording() {
recorderService!!.changeState(RecorderState.PAUSED)
recorderService!!.pauseRecording()
}
fun resumeRecording() {
recorderService!!.changeState(RecorderState.RECORDING)
recorderService!!.resumeRecording()
}
// Bind functions used to manually bind to the service if the app
// is closed and reopened for example
fun bindToService(context: Context) {
Intent(context, intentClass).also { intent ->
context.bindService(intent, connection, 0)

View File

@ -113,6 +113,7 @@ fun RecorderScreen(
videoRecorder = videoRecorder,
appSettings = appSettings,
onSaveLastRecording = {
// TODO: Improve onSave!
},
showAudioRecorder = topBarVisible,
onHideTopBar = {