refactor: Migrate foreground services to new architecture

This commit is contained in:
Myzel394 2023-08-08 19:07:54 +02:00
parent bc42f35eba
commit 0bfed18314
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
13 changed files with 164 additions and 123 deletions

View File

@ -37,7 +37,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name=".services.RecorderService" android:foregroundServiceType="microphone" /> <service android:name=".services.AudioRecorderService" android:foregroundServiceType="microphone|camera" />
</application> </application>
</manifest> </manifest>

View File

@ -4,11 +4,13 @@ import android.media.MediaRecorder
import android.os.Build import android.os.Build
class AudioRecorderService: IntervalRecorderService() { class AudioRecorderService: IntervalRecorderService() {
var amplitudesAmount = 1000
var recorder: MediaRecorder? = null var recorder: MediaRecorder? = null
private set private set
val filePath: String val filePath: String
get() = "$folder/$counter.${settings.fileExtension}" get() = "$folder/$counter.${settings!!.fileExtension}"
private fun createRecorder(): MediaRecorder { private fun createRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -18,10 +20,10 @@ class AudioRecorderService: IntervalRecorderService() {
}.apply { }.apply {
setAudioSource(MediaRecorder.AudioSource.MIC) setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFile(filePath) setOutputFile(filePath)
setOutputFormat(settings.outputFormat) setOutputFormat(settings!!.outputFormat)
setAudioEncoder(settings.encoder) setAudioEncoder(settings!!.encoder)
setAudioEncodingBitRate(settings.bitRate) setAudioEncodingBitRate(settings!!.bitRate)
setAudioSamplingRate(settings.samplingRate) setAudioSamplingRate(settings!!.samplingRate)
} }
} }
@ -47,9 +49,13 @@ class AudioRecorderService: IntervalRecorderService() {
recorder = newRecorder recorder = newRecorder
} }
override fun getAmplitudeAmount(): Int { override fun stop() {
return 100 super.stop()
resetRecorder()
} }
override fun getAmplitudeAmount(): Int = amplitudesAmount
override fun getAmplitude(): Int = recorder?.maxAmplitude ?: 0 override fun getAmplitude(): Int = recorder?.maxAmplitude ?: 0
} }

View File

@ -2,6 +2,7 @@ package app.myzel394.alibi.services
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import app.myzel394.alibi.enums.RecorderState
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -12,12 +13,12 @@ abstract class ExtraRecorderInformationService: RecorderService() {
abstract fun getAmplitudeAmount(): Int abstract fun getAmplitudeAmount(): Int
abstract fun getAmplitude(): Int abstract fun getAmplitude(): Int
private var recordingTime = 0L var recordingTime = 0L
private set
private lateinit var recordingTimeTimer: ScheduledExecutorService private lateinit var recordingTimeTimer: ScheduledExecutorService
var amplitudes = mutableListOf<Int>() var amplitudes = mutableListOf<Int>()
private set private set
private lateinit var amplitudesTimer: Timer
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@ -29,6 +30,7 @@ abstract class ExtraRecorderInformationService: RecorderService() {
it.scheduleAtFixedRate( it.scheduleAtFixedRate(
{ {
recordingTime += 1000 recordingTime += 1000
onRecordingTimeChange?.invoke(recordingTime)
}, },
0, 0,
1000, 1000,
@ -38,7 +40,12 @@ abstract class ExtraRecorderInformationService: RecorderService() {
} }
private fun updateAmplitude() { private fun updateAmplitude() {
if (state !== RecorderState.RECORDING) {
return
}
amplitudes.add(getAmplitude()) amplitudes.add(getAmplitude())
onAmplitudeChange?.invoke(amplitudes)
// Delete old amplitudes // Delete old amplitudes
if (amplitudes.size > getAmplitudeAmount()) { if (amplitudes.size > getAmplitudeAmount()) {
@ -49,17 +56,7 @@ abstract class ExtraRecorderInformationService: RecorderService() {
} }
private fun createAmplitudesTimer() { private fun createAmplitudesTimer() {
amplitudesTimer = Timer().also { handler.postDelayed(::updateAmplitude, 100)
it.scheduleAtFixedRate(
object: TimerTask() {
override fun run() {
updateAmplitude()
}
},
0,
100,
)
}
} }
override fun start() { override fun start() {
@ -69,7 +66,6 @@ abstract class ExtraRecorderInformationService: RecorderService() {
override fun pause() { override fun pause() {
recordingTimeTimer.shutdown() recordingTimeTimer.shutdown()
amplitudesTimer.cancel()
} }
override fun resume() { override fun resume() {
@ -79,7 +75,6 @@ abstract class ExtraRecorderInformationService: RecorderService() {
override fun stop() { override fun stop() {
recordingTimeTimer.shutdown() recordingTimeTimer.shutdown()
amplitudesTimer.cancel()
} }
} }

View File

@ -1,21 +1,33 @@
package app.myzel394.alibi.services package app.myzel394.alibi.services
import android.content.Context
import android.media.MediaRecorder import android.media.MediaRecorder
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AudioRecorderSettings import app.myzel394.alibi.db.AudioRecorderSettings
import app.myzel394.alibi.db.LastRecording import app.myzel394.alibi.db.LastRecording
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.time.LocalDateTime
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
import java.util.UUID
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
abstract class IntervalRecorderService: ExtraRecorderInformationService() { abstract class IntervalRecorderService: ExtraRecorderInformationService() {
private var job = SupervisorJob()
private var scope = CoroutineScope(Dispatchers.IO + job)
protected var counter = 0 protected var counter = 0
private set private set
protected lateinit var folder: File protected lateinit var folder: File
lateinit var settings: Settings var settings: Settings? = null
protected set protected set
private lateinit var cycleTimer: ScheduledExecutorService private lateinit var cycleTimer: ScheduledExecutorService
@ -23,10 +35,10 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
fun createLastRecording(): LastRecording = LastRecording( fun createLastRecording(): LastRecording = LastRecording(
folderPath = folder.absolutePath, folderPath = folder.absolutePath,
recordingStart = recordingStart, recordingStart = recordingStart,
maxDuration = settings.maxDuration, maxDuration = settings!!.maxDuration,
fileExtension = settings.fileExtension, fileExtension = settings!!.fileExtension,
intervalDuration = settings.intervalDuration, intervalDuration = settings!!.intervalDuration,
forceExactMaxDuration = settings.forceExactMaxDuration, forceExactMaxDuration = settings!!.forceExactMaxDuration,
) )
// Make overrideable // Make overrideable
@ -41,32 +53,53 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
startNewCycle() startNewCycle()
}, },
0, 0,
settings.intervalDuration, settings!!.intervalDuration,
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS
) )
} }
} }
private fun getRandomFileFolder(): String {
// uuid
val folder = UUID.randomUUID().toString()
return "${externalCacheDir!!.absolutePath}/$folder"
}
override fun start() { override fun start() {
super.start()
folder = File(getRandomFileFolder())
folder.mkdirs() folder.mkdirs()
createTimer() scope.launch {
dataStore.data.collectLatest { preferenceSettings ->
if (settings == null) {
settings = Settings.from(preferenceSettings.audioRecorderSettings)
createTimer()
}
}
}
} }
override fun pause() { override fun pause() {
super.pause()
cycleTimer.shutdown() cycleTimer.shutdown()
} }
override fun resume() { override fun resume() {
super.resume()
createTimer() createTimer()
} }
override fun stop() { override fun stop() {
super.stop()
cycleTimer.shutdown() cycleTimer.shutdown()
} }
private fun deleteOldRecordings() { private fun deleteOldRecordings() {
val timeMultiplier = settings.maxDuration / settings.intervalDuration val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration
val earliestCounter = counter - timeMultiplier val earliestCounter = counter - timeMultiplier
folder.listFiles()?.forEach { file -> folder.listFiles()?.forEach { file ->

View File

@ -26,7 +26,7 @@ abstract class RecorderService: Service() {
lateinit var recordingStart: LocalDateTime lateinit var recordingStart: LocalDateTime
private set private set
var state = RecorderState.RECORDING var state = RecorderState.IDLE
private set private set
var onStateChange: ((RecorderState) -> Unit)? = null var onStateChange: ((RecorderState) -> Unit)? = null
@ -60,28 +60,34 @@ abstract class RecorderService: Service() {
pause() pause()
isPaused = true isPaused = true
} }
else -> throw IllegalStateException("$newState is not a valid state. Destroy or recreate the service instead.") RecorderState.IDLE -> stop()
} }
state = newState state = newState
onStateChange?.invoke(newState) onStateChange?.invoke(newState)
} }
// Must be called immediately after the service is created
fun startRecording() {
recordingStart = LocalDateTime.now()
val notification = buildNotification()
startForeground(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, notification)
// Start
changeState(RecorderState.RECORDING)
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
val notification = buildNotification() startRecording()
startForeground(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, notification)
recordingStart = LocalDateTime.now()
start()
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
stop() changeState(RecorderState.IDLE)
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()

View File

@ -11,11 +11,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import app.myzel394.alibi.dataStore import app.myzel394.alibi.dataStore
import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.ui.enums.Screen
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.screens.AudioRecorder import app.myzel394.alibi.ui.screens.AudioRecorder
import app.myzel394.alibi.ui.screens.SettingsScreen import app.myzel394.alibi.ui.screens.SettingsScreen
import app.myzel394.alibi.ui.screens.WelcomeScreen import app.myzel394.alibi.ui.screens.WelcomeScreen
@ -23,7 +25,9 @@ import app.myzel394.alibi.ui.screens.WelcomeScreen
const val SCALE_IN = 1.25f const val SCALE_IN = 1.25f
@Composable @Composable
fun Navigation() { fun Navigation(
audioRecorder: AudioRecorderModel = viewModel()
) {
val navController = rememberNavController() val navController = rememberNavController()
val context = LocalContext.current val context = LocalContext.current
val settings = context val settings = context
@ -32,6 +36,8 @@ fun Navigation() {
.collectAsState(initial = null) .collectAsState(initial = null)
.value ?: return .value ?: return
audioRecorder.BindToService(context)
NavHost( NavHost(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.background), .background(MaterialTheme.colorScheme.background),
@ -53,7 +59,10 @@ fun Navigation() {
scaleOut(targetScale = SCALE_IN) + fadeOut(tween(durationMillis = 150)) scaleOut(targetScale = SCALE_IN) + fadeOut(tween(durationMillis = 150))
} }
) { ) {
AudioRecorder(navController = navController) AudioRecorder(
navController = navController,
audioRecorder = audioRecorder,
)
} }
composable( composable(
Screen.Settings.route, Screen.Settings.route,
@ -64,7 +73,10 @@ fun Navigation() {
scaleOut(targetScale = 1 / SCALE_IN) + fadeOut(tween(durationMillis = 150)) scaleOut(targetScale = 1 / SCALE_IN) + fadeOut(tween(durationMillis = 150))
} }
) { ) {
SettingsScreen(navController = navController) SettingsScreen(
navController = navController,
audioRecorder = audioRecorder,
)
} }
} }
} }

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.services.RecorderService
import app.myzel394.alibi.ui.MAX_AMPLITUDE import app.myzel394.alibi.ui.MAX_AMPLITUDE
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.utils.clamp import app.myzel394.alibi.ui.utils.clamp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.ceil import kotlin.math.ceil
@ -34,10 +35,10 @@ private const val GROW_END = BOX_DIFF * 4
@Composable @Composable
fun RealtimeAudioVisualizer( fun RealtimeAudioVisualizer(
service: RecorderService, audioRecorder: AudioRecorderModel,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val amplitudes = service.amplitudes val amplitudes = audioRecorder.amplitudes!!
val primary = MaterialTheme.colorScheme.primary val primary = MaterialTheme.colorScheme.primary
val primaryMuted = primary.copy(alpha = 0.3f) val primaryMuted = primary.copy(alpha = 0.3f)
@ -47,7 +48,7 @@ fun RealtimeAudioVisualizer(
val animationProgress = remember { Animatable(0f) } val animationProgress = remember { Animatable(0f) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
service.setOnAmplitudeUpdateListener { audioRecorder.onAmplitudeChange = {
scope.launch { scope.launch {
animationProgress.snapTo(0f) animationProgress.snapTo(0f)
animationProgress.animateTo( animationProgress.animateTo(
@ -66,7 +67,7 @@ fun RealtimeAudioVisualizer(
LaunchedEffect(screenWidth) { LaunchedEffect(screenWidth) {
// Add 1 to allow the visualizer to overflow the screen // Add 1 to allow the visualizer to overflow the screen
service.maxAmplitudes = ceil(screenWidth.toInt() / BOX_DIFF).toInt() + 1 audioRecorder.setMaxAmplitudesAmount(ceil(screenWidth.toInt() / BOX_DIFF).toInt() + 1)
} }
Canvas( Canvas(

View File

@ -88,13 +88,9 @@ fun SaveRecordingButton(
.then(modifier), .then(modifier),
onClick = { onClick = {
isProcessingAudio = true isProcessingAudio = true
RecorderService.stopService(context)
scope.launch { scope.launch {
try { try {
val file = service.concatenateFiles()
onSaveFile(file)
} catch (error: Exception) { } catch (error: Exception) {
Log.getStackTraceString(error) Log.getStackTraceString(error)
} finally { } finally {

View File

@ -47,6 +47,7 @@ import app.myzel394.alibi.ui.components.AudioRecorder.atoms.ConfirmDeletionDialo
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer import app.myzel394.alibi.ui.components.AudioRecorder.atoms.RealtimeAudioVisualizer
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton
import app.myzel394.alibi.ui.components.atoms.Pulsating import app.myzel394.alibi.ui.components.atoms.Pulsating
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.utils.KeepScreenOn import app.myzel394.alibi.ui.utils.KeepScreenOn
import app.myzel394.alibi.ui.utils.formatDuration import app.myzel394.alibi.ui.utils.formatDuration
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -57,15 +58,12 @@ import java.time.ZoneId
@Composable @Composable
fun RecordingStatus( fun RecordingStatus(
service: RecorderService, audioRecorder: AudioRecorderModel,
onSaveFile: (File) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
var now by remember { mutableStateOf(LocalDateTime.now()) } var now by remember { mutableStateOf(LocalDateTime.now()) }
val progress = service.recordingTime.value!! / (service.settings!!.maxDuration / 1000f)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
while (true) { while (true) {
now = LocalDateTime.now() now = LocalDateTime.now()
@ -74,7 +72,7 @@ fun RecordingStatus(
} }
// Only show animation when the recording has just started // Only show animation when the recording has just started
val recordingJustStarted = service.recordingTime.value!! < 1 val recordingJustStarted = audioRecorder.recordingTime!! <= 1000L
var progressVisible by remember { mutableStateOf(!recordingJustStarted) } var progressVisible by remember { mutableStateOf(!recordingJustStarted) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
progressVisible = true progressVisible = true
@ -89,7 +87,7 @@ fun RecordingStatus(
verticalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.SpaceBetween,
) { ) {
Box {} Box {}
RealtimeAudioVisualizer(service = service) RealtimeAudioVisualizer(audioRecorder = audioRecorder)
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
@ -97,7 +95,7 @@ fun RecordingStatus(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
val distance = Duration.between(service.recordingStart!!, now).toMillis() val distance = Duration.between(audioRecorder.recorderService!!.recordingStart, now).toMillis()
Pulsating { Pulsating {
Box( Box(
@ -121,7 +119,7 @@ fun RecordingStatus(
) )
) { ) {
LinearProgressIndicator( LinearProgressIndicator(
progress = progress, progress = audioRecorder.progress,
modifier = Modifier modifier = Modifier
.width(300.dp) .width(300.dp)
) )
@ -137,8 +135,7 @@ fun RecordingStatus(
}, },
onConfirm = { onConfirm = {
showDeleteDialog = false showDeleteDialog = false
RecorderService.stopService(context) audioRecorder.stopRecording(context)
service.reset()
}, },
) )
} }
@ -163,11 +160,5 @@ fun RecordingStatus(
} }
} }
val alpha by animateFloatAsState(if (progressVisible) 1f else 0f, tween(1000)) val alpha by animateFloatAsState(if (progressVisible) 1f else 0f, tween(1000))
SaveRecordingButton(
modifier = Modifier
.alpha(alpha),
service = service,
onSaveFile = onSaveFile,
)
} }
} }

View File

@ -34,6 +34,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -52,15 +53,14 @@ import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.AudioVisualizer import app.myzel394.alibi.ui.components.AudioRecorder.atoms.AudioVisualizer
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton import app.myzel394.alibi.ui.components.AudioRecorder.atoms.SaveRecordingButton
import app.myzel394.alibi.ui.components.atoms.PermissionRequester import app.myzel394.alibi.ui.components.atoms.PermissionRequester
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
@Composable @Composable
fun StartRecording( fun StartRecording(
connection: ServiceConnection, audioRecorder: AudioRecorderModel,
service: RecorderService? = null,
onStart: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val saveFile = rememberFileSaverDialog("audio/*") val saveFile = rememberFileSaverDialog("audio/*")
@ -80,17 +80,7 @@ fun StartRecording(
) )
}, },
onPermissionAvailable = { onPermissionAvailable = {
RecorderService.startService(context, connection) audioRecorder.startRecording(context)
if (service == null) {
onStart()
} else {
// To avoid any leaks from the previous recording, we need to wait until it
// fully started
service.setOnStartedListener {
onStart()
}
}
}, },
) { trigger -> ) { trigger ->
val label = stringResource(R.string.ui_audioRecorder_action_start_label) val label = stringResource(R.string.ui_audioRecorder_action_start_label)
@ -140,21 +130,12 @@ fun StartRecording(
.fillMaxWidth(), .fillMaxWidth(),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
if (service?.hasRecordingAvailable == true) if (false)
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
) { ) {
SaveRecordingButton(
service = service,
onSaveFile = saveFile,
label = stringResource(
R.string.ui_audioRecorder_action_saveOldRecording_label,
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(service.recordingStart),
),
)
} }
else else
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))

View File

@ -5,6 +5,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -19,11 +21,20 @@ class AudioRecorderModel: ViewModel() {
private set private set
var recordingTime by mutableStateOf<Long?>(null) var recordingTime by mutableStateOf<Long?>(null)
private set private set
var amplitudes by mutableStateOf<List<Int>?>(null) var amplitudes by mutableStateOf<List<Int>>(emptyList())
private set private set
var onAmplitudeChange: () -> Unit = {}
val isInRecording: Boolean
get() = recorderState !== RecorderState.IDLE && recordingTime != null
val progress: Float
get() = (recordingTime!! / recorderService!!.settings!!.maxDuration).toFloat()
private var intent: Intent? = null private var intent: Intent? = null
private var recorderService: RecorderService? = null var recorderService: AudioRecorderService? = null
private set
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
@ -36,8 +47,12 @@ class AudioRecorderModel: ViewModel() {
} }
recorder.onAmplitudeChange = { amps -> recorder.onAmplitudeChange = { amps ->
amplitudes = amps amplitudes = amps
onAmplitudeChange()
} }
} }
recorderState = recorderService!!.state
recordingTime = recorderService!!.recordingTime
amplitudes = recorderService!!.amplitudes
} }
override fun onServiceDisconnected(arg0: ComponentName) { override fun onServiceDisconnected(arg0: ComponentName) {
@ -49,7 +64,7 @@ class AudioRecorderModel: ViewModel() {
fun reset() { fun reset() {
recorderState = RecorderState.IDLE recorderState = RecorderState.IDLE
recordingTime = null recordingTime = null
amplitudes = null amplitudes = emptyList()
} }
fun startRecording(context: Context) { fun startRecording(context: Context) {
@ -57,13 +72,30 @@ class AudioRecorderModel: ViewModel() {
context.unbindService(connection) context.unbindService(connection)
} }
val intent = Intent(context, AudioRecorderService::class.java) intent = Intent(context, AudioRecorderService::class.java)
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent!!)
context.bindService(intent, connection, Context.BIND_AUTO_CREATE) context.bindService(intent!!, connection, Context.BIND_AUTO_CREATE)
} }
fun stopRecording(context: Context) { fun stopRecording(context: Context) {
context.stopService(intent) runCatching {
context.unbindService(connection) context.unbindService(connection)
context.stopService(intent)
}
reset()
}
fun setMaxAmplitudesAmount(amount: Int) {
recorderService?.amplitudesAmount = amount
}
@Composable
fun BindToService(context: Context) {
LaunchedEffect(Unit) {
Intent(context, AudioRecorderService::class.java).also { intent ->
context.bindService(intent, connection, 0)
}
}
} }
} }

View File

@ -14,26 +14,25 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import app.myzel394.alibi.services.bindToRecorderService
import app.myzel394.alibi.ui.components.AudioRecorder.molecules.RecordingStatus import app.myzel394.alibi.ui.components.AudioRecorder.molecules.RecordingStatus
import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording
import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.ui.enums.Screen
import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
import app.myzel394.alibi.R import app.myzel394.alibi.R
import app.myzel394.alibi.ui.models.AudioRecorderModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AudioRecorder( fun AudioRecorder(
navController: NavController, navController: NavController,
audioRecorder: AudioRecorderModel
) { ) {
val context = LocalContext.current
val saveFile = rememberFileSaverDialog("audio/aac") val saveFile = rememberFileSaverDialog("audio/aac")
val (connection, service) = bindToRecorderService()
var showRecorderStatus by remember {
mutableStateOf(service?.isRecording ?: false)
}
Scaffold( Scaffold(
topBar = { topBar = {
@ -61,22 +60,12 @@ fun AudioRecorder(
.fillMaxSize() .fillMaxSize()
.padding(padding), .padding(padding),
) { ) {
if (showRecorderStatus && service?.recordingTime?.value != null) if (audioRecorder.isInRecording)
RecordingStatus( RecordingStatus(
service = service, audioRecorder = audioRecorder,
onSaveFile = {
saveFile(it)
showRecorderStatus = false
}
) )
else else
StartRecording( StartRecording(audioRecorder = audioRecorder)
connection = connection,
service = service,
onStart = {
showRecorderStatus = true
}
)
} }
} }
} }

View File

@ -32,7 +32,6 @@ import androidx.navigation.NavController
import app.myzel394.alibi.R import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.services.bindToRecorderService
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile
@ -43,15 +42,15 @@ import app.myzel394.alibi.ui.components.SettingsScreen.atoms.SamplingRateTile
import app.myzel394.alibi.ui.components.atoms.GlobalSwitch import app.myzel394.alibi.ui.components.atoms.GlobalSwitch
import app.myzel394.alibi.ui.components.atoms.MessageBox import app.myzel394.alibi.ui.components.atoms.MessageBox
import app.myzel394.alibi.ui.components.atoms.MessageType import app.myzel394.alibi.ui.components.atoms.MessageType
import app.myzel394.alibi.ui.models.AudioRecorderModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
navController: NavController navController: NavController,
audioRecorder: AudioRecorderModel,
) { ) {
val (_, service) = bindToRecorderService()
val isRecording = service?.isRecording ?: false
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState() rememberTopAppBarState()
) )
@ -91,7 +90,7 @@ fun SettingsScreen(
.value .value
// Show alert // Show alert
if (isRecording) if (audioRecorder.isInRecording)
Box( Box(
modifier = Modifier modifier = Modifier
.padding(16.dp) .padding(16.dp)