mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
refactor: Migrate foreground services to new architecture
This commit is contained in:
parent
bc42f35eba
commit
0bfed18314
@ -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>
|
@ -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
|
||||||
}
|
}
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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 ->
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user