mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-19 07:15:25 +02:00
current stand
This commit is contained in:
parent
542170f189
commit
47b85e74d2
@ -102,6 +102,7 @@ dependencies {
|
|||||||
implementation 'androidx.compose.material3:material3'
|
implementation 'androidx.compose.material3:material3'
|
||||||
implementation "androidx.compose.material:material-icons-extended:1.5.1"
|
implementation "androidx.compose.material:material-icons-extended:1.5.1"
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
package app.myzel394.alibi.services
|
package app.myzel394.alibi.services
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioDeviceCallback
|
import android.media.AudioDeviceCallback
|
||||||
import android.media.AudioDeviceInfo
|
import android.media.AudioDeviceInfo
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.media.MediaRecorder.OnErrorListener
|
import android.media.MediaRecorder.OnErrorListener
|
||||||
|
import android.net.Uri
|
||||||
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.content.ContextCompat.getSystemService
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import app.myzel394.alibi.enums.RecorderState
|
import app.myzel394.alibi.enums.RecorderState
|
||||||
import app.myzel394.alibi.ui.utils.MicrophoneInfo
|
import app.myzel394.alibi.ui.utils.MicrophoneInfo
|
||||||
import java.lang.IllegalStateException
|
import java.lang.IllegalStateException
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
class AudioRecorderService : IntervalRecorderService() {
|
class AudioRecorderService : IntervalRecorderService() {
|
||||||
var amplitudesAmount = 1000
|
var amplitudesAmount = 1000
|
||||||
@ -27,9 +26,6 @@ class AudioRecorderService : IntervalRecorderService() {
|
|||||||
var onMicrophoneDisconnected: () -> Unit = {}
|
var onMicrophoneDisconnected: () -> Unit = {}
|
||||||
var onMicrophoneReconnected: () -> Unit = {}
|
var onMicrophoneReconnected: () -> Unit = {}
|
||||||
|
|
||||||
val filePath: String
|
|
||||||
get() = "${outputFolder}/$counter.${settings!!.fileExtension}"
|
|
||||||
|
|
||||||
/// Tell Android to use the correct bluetooth microphone, if any selected
|
/// Tell Android to use the correct bluetooth microphone, if any selected
|
||||||
private fun startAudioDevice() {
|
private fun startAudioDevice() {
|
||||||
if (selectedMicrophone == null) {
|
if (selectedMicrophone == null) {
|
||||||
@ -61,6 +57,26 @@ class AudioRecorderService : IntervalRecorderService() {
|
|||||||
} else {
|
} else {
|
||||||
MediaRecorder()
|
MediaRecorder()
|
||||||
}.apply {
|
}.apply {
|
||||||
|
setOutputFormat(settings!!.outputFormat)
|
||||||
|
|
||||||
|
// Setting file path
|
||||||
|
if (customOutputFolder == null) {
|
||||||
|
val newFilePath = "${defaultOutputFolder}/$counter.${settings!!.fileExtension}"
|
||||||
|
|
||||||
|
println("newfile path: ${newFilePath}")
|
||||||
|
|
||||||
|
setOutputFile(newFilePath)
|
||||||
|
} else {
|
||||||
|
customOutputFolder!!.createFile(
|
||||||
|
"audio/${settings!!.fileExtension}",
|
||||||
|
"${counter}.${settings!!.fileExtension}"
|
||||||
|
)!!.let {
|
||||||
|
val fileDescriptor =
|
||||||
|
contentResolver.openFileDescriptor(it.uri, "w")!!.fileDescriptor
|
||||||
|
setOutputFile(fileDescriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro
|
// Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro
|
||||||
// and Redmi Buds 3 Pro:
|
// and Redmi Buds 3 Pro:
|
||||||
// - MIC: Uses the bottom microphone of the phone (17)
|
// - MIC: Uses the bottom microphone of the phone (17)
|
||||||
@ -68,8 +84,7 @@ class AudioRecorderService : IntervalRecorderService() {
|
|||||||
// - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17)
|
// - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17)
|
||||||
// - DEFAULT: Uses the bottom microphone of the phone (17)
|
// - DEFAULT: Uses the bottom microphone of the phone (17)
|
||||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||||
setOutputFile(filePath)
|
|
||||||
setOutputFormat(settings!!.outputFormat)
|
|
||||||
setAudioEncoder(settings!!.encoder)
|
setAudioEncoder(settings!!.encoder)
|
||||||
setAudioEncodingBitRate(settings!!.bitRate)
|
setAudioEncodingBitRate(settings!!.bitRate)
|
||||||
setAudioSamplingRate(settings!!.samplingRate)
|
setAudioSamplingRate(settings!!.samplingRate)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package app.myzel394.alibi.services
|
package app.myzel394.alibi.services
|
||||||
|
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import app.myzel394.alibi.dataStore
|
import app.myzel394.alibi.dataStore
|
||||||
import app.myzel394.alibi.db.AudioRecorderSettings
|
import app.myzel394.alibi.db.AudioRecorderSettings
|
||||||
import app.myzel394.alibi.db.RecordingInformation
|
import app.myzel394.alibi.db.RecordingInformation
|
||||||
@ -10,6 +12,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.w3c.dom.DocumentFragment
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.ScheduledExecutorService
|
import java.util.concurrent.ScheduledExecutorService
|
||||||
@ -27,11 +30,15 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() {
|
|||||||
|
|
||||||
private lateinit var cycleTimer: ScheduledExecutorService
|
private lateinit var cycleTimer: ScheduledExecutorService
|
||||||
|
|
||||||
protected val outputFolder: File
|
protected val defaultOutputFolder: File
|
||||||
get() = AudioRecorderExporter.getFolder(this)
|
get() = AudioRecorderExporter.getFolder(this)
|
||||||
|
|
||||||
|
var customOutputFolder: DocumentFile? = null
|
||||||
|
|
||||||
|
var onCustomOutputFolderNotAccessible: () -> Unit = {}
|
||||||
|
|
||||||
fun getRecordingInformation(): RecordingInformation = RecordingInformation(
|
fun getRecordingInformation(): RecordingInformation = RecordingInformation(
|
||||||
folderPath = outputFolder.absolutePath,
|
folderPath = customOutputFolder?.uri?.toString() ?: defaultOutputFolder.absolutePath,
|
||||||
recordingStart = recordingStart,
|
recordingStart = recordingStart,
|
||||||
maxDuration = settings!!.maxDuration,
|
maxDuration = settings!!.maxDuration,
|
||||||
fileExtension = settings!!.fileExtension,
|
fileExtension = settings!!.fileExtension,
|
||||||
@ -61,15 +68,29 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() {
|
|||||||
override fun start() {
|
override fun start() {
|
||||||
super.start()
|
super.start()
|
||||||
|
|
||||||
outputFolder.mkdirs()
|
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
dataStore.data.collectLatest { preferenceSettings ->
|
dataStore.data.collectLatest { preferenceSettings ->
|
||||||
if (settings == null) {
|
if (settings == null) {
|
||||||
settings = Settings.from(preferenceSettings.audioRecorderSettings)
|
settings = Settings.from(preferenceSettings.audioRecorderSettings)
|
||||||
|
|
||||||
|
if (settings!!.folder != null) {
|
||||||
|
customOutputFolder = DocumentFile.fromTreeUri(
|
||||||
|
this@IntervalRecorderService,
|
||||||
|
Uri.parse(settings!!.folder)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!customOutputFolder!!.canRead() || !customOutputFolder!!.canWrite()) {
|
||||||
|
customOutputFolder = null
|
||||||
|
onCustomOutputFolderNotAccessible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createTimer()
|
createTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customOutputFolder == null) {
|
||||||
|
defaultOutputFolder.mkdirs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,15 +111,37 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() {
|
|||||||
cycleTimer.shutdown()
|
cycleTimer.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearAllRecordings() {
|
||||||
|
if (customOutputFolder != null) {
|
||||||
|
customOutputFolder!!.listFiles().forEach {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defaultOutputFolder.listFiles()?.forEach {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
outputFolder.listFiles()?.forEach { file ->
|
if (customOutputFolder != null) {
|
||||||
val fileCounter = file.nameWithoutExtension.toIntOrNull() ?: return
|
customOutputFolder!!.listFiles().forEach {
|
||||||
|
val fileCounter = it.name?.substringBeforeLast(".")?.toIntOrNull() ?: return@forEach
|
||||||
|
|
||||||
if (fileCounter < earliestCounter) {
|
if (fileCounter < earliestCounter) {
|
||||||
file.delete()
|
it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defaultOutputFolder.listFiles()?.forEach {
|
||||||
|
val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return
|
||||||
|
|
||||||
|
if (fileCounter < earliestCounter) {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,6 +154,7 @@ abstract class IntervalRecorderService : ExtraRecorderInformationService() {
|
|||||||
val samplingRate: Int,
|
val samplingRate: Int,
|
||||||
val outputFormat: Int,
|
val outputFormat: Int,
|
||||||
val encoder: Int,
|
val encoder: Int,
|
||||||
|
val folder: String? = null,
|
||||||
) {
|
) {
|
||||||
val fileExtension: String
|
val fileExtension: String
|
||||||
get() = when (outputFormat) {
|
get() = when (outputFormat) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
|
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -38,11 +39,13 @@ import androidx.compose.ui.semantics.contentDescription
|
|||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import app.myzel394.alibi.NotificationHelper
|
import app.myzel394.alibi.NotificationHelper
|
||||||
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.helpers.AudioRecorderExporter
|
import app.myzel394.alibi.helpers.AudioRecorderExporter
|
||||||
|
import app.myzel394.alibi.helpers.AudioRecorderExporter.Companion.clearAllRecordings
|
||||||
import app.myzel394.alibi.services.RecorderNotificationHelper
|
import app.myzel394.alibi.services.RecorderNotificationHelper
|
||||||
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
|
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
|
||||||
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
|
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
|
||||||
@ -72,7 +75,9 @@ fun StartRecording(
|
|||||||
LaunchedEffect(startRecording) {
|
LaunchedEffect(startRecording) {
|
||||||
if (startRecording) {
|
if (startRecording) {
|
||||||
startRecording = false
|
startRecording = false
|
||||||
audioRecorder.notificationDetails = appSettings.notificationSettings.let {
|
|
||||||
|
audioRecorder.let { recorder ->
|
||||||
|
recorder.notificationDetails = appSettings.notificationSettings.let {
|
||||||
if (it == null)
|
if (it == null)
|
||||||
null
|
null
|
||||||
else
|
else
|
||||||
@ -81,10 +86,15 @@ fun StartRecording(
|
|||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
recorder.customOutputFolder = appSettings.audioRecorderSettings.saveFolder.let {
|
||||||
|
if (it == null)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
DocumentFile.fromTreeUri(context, Uri.parse(it))
|
||||||
|
}
|
||||||
|
|
||||||
AudioRecorderExporter.clearAllRecordings(context)
|
recorder.startRecording(context)
|
||||||
|
}
|
||||||
audioRecorder.startRecording(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package app.myzel394.alibi.ui.components.SettingsScreen.atoms
|
package app.myzel394.alibi.ui.components.SettingsScreen.atoms
|
||||||
|
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -42,7 +43,6 @@ import app.myzel394.alibi.ui.components.atoms.SettingsTile
|
|||||||
import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog
|
import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog
|
||||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SaveFolderTile(
|
fun SaveFolderTile(
|
||||||
@ -67,7 +67,7 @@ fun SaveFolderTile(
|
|||||||
return@rememberFolderSelectorDialog
|
return@rememberFolderSelectorDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
updateValue(folder.path)
|
updateValue(folder.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
var showWarning by remember { mutableStateOf(false) }
|
var showWarning by remember { mutableStateOf(false) }
|
||||||
|
@ -9,9 +9,11 @@ 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
|
||||||
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.RecordingInformation
|
import app.myzel394.alibi.db.RecordingInformation
|
||||||
import app.myzel394.alibi.enums.RecorderState
|
import app.myzel394.alibi.enums.RecorderState
|
||||||
|
import app.myzel394.alibi.helpers.AudioRecorderExporter
|
||||||
import app.myzel394.alibi.services.AudioRecorderService
|
import app.myzel394.alibi.services.AudioRecorderService
|
||||||
import app.myzel394.alibi.services.RecorderNotificationHelper
|
import app.myzel394.alibi.services.RecorderNotificationHelper
|
||||||
import app.myzel394.alibi.services.RecorderService
|
import app.myzel394.alibi.services.RecorderService
|
||||||
@ -45,6 +47,7 @@ class AudioRecorderModel : ViewModel() {
|
|||||||
var onRecordingSave: () -> Unit = {}
|
var onRecordingSave: () -> Unit = {}
|
||||||
var onError: () -> Unit = {}
|
var onError: () -> Unit = {}
|
||||||
var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
|
var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
|
||||||
|
var customOutputFolder: DocumentFile? = null
|
||||||
|
|
||||||
var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED
|
var microphoneStatus: MicrophoneConnectivityStatus = MicrophoneConnectivityStatus.CONNECTED
|
||||||
private set
|
private set
|
||||||
@ -58,6 +61,8 @@ class AudioRecorderModel : ViewModel() {
|
|||||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||||
recorderService =
|
recorderService =
|
||||||
((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder ->
|
((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder ->
|
||||||
|
recorder.clearAllRecordings()
|
||||||
|
|
||||||
// Update UI when the service changes
|
// Update UI when the service changes
|
||||||
recorder.onStateChange = { state ->
|
recorder.onStateChange = { state ->
|
||||||
recorderState = state
|
recorderState = state
|
||||||
@ -81,6 +86,7 @@ class AudioRecorderModel : ViewModel() {
|
|||||||
recorder.onMicrophoneReconnected = {
|
recorder.onMicrophoneReconnected = {
|
||||||
microphoneStatus = MicrophoneConnectivityStatus.CONNECTED
|
microphoneStatus = MicrophoneConnectivityStatus.CONNECTED
|
||||||
}
|
}
|
||||||
|
recorder.customOutputFolder = customOutputFolder
|
||||||
}.also {
|
}.also {
|
||||||
// Init UI from the service
|
// Init UI from the service
|
||||||
it.startRecording()
|
it.startRecording()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user