mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-09-03 09:49:12 +02:00
android: refactor... stuff (5, see description)
reimplement recording mainily
This commit is contained in:
@@ -55,7 +55,8 @@ kotlin {
|
|||||||
"kotlinx.coroutines.FlowPreview",
|
"kotlinx.coroutines.FlowPreview",
|
||||||
"kotlinx.serialization.ExperimentalSerializationApi",
|
"kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
"kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi"
|
"dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi",
|
||||||
|
"com.google.accompanist.permissions.ExperimentalPermissionsApi"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import android.media.MediaCodecInfo
|
|||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
class EldDecoder {
|
class EldDecoder: AutoCloseable{
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val ASC = byteArrayOf(
|
private val ASC = byteArrayOf(
|
||||||
@@ -98,7 +98,7 @@ class EldDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
override fun close() {
|
||||||
codec.stop()
|
codec.stop()
|
||||||
codec.release()
|
codec.release()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ class WavWriter(
|
|||||||
private val bitsPerSample: Int = 16
|
private val bitsPerSample: Int = 16
|
||||||
) : AutoCloseable {
|
) : AutoCloseable {
|
||||||
|
|
||||||
private val raf = RandomAccessFile(file, "rw")
|
private val raf = try {
|
||||||
|
RandomAccessFile(file, "rw")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Failed to open file for writing", e)
|
||||||
|
}
|
||||||
private var dataSize = 0L
|
private var dataSize = 0L
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|||||||
+10
-14
@@ -522,9 +522,9 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
|
|
||||||
device.updateState {
|
device.updateState {
|
||||||
it.copy(
|
it.copy(
|
||||||
headphoneAccomodation = eqData,
|
headphoneAccommodation = eqData,
|
||||||
headphoneAccomodationEnabledForMedia = eqOnMedia,
|
headphoneAccommodationEnabledForMedia = eqOnMedia,
|
||||||
headphoneAccomodationEnabledForPhone = eqOnPhone,
|
headphoneAccommodationEnabledForPhone = eqOnPhone,
|
||||||
aacpPackets = it.aacpPackets + packet
|
aacpPackets = it.aacpPackets + packet
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -561,10 +561,8 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
MessageOpcode.MICROPHONE_STREAM -> {
|
MessageOpcode.MICROPHONE_STREAM -> {
|
||||||
try {
|
try {
|
||||||
MicrophoneFrame.parsePacket(packet).forEach{ frame ->
|
MicrophoneFrame.parsePacket(packet).forEach{ frame ->
|
||||||
device.updateState {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
it.copy(
|
device.emitMicrophoneFrame(frame)
|
||||||
microphoneFrames = it.microphoneFrames + frame
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1007,9 +1005,9 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
|
|
||||||
device.updateState {
|
device.updateState {
|
||||||
it.copy(
|
it.copy(
|
||||||
headphoneAccomodation = eq.copyOf(),
|
headphoneAccommodation = eq.copyOf(),
|
||||||
headphoneAccomodationEnabledForMedia = media == 0x01.toByte(),
|
headphoneAccommodationEnabledForMedia = media == 0x01.toByte(),
|
||||||
headphoneAccomodationEnabledForPhone = phone == 0x01.toByte()
|
headphoneAccommodationEnabledForPhone = phone == 0x01.toByte()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1042,7 +1040,6 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
it.copy(
|
it.copy(
|
||||||
battery = emptySet(),
|
battery = emptySet(),
|
||||||
connectedDevices = emptyList(),
|
connectedDevices = emptyList(),
|
||||||
microphoneFrames = emptyList(),
|
|
||||||
controlStates = emptyMap(),
|
controlStates = emptyMap(),
|
||||||
aacpPackets = emptyList(),
|
aacpPackets = emptyList(),
|
||||||
headTrackingState = BuddyState.INACTIVE,
|
headTrackingState = BuddyState.INACTIVE,
|
||||||
@@ -1072,7 +1069,7 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
fun requestMicrophoneStream(): Boolean {
|
fun requestMicrophoneStream(): Boolean {
|
||||||
val payload = byteArrayOf(
|
val payload = byteArrayOf(
|
||||||
0x00, 0x00,
|
0x00, 0x00,
|
||||||
0x09, 0x00,
|
0x09, 0x00, // length
|
||||||
0x00, 0x01,
|
0x00, 0x01,
|
||||||
0x82.toByte(), 0x00,
|
0x82.toByte(), 0x00,
|
||||||
0x00, 0x00,
|
0x00, 0x00,
|
||||||
@@ -1090,9 +1087,8 @@ class AACPManager(private val device: AppleDevice) {
|
|||||||
|
|
||||||
fun endMicrophoneStream(): Boolean {
|
fun endMicrophoneStream(): Boolean {
|
||||||
val payload = byteArrayOf(
|
val payload = byteArrayOf(
|
||||||
MessageOpcode.MICROPHONE_STREAM.value, 0x00,
|
|
||||||
0x00, 0x00,
|
0x00, 0x00,
|
||||||
0x02, 0x00,
|
0x02, 0x00, // length
|
||||||
0x03, 0x01
|
0x03, 0x01
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ class RTBuddyManager(
|
|||||||
is SensorDataWxBuddyPayload -> {
|
is SensorDataWxBuddyPayload -> {
|
||||||
val data = payload.data
|
val data = payload.data
|
||||||
observeSequence(data)
|
observeSequence(data)
|
||||||
|
|
||||||
// handleSensorData(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -48,28 +46,6 @@ class RTBuddyManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun handleSensorData(data: SensorDataWX) {
|
|
||||||
// if (data.hasServiceSettings()) {
|
|
||||||
// when (data.serviceSettings.service) {
|
|
||||||
// SensorServiceType.ACTIVITY,
|
|
||||||
// SensorServiceType.DEVMOTION6 -> {
|
|
||||||
// // Head tracking data/configuration
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// SensorServiceType.HEARTRATE -> {
|
|
||||||
// // Heart rate
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// else -> {
|
|
||||||
// Log.d(
|
|
||||||
// TAG,
|
|
||||||
// "Unhandled sensor service: ${data.serviceSettings.service}"
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun sendSensorData(
|
fun sendSensorData(
|
||||||
data: SensorDataWX,
|
data: SensorDataWX,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package me.kavishdevar.librepods.bluetooth.aacp.types
|
package me.kavishdevar.librepods.bluetooth.aacp.types
|
||||||
|
|
||||||
|
import me.kavishdevar.librepods.data.audio.MicrophoneFrame
|
||||||
|
|
||||||
sealed interface AppleEvent {
|
sealed interface AppleEvent {
|
||||||
data class StemPress(
|
data class StemPress(
|
||||||
val pressType: StemPressType,
|
val pressType: StemPressType,
|
||||||
@@ -15,7 +17,7 @@ sealed interface AppleEvent {
|
|||||||
val reverseTapped: Boolean
|
val reverseTapped: Boolean
|
||||||
): AppleEvent
|
): AppleEvent
|
||||||
|
|
||||||
data class HeadGesturesResult(
|
data class MicrophoneFrameEvent(
|
||||||
val yes: Boolean,
|
val frame: MicrophoneFrame
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
package me.kavishdevar.librepods.data.recording
|
package me.kavishdevar.librepods.data.recording
|
||||||
|
|
||||||
class RecordingState {
|
data class RecordingState(
|
||||||
val isRecording: Boolean = false
|
val isRecording: Boolean = false,
|
||||||
val recording: Recording? = null
|
val recording: Recording? = null
|
||||||
}
|
)
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package me.kavishdevar.librepods.devices
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.google.protobuf.copy
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -25,6 +27,10 @@ import me.kavishdevar.librepods.bluetooth.att.ATTHandle
|
|||||||
import me.kavishdevar.librepods.bluetooth.att.ATTManager
|
import me.kavishdevar.librepods.bluetooth.att.ATTManager
|
||||||
import me.kavishdevar.librepods.data.StemAction
|
import me.kavishdevar.librepods.data.StemAction
|
||||||
import me.kavishdevar.librepods.data.apple.BuddyState
|
import me.kavishdevar.librepods.data.apple.BuddyState
|
||||||
|
import me.kavishdevar.librepods.data.audio.MicrophoneFrame
|
||||||
|
import me.kavishdevar.librepods.data.audio.MicrophoneState
|
||||||
|
import me.kavishdevar.librepods.data.recording.Recording
|
||||||
|
import me.kavishdevar.librepods.data.recording.RecordingState
|
||||||
import me.kavishdevar.librepods.utils.GestureDetector
|
import me.kavishdevar.librepods.utils.GestureDetector
|
||||||
import me.kavishdevar.librepods.utils.HeadTracking
|
import me.kavishdevar.librepods.utils.HeadTracking
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
@@ -35,6 +41,7 @@ private const val TAG = "AppleDevice"
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
class AppleDevice(
|
class AppleDevice(
|
||||||
|
override val context: Context,
|
||||||
override val bluetoothAdapter: BluetoothAdapter,
|
override val bluetoothAdapter: BluetoothAdapter,
|
||||||
override val bluetoothDevice: BluetoothDevice,
|
override val bluetoothDevice: BluetoothDevice,
|
||||||
currentState: ConnectionState
|
currentState: ConnectionState
|
||||||
@@ -55,6 +62,11 @@ class AppleDevice(
|
|||||||
private val _events = MutableSharedFlow<AppleEvent>()
|
private val _events = MutableSharedFlow<AppleEvent>()
|
||||||
val events = _events.asSharedFlow()
|
val events = _events.asSharedFlow()
|
||||||
|
|
||||||
|
private val _microphoneFrames = MutableSharedFlow<MicrophoneFrame>(
|
||||||
|
extraBufferCapacity = 64
|
||||||
|
)
|
||||||
|
val microphoneFrames = _microphoneFrames.asSharedFlow()
|
||||||
|
|
||||||
private val _connectionNumber = MutableStateFlow(0)
|
private val _connectionNumber = MutableStateFlow(0)
|
||||||
override val connectionNumber = _connectionNumber.asStateFlow()
|
override val connectionNumber = _connectionNumber.asStateFlow()
|
||||||
|
|
||||||
@@ -86,6 +98,10 @@ class AppleDevice(
|
|||||||
_events.emit(event)
|
_events.emit(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal suspend fun emitMicrophoneFrame(frame: MicrophoneFrame) {
|
||||||
|
_microphoneFrames.emit(frame)
|
||||||
|
}
|
||||||
|
|
||||||
val aacp = AACPManager(this)
|
val aacp = AACPManager(this)
|
||||||
val att = ATTManager(this)
|
val att = ATTManager(this)
|
||||||
|
|
||||||
@@ -191,9 +207,35 @@ class AppleDevice(
|
|||||||
|
|
||||||
suspend fun readATTCharacteristic(handle: ATTHandle): ByteArray? = att.readCharacteristic(handle)
|
suspend fun readATTCharacteristic(handle: ATTHandle): ByteArray? = att.readCharacteristic(handle)
|
||||||
|
|
||||||
// TODO: handle recording session
|
fun requestMicrophoneStream() = aacp.requestMicrophoneStream()
|
||||||
fun startRecording() = aacp.requestMicrophoneStream()
|
fun endMicrophoneStream() {
|
||||||
fun stopRecording() = aacp.endMicrophoneStream()
|
aacp.endMicrophoneStream()
|
||||||
|
reconnectA2dp()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startRecording(recording: Recording) {
|
||||||
|
requestMicrophoneStream()
|
||||||
|
updateState {
|
||||||
|
it.copy(
|
||||||
|
recordingState = RecordingState(
|
||||||
|
isRecording = true,
|
||||||
|
recording = recording
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun stopRecording() {
|
||||||
|
endMicrophoneStream()
|
||||||
|
updateState {
|
||||||
|
it.copy(
|
||||||
|
recordingState = RecordingState(
|
||||||
|
isRecording = false,
|
||||||
|
recording = null
|
||||||
|
),
|
||||||
|
microphoneState = MicrophoneState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleListeningMode(modeBit: Int) {
|
fun toggleListeningMode(modeBit: Int) {
|
||||||
val currentByte = state.value.controlStates[ControlCommandIdentifier.LISTENING_MODE_CONFIGS]?.get(0)?.toInt() ?: 0
|
val currentByte = state.value.controlStates[ControlCommandIdentifier.LISTENING_MODE_CONFIGS]?.get(0)?.toInt() ?: 0
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ data class AppleState(
|
|||||||
|
|
||||||
val customEq: CustomEq = CustomEq(1, 50, 50, 50),
|
val customEq: CustomEq = CustomEq(1, 50, 50, 50),
|
||||||
|
|
||||||
val microphoneFrames: List<MicrophoneFrame> = emptyList(),
|
|
||||||
val microphoneState: MicrophoneState = MicrophoneState(),
|
val microphoneState: MicrophoneState = MicrophoneState(),
|
||||||
val recordingState: RecordingState = RecordingState(),
|
val recordingState: RecordingState = RecordingState(),
|
||||||
|
|
||||||
@@ -50,9 +49,9 @@ data class AppleState(
|
|||||||
|
|
||||||
val leftIsPrimary: Boolean = true,
|
val leftIsPrimary: Boolean = true,
|
||||||
|
|
||||||
val headphoneAccomodation: FloatArray = FloatArray(8),
|
val headphoneAccommodation: FloatArray = FloatArray(8),
|
||||||
val headphoneAccomodationEnabledForMedia: Boolean = false,
|
val headphoneAccommodationEnabledForMedia: Boolean = false,
|
||||||
val headphoneAccomodationEnabledForPhone: Boolean = false,
|
val headphoneAccommodationEnabledForPhone: Boolean = false,
|
||||||
|
|
||||||
val currentHeartRate: HeartRateSample? = null,
|
val currentHeartRate: HeartRateSample? = null,
|
||||||
val hrmState: BuddyState = BuddyState.INACTIVE,
|
val hrmState: BuddyState = BuddyState.INACTIVE,
|
||||||
|
|||||||
@@ -13,14 +13,22 @@ import android.content.IntentFilter
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.bluetooth.MacAddress
|
import me.kavishdevar.librepods.bluetooth.MacAddress
|
||||||
import me.kavishdevar.librepods.bluetooth.createBluetoothSocket
|
import me.kavishdevar.librepods.bluetooth.createBluetoothSocket
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
||||||
private val TAG: String
|
private val TAG: String
|
||||||
get() = "LibrePodsDevice<${macAddress.toRedactedString()}>"
|
get() = "LibrePodsDevice<${macAddress.toRedactedString()}>"
|
||||||
|
|
||||||
|
// todo: somehow remove context from here. maybe get Profile from service?
|
||||||
|
val context: Context
|
||||||
|
|
||||||
val macAddress: MacAddress
|
val macAddress: MacAddress
|
||||||
|
|
||||||
val bluetoothAdapter: BluetoothAdapter
|
val bluetoothAdapter: BluetoothAdapter
|
||||||
@@ -45,12 +53,12 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
psm = psm
|
psm = psm
|
||||||
)
|
)
|
||||||
|
|
||||||
fun disableAudio(context: Context) {
|
fun disableAudio() {
|
||||||
disableA2dp(context)
|
disableA2dp()
|
||||||
disableHeadset(context)
|
disableHeadset()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableA2dp(context: Context) {
|
fun disableA2dp() {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
|
if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
|
||||||
@@ -80,7 +88,7 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableHeadset(context: Context) {
|
fun disableHeadset() {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
if (context.checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
|
if (context.checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
|
||||||
@@ -105,12 +113,12 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableAudio(context: Context) {
|
fun enableAudio() {
|
||||||
enableA2dp(context)
|
enableA2dp()
|
||||||
enableHeadset(context)
|
enableHeadset()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableA2dp(context: Context) {
|
fun enableA2dp() {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
@@ -137,7 +145,7 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
}, BluetoothProfile.A2DP)
|
}, BluetoothProfile.A2DP)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableHeadset(context: Context) {
|
fun enableHeadset() {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
@@ -167,12 +175,12 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
}, BluetoothProfile.HEADSET)
|
}, BluetoothProfile.HEADSET)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connectAudio(context: Context) {
|
fun connectAudio() {
|
||||||
connectA2dp(context)
|
connectA2dp()
|
||||||
connectHeadset(context)
|
connectHeadset()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connectA2dp(context: Context) {
|
fun connectA2dp() {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
@@ -193,7 +201,7 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
}, BluetoothProfile.A2DP)
|
}, BluetoothProfile.A2DP)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connectHeadset(context: Context) {
|
fun connectHeadset() {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
@@ -214,12 +222,12 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
}, BluetoothProfile.HEADSET)
|
}, BluetoothProfile.HEADSET)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disconnectAudio(context: Context) {
|
fun disconnectAudio() {
|
||||||
disconnectA2dp(context)
|
disconnectA2dp()
|
||||||
disconnectHeadset(context)
|
disconnectHeadset()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disconnectA2dp(context: Context) {
|
fun disconnectA2dp() {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
@@ -240,7 +248,7 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
}, BluetoothProfile.A2DP)
|
}, BluetoothProfile.A2DP)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disconnectHeadset(context: Context) {
|
fun disconnectHeadset() {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
@@ -299,6 +307,46 @@ sealed interface Device<S: DeviceState, T: DeviceSettings, M: DeviceMetadata> {
|
|||||||
|
|
||||||
return receiver
|
return receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reconnectA2dp() {
|
||||||
|
bluetoothAdapter.getProfileProxy(
|
||||||
|
context,
|
||||||
|
object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(
|
||||||
|
profile: Int,
|
||||||
|
proxy: BluetoothProfile
|
||||||
|
) {
|
||||||
|
if (profile != BluetoothProfile.A2DP) return
|
||||||
|
|
||||||
|
val a2dp = proxy as BluetoothA2dp
|
||||||
|
|
||||||
|
try {
|
||||||
|
val connectMethod = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
|
||||||
|
val disconnectMethod = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
|
||||||
|
|
||||||
|
disconnectMethod.invoke(proxy, bluetoothDevice)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
delay(500.milliseconds)
|
||||||
|
|
||||||
|
connectMethod.invoke(proxy, bluetoothDevice)
|
||||||
|
|
||||||
|
bluetoothAdapter.closeProfileProxy(
|
||||||
|
BluetoothProfile.A2DP,
|
||||||
|
a2dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to reconnect A2DP", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(profile: Int) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BluetoothProfile.A2DP
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeviceState
|
interface DeviceState
|
||||||
|
|||||||
+1
-2
@@ -163,8 +163,7 @@ fun NoiseControlSettings(
|
|||||||
options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
|
options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
|
||||||
else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
|
else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
|
||||||
},
|
},
|
||||||
colors = ToggleButtonDefaults.toggleButtonColors()
|
colors = ToggleButtonDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
+27
-16
@@ -46,7 +46,6 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.health.connect.client.permission.HealthPermission
|
import androidx.health.connect.client.permission.HealthPermission
|
||||||
import androidx.health.connect.client.records.HeartRateRecord
|
import androidx.health.connect.client.records.HeartRateRecord
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
@@ -60,7 +59,6 @@ import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffol
|
|||||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
|
||||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
|
||||||
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
import me.kavishdevar.librepods.presentation.icons.LocalIcons
|
||||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
|
||||||
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
|
||||||
@@ -88,7 +86,6 @@ fun HeartRateRoute(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HeartRateScreen(
|
fun HeartRateScreen(
|
||||||
uiState: AppleUiState,
|
uiState: AppleUiState,
|
||||||
@@ -108,6 +105,11 @@ fun HeartRateScreen(
|
|||||||
navigateBack = navigateBack,
|
navigateBack = navigateBack,
|
||||||
actionButtons = listOf(
|
actionButtons = listOf(
|
||||||
{ scaffoldBackdrop ->
|
{ scaffoldBackdrop ->
|
||||||
|
val icon = when (state.hrmState) {
|
||||||
|
BuddyState.INACTIVE -> LocalIcons.current.Play
|
||||||
|
BuddyState.WAITING -> LocalIcons.current.Circle
|
||||||
|
BuddyState.ACTIVE -> LocalIcons.current.Pause
|
||||||
|
}
|
||||||
if (LocalDesignSystem.current == DesignSystem.Material) {
|
if (LocalDesignSystem.current == DesignSystem.Material) {
|
||||||
FilledTonalIconToggleButton(
|
FilledTonalIconToggleButton(
|
||||||
checked = state.hrmState == BuddyState.ACTIVE,
|
checked = state.hrmState == BuddyState.ACTIVE,
|
||||||
@@ -116,26 +118,32 @@ fun HeartRateScreen(
|
|||||||
.minimumInteractiveComponentSize()
|
.minimumInteractiveComponentSize()
|
||||||
.size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Uniform)),
|
.size(IconButtonDefaults.mediumContainerSize(IconButtonDefaults.IconButtonWidthOption.Uniform)),
|
||||||
shape = IconButtonDefaults.mediumRoundShape,
|
shape = IconButtonDefaults.mediumRoundShape,
|
||||||
enabled = state.hrmState != BuddyState.WAITING
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
AnimatedContent(
|
||||||
imageVector = if (state.hrmState == BuddyState.ACTIVE) MaterialIcons.Pause else Icons.Default.PlayArrow,
|
icon
|
||||||
contentDescription = "Start/Stop",
|
) {
|
||||||
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
|
Icon(
|
||||||
)
|
imageVector = it,
|
||||||
|
contentDescription = "Start/Stop",
|
||||||
|
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StyledIconButton(
|
StyledIconButton(
|
||||||
onClick = { if (state.hrmState == BuddyState.INACTIVE) startHr() else stopHr() },
|
onClick = { if (state.hrmState == BuddyState.INACTIVE) startHr() else stopHr() },
|
||||||
backdrop = scaffoldBackdrop,
|
backdrop = scaffoldBackdrop,
|
||||||
enabled = state.hrmState != BuddyState.WAITING
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
AnimatedContent(
|
||||||
imageVector = if (state.hrmState == BuddyState.ACTIVE) LocalIcons.current.Pause else LocalIcons.current.Play,
|
icon
|
||||||
contentDescription = "Start/Stop",
|
) {
|
||||||
modifier = Modifier.size(16.dp),
|
Icon(
|
||||||
tint = MaterialTheme.colorScheme.onBackground
|
imageVector = it,
|
||||||
)
|
contentDescription = "Start/Stop",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,6 +305,9 @@ fun HeartRateScreen(
|
|||||||
updateSettings {
|
updateSettings {
|
||||||
it.copy(hrmAlertEnabled = enabled)
|
it.copy(hrmAlertEnabled = enabled)
|
||||||
}
|
}
|
||||||
|
if (enabled && state.hrmState == BuddyState.INACTIVE) {
|
||||||
|
startHr()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+24
-31
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -34,6 +35,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLocale
|
import androidx.compose.ui.platform.LocalLocale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.drawText
|
||||||
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.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
@@ -65,12 +67,6 @@ fun RecordingScreenRoute(
|
|||||||
startRecording = viewModel::startRecording,
|
startRecording = viewModel::startRecording,
|
||||||
stopRecording = viewModel::stopRecording,
|
stopRecording = viewModel::stopRecording,
|
||||||
)
|
)
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
viewModel.stopRecording()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -140,18 +136,19 @@ fun RecordingScreen(
|
|||||||
) { recording ->
|
) { recording ->
|
||||||
if (recording) {
|
if (recording) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.surface,
|
|
||||||
RoundedCornerShape(28.dp)
|
|
||||||
)
|
|
||||||
.padding(24.dp)
|
|
||||||
) {
|
) {
|
||||||
// AI-generated
|
// AI-generated
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
RoundedCornerShape(28.dp)
|
||||||
|
)
|
||||||
|
.padding(vertical = 24.dp)
|
||||||
) {
|
) {
|
||||||
if (history.isEmpty()) return@Canvas
|
if (history.isEmpty()) return@Canvas
|
||||||
|
|
||||||
@@ -196,24 +193,20 @@ fun RecordingScreen(
|
|||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
Text(
|
// todo: duration is wrong, don't do +30ms
|
||||||
text = buildString {
|
// Text(
|
||||||
val total = state.microphoneState.durationMs
|
// text = buildString {
|
||||||
|
// val total = state.microphoneState.durationMs
|
||||||
append((total / 60000).toString().padStart(2, '0'))
|
//
|
||||||
append(':')
|
// append((total / 60000).toString().padStart(2, '0'))
|
||||||
|
// append(':')
|
||||||
append(((total / 1000) % 60).toString().padStart(2, '0'))
|
//
|
||||||
append('.')
|
// append(((total / 1000) % 60).toString().padStart(2, '0'))
|
||||||
|
// },
|
||||||
append(((total % 1000) / 10).toString().padStart(2, '0'))
|
// modifier = Modifier.fillMaxWidth(),
|
||||||
},
|
// textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
// style = MaterialTheme.typography.displaySmall
|
||||||
textAlign = TextAlign.Center,
|
// )
|
||||||
style = MaterialTheme.typography.displaySmall
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|||||||
-4
@@ -40,7 +40,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
import me.kavishdevar.librepods.presentation.theme.DesignSystem
|
||||||
@@ -49,7 +48,6 @@ import me.kavishdevar.librepods.utils.XposedState
|
|||||||
import me.kavishdevar.librepods.utils.bypassDeviceCheck
|
import me.kavishdevar.librepods.utils.bypassDeviceCheck
|
||||||
import me.kavishdevar.librepods.utils.isSupported
|
import me.kavishdevar.librepods.utils.isSupported
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingScreen(
|
fun OnboardingScreen(
|
||||||
onOnboardingComplete: () -> Unit,
|
onOnboardingComplete: () -> Unit,
|
||||||
@@ -215,8 +213,6 @@ fun OnboardingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingScreenPreview(){
|
fun OnboardingScreenPreview(){
|
||||||
|
|||||||
-1
@@ -55,7 +55,6 @@ import me.kavishdevar.librepods.presentation.components.primitives.StyledListIte
|
|||||||
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
|
||||||
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
import me.kavishdevar.librepods.presentation.icons.MaterialIcons
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PermissionsPage(
|
fun PermissionsPage(
|
||||||
onBackward: () -> Unit,
|
onBackward: () -> Unit,
|
||||||
|
|||||||
+1
-1
@@ -168,7 +168,7 @@ class AppleViewModel(
|
|||||||
attObserveJob = null
|
attObserveJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startRecording() = device.startRecording()
|
fun startRecording() = device.startRecording(recordingRepository.createRecording())
|
||||||
fun stopRecording() = device.stopRecording()
|
fun stopRecording() = device.stopRecording()
|
||||||
|
|
||||||
fun toggleListeningMode(modeBit: Int) = device.toggleListeningMode(modeBit)
|
fun toggleListeningMode(modeBit: Int) = device.toggleListeningMode(modeBit)
|
||||||
|
|||||||
@@ -48,11 +48,14 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.LibrePodsApplication
|
import me.kavishdevar.librepods.LibrePodsApplication
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.audio.EldDecoder
|
||||||
|
import me.kavishdevar.librepods.audio.WavWriter
|
||||||
import me.kavishdevar.librepods.bluetooth.MacAddress
|
import me.kavishdevar.librepods.bluetooth.MacAddress
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
|
||||||
import me.kavishdevar.librepods.bluetooth.aacp.types.MagicKeyType
|
import me.kavishdevar.librepods.bluetooth.aacp.types.MagicKeyType
|
||||||
import me.kavishdevar.librepods.bluetooth.verifyRPA
|
import me.kavishdevar.librepods.bluetooth.verifyRPA
|
||||||
import me.kavishdevar.librepods.data.heartrate.HeartRateSample
|
import me.kavishdevar.librepods.data.heartrate.HeartRateSample
|
||||||
|
import me.kavishdevar.librepods.data.recording.Recording
|
||||||
import me.kavishdevar.librepods.database.app.AppSettingsEntity
|
import me.kavishdevar.librepods.database.app.AppSettingsEntity
|
||||||
import me.kavishdevar.librepods.devices.AppleDevice
|
import me.kavishdevar.librepods.devices.AppleDevice
|
||||||
import me.kavishdevar.librepods.devices.AppleSettings
|
import me.kavishdevar.librepods.devices.AppleSettings
|
||||||
@@ -68,6 +71,7 @@ import me.kavishdevar.librepods.presentation.overlays.IslandType
|
|||||||
import me.kavishdevar.librepods.presentation.overlays.IslandWindow
|
import me.kavishdevar.librepods.presentation.overlays.IslandWindow
|
||||||
import me.kavishdevar.librepods.presentation.widgets.BatteryWidget
|
import me.kavishdevar.librepods.presentation.widgets.BatteryWidget
|
||||||
import me.kavishdevar.librepods.utils.MediaController
|
import me.kavishdevar.librepods.utils.MediaController
|
||||||
|
import me.kavishdevar.librepods.utils.calculateLevel
|
||||||
import me.kavishdevar.librepods.utils.redactMac
|
import me.kavishdevar.librepods.utils.redactMac
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
@@ -96,6 +100,10 @@ class LibrePodsService: Service() {
|
|||||||
|
|
||||||
private var islandWindow: IslandWindow? = null
|
private var islandWindow: IslandWindow? = null
|
||||||
|
|
||||||
|
private val eldDecoder = EldDecoder()
|
||||||
|
|
||||||
|
private val wavWriters = mutableMapOf<Recording, WavWriter>()
|
||||||
|
|
||||||
private val appleRepository by lazy {
|
private val appleRepository by lazy {
|
||||||
(application as LibrePodsApplication).appleRepository
|
(application as LibrePodsApplication).appleRepository
|
||||||
}
|
}
|
||||||
@@ -112,6 +120,10 @@ class LibrePodsService: Service() {
|
|||||||
(application as LibrePodsApplication).heartRateRepository
|
(application as LibrePodsApplication).heartRateRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val recordingRepository by lazy {
|
||||||
|
(application as LibrePodsApplication).recordingRepository
|
||||||
|
}
|
||||||
|
|
||||||
private val healthConnectClient by lazy {
|
private val healthConnectClient by lazy {
|
||||||
(application as LibrePodsApplication).healthConnectClient
|
(application as LibrePodsApplication).healthConnectClient
|
||||||
}
|
}
|
||||||
@@ -301,6 +313,7 @@ class LibrePodsService: Service() {
|
|||||||
deviceJobs[MacAddress(bluetoothDevice.address)]?.add(observeAppleState(device))
|
deviceJobs[MacAddress(bluetoothDevice.address)]?.add(observeAppleState(device))
|
||||||
deviceJobs[MacAddress(bluetoothDevice.address)]?.add(observeAppleSettings(device))
|
deviceJobs[MacAddress(bluetoothDevice.address)]?.add(observeAppleSettings(device))
|
||||||
deviceJobs[MacAddress(bluetoothDevice.address)]?.add(observeAppleMetadata(device))
|
deviceJobs[MacAddress(bluetoothDevice.address)]?.add(observeAppleMetadata(device))
|
||||||
|
deviceJobs[MacAddress(bluetoothDevice.address)]?.add(observeAppleMicrophoneFrames(device))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +557,7 @@ class LibrePodsService: Service() {
|
|||||||
bluetoothDevice.uuids?.contains(aacpUuid) == true -> {
|
bluetoothDevice.uuids?.contains(aacpUuid) == true -> {
|
||||||
Log.i(TAG, "Apple device detected: ${bluetoothDevice.address.redactMac()}")
|
Log.i(TAG, "Apple device detected: ${bluetoothDevice.address.redactMac()}")
|
||||||
AppleDevice(
|
AppleDevice(
|
||||||
|
context = this,
|
||||||
bluetoothAdapter = bluetoothAdapter,
|
bluetoothAdapter = bluetoothAdapter,
|
||||||
bluetoothDevice = bluetoothDevice,
|
bluetoothDevice = bluetoothDevice,
|
||||||
currentState = if (isConnectedMethod.invoke(bluetoothDevice) as Boolean) ConnectionState.AVAILABLE else ConnectionState.DISCONNECTED
|
currentState = if (isConnectedMethod.invoke(bluetoothDevice) as Boolean) ConnectionState.AVAILABLE else ConnectionState.DISCONNECTED
|
||||||
@@ -809,6 +823,30 @@ class LibrePodsService: Service() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.recordingState != previousState.recordingState -> {
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"recording state changed for device ${device.macAddress.toRedactedString()}"
|
||||||
|
)
|
||||||
|
Log.d(TAG, "recording state: ${state.recordingState}")
|
||||||
|
if (previousState.recordingState.isRecording && !state.recordingState.isRecording) {
|
||||||
|
previousState.recordingState.recording?.let { recording ->
|
||||||
|
wavWriters[recording]?.close()
|
||||||
|
}
|
||||||
|
} else if (!previousState.recordingState.isRecording && state.recordingState.isRecording) {
|
||||||
|
state.recordingState.recording?.let { recording ->
|
||||||
|
try {
|
||||||
|
val wavWriter = WavWriter(recording.file)
|
||||||
|
wavWriters[recording] = wavWriter
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to create WavWriter for recording: ${recording.file}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Recording state changed but no action taken. previous: ${previousState.recordingState}, current: ${state.recordingState}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previousState = state
|
previousState = state
|
||||||
}
|
}
|
||||||
@@ -850,6 +888,31 @@ class LibrePodsService: Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun observeAppleMicrophoneFrames(device: AppleDevice): Job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
device.microphoneFrames.collect { frame ->
|
||||||
|
val state = device.state.value
|
||||||
|
|
||||||
|
if (state.recordingState.isRecording) {
|
||||||
|
eldDecoder.decode(
|
||||||
|
accessUnit = frame.accessUnit
|
||||||
|
) { pcm ->
|
||||||
|
wavWriters[state.recordingState.recording]?.write(pcm)
|
||||||
|
|
||||||
|
device.updateState {
|
||||||
|
it.copy(
|
||||||
|
microphoneState = state.microphoneState.copy(
|
||||||
|
isActive = true,
|
||||||
|
packetsReceived = it.microphoneState.packetsReceived + 1,
|
||||||
|
durationMs = it.microphoneState.durationMs + 30,
|
||||||
|
level = calculateLevel(pcm)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun showIsland(
|
fun showIsland(
|
||||||
device: Device<*, *, *>,
|
device: Device<*, *, *>,
|
||||||
type: IslandType = IslandType.CONNECTED,
|
type: IslandType = IslandType.CONNECTED,
|
||||||
@@ -1169,8 +1232,8 @@ class LibrePodsService: Service() {
|
|||||||
TAG,
|
TAG,
|
||||||
"User put in at least one component, enabling audio for device ${device.macAddress.toRedactedString()}"
|
"User put in at least one component, enabling audio for device ${device.macAddress.toRedactedString()}"
|
||||||
)
|
)
|
||||||
device.enableAudio(this)
|
device.enableAudio()
|
||||||
device.connectA2dp(this)
|
device.connectA2dp()
|
||||||
justEnabledA2dp = true
|
justEnabledA2dp = true
|
||||||
|
|
||||||
device.waitForA2dpConnection(this) {
|
device.waitForA2dpConnection(this) {
|
||||||
@@ -1194,8 +1257,8 @@ class LibrePodsService: Service() {
|
|||||||
TAG,
|
TAG,
|
||||||
"Disconnecting audio for device ${device.macAddress.toRedactedString()} because user took out all components and disconnectWhenNotWearing is true"
|
"Disconnecting audio for device ${device.macAddress.toRedactedString()} because user took out all components and disconnectWhenNotWearing is true"
|
||||||
)
|
)
|
||||||
device.disableAudio(this)
|
device.disableAudio()
|
||||||
device.disconnectAudio(this)
|
device.disconnectAudio()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user