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 package app.myzel394.alibi.enums
enum class RecorderState { enum class RecorderState {
IDLE, STOPPED,
RECORDING, RECORDING,
PAUSED, 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 package app.myzel394.alibi.services
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo
import android.media.AudioDeviceCallback import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo import android.media.AudioDeviceInfo
import android.media.AudioManager import android.media.AudioManager
@ -9,6 +10,8 @@ import android.media.MediaRecorder.OnErrorListener
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper 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.AppSettings
import app.myzel394.alibi.db.AudioRecorderSettings import app.myzel394.alibi.db.AudioRecorderSettings
import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.db.RecordingInformation
@ -71,11 +74,10 @@ class AudioRecorderService :
} }
override suspend fun stop() { override suspend fun stop() {
super.stop()
resetRecorder() resetRecorder()
selectedMicrophone = null
unregisterMicrophoneListener() unregisterMicrophoneListener()
super.stop()
} }
override fun resume() { override fun resume() {
@ -83,6 +85,19 @@ class AudioRecorderService :
createAmplitudesTimer() 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 ==== // ==== Amplitude related ====
private fun getAmplitudeAmount(): Int = amplitudesAmount private fun getAmplitudeAmount(): Int = amplitudesAmount

View File

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

View File

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

View File

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

View File

@ -2,9 +2,10 @@ package app.myzel394.alibi.services
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Range import android.util.Range
import androidx.camera.core.Camera import androidx.camera.core.Camera
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.TorchState import androidx.camera.core.TorchState
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
@ -15,7 +16,9 @@ import androidx.camera.video.Recorder
import androidx.camera.video.Recording import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent import androidx.camera.video.VideoRecordEvent
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.enums.RecorderState
@ -25,7 +28,6 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
@ -100,6 +102,19 @@ class VideoRecorderService :
stopActiveRecording() 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") @SuppressLint("MissingPermission")
override fun startNewCycle() { override fun startNewCycle() {
super.startNewCycle() super.startNewCycle()
@ -109,7 +124,7 @@ class VideoRecorderService :
val newRecording = prepareVideoRecording() val newRecording = prepareVideoRecording()
activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event -> 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) _videoFinalizerListener.complete(Unit)
} }
} }

View File

@ -4,22 +4,19 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder import android.os.IBinder
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.services.IntervalRecorderService import app.myzel394.alibi.services.IntervalRecorderService
import app.myzel394.alibi.services.RecorderNotificationHelper import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.services.RecorderService
import kotlinx.coroutines.delay
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : IntervalRecorderService<S, I>, B : BatchesFolder?> : 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) var recorderState by mutableStateOf(RecorderState.IDLE)
protected set protected set
var recordingTime by mutableStateOf<Long?>(null) var recordingTime by mutableLongStateOf(0)
protected set protected set
open val isInRecording: Boolean open val isInRecording: Boolean
get() = recorderState !== RecorderState.IDLE && recordingTime != null && recorderService != null get() = recorderService != null
val isPaused: Boolean val isPaused: Boolean
get() = recorderState === RecorderState.PAUSED get() = recorderState === RecorderState.PAUSED
val progress: Float 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 protected set
var onRecordingSave: () -> Unit = {} var onRecordingSave: () -> Unit = {}
@ -80,14 +77,14 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
} }
override fun onServiceDisconnected(arg0: ComponentName) { override fun onServiceDisconnected(arg0: ComponentName) {
recorderService = null
reset() reset()
} }
} }
open fun reset() { open fun reset() {
recorderService = null
recorderState = RecorderState.IDLE recorderState = RecorderState.IDLE
recordingTime = null recordingTime = 0
} }
protected open fun handleIntent(intent: Intent) = intent protected open fun handleIntent(intent: Intent) = intent
@ -99,6 +96,7 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
) { ) {
this.settings = settings this.settings = settings
// Clean up
runCatching { runCatching {
recorderService?.clearAllRecordings() recorderService?.clearAllRecordings()
context.unbindService(connection) context.unbindService(connection)
@ -132,22 +130,25 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
} }
suspend fun stopRecording(context: Context) { 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() recorderService!!.stopRecording()
val intent = Intent(context, intentClass) val intent = Intent(context, intentClass)
context.unbindService(connection)
unbindFromService(context)
context.stopService(intent) context.stopService(intent)
} }
fun pauseRecording() { fun pauseRecording() {
recorderService!!.changeState(RecorderState.PAUSED) recorderService!!.pauseRecording()
} }
fun resumeRecording() { 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) { fun bindToService(context: Context) {
Intent(context, intentClass).also { intent -> Intent(context, intentClass).also { intent ->
context.bindService(intent, connection, 0) context.bindService(intent, connection, 0)

View File

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