mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-05-31 05:22:41 +00:00
android: move ATT code to viewmodel from screens and enable notifications
This commit is contained in:
@@ -466,7 +466,7 @@ fun Main() {
|
||||
OpenSourceLicensesScreen(navController)
|
||||
}
|
||||
composable("update_hearing_test") {
|
||||
if (airPodsViewModel != null) UpdateHearingTestScreen()
|
||||
if (airPodsViewModel != null) UpdateHearingTestScreen(airPodsViewModel)
|
||||
}
|
||||
composable("version_info") {
|
||||
if (airPodsViewModel != null) VersionScreen(airPodsViewModel)
|
||||
|
||||
@@ -16,247 +16,196 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* This is a very basic ATT (Attribute Protocol) implementation. I have only implemented
|
||||
* what is necessary for LibrePods to function, i.e. reading and writing characteristics,
|
||||
* and receiving notifications. It is not a complete implementation of the ATT protocol.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.bluetooth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
private const val TAG = "ATTManager"
|
||||
|
||||
enum class ATTHandles(val value: Int) {
|
||||
TRANSPARENCY(0x18),
|
||||
LOUD_SOUND_REDUCTION(0x1B),
|
||||
HEARING_AID(0x2A),
|
||||
HEARING_AID(0x2A)
|
||||
}
|
||||
|
||||
enum class ATTCCCDHandles(val value: Int) {
|
||||
TRANSPARENCY(ATTHandles.TRANSPARENCY.value + 1),
|
||||
LOUD_SOUND_REDUCTION(ATTHandles.LOUD_SOUND_REDUCTION.value + 1),
|
||||
HEARING_AID(ATTHandles.HEARING_AID.value + 1),
|
||||
// LOUD_SOUND_REDUCTION(ATTHandles.LOUD_SOUND_REDUCTION.value + 1), // doesn't work
|
||||
HEARING_AID(ATTHandles.HEARING_AID.value + 1)
|
||||
}
|
||||
|
||||
class ATTManager(private val adapter: BluetoothAdapter, private val device: BluetoothDevice) {
|
||||
companion object {
|
||||
private const val OPCODE_READ_REQUEST: Byte = 0x0A
|
||||
private const val OPCODE_WRITE_REQUEST: Byte = 0x12
|
||||
private const val OPCODE_HANDLE_VALUE_NTF: Byte = 0x1B
|
||||
class ATTManagerv2 {
|
||||
val characteristicList = mutableMapOf<ATTHandles, ByteArray>()
|
||||
|
||||
private val responseQueues = ConcurrentHashMap<Byte, LinkedBlockingQueue<ByteArray>>()
|
||||
|
||||
private val readerRunning = AtomicBoolean(false)
|
||||
private var readerThread: Thread? = null
|
||||
|
||||
private var onNotificationReceived: ((handle: Byte, value: ByteArray) -> Unit)? = null
|
||||
|
||||
fun startReader() {
|
||||
if (readerRunning.getAndSet(true)) return
|
||||
|
||||
readerThread = Thread {
|
||||
try {
|
||||
runReaderLoop()
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "reader thread crashed: ${t.message}", t)
|
||||
} finally {
|
||||
readerRunning.set(false)
|
||||
Log.d(TAG, "reader thread stopped")
|
||||
}
|
||||
}.also { it.name = "ATT-Reader"; it.isDaemon = true; it.start() }
|
||||
Log.d(TAG, "reader started")
|
||||
}
|
||||
private val id = System.identityHashCode(this)
|
||||
@Suppress("PrivatePropertyName")
|
||||
private val TAG = "ATTManager[$id]"
|
||||
var socket: BluetoothSocket? = null
|
||||
private var input: InputStream? = null
|
||||
private var output: OutputStream? = null
|
||||
private val listeners = mutableMapOf<Int, MutableList<(ByteArray) -> Unit>>()
|
||||
private var notificationJob: Job? = null
|
||||
|
||||
// queue for non-notification PDUs (responses to requests)
|
||||
private val responses = LinkedBlockingQueue<ByteArray>()
|
||||
fun stopReader() {
|
||||
readerRunning.set(false)
|
||||
readerThread?.interrupt()
|
||||
readerThread = null
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun connect() {
|
||||
val uuid = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000")
|
||||
fun setOnNotificationReceived(listener: ((handle: Byte, value: ByteArray) -> Unit)?) {
|
||||
onNotificationReceived = listener
|
||||
}
|
||||
|
||||
if (socket == null) {
|
||||
Log.d(TAG, "Socket doesn't exist, creating")
|
||||
try {
|
||||
socket = createBluetoothSocket(adapter, device, uuid)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to create socket")
|
||||
e.printStackTrace()
|
||||
fun enableNotification(handle: ATTCCCDHandles) {
|
||||
writeCharacteristic(handle.value.toByte(), byteArrayOf(0x01))
|
||||
}
|
||||
|
||||
fun getCharacteristic(handle: ATTHandles): ByteArray? {
|
||||
val storedValue = characteristicList[handle]
|
||||
return if (storedValue?.isNotEmpty() != true) {
|
||||
readCharacteristic(handle)
|
||||
} else storedValue
|
||||
}
|
||||
|
||||
fun readCharacteristic(handle: ATTHandles, timeoutMillis: Long = 2000): ByteArray? {
|
||||
val socket = BluetoothConnectionManager.getATTSocket() ?: return null
|
||||
try {
|
||||
val output = socket.outputStream
|
||||
val pdu = byteArrayOf(0x0A, handle.value.toByte(), 0x00)
|
||||
synchronized(output) {
|
||||
output.write(pdu)
|
||||
output.flush()
|
||||
}
|
||||
Log.d(TAG, "sending read request: ${pdu.joinToString(" ") { String.format("%02X", it) }}")
|
||||
|
||||
val resp = waitForResponse(0x0B, timeoutMillis) ?: run {
|
||||
Log.e(TAG, "Timeout waiting for Read Response (0x0B) for handle ${handle.value}")
|
||||
return null
|
||||
}
|
||||
|
||||
Log.d(TAG, "read response: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||
val value = resp.copyOfRange(1, resp.size)
|
||||
characteristicList[handle] = value
|
||||
return value
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "error reading characteristic: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun writeCharacteristic(handle: ATTHandles, data: ByteArray, timeoutMillis: Long = 2000) {
|
||||
characteristicList[handle] = data
|
||||
writeCharacteristic(handle.value.toByte(), data, timeoutMillis)
|
||||
}
|
||||
|
||||
fun writeCharacteristic(handle: Byte, data: ByteArray, timeoutMillis: Long = 2000) {
|
||||
val socket = BluetoothConnectionManager.getATTSocket() ?: return
|
||||
try {
|
||||
val output = socket.outputStream
|
||||
val pdu = byteArrayOf(0x12, handle, 0x00) + data // 0x00 for LE
|
||||
synchronized(output) {
|
||||
output.write(pdu)
|
||||
output.flush()
|
||||
}
|
||||
Log.d(TAG, "sending write request: ${pdu.joinToString(" ") { String.format("%02X", it) }}")
|
||||
|
||||
val resp = waitForResponse(0x13, timeoutMillis) ?: run {
|
||||
Log.e(TAG, "timeout waiting for response (0x13) for handle ${String.format("%02X", handle)}")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (socket?.isConnected != true) {
|
||||
Log.d(TAG, "Connection to socket")
|
||||
try {
|
||||
socket!!.connect()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "ATT socket failed to connect")
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
input = socket!!.inputStream
|
||||
output = socket!!.outputStream
|
||||
Log.d(TAG, "Connected to ATT")
|
||||
|
||||
notificationJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
while (socket?.isConnected == true) {
|
||||
try {
|
||||
val pdu = readPDU()
|
||||
if (pdu.isNotEmpty() && pdu[0] == OPCODE_HANDLE_VALUE_NTF) {
|
||||
// notification -> dispatch to listeners
|
||||
val handle = (pdu[1].toInt() and 0xFF) or ((pdu[2].toInt() and 0xFF) shl 8)
|
||||
val value = pdu.copyOfRange(3, pdu.size)
|
||||
listeners[handle]?.forEach { listener ->
|
||||
try {
|
||||
listener(value)
|
||||
Log.d(TAG, "Dispatched notification for handle $handle to listener, with value ${value.joinToString(" ") { String.format("%02X", it) }}")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error in listener for handle $handle: ${e.message}")
|
||||
}
|
||||
Log.d(TAG, "write respose: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "error writing characteristic: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnected() {
|
||||
characteristicList.clear()
|
||||
stopReader()
|
||||
val socket = BluetoothConnectionManager.getATTSocket() ?: return
|
||||
try {
|
||||
socket.close()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "error closing socket: ${e.message}")
|
||||
}
|
||||
Log.d(TAG, "ATT disconnected")
|
||||
}
|
||||
|
||||
private fun runReaderLoop() {
|
||||
val socket = BluetoothConnectionManager.getATTSocket() ?: run {
|
||||
Log.w(TAG, "ATT socket not available. stopping reader")
|
||||
readerRunning.set(false)
|
||||
return
|
||||
}
|
||||
|
||||
val input = socket.inputStream
|
||||
val buffer = ByteArray(512)
|
||||
|
||||
while (readerRunning.get()) {
|
||||
try {
|
||||
val len = input.read(buffer)
|
||||
if (len == -1) {
|
||||
Log.w(TAG, "ATT input stream ended")
|
||||
break
|
||||
}
|
||||
val data = buffer.copyOfRange(0, len)
|
||||
if (data.isEmpty()) continue
|
||||
|
||||
val opcode = data[0]
|
||||
Log.d(TAG, "pdu received ${data.joinToString(" ") { String.format("%02X", it) }}")
|
||||
|
||||
val queue = responseQueues.computeIfAbsent(opcode) { LinkedBlockingQueue() }
|
||||
queue.offer(data)
|
||||
|
||||
if (opcode == 0x1B.toByte()) {
|
||||
if (data.size >= 3) {
|
||||
val handle = data[1]
|
||||
val value = if (data.size > 3) data.copyOfRange(3, data.size) else ByteArray(0)
|
||||
Log.d(TAG, "notification/indication handle=0x${String.format("%02X", handle)} value=${value.toHexString()}")
|
||||
try {
|
||||
onNotificationReceived?.invoke(handle, value)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "onNotificationReceived threw: ${t.message}", t)
|
||||
}
|
||||
} else {
|
||||
// not a notification -> treat as a response for pending request(s)
|
||||
responses.put(pdu)
|
||||
Log.w(TAG, "notification PDU too short: ${data.joinToString(" ") { String.format("%02X", it) }}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error reading notification/response: ${e.message}")
|
||||
if (socket?.isConnected != true) break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (output != null) {
|
||||
Log.d(TAG, "sending read req for hearing aid declaration")
|
||||
output?.write(byteArrayOf(0x0A, 0x29, 0x00))
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
try {
|
||||
notificationJob?.cancel()
|
||||
socket?.close()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error closing socket: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun registerListener(handle: ATTHandles, listener: (ByteArray) -> Unit) {
|
||||
listeners.getOrPut(handle.value) { mutableListOf() }.add(listener)
|
||||
}
|
||||
|
||||
fun unregisterListener(handle: ATTHandles, listener: (ByteArray) -> Unit) {
|
||||
listeners[handle.value]?.remove(listener)
|
||||
}
|
||||
|
||||
fun enableNotifications(handle: ATTHandles) {
|
||||
write(ATTCCCDHandles.valueOf(handle.name), byteArrayOf(0x01, 0x00))
|
||||
}
|
||||
|
||||
fun read(handle: ATTHandles): ByteArray {
|
||||
val lsb = (handle.value and 0xFF).toByte()
|
||||
val msb = ((handle.value shr 8) and 0xFF).toByte()
|
||||
val pdu = byteArrayOf(OPCODE_READ_REQUEST, lsb, msb)
|
||||
writeRaw(pdu)
|
||||
// wait for response placed into responses queue by the reader coroutine
|
||||
return readResponse()
|
||||
}
|
||||
|
||||
fun write(handle: ATTHandles, value: ByteArray) {
|
||||
val lsb = (handle.value and 0xFF).toByte()
|
||||
val msb = ((handle.value shr 8) and 0xFF).toByte()
|
||||
val pdu = byteArrayOf(OPCODE_WRITE_REQUEST, lsb, msb) + value
|
||||
writeRaw(pdu)
|
||||
// usually a Write Response (0x13) will arrive; wait for it (but discard return)
|
||||
try {
|
||||
readResponse()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "No write response received: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun write(handle: ATTCCCDHandles, value: ByteArray) {
|
||||
val lsb = (handle.value and 0xFF).toByte()
|
||||
val msb = ((handle.value shr 8) and 0xFF).toByte()
|
||||
val pdu = byteArrayOf(OPCODE_WRITE_REQUEST, lsb, msb) + value
|
||||
writeRaw(pdu)
|
||||
// usually a Write Response (0x13) will arrive; wait for it (but discard return)
|
||||
try {
|
||||
readResponse()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "No write response received: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeRaw(pdu: ByteArray) {
|
||||
if (output == null) return
|
||||
output?.write(pdu)
|
||||
output?.flush()
|
||||
Log.d(TAG, "writeRaw: ${pdu.joinToString(" ") { String.format("%02X", it) }}")
|
||||
}
|
||||
|
||||
// rename / specialize: read raw PDU directly from input stream (blocking)
|
||||
private fun readPDU(): ByteArray {
|
||||
val inp = input ?: throw IllegalStateException("Not connected")
|
||||
val buffer = ByteArray(512)
|
||||
val len = inp.read(buffer)
|
||||
if (len == -1) {
|
||||
disconnect()
|
||||
throw IllegalStateException("End of stream reached")
|
||||
}
|
||||
val data = buffer.copyOfRange(0, len)
|
||||
Log.d(TAG, "readPDU: ${data.joinToString(" ") { String.format("%02X", it) }}")
|
||||
return data
|
||||
}
|
||||
|
||||
// wait for a response PDU produced by the background reader
|
||||
private fun readResponse(timeoutMs: Long = 2000): ByteArray {
|
||||
try {
|
||||
val resp = responses.poll(timeoutMs, TimeUnit.MILLISECONDS)
|
||||
?: throw IllegalStateException("No response read from ATT socket within $timeoutMs ms")
|
||||
Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||
return resp.copyOfRange(1, resp.size)
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
throw IllegalStateException("Interrupted while waiting for ATT response", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBluetoothSocket(adapter: BluetoothAdapter, device: BluetoothDevice, uuid: ParcelUuid): BluetoothSocket {
|
||||
val type = 3 // L2CAP
|
||||
val constructorSpecs = listOf(
|
||||
arrayOf(adapter, device, type, true, true, 31, uuid),
|
||||
arrayOf(device, type, true, true, 31, uuid),
|
||||
arrayOf(device, type, 1, true, true, 31, uuid),
|
||||
arrayOf(type, 1, true, true, device, 31, uuid),
|
||||
arrayOf(type, true, true, device, 31, uuid)
|
||||
)
|
||||
|
||||
val constructors = BluetoothSocket::class.java.declaredConstructors
|
||||
Log.d(TAG, "BluetoothSocket has ${constructors.size} constructors:")
|
||||
|
||||
constructors.forEachIndexed { index, constructor ->
|
||||
val params = constructor.parameterTypes.joinToString(", ") { it.simpleName }
|
||||
Log.d(TAG, "Constructor $index: ($params)")
|
||||
}
|
||||
|
||||
var lastException: Exception? = null
|
||||
var attemptedConstructors = 0
|
||||
|
||||
for ((index, params) in constructorSpecs.withIndex()) {
|
||||
try {
|
||||
Log.d(TAG, "Trying constructor signature #${index + 1}")
|
||||
attemptedConstructors++
|
||||
|
||||
val paramTypes = params.map { it::class.javaPrimitiveType ?: it::class.java }.toTypedArray()
|
||||
val constructor = BluetoothSocket::class.java.getDeclaredConstructor(*paramTypes)
|
||||
constructor.isAccessible = true
|
||||
return constructor.newInstance(*params) as BluetoothSocket
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Constructor signature #${index + 1} failed: ${e.message}")
|
||||
lastException = e
|
||||
Log.e(TAG, "error in reader loop: ${e.message}", e)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val errorMessage = "Failed to create BluetoothSocket after trying $attemptedConstructors constructor signatures"
|
||||
Log.e(TAG, errorMessage)
|
||||
throw lastException ?: IllegalStateException(errorMessage)
|
||||
readerRunning.set(false)
|
||||
}
|
||||
|
||||
private fun waitForResponse(opcode: Byte, timeoutMillis: Long): ByteArray? {
|
||||
val queue = responseQueues.computeIfAbsent(opcode) { LinkedBlockingQueue() }
|
||||
return try {
|
||||
queue.poll(timeoutMillis, TimeUnit.MILLISECONDS)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package me.kavishdevar.librepods.bluetooth
|
||||
|
||||
import android.util.Log
|
||||
|
||||
private const val TAG = "ATTManagerv2"
|
||||
|
||||
// the random disconnects were because of ATT, apparently. seems like we will have to accept no notifications for external changes (mainly amplification in hearing aid)
|
||||
object ATTManagerv2 {
|
||||
fun readCharacteristic(handle: ATTHandles): ByteArray? {
|
||||
val socket = BluetoothConnectionManager.getATTSocket()?: return null
|
||||
try {
|
||||
// socket.connect()
|
||||
val input = socket.inputStream
|
||||
val output = socket.outputStream
|
||||
|
||||
val pdu = byteArrayOf(0x0A, handle.value.toByte(), 0x00)
|
||||
output.write(pdu)
|
||||
output.flush()
|
||||
Log.d(TAG, "writeRaw: ${pdu.joinToString(" ") { String.format("%02X", it) }}")
|
||||
val buffer = ByteArray(512)
|
||||
val len = input.read(buffer)
|
||||
if (len == -1) {
|
||||
throw IllegalStateException("End of stream reached")
|
||||
}
|
||||
val data = buffer.copyOfRange(0, len)
|
||||
// socket.close()
|
||||
if (data[0] != 0x0B.toByte()) {
|
||||
throw IllegalStateException("Invalid response: ${data.joinToString(" ") { String.format("%02X", it) }}")
|
||||
}
|
||||
Log.d(TAG, "readPDU: ${data.joinToString(" ") { String.format("%02X", it) }}")
|
||||
return data.copyOfRange(1, data.size)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error reading characteristic: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun writeCharacteristic(handle: ATTHandles, data: ByteArray) {
|
||||
val socket = BluetoothConnectionManager.getATTSocket()?: return
|
||||
try {
|
||||
// socket.connect()
|
||||
val input = socket.inputStream
|
||||
val output = socket.outputStream
|
||||
val pdu = byteArrayOf(0x12, handle.value.toByte(), 0x00) + data // 0x0 because LE
|
||||
output.write(pdu)
|
||||
output.flush()
|
||||
Log.d(TAG, "writeRaw: ${pdu.joinToString(" ") { String.format("%02X", it) }}")
|
||||
val buffer = ByteArray(512)
|
||||
val len = input.read(buffer)
|
||||
if (len == -1) {
|
||||
throw IllegalStateException("End of stream reached")
|
||||
}
|
||||
val resp = buffer.copyOfRange(0, len)
|
||||
// socket.close()
|
||||
if (!resp.contentEquals(byteArrayOf(0x13))) {
|
||||
throw IllegalStateException("Invalid response: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||
}
|
||||
Log.d(TAG, "readPDU: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error writing characteristic: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,6 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||
import me.kavishdevar.librepods.bluetooth.ATTManager
|
||||
import me.kavishdevar.librepods.bluetooth.ATTManagerv2
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@@ -139,15 +137,15 @@ fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? {
|
||||
}
|
||||
|
||||
fun sendHearingAidSettings(
|
||||
// attManager: ATTManager,
|
||||
currentData: ByteArray,
|
||||
hearingAidSettings: HearingAidSettings,
|
||||
debounceJob: MutableState<Job?>
|
||||
debounceJob: MutableState<Job?>,
|
||||
sender: (ATTHandles, ByteArray) -> Unit
|
||||
) {
|
||||
debounceJob.value?.cancel()
|
||||
debounceJob.value = CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(100)
|
||||
try {
|
||||
val currentData = ATTManagerv2.readCharacteristic(ATTHandles.HEARING_AID)?: return@launch
|
||||
Log.d(TAG, "Current data before update: ${currentData.joinToString(" ") { String.format("%02X", it) }}")
|
||||
if (currentData.size < 104) {
|
||||
Log.w(TAG, "Current data size ${currentData.size} too small, cannot send settings")
|
||||
@@ -185,7 +183,7 @@ fun sendHearingAidSettings(
|
||||
|
||||
Log.d(TAG, "Sending updated settings: ${currentData.joinToString(" ") { String.format("%02X", it) }}")
|
||||
|
||||
ATTManagerv2.writeCharacteristic(ATTHandles.HEARING_AID, currentData)
|
||||
sender(ATTHandles.HEARING_AID, currentData)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
@@ -83,7 +83,8 @@ data class TransparencySettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings {
|
||||
fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? {
|
||||
if (data.size < 50) return null // 50 is arbitrary, too lazy to count
|
||||
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
val enabled = buffer.float
|
||||
|
||||
@@ -32,13 +32,12 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -48,24 +47,17 @@ import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||
import me.kavishdevar.librepods.bluetooth.ATTManagerv2
|
||||
import me.kavishdevar.librepods.data.HearingAidSettings
|
||||
import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse
|
||||
import me.kavishdevar.librepods.data.sendHearingAidSettings
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||
import java.io.IOException
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private var debounceJob: MutableState<Job?> = mutableStateOf(null)
|
||||
private const val TAG = "HearingAidAdjustments"
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@@ -76,14 +68,83 @@ fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) {
|
||||
isSystemInDarkTheme()
|
||||
val verticalScrollState = rememberScrollState()
|
||||
val hazeState = remember { HazeState() }
|
||||
// val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
|
||||
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
val backdrop = rememberLayerBackdrop()
|
||||
StyledScaffold(
|
||||
title = stringResource(R.string.adjustments)
|
||||
) { spacerHeight ->
|
||||
|
||||
val debounceJob = remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
val amplificationSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val balanceSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val toneSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val ambientNoiseReductionSliderValue = rememberSaveable { mutableFloatStateOf(0.0f) }
|
||||
val conversationBoostEnabled = rememberSaveable { mutableStateOf(false) }
|
||||
val leftEQ = rememberSaveable { mutableStateOf(FloatArray(8)) }
|
||||
val rightEQ = rememberSaveable { mutableStateOf(FloatArray(8)) }
|
||||
val ownVoiceAmplification = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
|
||||
val initialized = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val hearingAidSettings = remember { mutableStateOf(
|
||||
HearingAidSettings(
|
||||
leftEQ = leftEQ.value,
|
||||
rightEQ = rightEQ.value,
|
||||
leftAmplification = 0f,
|
||||
rightAmplification = 0f,
|
||||
leftTone = 0f,
|
||||
rightTone = 0f,
|
||||
leftConversationBoost = false,
|
||||
rightConversationBoost = false,
|
||||
leftAmbientNoiseReduction = 0f,
|
||||
rightAmbientNoiseReduction = 0f,
|
||||
netAmplification = 0f,
|
||||
balance = 0f,
|
||||
ownVoiceAmplification = 0f
|
||||
)
|
||||
) }
|
||||
|
||||
LaunchedEffect(state.hearingAidData) {
|
||||
parseHearingAidSettingsResponse(state.hearingAidData)?.let { parsed ->
|
||||
amplificationSliderValue.floatValue = parsed.netAmplification
|
||||
balanceSliderValue.floatValue = parsed.balance
|
||||
toneSliderValue.floatValue = parsed.leftTone
|
||||
ambientNoiseReductionSliderValue.floatValue = parsed.leftAmbientNoiseReduction
|
||||
conversationBoostEnabled.value = parsed.leftConversationBoost
|
||||
leftEQ.value = parsed.leftEQ.copyOf()
|
||||
rightEQ.value = parsed.rightEQ.copyOf()
|
||||
ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification
|
||||
initialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
amplificationSliderValue.floatValue,
|
||||
balanceSliderValue.floatValue,
|
||||
toneSliderValue.floatValue,
|
||||
conversationBoostEnabled.value,
|
||||
ambientNoiseReductionSliderValue.floatValue,
|
||||
ownVoiceAmplification.floatValue
|
||||
) {
|
||||
if (!initialized.value) return@LaunchedEffect
|
||||
hearingAidSettings.value = HearingAidSettings(
|
||||
leftEQ = leftEQ.value,
|
||||
rightEQ = rightEQ.value,
|
||||
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
||||
rightAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue > 0) balanceSliderValue.floatValue else 0f,
|
||||
leftTone = toneSliderValue.floatValue,
|
||||
rightTone = toneSliderValue.floatValue,
|
||||
leftConversationBoost = conversationBoostEnabled.value,
|
||||
rightConversationBoost = conversationBoostEnabled.value,
|
||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
netAmplification = amplificationSliderValue.floatValue,
|
||||
balance = balanceSliderValue.floatValue,
|
||||
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||
)
|
||||
Log.d(TAG, "Updated settings: ${hearingAidSettings.value}")
|
||||
sendHearingAidSettings(state.hearingAidData, hearingAidSettings.value, debounceJob, viewModel::setATTCharacteristicValue)
|
||||
}
|
||||
|
||||
StyledScaffold(title = stringResource(R.string.adjustments)) { spacerHeight ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.hazeSource(hazeState)
|
||||
@@ -95,136 +156,6 @@ fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) {
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(spacerHeight))
|
||||
|
||||
val amplificationSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val balanceSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val toneSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) }
|
||||
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
||||
val leftEQ = remember { mutableStateOf(FloatArray(8)) }
|
||||
val rightEQ = remember { mutableStateOf(FloatArray(8)) }
|
||||
val ownVoiceAmplification = remember { mutableFloatStateOf(0.5f) }
|
||||
|
||||
val initialLoadComplete = remember { mutableStateOf(false) }
|
||||
|
||||
val initialReadSucceeded = remember { mutableStateOf(false) }
|
||||
val initialReadAttempts = remember { mutableIntStateOf(0) }
|
||||
|
||||
val hearingAidSettings = remember {
|
||||
mutableStateOf(
|
||||
HearingAidSettings(
|
||||
leftEQ = leftEQ.value,
|
||||
rightEQ = rightEQ.value,
|
||||
leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2,
|
||||
rightAmplification = amplificationSliderValue.floatValue + (balanceSliderValue.floatValue - 0.5f) * amplificationSliderValue.floatValue * 2,
|
||||
leftTone = toneSliderValue.floatValue,
|
||||
rightTone = toneSliderValue.floatValue,
|
||||
leftConversationBoost = conversationBoostEnabled.value,
|
||||
rightConversationBoost = conversationBoostEnabled.value,
|
||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
netAmplification = amplificationSliderValue.floatValue,
|
||||
balance = balanceSliderValue.floatValue,
|
||||
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val hearingAidATTListener = remember {
|
||||
object : (ByteArray) -> Unit {
|
||||
override fun invoke(value: ByteArray) {
|
||||
val parsed = parseHearingAidSettingsResponse(value)
|
||||
if (parsed != null) {
|
||||
amplificationSliderValue.floatValue = parsed.netAmplification
|
||||
balanceSliderValue.floatValue = parsed.balance
|
||||
toneSliderValue.floatValue = parsed.leftTone
|
||||
ambientNoiseReductionSliderValue.floatValue = parsed.leftAmbientNoiseReduction
|
||||
conversationBoostEnabled.value = parsed.leftConversationBoost
|
||||
leftEQ.value = parsed.leftEQ.copyOf()
|
||||
rightEQ.value = parsed.rightEQ.copyOf()
|
||||
ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification
|
||||
Log.d(TAG, "Updated hearing aid settings from notification")
|
||||
} else {
|
||||
Log.w(TAG, "Failed to parse hearing aid settings from notification")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, ownVoiceAmplification.floatValue, initialLoadComplete.value, initialReadSucceeded.value) {
|
||||
if (!initialLoadComplete.value) {
|
||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (!initialReadSucceeded.value) {
|
||||
Log.d(TAG, "Initial device read not successful yet - skipping send until read succeeds")
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
hearingAidSettings.value = HearingAidSettings(
|
||||
leftEQ = leftEQ.value,
|
||||
rightEQ = rightEQ.value,
|
||||
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
||||
rightAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue > 0) balanceSliderValue.floatValue else 0f,
|
||||
leftTone = toneSliderValue.floatValue,
|
||||
rightTone = toneSliderValue.floatValue,
|
||||
leftConversationBoost = conversationBoostEnabled.value,
|
||||
rightConversationBoost = conversationBoostEnabled.value,
|
||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
netAmplification = amplificationSliderValue.floatValue,
|
||||
balance = balanceSliderValue.floatValue,
|
||||
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||
)
|
||||
Log.d(TAG, "Updated settings: ${hearingAidSettings.value}")
|
||||
sendHearingAidSettings(hearingAidSettings.value, debounceJob)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
Log.d(TAG, "Connecting to ATT...")
|
||||
try {
|
||||
// attManager.enableNotifications(ATTHandles.HEARING_AID)
|
||||
// attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener)
|
||||
|
||||
var parsedSettings: HearingAidSettings? = null
|
||||
for (attempt in 1..3) {
|
||||
initialReadAttempts.intValue = attempt
|
||||
try {
|
||||
val data = ATTManagerv2.readCharacteristic(ATTHandles.HEARING_AID) ?: return@LaunchedEffect
|
||||
parsedSettings = parseHearingAidSettingsResponse(data = data)
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Parsed settings on attempt $attempt")
|
||||
break
|
||||
} else {
|
||||
Log.d(TAG, "Parsing returned null on attempt $attempt")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Read attempt $attempt failed: ${e.message}")
|
||||
}
|
||||
delay(200.milliseconds)
|
||||
}
|
||||
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Initial hearing aid settings: $parsedSettings")
|
||||
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
||||
balanceSliderValue.floatValue = parsedSettings.balance
|
||||
toneSliderValue.floatValue = parsedSettings.leftTone
|
||||
ambientNoiseReductionSliderValue.floatValue = parsedSettings.leftAmbientNoiseReduction
|
||||
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
||||
leftEQ.value = parsedSettings.leftEQ.copyOf()
|
||||
rightEQ.value = parsedSettings.rightEQ.copyOf()
|
||||
ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification
|
||||
initialReadSucceeded.value = true
|
||||
} else {
|
||||
Log.d(TAG, "Failed to read/parse initial hearing aid settings after ${initialReadAttempts.intValue} attempts")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
initialLoadComplete.value = true
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.amplification),
|
||||
valueRange = -1f..1f,
|
||||
@@ -237,7 +168,6 @@ fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) {
|
||||
independent = true,
|
||||
)
|
||||
|
||||
|
||||
StyledToggle(
|
||||
label = stringResource(R.string.swipe_to_control_amplification),
|
||||
checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE]?.getOrNull(0) == 0x01.toByte(),
|
||||
|
||||
@@ -259,6 +259,10 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
|
||||
return@launch
|
||||
}
|
||||
val parsed = parseTransparencySettingsResponse(state.hearingAidData)
|
||||
if (parsed == null) {
|
||||
Log.w(TAG, "transparency parse failed")
|
||||
return@launch
|
||||
}
|
||||
val disabledSettings = parsed.copy(enabled = false)
|
||||
sendTransparencySettings(viewModel::setATTCharacteristicValue, disabledSettings)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -46,9 +46,10 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
@@ -64,17 +65,14 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import kotlinx.coroutines.delay
|
||||
import me.kavishdevar.librepods.BuildConfig
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.data.TransparencySettings
|
||||
import me.kavishdevar.librepods.data.parseTransparencySettingsResponse
|
||||
import me.kavishdevar.librepods.data.sendTransparencySettings
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSlider
|
||||
import me.kavishdevar.librepods.presentation.components.StyledToggle
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||
import java.io.IOException
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
private const val TAG = "TransparencySettings"
|
||||
@@ -112,19 +110,26 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
||||
Spacer(modifier = Modifier.height(topPadding))
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
|
||||
val enabled = remember { mutableStateOf(false) }
|
||||
val amplificationSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val balanceSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val toneSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) }
|
||||
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
||||
val eq = remember { mutableStateOf(FloatArray(8)) }
|
||||
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||
val enabled = rememberSaveable { mutableStateOf(false) }
|
||||
val amplificationSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val balanceSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val toneSliderValue = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val ambientNoiseReductionSliderValue = rememberSaveable { mutableFloatStateOf(0.0f) }
|
||||
val conversationBoostEnabled = rememberSaveable { mutableStateOf(false) }
|
||||
val eq = rememberSaveable(
|
||||
saver = Saver(
|
||||
save = { it.value.toList() },
|
||||
restore = { mutableStateOf(it.toFloatArray()) }
|
||||
)
|
||||
) { mutableStateOf(FloatArray(8)) }
|
||||
val phoneMediaEQ = rememberSaveable(
|
||||
saver = Saver(
|
||||
save = { it.value.toList() },
|
||||
restore = { mutableStateOf(it.toFloatArray()) }
|
||||
)
|
||||
) { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||
|
||||
val initialLoadComplete = remember { mutableStateOf(false) }
|
||||
|
||||
val initialReadSucceeded = remember { mutableStateOf(false) }
|
||||
val initialReadAttempts = remember { mutableIntStateOf(0) }
|
||||
val initialized = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val transparencySettings = remember {
|
||||
mutableStateOf(
|
||||
@@ -153,23 +158,9 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
||||
toneSliderValue.floatValue,
|
||||
conversationBoostEnabled.value,
|
||||
ambientNoiseReductionSliderValue.floatValue,
|
||||
eq.value,
|
||||
initialLoadComplete.value,
|
||||
initialReadSucceeded.value
|
||||
eq.value
|
||||
) {
|
||||
if (!initialLoadComplete.value) {
|
||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (!initialReadSucceeded.value) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Initial device read not successful yet - skipping send until read succeeds"
|
||||
)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (!initialized.value) return@LaunchedEffect
|
||||
transparencySettings.value = TransparencySettings(
|
||||
enabled = enabled.value,
|
||||
leftEQ = eq.value,
|
||||
@@ -189,59 +180,20 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
|
||||
sendTransparencySettings(viewModel::setATTCharacteristicValue, transparencySettings.value)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
Log.d(TAG, "Connecting to ATT...")
|
||||
try {
|
||||
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
|
||||
try {
|
||||
Log.d(TAG, "Found AACPManager, reading cached EQ data")
|
||||
val aacpEQ = state.eqData
|
||||
if (aacpEQ.isNotEmpty()) {
|
||||
eq.value = aacpEQ.copyOf()
|
||||
phoneMediaEQ.value = aacpEQ.copyOf()
|
||||
Log.d(TAG, "Populated EQ from AACPManager: ${aacpEQ.toList()}")
|
||||
} else {
|
||||
Log.d(TAG, "AACPManager EQ data empty")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
|
||||
}
|
||||
|
||||
var parsedSettings: TransparencySettings? = null
|
||||
for (attempt in 1..3) {
|
||||
initialReadAttempts.intValue = attempt
|
||||
try {
|
||||
val data = state.transparencyData
|
||||
parsedSettings = parseTransparencySettingsResponse(data = data)
|
||||
Log.d(TAG, "Parsed settings on attempt $attempt")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Read attempt $attempt failed: ${e.message}")
|
||||
}
|
||||
delay(200)
|
||||
}
|
||||
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Initial transparency settings: $parsedSettings")
|
||||
enabled.value = parsedSettings.enabled
|
||||
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
||||
balanceSliderValue.floatValue = parsedSettings.balance
|
||||
toneSliderValue.floatValue = parsedSettings.leftTone
|
||||
ambientNoiseReductionSliderValue.floatValue =
|
||||
parsedSettings.leftAmbientNoiseReduction
|
||||
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
||||
eq.value = parsedSettings.leftEQ.copyOf()
|
||||
initialReadSucceeded.value = true
|
||||
} else {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Failed to read/parse initial transparency settings after ${initialReadAttempts.intValue} attempts"
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
initialLoadComplete.value = true
|
||||
LaunchedEffect(state.transparencyData) {
|
||||
val parsedSettings = parseTransparencySettingsResponse(data = state.transparencyData) ?: return@LaunchedEffect
|
||||
Log.d(TAG, "Initial transparency settings: $parsedSettings")
|
||||
enabled.value = parsedSettings.enabled
|
||||
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
||||
balanceSliderValue.floatValue = parsedSettings.balance
|
||||
toneSliderValue.floatValue = parsedSettings.leftTone
|
||||
ambientNoiseReductionSliderValue.floatValue =
|
||||
parsedSettings.leftAmbientNoiseReduction
|
||||
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
||||
if (!eq.value.contentEquals(parsedSettings.leftEQ)) {
|
||||
eq.value = parsedSettings.leftEQ.copyOf()
|
||||
}
|
||||
initialized.value = true
|
||||
}
|
||||
|
||||
if (state.vendorIdHook) {
|
||||
|
||||
@@ -35,13 +35,14 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -57,35 +58,19 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||
import me.kavishdevar.librepods.bluetooth.ATTManagerv2
|
||||
import me.kavishdevar.librepods.data.HearingAidSettings
|
||||
import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse
|
||||
import me.kavishdevar.librepods.data.sendHearingAidSettings
|
||||
import java.io.IOException
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||
|
||||
private var debounceJob: MutableState<Job?> = mutableStateOf(null)
|
||||
private const val TAG = "HearingAidAdjustments"
|
||||
|
||||
@Composable
|
||||
fun UpdateHearingTestScreen() {
|
||||
fun UpdateHearingTestScreen(viewModel: AirPodsViewModel) {
|
||||
val verticalScrollState = rememberScrollState()
|
||||
// val attManager = ServiceManager.getService()?.attManager
|
||||
// if (attManager == null) {
|
||||
// Text(
|
||||
// text = stringResource(R.string.att_manager_is_null_try_reconnecting),
|
||||
// modifier = Modifier
|
||||
// .fillMaxSize()
|
||||
// .padding(16.dp),
|
||||
// textAlign = TextAlign.Center
|
||||
// )
|
||||
// return
|
||||
// }
|
||||
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val backdrop = rememberLayerBackdrop()
|
||||
StyledScaffold(
|
||||
title = stringResource(R.string.hearing_test)
|
||||
@@ -113,18 +98,31 @@ fun UpdateHearingTestScreen() {
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
val tone = remember { mutableFloatStateOf(0.5f) }
|
||||
val ambientNoiseReduction = remember { mutableFloatStateOf(0.0f) }
|
||||
val ownVoiceAmplification = remember { mutableFloatStateOf(0.5f) }
|
||||
val leftAmplification = remember { mutableFloatStateOf(0.5f) }
|
||||
val rightAmplification = remember { mutableFloatStateOf(0.5f) }
|
||||
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
||||
val leftEQ = remember { mutableStateOf(FloatArray(8)) }
|
||||
val rightEQ = remember { mutableStateOf(FloatArray(8)) }
|
||||
val tone = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val ambientNoiseReduction = rememberSaveable { mutableFloatStateOf(0.0f) }
|
||||
val ownVoiceAmplification = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val leftAmplification = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val rightAmplification = rememberSaveable { mutableFloatStateOf(0.5f) }
|
||||
val conversationBoostEnabled = rememberSaveable { mutableStateOf(false) }
|
||||
val leftEQ = rememberSaveable(
|
||||
saver = Saver(
|
||||
save = { it.value.toList() },
|
||||
restore = { mutableStateOf(it.toFloatArray()) }
|
||||
)
|
||||
) {
|
||||
mutableStateOf(FloatArray(8))
|
||||
}
|
||||
val rightEQ = rememberSaveable(
|
||||
saver = Saver(
|
||||
save = { it.value.toList() },
|
||||
restore = { mutableStateOf(it.toFloatArray()) }
|
||||
)
|
||||
) {
|
||||
mutableStateOf(FloatArray(8))
|
||||
}
|
||||
|
||||
val initialLoadComplete = remember { mutableStateOf(false) }
|
||||
val initialReadSucceeded = remember { mutableStateOf(false) }
|
||||
val initialReadAttempts = remember { mutableIntStateOf(0) }
|
||||
val debounceJob = remember { mutableStateOf<Job?>(null) }
|
||||
val initialized = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val hearingAidSettings = remember {
|
||||
mutableStateOf(
|
||||
@@ -146,59 +144,35 @@ fun UpdateHearingTestScreen() {
|
||||
)
|
||||
}
|
||||
|
||||
val hearingAidATTListener = remember {
|
||||
object : (ByteArray) -> Unit {
|
||||
override fun invoke(value: ByteArray) {
|
||||
val parsed = parseHearingAidSettingsResponse(value)
|
||||
if (parsed != null) {
|
||||
leftEQ.value = parsed.leftEQ.copyOf()
|
||||
rightEQ.value = parsed.rightEQ.copyOf()
|
||||
conversationBoostEnabled.value = parsed.leftConversationBoost
|
||||
tone.floatValue = parsed.leftTone
|
||||
ambientNoiseReduction.floatValue = parsed.leftAmbientNoiseReduction
|
||||
ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification
|
||||
leftAmplification.floatValue = parsed.leftAmplification
|
||||
rightAmplification.floatValue = parsed.rightAmplification
|
||||
Log.d(TAG, "Updated hearing aid settings from notification")
|
||||
} else {
|
||||
Log.w(TAG, "Failed to parse hearing aid settings from notification")
|
||||
}
|
||||
}
|
||||
LaunchedEffect(state.hearingAidData) {
|
||||
val parsed = parseHearingAidSettingsResponse(state.hearingAidData)
|
||||
if (parsed != null) {
|
||||
leftEQ.value = parsed.leftEQ.copyOf()
|
||||
rightEQ.value = parsed.rightEQ.copyOf()
|
||||
conversationBoostEnabled.value = parsed.leftConversationBoost
|
||||
tone.floatValue = parsed.leftTone
|
||||
ambientNoiseReduction.floatValue = parsed.leftAmbientNoiseReduction
|
||||
ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification
|
||||
leftAmplification.floatValue = parsed.leftAmplification
|
||||
rightAmplification.floatValue = parsed.rightAmplification
|
||||
initialized.value = true
|
||||
Log.d(TAG, "Updated hearing aid settings from notification")
|
||||
} else {
|
||||
Log.w(TAG, "Failed to parse hearing aid settings from notification")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// DisposableEffect(Unit) {
|
||||
// onDispose {
|
||||
// attManager.unregisterListener(ATTHandles.HEARING_AID, hearingAidATTListener)
|
||||
// }
|
||||
// }
|
||||
|
||||
LaunchedEffect(
|
||||
leftEQ.value,
|
||||
rightEQ.value,
|
||||
conversationBoostEnabled.value,
|
||||
initialLoadComplete.value,
|
||||
initialReadSucceeded.value,
|
||||
leftAmplification.floatValue,
|
||||
rightAmplification.floatValue,
|
||||
tone.floatValue,
|
||||
ambientNoiseReduction.floatValue,
|
||||
ownVoiceAmplification.floatValue
|
||||
) {
|
||||
if (!initialLoadComplete.value) {
|
||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (!initialReadSucceeded.value) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Initial device read not successful yet - skipping send until read succeeds"
|
||||
)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (!initialized.value) return@LaunchedEffect
|
||||
hearingAidSettings.value = HearingAidSettings(
|
||||
leftEQ = leftEQ.value,
|
||||
rightEQ = rightEQ.value,
|
||||
@@ -215,55 +189,7 @@ fun UpdateHearingTestScreen() {
|
||||
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||
)
|
||||
Log.d(TAG, "Updated settings: ${hearingAidSettings.value}")
|
||||
sendHearingAidSettings(hearingAidSettings.value, debounceJob)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
Log.d(TAG, "Connecting to ATT...")
|
||||
try {
|
||||
// attManager.enableNotifications(ATTHandles.HEARING_AID)
|
||||
// attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener)
|
||||
|
||||
var parsedSettings: HearingAidSettings? = null
|
||||
for (attempt in 1..3) {
|
||||
initialReadAttempts.intValue = attempt
|
||||
try {
|
||||
val data = ATTManagerv2.readCharacteristic(ATTHandles.HEARING_AID)?: byteArrayOf()
|
||||
parsedSettings = parseHearingAidSettingsResponse(data = data)
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Parsed settings on attempt $attempt")
|
||||
break
|
||||
} else {
|
||||
Log.d(TAG, "Parsing returned null on attempt $attempt")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Read attempt $attempt failed: ${e.message}")
|
||||
}
|
||||
delay(200)
|
||||
}
|
||||
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Initial hearing aid settings: $parsedSettings")
|
||||
leftEQ.value = parsedSettings.leftEQ.copyOf()
|
||||
rightEQ.value = parsedSettings.rightEQ.copyOf()
|
||||
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
||||
tone.floatValue = parsedSettings.leftTone
|
||||
ambientNoiseReduction.floatValue = parsedSettings.leftAmbientNoiseReduction
|
||||
ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification
|
||||
leftAmplification.floatValue = parsedSettings.leftAmplification
|
||||
rightAmplification.floatValue = parsedSettings.rightAmplification
|
||||
initialReadSucceeded.value = true
|
||||
} else {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Failed to read/parse initial hearing aid settings after ${initialReadAttempts.intValue} attempts"
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
initialLoadComplete.value = true
|
||||
}
|
||||
sendHearingAidSettings(state.hearingAidData, hearingAidSettings.value, debounceJob, viewModel::setATTCharacteristicValue)
|
||||
}
|
||||
|
||||
val frequencies =
|
||||
|
||||
@@ -29,7 +29,6 @@ import androidx.core.content.edit
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -39,8 +38,8 @@ import me.kavishdevar.librepods.BuildConfig
|
||||
import me.kavishdevar.librepods.billing.BillingManager
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers
|
||||
import me.kavishdevar.librepods.bluetooth.ATTCCCDHandles
|
||||
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||
import me.kavishdevar.librepods.bluetooth.ATTManagerv2
|
||||
import me.kavishdevar.librepods.data.AirPodsInstance
|
||||
import me.kavishdevar.librepods.data.AirPodsModels
|
||||
import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||
@@ -52,7 +51,6 @@ import me.kavishdevar.librepods.data.ControlCommandRepository
|
||||
import me.kavishdevar.librepods.data.StemAction
|
||||
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class AirPodsUiState(
|
||||
@@ -147,6 +145,7 @@ class AirPodsViewModel(
|
||||
loadSharedPreferences()
|
||||
setupControlObservers()
|
||||
loadControlList()
|
||||
loadATT()
|
||||
observeATT()
|
||||
observeSharedPreferences()
|
||||
observeBilling()
|
||||
@@ -527,27 +526,36 @@ class AirPodsViewModel(
|
||||
}
|
||||
|
||||
fun setATTCharacteristicValue(handle: ATTHandles, value: ByteArray) {
|
||||
if (handle == ATTHandles.LOUD_SOUND_REDUCTION) {
|
||||
_uiState.update { it.copy(loudSoundReductionEnabled = value[0].toInt() == 0x01) }
|
||||
when (handle) {
|
||||
// ideally should be using a different viewmodel for ATT based things because there are a lot of values, and I am not going to add all to this state, but there's loudsoundreduction.
|
||||
ATTHandles.LOUD_SOUND_REDUCTION -> {
|
||||
_uiState.value = _uiState.value.copy(loudSoundReductionEnabled = value[0].toInt() == 0x01)
|
||||
}
|
||||
ATTHandles.HEARING_AID -> {
|
||||
_uiState.value = _uiState.value.copy(hearingAidData = value)
|
||||
}
|
||||
ATTHandles.TRANSPARENCY -> {
|
||||
_uiState.value = _uiState.value.copy(transparencyData = value)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
ATTManagerv2.writeCharacteristic(handle, value)
|
||||
service.attManager.writeCharacteristic(handle, value)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshATT() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val loudSoundReduction = ATTManagerv2.readCharacteristic(ATTHandles.LOUD_SOUND_REDUCTION) ?: byteArrayOf()
|
||||
val loudSoundReductionEnabled = if (loudSoundReduction.isNotEmpty()) {
|
||||
loudSoundReduction[0].toInt() == 1
|
||||
} else false
|
||||
val transparencyData = ATTManagerv2.readCharacteristic(ATTHandles.TRANSPARENCY)?: byteArrayOf()
|
||||
val hearingAidData = ATTManagerv2.readCharacteristic(ATTHandles.HEARING_AID)?:byteArrayOf()
|
||||
_uiState.value = _uiState.value.copy(
|
||||
fun loadATT() {
|
||||
val loudSoundReduction = service.attManager.getCharacteristic(ATTHandles.LOUD_SOUND_REDUCTION) ?: byteArrayOf()
|
||||
val loudSoundReductionEnabled = if (loudSoundReduction.isNotEmpty()) {
|
||||
loudSoundReduction[0].toInt() == 1
|
||||
} else false
|
||||
val hearingAidData = service.attManager.getCharacteristic(ATTHandles.HEARING_AID) ?: byteArrayOf()
|
||||
val transparencyData = service.attManager.getCharacteristic(ATTHandles.TRANSPARENCY) ?: byteArrayOf()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
loudSoundReductionEnabled = loudSoundReductionEnabled,
|
||||
transparencyData = transparencyData,
|
||||
hearingAidData = hearingAidData
|
||||
@@ -557,9 +565,30 @@ class AirPodsViewModel(
|
||||
|
||||
fun observeATT() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
while (true) {
|
||||
refreshATT()
|
||||
delay(15000.milliseconds)
|
||||
service.attManager.enableNotification(ATTCCCDHandles.HEARING_AID)
|
||||
service.attManager.enableNotification(ATTCCCDHandles.TRANSPARENCY)
|
||||
// service.attManager.enableNotification(ATTCCCDHandles.LOUD_SOUND_REDUCTION)
|
||||
}
|
||||
service.attManager.setOnNotificationReceived { handle, value ->
|
||||
when (handle) {
|
||||
ATTHandles.LOUD_SOUND_REDUCTION.value.toByte() -> {
|
||||
val loudSoundReductionEnabled = if (value.isNotEmpty()) {
|
||||
value[0].toInt() == 1
|
||||
} else false
|
||||
_uiState.update {
|
||||
it.copy(loudSoundReductionEnabled = loudSoundReductionEnabled)
|
||||
}
|
||||
}
|
||||
ATTHandles.HEARING_AID.value.toByte() -> {
|
||||
_uiState.update {
|
||||
it.copy(hearingAidData = value)
|
||||
}
|
||||
}
|
||||
ATTHandles.TRANSPARENCY.value.toByte() -> {
|
||||
_uiState.update {
|
||||
it.copy(transparencyData = value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,9 @@ import me.kavishdevar.librepods.MainActivity
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.StemPressType
|
||||
import me.kavishdevar.librepods.bluetooth.ATTManager
|
||||
import me.kavishdevar.librepods.bluetooth.ATTCCCDHandles
|
||||
import me.kavishdevar.librepods.bluetooth.ATTHandles
|
||||
import me.kavishdevar.librepods.bluetooth.ATTManagerv2
|
||||
import me.kavishdevar.librepods.bluetooth.BLEManager
|
||||
import me.kavishdevar.librepods.bluetooth.BluetoothConnectionManager
|
||||
import me.kavishdevar.librepods.data.AirPodsInstance
|
||||
@@ -126,7 +128,6 @@ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_
|
||||
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -152,6 +153,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
var macAddress = ""
|
||||
var localMac = ""
|
||||
lateinit var aacpManager: AACPManager
|
||||
lateinit var attManager: ATTManagerv2
|
||||
var airpodsInstance: AirPodsInstance? = null
|
||||
var cameraActive = false
|
||||
private var disconnectedBecauseReversed = false
|
||||
@@ -380,6 +382,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
aacpManager = AACPManager()
|
||||
initializeAACPManagerCallback()
|
||||
|
||||
attManager = ATTManagerv2()
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
localMac = config.selfMacAddress
|
||||
@@ -693,7 +697,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
popupShown = false
|
||||
updateNotificationContent(false)
|
||||
aacpManager.disconnected()
|
||||
BluetoothConnectionManager.getATTSocket()?.close()
|
||||
attManager.disconnected()
|
||||
BluetoothConnectionManager.setCurrentConnection(null, null)
|
||||
}
|
||||
}
|
||||
@@ -2702,6 +2706,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
} else null
|
||||
attSocket?.connect()
|
||||
BluetoothConnectionManager.setCurrentConnection(socket, attSocket)
|
||||
if (attSocket != null) {
|
||||
attManager.startReader()
|
||||
attManager.readCharacteristic(ATTHandles.LOUD_SOUND_REDUCTION)
|
||||
attManager.readCharacteristic(ATTHandles.TRANSPARENCY)
|
||||
attManager.readCharacteristic(ATTHandles.HEARING_AID)
|
||||
attManager.enableNotification(ATTCCCDHandles.HEARING_AID)
|
||||
// attManager.enableNotification(ATTCCCDHandles.LOUD_SOUND_REDUCTION)
|
||||
attManager.enableNotification(ATTCCCDHandles.TRANSPARENCY)
|
||||
}
|
||||
|
||||
// Create AirPodsInstance from stored config if available
|
||||
if (airpodsInstance == null && config.airpodsModelNumber.isNotEmpty()) {
|
||||
@@ -2906,7 +2919,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
socket.close()
|
||||
// isConnectedLocally = false
|
||||
aacpManager.disconnected()
|
||||
BluetoothConnectionManager.getATTSocket()?.close()
|
||||
attManager.disconnected()
|
||||
BluetoothConnectionManager.setCurrentConnection(null, null)
|
||||
updateNotificationContent(false)
|
||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply {
|
||||
|
||||
Reference in New Issue
Block a user