android: ui tweaks

This commit is contained in:
Kavish Devar
2025-09-30 11:07:16 +05:30
parent 650b128d5d
commit 993f022087
5 changed files with 165 additions and 90 deletions

View File

@@ -110,7 +110,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
if (preview) {
batteryStatus.value = listOf(
Battery(BatteryComponent.LEFT, 100, BatteryStatus.NOT_CHARGING),
Battery(BatteryComponent.RIGHT, 94, BatteryStatus.NOT_CHARGING),
Battery(BatteryComponent.RIGHT, 94, BatteryStatus.CHARGING),
Battery(BatteryComponent.CASE, 40, BatteryStatus.CHARGING)
)
previousBatteryStatus.value = batteryStatus.value
@@ -177,7 +177,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
}
if (leftLevel > 0 && rightLevel > 0)
{
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(16.dp))
}
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED)
{

View File

@@ -126,6 +126,8 @@ import java.nio.ByteOrder
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
const val TAG = "AirPodsService"
object ServiceManager {
@ExperimentalEncodingApi
private var service: AirPodsService? = null
@@ -152,6 +154,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var attManager: ATTManager? = null
var cameraActive = false
private var disconnectedBecauseReversed = false
private var otherDeviceTookOver = false
data class ServiceConfig(
var deviceName: String = "AirPods",
var earDetectionEnabled: Boolean = true,
@@ -220,17 +223,17 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
sharedPreferences.edit {
putString("mac_address", macAddress)
}
Log.d("AirPodsBLEService", "BLE-only mode: stored MAC address ${device.address}")
Log.d(TAG, "BLE-only mode: stored MAC address ${device.address}")
}
if (device.connectionState == "Disconnected" && !config.bleOnlyMode) {
Log.d("AirPodsBLEService", "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 bluetoothDevice = bluetoothManager.adapter.getRemoteDevice(sharedPreferences.getString(
"mac_address", "") ?: "")
connectToSocket(bluetoothDevice)
}
Log.d("AirPodsBLEService", "Device status changed")
Log.d(TAG, "Device status changed")
if (isConnectedLocally) return
val leftLevel = bleManager.getMostRecentStatus()?.leftBattery?: 0
val rightLevel = bleManager.getMostRecentStatus()?.rightBattery?: 0
@@ -251,14 +254,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
override fun onBroadcastFromNewAddress(device: BLEManager.AirPodsStatus) {
Log.d("AirPodsService", "New address detected")
Log.d(TAG, "New address detected")
}
override fun onLidStateChanged(
lidOpen: Boolean,
) {
if (lidOpen) {
Log.d("AirPodsBLEService", "Lid opened")
Log.d(TAG, "Lid opened")
showPopup(
this@AirPodsService,
getSharedPreferences("settings", MODE_PRIVATE).getString("name", "AirPods Pro") ?: "AirPods"
@@ -281,7 +284,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
)
sendBatteryBroadcast()
} else {
Log.d("AirPodsBLEService", "Lid closed")
Log.d(TAG, "Lid closed")
}
}
@@ -290,11 +293,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
leftInEar: Boolean,
rightInEar: Boolean
) {
Log.d("AirPodsBLEService", "Ear state changed - Left: $leftInEar, Right: $rightInEar")
Log.d(TAG, "Ear state changed - Left: $leftInEar, Right: $rightInEar")
// In BLE-only mode, ear detection is purely based on BLE data
if (config.bleOnlyMode) {
Log.d("AirPodsBLEService", "BLE-only mode: ear detection from BLE data")
Log.d(TAG, "BLE-only mode: ear detection from BLE data")
}
}
@@ -316,7 +319,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
caseCharging = caseCharging == true
)
updateBattery()
Log.d("AirPodsBLEService", "Battery changed")
Log.d(TAG, "Battery changed")
}
override fun onDeviceDisappeared() {
Log.d(TAG, "All disappeared")
updateNotificationContent(
false
)
}
}
@@ -349,7 +359,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
gestureDetector = null
config.headGestures = false
sharedPreferences.edit { putBoolean("head_gestures", false) }
Log.d("AirPodsService", "Head gestures disabled as device is running Android 9 or below")
Log.d(TAG, "Head gestures disabled as device is running Android 9 or below")
}
bleManager = BLEManager(this)
@@ -504,7 +514,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value,
nextMode
)
Log.d("AirPodsService", "Cycling ANC mode from $currentMode to $nextMode (offListeningMode: $allowOffMode)")
Log.d(TAG, "Cycling ANC mode from $currentMode to $nextMode (offListeningMode: $allowOffMode)")
}
}
}
@@ -525,10 +535,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
MODE_PRIVATE
)
)
Log.d("AirPodsService", "Initializing CrossDevice")
Log.d(TAG, "Initializing CrossDevice")
CoroutineScope(Dispatchers.IO).launch {
CrossDevice.init(this@AirPodsService)
Log.d("AirPodsService", "CrossDevice initialized")
Log.d(TAG, "CrossDevice initialized")
}
sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
@@ -612,11 +622,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
Log.d("AirPodsCrossDevice", CrossDevice.isAvailable.toString())
if (!CrossDevice.isAvailable) {
Log.d("AirPodsService", "${config.deviceName} connected")
Log.d(TAG, "${config.deviceName} connected")
CoroutineScope(Dispatchers.IO).launch {
connectToSocket(device!!)
}
Log.d("AirPodsService", "Setting metadata")
Log.d(TAG, "Setting metadata")
setMetadatas(device!!)
isConnectedLocally = true
macAddress = device!!.address
@@ -725,7 +735,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
@Suppress("unused")
fun cameraOpened() {
Log.d("AirPodsService", "Camera opened, gonna handle stem presses and take action if enabled")
Log.d(TAG, "Camera opened, gonna handle stem presses and take action if enabled")
val isCameraShutterUsed = listOf(
config.leftSinglePressAction,
config.rightSinglePressAction,
@@ -738,7 +748,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
).any { it == StemAction.CAMERA_SHUTTER }
if (isCameraShutterUsed) {
Log.d("AirPodsService", "Camera opened, setting up stem actions")
Log.d(TAG, "Camera opened, setting up stem actions")
cameraActive = true
setupStemActions(isCameraActive = true)
}
@@ -755,7 +765,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
default: StemAction?,
isCameraActive: Boolean = false
): Boolean {
Log.d("AirPodsService", "Checking if action $action is custom against default $default, camera active: $isCameraActive")
Log.d(TAG, "Checking if action $action is custom against default $default, camera active: $isCameraActive")
return action != default && (action != StemAction.CAMERA_SHUTTER || isCameraActive)
}
@@ -773,7 +783,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
isCustomAction(config.rightTriplePressAction, triplePressDefault, isCameraActive)
val longPressCustomized = isCustomAction(config.leftLongPressAction, longPressDefault, isCameraActive) ||
isCustomAction(config.rightLongPressAction, longPressDefault, isCameraActive)
Log.d("AirPodsService", "Setting up stem actions: " +
Log.d(TAG, "Setting up stem actions: " +
"Single Press Customized: $singlePressCustomized, " +
"Double Press Customized: $doublePressCustomized, " +
"Triple Press Customized: $triplePressCustomized, " +
@@ -867,9 +877,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
Handler(Looper.getMainLooper()).postDelayed({
MediaController.recentlyLostOwnership = false
}, 3000)
Log.d("AirPodsService", "ownership lost")
Log.d(TAG, "ownership lost")
MediaController.sendPause()
MediaController.pausedForOtherDevice = true
otherDeviceTookOver = true
disconnectAudio(
this@AirPodsService,
device
)
}
}
@@ -878,13 +893,18 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
// handling reverse is a problem because we'd have to disconnect the audio, but there's no option connect audio again natively, so notification would have to be changed. I wish there was a way to just "change the audio output device".
// (20 minutes later) i've done it nonetheless :]
val senderName = aacpManager.connectedDevices.find { it.mac == sender }?.type ?: "Other device"
Log.d("AirPodsService", "other device has hijacked the connection, reasonReverseTapped: $reasonReverseTapped")
Log.d(TAG, "other device has hijacked the connection, reasonReverseTapped: $reasonReverseTapped")
aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION.value,
byteArrayOf(0x00)
)
otherDeviceTookOver = true
disconnectAudio(
this@AirPodsService,
device
)
if (reasonReverseTapped) {
Log.d("AirPodsService", "reverse tapped, disconnecting audio")
Log.d(TAG, "reverse tapped, disconnecting audio")
disconnectedBecauseReversed = true
disconnectAudio(this@AirPodsService, device)
showIsland(
@@ -960,7 +980,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
byteArrayOf(0x00)
)
// this also means that the other device has start playing the audio, and if that's true, we can again start listening for audio config changes
Log.d("AirPodsService", "Another device started playing audio, listening for audio config changes again")
Log.d(TAG, "Another device started playing audio, listening for audio config changes again")
MediaController.pausedForOtherDevice = false
}
}
@@ -977,7 +997,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
for (device in newDevices) {
Log.d("AirPodsParser", "New connected device: ${device.mac}, info1: ${device.info1}, info2: ${device.info2})")
Log.d("AirPodsService", "Sending new Tipi packet for device ${device.mac}, and sending media info to the device")
Log.d(TAG, "Sending new Tipi packet for device ${device.mac}, and sending media info to the device")
aacpManager.sendMediaInformationNewDevice(selfMacAddress = localMac, targetMacAddress = device.mac)
aacpManager.sendAddTiPiDevice(selfMacAddress = localMac, targetMacAddress = device.mac)
}
@@ -1300,7 +1320,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var popupShown = false
fun showPopup(service: Service, name: String) {
if (!Settings.canDrawOverlays(service)) {
Log.d("AirPodsService", "No permission for SYSTEM_ALERT_WINDOW")
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
return
}
if (popupShown) {
@@ -1315,9 +1335,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var islandWindow: IslandWindow? = null
@SuppressLint("MissingPermission")
fun showIsland(service: Service, batteryPercentage: Int, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false, otherDeviceName: String? = null) {
Log.d("AirPodsService", "Showing island window")
Log.d(TAG, "Showing island window")
if (!Settings.canDrawOverlays(service)) {
Log.d("AirPodsService", "No permission for SYSTEM_ALERT_WINDOW")
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
return
}
CoroutineScope(Dispatchers.Main).launch {
@@ -1742,7 +1762,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
notificationManager.notify(1, updatedNotification)
notificationManager.cancel(2)
} else if (!config.bleOnlyMode && !socket.isConnected && isConnectedLocally) {
Log.d("AirPodsService", "<LogCollector:Complete:Failed> Socket not connected")
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
}
}
@@ -1916,7 +1936,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
sendBroadcastAsUser(intent, UserHandle.getUserHandleForUid(-1))
}
} catch (e: Exception) {
Log.e("AirPodsService", "Failed to send vendor-specific event: ${e.message}")
Log.e(TAG, "Failed to send vendor-specific event: ${e.message}")
}
// Broadcast battery level changes
@@ -1932,7 +1952,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
sendBroadcastAsUser(batteryIntent, UserHandle.getUserHandleForUid(-1))
}
} catch (e: Exception) {
Log.e("AirPodsService", "Failed to send battery level broadcast: ${e.message}")
Log.e(TAG, "Failed to send battery level broadcast: ${e.message}")
}
// Update Android Settings Intelligence's battery widget
@@ -1944,10 +1964,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
try {
sendBroadcastAsUser(statusIntent, UserHandle.getUserHandleForUid(-1))
} catch (e: Exception) {
Log.e("AirPodsService", "Failed to send ASI battery level broadcast: ${e.message}")
Log.e(TAG, "Failed to send ASI battery level broadcast: ${e.message}")
}
Log.d("AirPodsService", "Broadcast battery level $batteryUnified% to system")
Log.d(TAG, "Broadcast battery level $batteryUnified% to system")
}
private fun setMetadatas(d: BluetoothDevice) {
@@ -2007,7 +2027,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
device.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
"20".toByteArray()
)
Log.d("AirPodsService", "Metadata set: $metadataSet")
Log.d(TAG, "Metadata set: $metadataSet")
}
}
@@ -2029,9 +2049,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val name = context?.getSharedPreferences("settings", MODE_PRIVATE)
?.getString("name", bluetoothDevice?.name)
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
Log.d("AirPodsService", "Received bluetooth connection broadcast")
Log.d(TAG, "Received bluetooth connection broadcast")
if (ServiceManager.getService()?.isConnectedLocally == true) {
Log.d("AirPodsService", "Checking if audio should be connected")
ServiceManager.getService()?.manuallyCheckForAudioSource()
return
}
@@ -2057,11 +2076,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
@SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("AirPodsService", "Service started with intent action: ${intent?.action}")
Log.d(TAG, "Service started with intent action: ${intent?.action}")
if (intent?.action == "me.kavishdevar.librepods.RECONNECT_AFTER_REVERSE") {
Log.d("AirPodsService", "reconnect after reversed received, taking over")
Log.d(TAG, "reconnect after reversed received, taking over")
disconnectedBecauseReversed = false
otherDeviceTookOver = false
takeOver("music", manualTakeOverAfterReversed = true)
}
@@ -2072,9 +2092,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
fun manuallyCheckForAudioSource() {
val shouldResume = MediaController.getMusicActive()
if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed) {
if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed || otherDeviceTookOver) {
Log.d(
"AirPodsService",
TAG,
"For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!"
)
disconnectAudio(this, device, shouldResume = shouldResume)
@@ -2095,22 +2115,26 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
aacpManager.sendHijackReversed(
localMac
)
connectAudio(
this@AirPodsService,
device
)
otherDeviceTookOver = false
}
Log.d("AirPodsService", "owns connection: ${aacpManager.getControlCommandStatus(AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION)?.value?.get(0)?.toInt()}")
Log.d(TAG, "owns connection: ${aacpManager.getControlCommandStatus(AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION)?.value?.get(0)?.toInt()}")
if (isConnectedLocally) {
if (aacpManager.getControlCommandStatus(AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION)?.value[0]?.toInt() != 1 || (aacpManager.audioSource?.mac != localMac && aacpManager.audioSource?.type != AACPManager.Companion.AudioSourceType.NONE)) {
if (disconnectedBecauseReversed) {
if (manualTakeOverAfterReversed) {
Log.d("AirPodsService", "forcefully taking over despite reverse as user requested")
connectAudio(this, device)
Log.d(TAG, "forcefully taking over despite reverse as user requested")
disconnectedBecauseReversed = false
} else {
Log.d("AirPodsService", "connected locally, but can not hijack as other device had reversed")
Log.d(TAG, "connected locally, but can not hijack as other device had reversed")
return
}
}
Log.d("AirPodsService", "already connected locally, hijacking connection by asking AirPods")
Log.d(TAG, "already connected locally, hijacking connection by asking AirPods")
aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION.value,
1
@@ -2124,34 +2148,42 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
aacpManager.sendHijackRequest(
localMac
)
otherDeviceTookOver = false
connectAudio(this, device)
showIsland(this, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!),
IslandType.CONNECTED)
CoroutineScope(Dispatchers.IO).launch {
delay(500)
delay(500) // a2dp takes time, and so does taking control + AirPods pause it for no reason after connecting
if (takingOverFor == "music") {
MediaController.sendPlay()
Log.d(TAG, "Resuming music after taking control")
MediaController.sendPlay(replayWhenPaused = true)
} else if (startHeadTrackingAgain) {
Log.d("AirPodsService", "Starting head tracking again after taking control")
Log.d(TAG, "Starting head tracking again after taking control")
Handler(Looper.getMainLooper()).postDelayed({
startHeadTracking()
}, 500)
}
delay(1000) // should ideally have a callback when it's taken over because for some reason android doesn't dispatch when it's paused
if (takingOverFor == "music") {
Log.d(TAG, "resuming again just in case")
MediaController.sendPlay(force = true)
}
}
} else {
Log.d("AirPodsService", "Already connected locally and already own connection, skipping takeover")
Log.d(TAG, "Already connected locally and already own connection, skipping takeover")
}
return
}
if (CrossDevice.isAvailable) {
Log.d("AirPodsService", "CrossDevice is available, continuing")
Log.d(TAG, "CrossDevice is available, continuing")
}
else if (bleManager.getMostRecentStatus()?.isLeftInEar == true || bleManager.getMostRecentStatus()?.isRightInEar == true) {
Log.d("AirPodsService", "At least one AirPod is in ear, continuing")
Log.d(TAG, "At least one AirPod is in ear, continuing")
}
else {
Log.d("AirPodsService", "CrossDevice not available and AirPods not in ear, skipping")
Log.d(TAG, "CrossDevice not available and AirPods not in ear, skipping")
return
}
@@ -2162,7 +2194,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
if (!shouldTakeOverPState) {
Log.d("AirPodsService", "Not taking over audio, phone state takeover disabled")
Log.d(TAG, "Not taking over audio, phone state takeover disabled")
return
}
@@ -2177,21 +2209,21 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
if (!shouldTakeOver) {
Log.d("AirPodsService", "Not taking over audio, airpods state takeover disabled")
Log.d(TAG, "Not taking over audio, airpods state takeover disabled")
return
}
if (takingOverFor == "music") {
Log.d("AirPodsService", "Pausing music so that it doesn't play through speakers")
Log.d(TAG, "Pausing music so that it doesn't play through speakers")
MediaController.pausedWhileTakingOver = true
MediaController.sendPause(true)
} else {
handleIncomingCallOnceConnected = true
}
Log.d("AirPodsService", "Taking over audio")
Log.d(TAG, "Taking over audio")
CrossDevice.sendRemotePacket(CrossDevicePackets.REQUEST_DISCONNECT.packet)
Log.d("AirPodsService", macAddress)
Log.d(TAG, macAddress)
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false) }
device = getSystemService(BluetoothManager::class.java).adapter.bondedDevices.find {
@@ -2201,7 +2233,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (device != null) {
if (config.bleOnlyMode) {
// In BLE-only mode, just show connecting status without actual L2CAP connection
Log.d("AirPodsService", "BLE-only mode: showing connecting status without L2CAP connection")
Log.d(TAG, "BLE-only mode: showing connecting status without L2CAP connection")
updateNotificationContent(
true,
config.deviceName,
@@ -2231,11 +2263,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
)
val constructors = BluetoothSocket::class.java.declaredConstructors
Log.d("AirPodsService", "BluetoothSocket has ${constructors.size} constructors:")
Log.d(TAG, "BluetoothSocket has ${constructors.size} constructors:")
constructors.forEachIndexed { index, constructor ->
val params = constructor.parameterTypes.joinToString(", ") { it.simpleName }
Log.d("AirPodsService", "Constructor $index: ($params)")
Log.d(TAG, "Constructor $index: ($params)")
}
var lastException: Exception? = null
@@ -2243,31 +2275,31 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
for ((index, params) in constructorSpecs.withIndex()) {
try {
Log.d("AirPodsService", "Trying constructor signature #${index + 1}")
Log.d(TAG, "Trying constructor signature #${index + 1}")
attemptedConstructors++
return HiddenApiBypass.newInstance(BluetoothSocket::class.java, *params) as BluetoothSocket
} catch (e: Exception) {
Log.e("AirPodsService", "Constructor signature #${index + 1} failed: ${e.message}")
Log.e(TAG, "Constructor signature #${index + 1} failed: ${e.message}")
lastException = e
}
}
val errorMessage = "Failed to create BluetoothSocket after trying $attemptedConstructors constructor signatures"
Log.e("AirPodsService", errorMessage)
Log.e(TAG, errorMessage)
showSocketConnectionFailureNotification(errorMessage)
throw lastException ?: IllegalStateException(errorMessage)
}
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
fun connectToSocket(device: BluetoothDevice) {
Log.d("AirPodsService", "<LogCollector:Start> Connecting to socket")
Log.d(TAG, "<LogCollector:Start> Connecting to socket")
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
if (!isConnectedLocally && !CrossDevice.isAvailable) {
socket = try {
createBluetoothSocket(device, uuid)
} catch (e: Exception) {
Log.e("AirPodsService", "Failed to create BluetoothSocket: ${e.message}")
Log.e(TAG, "Failed to create BluetoothSocket: ${e.message}")
showSocketConnectionFailureNotification("Failed to create Bluetooth socket: ${e.message}")
return
}
@@ -2290,24 +2322,24 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
config.deviceName,
batteryNotification.getBattery()
)
Log.d("AirPodsService", "<LogCollector:Complete:Success> Socket connected")
Log.d(TAG, "<LogCollector:Complete:Success> Socket connected")
} catch (e: Exception) {
Log.d("AirPodsService", "<LogCollector:Complete:Failed> Socket not connected")
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
throw e
}
}
if (!socket.isConnected) {
Log.d("AirPodsService", "<LogCollector:Complete:Failed> Socket not connected")
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
}
}
this@AirPodsService.device = device
socket.let { it ->
socket.let {
aacpManager.sendPacket(aacpManager.createHandshakePacket())
aacpManager.sendSetFeatureFlagsPacket()
aacpManager.sendNotificationRequest()
Log.d("AirPodsService", "Requesting proximity keys")
Log.d(TAG, "Requesting proximity keys")
aacpManager.sendRequestProximityKeys((AACPManager.Companion.ProximityKeyType.IRK.value + AACPManager.Companion.ProximityKeyType.ENC_KEY.value).toByte())
CoroutineScope(Dispatchers.IO).launch {
aacpManager.sendPacket(aacpManager.createHandshakePacket())
@@ -2373,12 +2405,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
isConnectedLocally = false
socket.close()
aacpManager.disconnected()
updateNotificationContent(false)
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.d("AirPodsService", "Failed to connect to socket: ${e.message}")
Log.d(TAG, "Failed to connect to socket: ${e.message}")
showSocketConnectionFailureNotification("Failed to establish connection: ${e.message}")
isConnectedLocally = false
this@AirPodsService.device = device
@@ -2391,7 +2424,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (!this::socket.isInitialized) return
socket.close()
MediaController.pausedWhileTakingOver = false
Log.d("AirPodsService", "Disconnected from AirPods, showing island.")
Log.d(TAG, "Disconnected from AirPods, showing island.")
showIsland(this, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!),
IslandType.MOVED_TO_REMOTE)
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
@@ -2447,7 +2480,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (profile == BluetoothProfile.A2DP) {
try {
if (proxy.getConnectionState(device) == BluetoothProfile.STATE_DISCONNECTED) {
Log.d("AirPodsService", "Already disconnected from A2DP")
Log.d(TAG, "Already disconnected from A2DP")
return
}
val method =
@@ -2540,13 +2573,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
updateNotificationContent(true, name, batteryNotification.getBattery())
Log.d("AirPodsService", "setName: $name")
Log.d(TAG, "setName: $name")
}
@SuppressLint("MissingPermission")
override fun onDestroy() {
clearPacketLogs()
Log.d("AirPodsService", "Service stopped is being destroyed for some reason!")
Log.d(TAG, "Service stopped is being destroyed for some reason!")
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
@@ -2588,9 +2621,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val useAlternatePackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && aacpManager.getControlCommandStatus(AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION)?.value?.get(0)?.toInt() != 1) {
takeOver("call", startHeadTrackingAgain = true)
Log.d("AirPodsService", "Taking over for head tracking")
Log.d(TAG, "Taking over for head tracking")
} else {
Log.w("AirPodsService", "Will not be taking over for head tracking, might not work.")
Log.w(TAG, "Will not be taking over for head tracking, might not work.")
}
if (useAlternatePackets) {
aacpManager.sendDataPacket(aacpManager.createAlternateStartHeadTrackingPacket())

View File

@@ -69,6 +69,7 @@ class BLEManager(private val context: Context) {
fun onLidStateChanged(lidOpen: Boolean)
fun onEarStateChanged(device: AirPodsStatus, leftInEar: Boolean, rightInEar: Boolean)
fun onBatteryChanged(device: AirPodsStatus)
fun onDeviceDisappeared()
}
private var mBluetoothLeScanner: BluetoothLeScanner? = null
@@ -335,7 +336,7 @@ class BLEManager(private val context: Context) {
val model = modelNames[modelId] ?: "Unknown ($modelId)"
val status = data[5].toInt() and 0xFF
val flagsCase = data[7].toInt() and 0xFF
// val flagsCase = data[7].toInt() and 0xFF
val lid = data[8].toInt() and 0xFF
val color = colorNames[data[9].toInt()] ?: "Unknown"
val conn = connStates[data[10].toInt()] ?: "Unknown (${data[10].toInt()})"
@@ -396,6 +397,10 @@ class BLEManager(private val context: Context) {
deviceStatusMap.remove(device.key)
Log.d(TAG, "Removed stale device from tracking: ${device.key}")
}
if (deviceStatusMap.isEmpty()) {
airPodsStatusListener?.onDeviceDisappeared()
}
}
private fun checkLidStateTimeout() {
@@ -483,8 +488,8 @@ class BLEManager(private val context: Context) {
companion object {
private const val TAG = "AirPodsBLE"
private const val CLEANUP_INTERVAL_MS = 30000L
private const val STALE_DEVICE_TIMEOUT_MS = 60000L
private const val LID_CLOSE_TIMEOUT_MS = 2000L
private const val CLEANUP_INTERVAL_MS = 10000L
private const val STALE_DEVICE_TIMEOUT_MS = 15000L
private const val LID_CLOSE_TIMEOUT_MS = 2500L
}
}

View File

@@ -653,7 +653,7 @@ class IslandWindow(private val context: Context) {
try {
context.unregisterReceiver(batteryReceiver)
} catch (e: Exception) {
e.printStackTrace()
// e.printStackTrace()
}
ServiceManager.getService()?.islandOpen = false

View File

@@ -63,6 +63,9 @@ object MediaController {
var recentlyLostOwnership: Boolean = false
private var lastPlayWithReplay: Boolean = false
private var lastPlayTime: Long = 0L
fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) {
if (this::audioManager.isInitialized) {
return
@@ -101,6 +104,14 @@ object MediaController {
val isActive = audioManager.isMusicActive
Log.d("MediaController", "Playback config changed, iPausedTheMedia: $iPausedTheMedia, isActive: $isActive, pausedForOtherDevice: $pausedForOtherDevice, lastKnownIsMusicActive: $lastKnownIsMusicActive")
if (!isActive && lastPlayWithReplay && now - lastPlayTime < 2500L) {
Log.d("MediaController", "Music paused shortly after play with replay; retrying play")
lastPlayWithReplay = false
sendPlay()
lastKnownIsMusicActive = true
return
}
if (now - lastPlaybackCallbackAt < PLAYBACK_DEBOUNCE_MS) {
Log.d("MediaController", "Ignoring playback callback due to debounce (${now - lastPlaybackCallbackAt}ms)")
lastPlaybackCallbackAt = now
@@ -114,20 +125,42 @@ object MediaController {
return
}
Log.d("MediaController", "Configs received: ${configs?.size ?: 0} configurations")
val currentActiveContentTypes = configs?.flatMap { config ->
Log.d("MediaController", "Processing config: ${config}, audioAttributes: ${config.audioAttributes}")
config.audioAttributes?.let { attrs ->
val contentType = attrs.contentType
Log.d("MediaController", "Config content type: $contentType")
listOf(contentType)
} ?: run {
Log.d("MediaController", "Config has no audioAttributes")
emptyList()
}
}?.toSet() ?: emptySet()
Log.d("MediaController", "Current active content types: $currentActiveContentTypes")
val hasNewMusicOrMovie = currentActiveContentTypes.any { contentType ->
contentType == android.media.AudioAttributes.CONTENT_TYPE_MUSIC ||
contentType == android.media.AudioAttributes.CONTENT_TYPE_MOVIE
}
Log.d("MediaController", "Has new music or movie: $hasNewMusicOrMovie")
if (pausedForOtherDevice) {
handler.removeCallbacks(clearPausedForOtherDeviceRunnable)
handler.postDelayed(clearPausedForOtherDeviceRunnable, PAUSED_FOR_OTHER_DEVICE_CLEAR_MS)
if (isActive) {
Log.d("MediaController", "Detected play while pausedForOtherDevice; attempting to take over")
if (!recentlyLostOwnership) {
if (!recentlyLostOwnership && hasNewMusicOrMovie) {
pausedForOtherDevice = false
userPlayedTheMedia = true
if (!pausedWhileTakingOver) {
ServiceManager.getService()?.takeOver("music")
}
} else {
Log.d("MediaController", "Skipping take-over due to recent ownership loss")
Log.d("MediaController", "Skipping take-over due to recent ownership loss or no new music/movie")
}
} else {
Log.d("MediaController", "Still not active while pausedForOtherDevice; will clear state after timeout")
@@ -152,10 +185,10 @@ object MediaController {
}
Log.d("MediaController", "pausedWhileTakingOver: $pausedWhileTakingOver")
if (!pausedWhileTakingOver && isActive) {
if (!pausedWhileTakingOver && isActive && hasNewMusicOrMovie) {
if (lastKnownIsMusicActive != true) {
if (!recentlyLostOwnership) {
Log.d("MediaController", "Music is active and not pausedWhileTakingOver; requesting takeOver")
Log.d("MediaController", "Music/movie is active and not pausedWhileTakingOver; requesting takeOver")
ServiceManager.getService()?.takeOver("music")
} else {
Log.d("MediaController", "Skipping take-over due to recent ownership loss")
@@ -242,9 +275,13 @@ object MediaController {
}
@Synchronized
fun sendPlay() {
Log.d("MediaController", "Sending play with iPausedTheMedia: $iPausedTheMedia")
if (iPausedTheMedia) {
fun sendPlay(replayWhenPaused: Boolean = false, force: Boolean = false) {
Log.d("MediaController", "Sending play with iPausedTheMedia: $iPausedTheMedia, replayWhenPaused: $replayWhenPaused, force: $force")
if (replayWhenPaused) {
lastPlayWithReplay = true
lastPlayTime = SystemClock.uptimeMillis()
}
if (iPausedTheMedia || force) { // very creative, ik. thanks.
Log.d("MediaController", "Sending play and setting userPlayedTheMedia to false")
userPlayedTheMedia = false
audioManager.dispatchMediaKeyEvent(