diff --git a/app/src/main/java/app/myzel394/alibi/enums/RecorderState.kt b/app/src/main/java/app/myzel394/alibi/enums/RecorderState.kt index ed3cb12..2642e6c 100644 --- a/app/src/main/java/app/myzel394/alibi/enums/RecorderState.kt +++ b/app/src/main/java/app/myzel394/alibi/enums/RecorderState.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index 170ab53..4de2300 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -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 diff --git a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt index 2388308..7fb46f3 100644 --- a/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/IntervalRecorderService.kt @@ -57,6 +57,7 @@ abstract class IntervalRecorderService override suspend fun stop() { cycleTimer.shutdown() + super.stop() } fun clearAllRecordings() { diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt index 86ad9bb..2464083 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt @@ -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, diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index 7a42ced..146dc65 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -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() diff --git a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt index 349aa78..92645e8 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -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) } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt index 3e51e1e..1f5415b 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/BaseRecorderModel.kt @@ -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, B : BatchesFolder?> : @@ -28,19 +25,19 @@ abstract class BaseRecorderModel(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(null) protected set var onRecordingSave: () -> Unit = {} @@ -80,14 +77,14 @@ abstract class BaseRecorderModel context.bindService(intent, connection, 0) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt index 4cf8c96..a6e0721 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt @@ -113,6 +113,7 @@ fun RecorderScreen( videoRecorder = videoRecorder, appSettings = appSettings, onSaveLastRecording = { + // TODO: Improve onSave! }, showAudioRecorder = topBarVisible, onHideTopBar = {