android: refactor AACP socket handling

This commit is contained in:
Kavish Devar
2026-06-01 14:53:33 +05:30
parent 0477674810
commit 57d692c4ae
5 changed files with 69 additions and 71 deletions

View File

@@ -118,6 +118,7 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import me.kavishdevar.librepods.bluetooth.BluetoothConnectionManager
import me.kavishdevar.librepods.data.AirPodsNotifications import me.kavishdevar.librepods.data.AirPodsNotifications
import me.kavishdevar.librepods.data.ControlCommandRepository import me.kavishdevar.librepods.data.ControlCommandRepository
import me.kavishdevar.librepods.presentation.components.AppInfoCard import me.kavishdevar.librepods.presentation.components.AppInfoCard
@@ -541,7 +542,7 @@ fun Main() {
Context.BIND_AUTO_CREATE Context.BIND_AUTO_CREATE
) )
if (airPodsService.value?.isConnected() == true) { if (BluetoothConnectionManager.getAACPSocket()?.isConnected == true) {
isConnected.value = true isConnected.value = true
} }
} else { } else {

View File

@@ -31,9 +31,8 @@ import kotlin.io.encoding.ExperimentalEncodingApi
* constructing and parsing packets for communication with AirPods. * constructing and parsing packets for communication with AirPods.
*/ */
class AACPManager { class AACPManager {
private val TAG = "AACPManager[${System.identityHashCode(this)}]"
companion object { companion object {
private const val TAG = "AACPManager"
@Suppress("unused") @Suppress("unused")
object Opcodes { object Opcodes {
const val SET_FEATURE_FLAGS: Byte = 0x4D const val SET_FEATURE_FLAGS: Byte = 0x4D

View File

@@ -24,6 +24,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -40,6 +41,7 @@ import me.kavishdevar.librepods.bluetooth.AACPManager
import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers
import me.kavishdevar.librepods.bluetooth.ATTCCCDHandles import me.kavishdevar.librepods.bluetooth.ATTCCCDHandles
import me.kavishdevar.librepods.bluetooth.ATTHandles import me.kavishdevar.librepods.bluetooth.ATTHandles
import me.kavishdevar.librepods.bluetooth.BluetoothConnectionManager
import me.kavishdevar.librepods.data.AirPodsInstance import me.kavishdevar.librepods.data.AirPodsInstance
import me.kavishdevar.librepods.data.AirPodsModels import me.kavishdevar.librepods.data.AirPodsModels
import me.kavishdevar.librepods.data.AirPodsNotifications import me.kavishdevar.librepods.data.AirPodsNotifications
@@ -352,7 +354,7 @@ class AirPodsViewModel(
service.let { service -> service.let { service ->
_uiState.update { _uiState.update {
it.copy( it.copy(
isLocallyConnected = service.isConnected(), battery = service.getBattery() isLocallyConnected = BluetoothConnectionManager.getAACPSocket()?.isConnected == true, battery = service.getBattery()
) )
} }
} }
@@ -382,7 +384,6 @@ class AirPodsViewModel(
val connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false) val connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
val fossUpgraded = sharedPreferences.getBoolean("foss_upgraded", false)
_uiState.update { _uiState.update {
it.copy( it.copy(
offListeningMode = offListeningModeEnabled, offListeningMode = offListeningModeEnabled,
@@ -398,8 +399,8 @@ class AirPodsViewModel(
} }
// faulty update on Play caused PLAY_BUILD to be false and resulted in use of FOSS billing in Play. since FOSS is not verified, we need to give 2 weeks to verify the purchase // faulty update on Play caused PLAY_BUILD to be false and resulted in use of FOSS billing in Play. since FOSS is not verified, we need to give 2 weeks to verify the purchase
if (BuildConfig.PLAY_BUILD) { if (BuildConfig.PLAY_BUILD) {
val fossUpgraded = sharedPreferences.getBoolean("foss_upgraded", false)
val expiryTime = sharedPreferences.getLong("premium_expiry_time", 0L) val expiryTime = sharedPreferences.getLong("premium_expiry_time", 0L)
val now = System.currentTimeMillis() val now = System.currentTimeMillis()

View File

@@ -35,9 +35,10 @@ import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import me.kavishdevar.librepods.QuickSettingsDialogActivity import me.kavishdevar.librepods.QuickSettingsDialogActivity
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.AACPManager
import me.kavishdevar.librepods.bluetooth.BluetoothConnectionManager
import me.kavishdevar.librepods.data.AirPodsNotifications import me.kavishdevar.librepods.data.AirPodsNotifications
import me.kavishdevar.librepods.data.NoiseControlMode import me.kavishdevar.librepods.data.NoiseControlMode
import me.kavishdevar.librepods.bluetooth.AACPManager
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
@@ -98,7 +99,7 @@ class AirPodsQSService : TileService() {
Log.d("AirPodsQSService", "onStartListening") Log.d("AirPodsQSService", "onStartListening")
val service = ServiceManager.getService() val service = ServiceManager.getService()
isAirPodsConnected = service?.isConnected() == true isAirPodsConnected = BluetoothConnectionManager.getAACPSocket()?.isConnected == true
currentAncMode = service?.getANC() ?: (NoiseControlMode.OFF.ordinal + 1) currentAncMode = service?.getANC() ?: (NoiseControlMode.OFF.ordinal + 1)
if (currentAncMode == NoiseControlMode.OFF.ordinal + 1 && !isOffModeEnabled()) { if (currentAncMode == NoiseControlMode.OFF.ordinal + 1 && !isOffModeEnabled()) {

View File

@@ -233,8 +233,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
lateinit var bleManager: BLEManager lateinit var bleManager: BLEManager
private lateinit var socket: BluetoothSocket
companion object { companion object {
init { init {
System.loadLibrary("bluetooth_socket") System.loadLibrary("bluetooth_socket")
@@ -246,7 +244,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
override fun onDeviceStatusChanged( override fun onDeviceStatusChanged(
device: BLEManager.AirPodsStatus, previousStatus: BLEManager.AirPodsStatus? device: BLEManager.AirPodsStatus, previousStatus: BLEManager.AirPodsStatus?
) { ) {
if (device.connectionState == "Disconnected" && !isConnected()) { // should never happen unless android messes up and sends us a stale broadcast if (device.connectionState == "Disconnected" && BluetoothConnectionManager.getAACPSocket()?.isConnected != true) { // should never happen unless android messes up and sends us a stale broadcast
Log.d(TAG, "Seems no device has taken over, we will.") Log.d(TAG, "Seems no device has taken over, we will.")
val bluetoothManager = getSystemService(BluetoothManager::class.java) val bluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter = bluetoothManager.adapter val bluetoothAdapter = bluetoothManager.adapter
@@ -258,7 +256,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
connectToSocket(bluetoothAdapter, bluetoothDevice) connectToSocket(bluetoothAdapter, bluetoothDevice)
} }
Log.d(TAG, "Device status changed") Log.d(TAG, "Device status changed")
if (socket.isConnected) return if (BluetoothConnectionManager.getAACPSocket()?.isConnected == true) return
val leftLevel = bleManager.getMostRecentStatus()?.leftBattery ?: 0 val leftLevel = bleManager.getMostRecentStatus()?.leftBattery ?: 0
val rightLevel = bleManager.getMostRecentStatus()?.rightBattery ?: 0 val rightLevel = bleManager.getMostRecentStatus()?.rightBattery ?: 0
val caseLevel = bleManager.getMostRecentStatus()?.caseBattery ?: 0 val caseLevel = bleManager.getMostRecentStatus()?.caseBattery ?: 0
@@ -291,7 +289,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
getSharedPreferences("settings", MODE_PRIVATE).getString("name", "AirPods Pro") getSharedPreferences("settings", MODE_PRIVATE).getString("name", "AirPods Pro")
?: "AirPods" ?: "AirPods"
) )
if (socket.isConnected) return if (BluetoothConnectionManager.getAACPSocket()?.isConnected == true) return
val leftLevel = bleManager.getMostRecentStatus()?.leftBattery ?: 0 val leftLevel = bleManager.getMostRecentStatus()?.leftBattery ?: 0
val rightLevel = bleManager.getMostRecentStatus()?.rightBattery ?: 0 val rightLevel = bleManager.getMostRecentStatus()?.rightBattery ?: 0
val caseLevel = bleManager.getMostRecentStatus()?.caseBattery ?: 0 val caseLevel = bleManager.getMostRecentStatus()?.caseBattery ?: 0
@@ -325,7 +323,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
override fun onBatteryChanged(device: BLEManager.AirPodsStatus) { override fun onBatteryChanged(device: BLEManager.AirPodsStatus) {
if (socket.isConnected) return if (BluetoothConnectionManager.getAACPSocket()?.isConnected == true) return
val leftLevel = bleManager.getMostRecentStatus()?.leftBattery ?: 0 val leftLevel = bleManager.getMostRecentStatus()?.leftBattery ?: 0
val rightLevel = bleManager.getMostRecentStatus()?.rightBattery ?: 0 val rightLevel = bleManager.getMostRecentStatus()?.rightBattery ?: 0
val caseLevel = bleManager.getMostRecentStatus()?.caseBattery ?: 0 val caseLevel = bleManager.getMostRecentStatus()?.caseBattery ?: 0
@@ -1739,7 +1737,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val socketFailureChannel = NotificationChannel( val socketFailureChannel = NotificationChannel(
"socket_connection_failure", "socket_connection_failure",
"AirPods Socket Connection Issues", "AirPods BluetoothConnectionManager.getAACPSocket()? Connection Issues",
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
).apply { ).apply {
description = "Notifications about problems connecting to AirPods protocol" description = "Notifications about problems connecting to AirPods protocol"
@@ -1785,7 +1783,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (BuildConfig.FLAVOR != "xposed") { if (BuildConfig.FLAVOR != "xposed") {
Log.w( Log.w(
TAG, TAG,
"Not showing socket error notification to user, the service shouldn't be running if it isn't supported." "Not showing BluetoothConnectionManager.getAACPSocket()? error notification to user, the service shouldn't be running if it isn't supported."
) )
return return
} }
@@ -2040,10 +2038,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
if (!::socket.isInitialized) { if (BluetoothConnectionManager.getAACPSocket() == null) {
return return
} }
if (connected && (config.bleOnlyMode || socket.isConnected)) { if (BluetoothConnectionManager.getAACPSocket()?.isConnected == true) {
val updatedNotificationBuilder = val updatedNotificationBuilder =
NotificationCompat.Builder(this, "airpods_connection_status") NotificationCompat.Builder(this, "airpods_connection_status")
.setSmallIcon(R.drawable.airpods) .setSmallIcon(R.drawable.airpods)
@@ -2091,8 +2089,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
notificationManager.cancel(1) notificationManager.cancel(1)
} else if (!connected) { } else if (!connected) {
notificationManager.cancel(2) notificationManager.cancel(2)
} else if (!config.bleOnlyMode && !socket.isConnected) { } else if (!config.bleOnlyMode && BluetoothConnectionManager.getAACPSocket()?.isConnected != true) {
showSocketConnectionFailureNotification("Socket created, but not connected. Check logs") showSocketConnectionFailureNotification("BluetoothConnectionManager.getAACPSocket()? created, but not connected. Check logs")
} }
} }
@@ -2467,8 +2465,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
Log.d( Log.d(
TAG, "owns connection: $ownsConnection" TAG, "owns connection: $ownsConnection"
) )
if (!::socket.isInitialized) return if (BluetoothConnectionManager.getAACPSocket()?.isConnected == true) {
if (socket.isConnected) {
if (!XposedRemotePrefProvider.create().getBoolean("vendor_id_hook", false) || ownsConnection == 0) { if (!XposedRemotePrefProvider.create().getBoolean("vendor_id_hook", false) || ownsConnection == 0) {
Log.d(TAG, "not taking over, vendorid is probably not set to apple") Log.d(TAG, "not taking over, vendorid is probably not set to apple")
return return
@@ -2677,10 +2674,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
fun connectToSocket( fun connectToSocket(
adapter: BluetoothAdapter, device: BluetoothDevice, manual: Boolean = false adapter: BluetoothAdapter, device: BluetoothDevice, manual: Boolean = false
) { ) {
if (BluetoothConnectionManager.getAACPSocket() != null && BluetoothConnectionManager.getAACPSocket()?.isConnected == true) return
Log.d(TAG, "<LogCollector:Start> Connecting to socket") Log.d(TAG, "<LogCollector:Start> Connecting to socket")
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a") val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
// if (!isConnectedLocally) { // if (!isConnectedLocally) {
socket = try { val socket = try {
createBluetoothSocket(adapter, device, uuid, 4097) createBluetoothSocket(adapter, device, uuid, 4097)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to create BluetoothSocket: ${e.message}") Log.e(TAG, "Failed to create BluetoothSocket: ${e.message}")
@@ -2768,7 +2766,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
} }
if (!socket.isConnected) { if (!socket.isConnected) {
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected") Log.d(TAG, "<LogCollector:Complete:Failed> socket not connected")
if (manual) { if (manual) {
sendToast( sendToast(
"Couldn't connect to socket: timeout." "Couldn't connect to socket: timeout."
@@ -2779,13 +2777,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
return return
} }
this@AirPodsService.device = device this@AirPodsService.device = device
socket.let { BluetoothConnectionManager.getAACPSocket()?.let {
aacpManager.sendPacket(aacpManager.createHandshakePacket()) aacpManager.sendPacket(aacpManager.createHandshakePacket())
aacpManager.sendSetFeatureFlagsPacket() aacpManager.sendSetFeatureFlagsPacket()
aacpManager.sendNotificationRequest() aacpManager.sendNotificationRequest()
Log.d(TAG, "Requesting proximity keys") Log.d(TAG, "Requesting proximity keys")
aacpManager.sendRequestProximityKeys((AACPManager.Companion.ProximityKeyType.IRK.value + AACPManager.Companion.ProximityKeyType.ENC_KEY.value).toByte()) aacpManager.sendRequestProximityKeys((AACPManager.Companion.ProximityKeyType.IRK.value + AACPManager.Companion.ProximityKeyType.ENC_KEY.value).toByte())
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
delay(200)
aacpManager.sendPacket(aacpManager.createHandshakePacket()) aacpManager.sendPacket(aacpManager.createHandshakePacket())
delay(200) delay(200)
aacpManager.sendSetFeatureFlagsPacket() aacpManager.sendSetFeatureFlagsPacket()
@@ -2813,55 +2812,53 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
setupStemActions() setupStemActions()
while (socket.isConnected) { while (socket.isConnected) {
socket.let { it -> try {
try { val buffer = ByteArray(1024)
val buffer = ByteArray(1024) val bytesRead = it.inputStream.read(buffer)
val bytesRead = it.inputStream.read(buffer) var data: ByteArray
var data: ByteArray if (bytesRead > 0) {
if (bytesRead > 0) { data = buffer.copyOfRange(0, bytesRead)
data = buffer.copyOfRange(0, bytesRead) sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply { putExtra("data", buffer.copyOfRange(0, bytesRead))
putExtra("data", buffer.copyOfRange(0, bytesRead)) setPackage(packageName)
setPackage(packageName) })
}) val bytes = buffer.copyOfRange(0, bytesRead)
val bytes = buffer.copyOfRange(0, bytesRead) val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
// CrossDevice.sendReceivedPacket(bytes) // CrossDevice.sendReceivedPacket(bytes)
updateNotificationContent( updateNotificationContent(
true, true,
sharedPreferences.getString("name", device.name), sharedPreferences.getString("name", device.name),
batteryNotification.getBattery() batteryNotification.getBattery()
) )
aacpManager.receivePacket(data) aacpManager.receivePacket(data)
if (!isHeadTrackingData(data)) { if (!isHeadTrackingData(data)) {
Log.d("AirPodsData", "Data received: $formattedHex") Log.d("AirPodsData", "Data received: $formattedHex")
logPacket(data, "AirPods") logPacket(data, "AirPods")
}
} else if (bytesRead == -1) {
Log.d("AirPods Service", "Socket closed (bytesRead = -1)")
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply {
setPackage(packageName)
})
aacpManager.disconnected()
return@launch
} }
} catch (e: Exception) {
Log.w(TAG, "Error reading data, we have probably disconnected.") } else if (bytesRead == -1) {
e.printStackTrace() Log.d("AirPods Service", "BluetoothConnectionManager.getAACPSocket()? closed (bytesRead = -1)")
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply {
setPackage(packageName) setPackage(packageName)
}) })
aacpManager.disconnected() aacpManager.disconnected()
return@launch return@launch
} }
} catch (e: Exception) {
Log.w(TAG, "Error reading data, we have probably disconnected.")
e.printStackTrace()
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply {
setPackage(packageName)
})
aacpManager.disconnected()
return@launch
} }
} }
Log.d("AirPods Service", "Socket closed") Log.d("AirPods Service", "socket closed")
// isConnectedLocally = false // isConnectedLocally = false
socket.close()
aacpManager.disconnected() aacpManager.disconnected()
updateNotificationContent(false) updateNotificationContent(false)
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply {
@@ -2871,20 +2868,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Log.d(TAG, "Failed to connect to socket: ${e.message}") Log.d(TAG, "Failed to connect to BluetoothConnectionManager.getAACPSocket()?: ${e.message}")
showSocketConnectionFailureNotification("Failed to establish connection: ${e.localizedMessage}") showSocketConnectionFailureNotification("Failed to establish connection: ${e.localizedMessage}")
// isConnectedLocally = false // isConnectedLocally = false
this@AirPodsService.device = device this@AirPodsService.device = device
updateNotificationContent(false) updateNotificationContent(false)
} }
// } else { // } else {
// Log.d(TAG, "Already connected locally, skipping socket connection (isConnectedLocally = $isConnectedLocally, socket.isConnected = ${this::socket.isInitialized && socket.isConnected})") // Log.d(TAG, "Already connected locally, skipping BluetoothConnectionManager.getAACPSocket()? connection (isConnectedLocally = $isConnectedLocally, BluetoothConnectionManager.getAACPSocket()?.isConnected = ${this::BluetoothConnectionManager.getAACPSocket()?.isInitialized && BluetoothConnectionManager.getAACPSocket()?.isConnected})")
// } // }
} }
fun disconnectForCD() { fun disconnectForCD() {
if (!this::socket.isInitialized) return BluetoothConnectionManager.getAACPSocket()?.close()
socket.close()
MediaController.pausedWhileTakingOver = false MediaController.pausedWhileTakingOver = false
Log.d(TAG, "Disconnected from AirPods, showing island.") Log.d(TAG, "Disconnected from AirPods, showing island.")
showIsland( showIsland(
@@ -2915,8 +2911,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
fun disconnectAirPods() { fun disconnectAirPods() {
if (!this::socket.isInitialized) return if (BluetoothConnectionManager.getAACPSocket() == null) return
socket.close() try {
BluetoothConnectionManager.getAACPSocket()?.close()
} catch(e: Exception) {
Log.e(TAG, "error closing aacp socket ${e.message}")
}
// isConnectedLocally = false // isConnectedLocally = false
aacpManager.disconnected() aacpManager.disconnected()
attManager.disconnected() attManager.disconnected()
@@ -3228,10 +3228,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
} }
} }
fun isConnected(): Boolean {
return if (::socket.isInitialized) socket.isConnected else false
}
} }
private fun Int.dpToPx(): Int { private fun Int.dpToPx(): Int {