mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-07-21 04:56:45 +02:00
android: ui tweaks
This commit is contained in:
@@ -110,7 +110,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
|||||||
if (preview) {
|
if (preview) {
|
||||||
batteryStatus.value = listOf(
|
batteryStatus.value = listOf(
|
||||||
Battery(BatteryComponent.LEFT, 100, BatteryStatus.NOT_CHARGING),
|
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)
|
Battery(BatteryComponent.CASE, 40, BatteryStatus.CHARGING)
|
||||||
)
|
)
|
||||||
previousBatteryStatus.value = batteryStatus.value
|
previousBatteryStatus.value = batteryStatus.value
|
||||||
@@ -177,7 +177,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
|||||||
}
|
}
|
||||||
if (leftLevel > 0 && rightLevel > 0)
|
if (leftLevel > 0 && rightLevel > 0)
|
||||||
{
|
{
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
}
|
}
|
||||||
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED)
|
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ import java.nio.ByteOrder
|
|||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
const val TAG = "AirPodsService"
|
||||||
|
|
||||||
object ServiceManager {
|
object ServiceManager {
|
||||||
@ExperimentalEncodingApi
|
@ExperimentalEncodingApi
|
||||||
private var service: AirPodsService? = null
|
private var service: AirPodsService? = null
|
||||||
@@ -152,6 +154,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
var attManager: ATTManager? = null
|
var attManager: ATTManager? = null
|
||||||
var cameraActive = false
|
var cameraActive = false
|
||||||
private var disconnectedBecauseReversed = false
|
private var disconnectedBecauseReversed = false
|
||||||
|
private var otherDeviceTookOver = false
|
||||||
data class ServiceConfig(
|
data class ServiceConfig(
|
||||||
var deviceName: String = "AirPods",
|
var deviceName: String = "AirPods",
|
||||||
var earDetectionEnabled: Boolean = true,
|
var earDetectionEnabled: Boolean = true,
|
||||||
@@ -220,17 +223,17 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putString("mac_address", macAddress)
|
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) {
|
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 bluetoothManager = getSystemService(BluetoothManager::class.java)
|
||||||
val bluetoothDevice = bluetoothManager.adapter.getRemoteDevice(sharedPreferences.getString(
|
val bluetoothDevice = bluetoothManager.adapter.getRemoteDevice(sharedPreferences.getString(
|
||||||
"mac_address", "") ?: "")
|
"mac_address", "") ?: "")
|
||||||
connectToSocket(bluetoothDevice)
|
connectToSocket(bluetoothDevice)
|
||||||
}
|
}
|
||||||
Log.d("AirPodsBLEService", "Device status changed")
|
Log.d(TAG, "Device status changed")
|
||||||
if (isConnectedLocally) return
|
if (isConnectedLocally) 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
|
||||||
@@ -251,14 +254,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBroadcastFromNewAddress(device: BLEManager.AirPodsStatus) {
|
override fun onBroadcastFromNewAddress(device: BLEManager.AirPodsStatus) {
|
||||||
Log.d("AirPodsService", "New address detected")
|
Log.d(TAG, "New address detected")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLidStateChanged(
|
override fun onLidStateChanged(
|
||||||
lidOpen: Boolean,
|
lidOpen: Boolean,
|
||||||
) {
|
) {
|
||||||
if (lidOpen) {
|
if (lidOpen) {
|
||||||
Log.d("AirPodsBLEService", "Lid opened")
|
Log.d(TAG, "Lid opened")
|
||||||
showPopup(
|
showPopup(
|
||||||
this@AirPodsService,
|
this@AirPodsService,
|
||||||
getSharedPreferences("settings", MODE_PRIVATE).getString("name", "AirPods Pro") ?: "AirPods"
|
getSharedPreferences("settings", MODE_PRIVATE).getString("name", "AirPods Pro") ?: "AirPods"
|
||||||
@@ -281,7 +284,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
)
|
)
|
||||||
sendBatteryBroadcast()
|
sendBatteryBroadcast()
|
||||||
} else {
|
} else {
|
||||||
Log.d("AirPodsBLEService", "Lid closed")
|
Log.d(TAG, "Lid closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,11 +293,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
leftInEar: Boolean,
|
leftInEar: Boolean,
|
||||||
rightInEar: 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
|
// In BLE-only mode, ear detection is purely based on BLE data
|
||||||
if (config.bleOnlyMode) {
|
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
|
caseCharging = caseCharging == true
|
||||||
)
|
)
|
||||||
updateBattery()
|
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
|
gestureDetector = null
|
||||||
config.headGestures = false
|
config.headGestures = false
|
||||||
sharedPreferences.edit { putBoolean("head_gestures", 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)
|
bleManager = BLEManager(this)
|
||||||
@@ -504,7 +514,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value,
|
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value,
|
||||||
nextMode
|
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
|
MODE_PRIVATE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Log.d("AirPodsService", "Initializing CrossDevice")
|
Log.d(TAG, "Initializing CrossDevice")
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
CrossDevice.init(this@AirPodsService)
|
CrossDevice.init(this@AirPodsService)
|
||||||
Log.d("AirPodsService", "CrossDevice initialized")
|
Log.d(TAG, "CrossDevice initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
|
sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
@@ -612,11 +622,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
|
|
||||||
Log.d("AirPodsCrossDevice", CrossDevice.isAvailable.toString())
|
Log.d("AirPodsCrossDevice", CrossDevice.isAvailable.toString())
|
||||||
if (!CrossDevice.isAvailable) {
|
if (!CrossDevice.isAvailable) {
|
||||||
Log.d("AirPodsService", "${config.deviceName} connected")
|
Log.d(TAG, "${config.deviceName} connected")
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
connectToSocket(device!!)
|
connectToSocket(device!!)
|
||||||
}
|
}
|
||||||
Log.d("AirPodsService", "Setting metadata")
|
Log.d(TAG, "Setting metadata")
|
||||||
setMetadatas(device!!)
|
setMetadatas(device!!)
|
||||||
isConnectedLocally = true
|
isConnectedLocally = true
|
||||||
macAddress = device!!.address
|
macAddress = device!!.address
|
||||||
@@ -725,7 +735,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun cameraOpened() {
|
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(
|
val isCameraShutterUsed = listOf(
|
||||||
config.leftSinglePressAction,
|
config.leftSinglePressAction,
|
||||||
config.rightSinglePressAction,
|
config.rightSinglePressAction,
|
||||||
@@ -738,7 +748,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
).any { it == StemAction.CAMERA_SHUTTER }
|
).any { it == StemAction.CAMERA_SHUTTER }
|
||||||
|
|
||||||
if (isCameraShutterUsed) {
|
if (isCameraShutterUsed) {
|
||||||
Log.d("AirPodsService", "Camera opened, setting up stem actions")
|
Log.d(TAG, "Camera opened, setting up stem actions")
|
||||||
cameraActive = true
|
cameraActive = true
|
||||||
setupStemActions(isCameraActive = true)
|
setupStemActions(isCameraActive = true)
|
||||||
}
|
}
|
||||||
@@ -755,7 +765,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
default: StemAction?,
|
default: StemAction?,
|
||||||
isCameraActive: Boolean = false
|
isCameraActive: Boolean = false
|
||||||
): Boolean {
|
): 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)
|
return action != default && (action != StemAction.CAMERA_SHUTTER || isCameraActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,7 +783,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
isCustomAction(config.rightTriplePressAction, triplePressDefault, isCameraActive)
|
isCustomAction(config.rightTriplePressAction, triplePressDefault, isCameraActive)
|
||||||
val longPressCustomized = isCustomAction(config.leftLongPressAction, longPressDefault, isCameraActive) ||
|
val longPressCustomized = isCustomAction(config.leftLongPressAction, longPressDefault, isCameraActive) ||
|
||||||
isCustomAction(config.rightLongPressAction, 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, " +
|
"Single Press Customized: $singlePressCustomized, " +
|
||||||
"Double Press Customized: $doublePressCustomized, " +
|
"Double Press Customized: $doublePressCustomized, " +
|
||||||
"Triple Press Customized: $triplePressCustomized, " +
|
"Triple Press Customized: $triplePressCustomized, " +
|
||||||
@@ -867,9 +877,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
Handler(Looper.getMainLooper()).postDelayed({
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
MediaController.recentlyLostOwnership = false
|
MediaController.recentlyLostOwnership = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
Log.d("AirPodsService", "ownership lost")
|
Log.d(TAG, "ownership lost")
|
||||||
MediaController.sendPause()
|
MediaController.sendPause()
|
||||||
MediaController.pausedForOtherDevice = true
|
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".
|
// 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 :]
|
// (20 minutes later) i've done it nonetheless :]
|
||||||
val senderName = aacpManager.connectedDevices.find { it.mac == sender }?.type ?: "Other device"
|
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.sendControlCommand(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION.value,
|
AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION.value,
|
||||||
byteArrayOf(0x00)
|
byteArrayOf(0x00)
|
||||||
)
|
)
|
||||||
|
otherDeviceTookOver = true
|
||||||
|
disconnectAudio(
|
||||||
|
this@AirPodsService,
|
||||||
|
device
|
||||||
|
)
|
||||||
if (reasonReverseTapped) {
|
if (reasonReverseTapped) {
|
||||||
Log.d("AirPodsService", "reverse tapped, disconnecting audio")
|
Log.d(TAG, "reverse tapped, disconnecting audio")
|
||||||
disconnectedBecauseReversed = true
|
disconnectedBecauseReversed = true
|
||||||
disconnectAudio(this@AirPodsService, device)
|
disconnectAudio(this@AirPodsService, device)
|
||||||
showIsland(
|
showIsland(
|
||||||
@@ -960,7 +980,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
byteArrayOf(0x00)
|
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
|
// 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
|
MediaController.pausedForOtherDevice = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -977,7 +997,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
|
|
||||||
for (device in newDevices) {
|
for (device in newDevices) {
|
||||||
Log.d("AirPodsParser", "New connected device: ${device.mac}, info1: ${device.info1}, info2: ${device.info2})")
|
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.sendMediaInformationNewDevice(selfMacAddress = localMac, targetMacAddress = device.mac)
|
||||||
aacpManager.sendAddTiPiDevice(selfMacAddress = localMac, targetMacAddress = device.mac)
|
aacpManager.sendAddTiPiDevice(selfMacAddress = localMac, targetMacAddress = device.mac)
|
||||||
}
|
}
|
||||||
@@ -1300,7 +1320,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
var popupShown = false
|
var popupShown = false
|
||||||
fun showPopup(service: Service, name: String) {
|
fun showPopup(service: Service, name: String) {
|
||||||
if (!Settings.canDrawOverlays(service)) {
|
if (!Settings.canDrawOverlays(service)) {
|
||||||
Log.d("AirPodsService", "No permission for SYSTEM_ALERT_WINDOW")
|
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (popupShown) {
|
if (popupShown) {
|
||||||
@@ -1315,9 +1335,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
var islandWindow: IslandWindow? = null
|
var islandWindow: IslandWindow? = null
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun showIsland(service: Service, batteryPercentage: Int, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false, otherDeviceName: String? = null) {
|
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)) {
|
if (!Settings.canDrawOverlays(service)) {
|
||||||
Log.d("AirPodsService", "No permission for SYSTEM_ALERT_WINDOW")
|
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@@ -1742,7 +1762,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
notificationManager.notify(1, updatedNotification)
|
notificationManager.notify(1, updatedNotification)
|
||||||
notificationManager.cancel(2)
|
notificationManager.cancel(2)
|
||||||
} else if (!config.bleOnlyMode && !socket.isConnected && isConnectedLocally) {
|
} 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?")
|
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))
|
sendBroadcastAsUser(intent, UserHandle.getUserHandleForUid(-1))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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
|
// Broadcast battery level changes
|
||||||
@@ -1932,7 +1952,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
sendBroadcastAsUser(batteryIntent, UserHandle.getUserHandleForUid(-1))
|
sendBroadcastAsUser(batteryIntent, UserHandle.getUserHandleForUid(-1))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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
|
// Update Android Settings Intelligence's battery widget
|
||||||
@@ -1944,10 +1964,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
try {
|
try {
|
||||||
sendBroadcastAsUser(statusIntent, UserHandle.getUserHandleForUid(-1))
|
sendBroadcastAsUser(statusIntent, UserHandle.getUserHandleForUid(-1))
|
||||||
} catch (e: Exception) {
|
} 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) {
|
private fun setMetadatas(d: BluetoothDevice) {
|
||||||
@@ -2007,7 +2027,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
device.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
|
device.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
|
||||||
"20".toByteArray()
|
"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)
|
val name = context?.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
?.getString("name", bluetoothDevice?.name)
|
?.getString("name", bluetoothDevice?.name)
|
||||||
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
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) {
|
if (ServiceManager.getService()?.isConnectedLocally == true) {
|
||||||
Log.d("AirPodsService", "Checking if audio should be connected")
|
|
||||||
ServiceManager.getService()?.manuallyCheckForAudioSource()
|
ServiceManager.getService()?.manuallyCheckForAudioSource()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2057,11 +2076,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
|
|
||||||
@SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
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") {
|
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
|
disconnectedBecauseReversed = false
|
||||||
|
otherDeviceTookOver = false
|
||||||
takeOver("music", manualTakeOverAfterReversed = true)
|
takeOver("music", manualTakeOverAfterReversed = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2072,9 +2092,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
|
|
||||||
fun manuallyCheckForAudioSource() {
|
fun manuallyCheckForAudioSource() {
|
||||||
val shouldResume = MediaController.getMusicActive()
|
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(
|
Log.d(
|
||||||
"AirPodsService",
|
TAG,
|
||||||
"For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!"
|
"For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!"
|
||||||
)
|
)
|
||||||
disconnectAudio(this, device, shouldResume = shouldResume)
|
disconnectAudio(this, device, shouldResume = shouldResume)
|
||||||
@@ -2095,22 +2115,26 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
aacpManager.sendHijackReversed(
|
aacpManager.sendHijackReversed(
|
||||||
localMac
|
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 (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 (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 (disconnectedBecauseReversed) {
|
||||||
if (manualTakeOverAfterReversed) {
|
if (manualTakeOverAfterReversed) {
|
||||||
Log.d("AirPodsService", "forcefully taking over despite reverse as user requested")
|
Log.d(TAG, "forcefully taking over despite reverse as user requested")
|
||||||
connectAudio(this, device)
|
|
||||||
disconnectedBecauseReversed = false
|
disconnectedBecauseReversed = false
|
||||||
} else {
|
} 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
|
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.sendControlCommand(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION.value,
|
AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION.value,
|
||||||
1
|
1
|
||||||
@@ -2124,34 +2148,42 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
aacpManager.sendHijackRequest(
|
aacpManager.sendHijackRequest(
|
||||||
localMac
|
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!!),
|
showIsland(this, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!),
|
||||||
IslandType.CONNECTED)
|
IslandType.CONNECTED)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
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") {
|
if (takingOverFor == "music") {
|
||||||
MediaController.sendPlay()
|
Log.d(TAG, "Resuming music after taking control")
|
||||||
|
MediaController.sendPlay(replayWhenPaused = true)
|
||||||
} else if (startHeadTrackingAgain) {
|
} 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({
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
startHeadTracking()
|
startHeadTracking()
|
||||||
}, 500)
|
}, 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 {
|
} 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CrossDevice.isAvailable) {
|
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) {
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2162,7 +2194,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldTakeOverPState) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2177,21 +2209,21 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldTakeOver) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (takingOverFor == "music") {
|
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.pausedWhileTakingOver = true
|
||||||
MediaController.sendPause(true)
|
MediaController.sendPause(true)
|
||||||
} else {
|
} else {
|
||||||
handleIncomingCallOnceConnected = true
|
handleIncomingCallOnceConnected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("AirPodsService", "Taking over audio")
|
Log.d(TAG, "Taking over audio")
|
||||||
CrossDevice.sendRemotePacket(CrossDevicePackets.REQUEST_DISCONNECT.packet)
|
CrossDevice.sendRemotePacket(CrossDevicePackets.REQUEST_DISCONNECT.packet)
|
||||||
Log.d("AirPodsService", macAddress)
|
Log.d(TAG, macAddress)
|
||||||
|
|
||||||
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false) }
|
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false) }
|
||||||
device = getSystemService(BluetoothManager::class.java).adapter.bondedDevices.find {
|
device = getSystemService(BluetoothManager::class.java).adapter.bondedDevices.find {
|
||||||
@@ -2201,7 +2233,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
if (device != null) {
|
if (device != null) {
|
||||||
if (config.bleOnlyMode) {
|
if (config.bleOnlyMode) {
|
||||||
// In BLE-only mode, just show connecting status without actual L2CAP connection
|
// 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(
|
updateNotificationContent(
|
||||||
true,
|
true,
|
||||||
config.deviceName,
|
config.deviceName,
|
||||||
@@ -2231,11 +2263,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
)
|
)
|
||||||
|
|
||||||
val constructors = BluetoothSocket::class.java.declaredConstructors
|
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 ->
|
constructors.forEachIndexed { index, constructor ->
|
||||||
val params = constructor.parameterTypes.joinToString(", ") { it.simpleName }
|
val params = constructor.parameterTypes.joinToString(", ") { it.simpleName }
|
||||||
Log.d("AirPodsService", "Constructor $index: ($params)")
|
Log.d(TAG, "Constructor $index: ($params)")
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastException: Exception? = null
|
var lastException: Exception? = null
|
||||||
@@ -2243,31 +2275,31 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
|
|
||||||
for ((index, params) in constructorSpecs.withIndex()) {
|
for ((index, params) in constructorSpecs.withIndex()) {
|
||||||
try {
|
try {
|
||||||
Log.d("AirPodsService", "Trying constructor signature #${index + 1}")
|
Log.d(TAG, "Trying constructor signature #${index + 1}")
|
||||||
attemptedConstructors++
|
attemptedConstructors++
|
||||||
return HiddenApiBypass.newInstance(BluetoothSocket::class.java, *params) as BluetoothSocket
|
return HiddenApiBypass.newInstance(BluetoothSocket::class.java, *params) as BluetoothSocket
|
||||||
} catch (e: Exception) {
|
} 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
|
lastException = e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val errorMessage = "Failed to create BluetoothSocket after trying $attemptedConstructors constructor signatures"
|
val errorMessage = "Failed to create BluetoothSocket after trying $attemptedConstructors constructor signatures"
|
||||||
Log.e("AirPodsService", errorMessage)
|
Log.e(TAG, errorMessage)
|
||||||
showSocketConnectionFailureNotification(errorMessage)
|
showSocketConnectionFailureNotification(errorMessage)
|
||||||
throw lastException ?: IllegalStateException(errorMessage)
|
throw lastException ?: IllegalStateException(errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||||
fun connectToSocket(device: BluetoothDevice) {
|
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;")
|
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||||
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
if (!isConnectedLocally && !CrossDevice.isAvailable) {
|
if (!isConnectedLocally && !CrossDevice.isAvailable) {
|
||||||
socket = try {
|
socket = try {
|
||||||
createBluetoothSocket(device, uuid)
|
createBluetoothSocket(device, uuid)
|
||||||
} catch (e: Exception) {
|
} 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}")
|
showSocketConnectionFailureNotification("Failed to create Bluetooth socket: ${e.message}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2290,24 +2322,24 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
config.deviceName,
|
config.deviceName,
|
||||||
batteryNotification.getBattery()
|
batteryNotification.getBattery()
|
||||||
)
|
)
|
||||||
Log.d("AirPodsService", "<LogCollector:Complete:Success> Socket connected")
|
Log.d(TAG, "<LogCollector:Complete:Success> Socket connected")
|
||||||
} catch (e: Exception) {
|
} 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?")
|
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!socket.isConnected) {
|
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?")
|
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this@AirPodsService.device = device
|
this@AirPodsService.device = device
|
||||||
socket.let { it ->
|
socket.let {
|
||||||
aacpManager.sendPacket(aacpManager.createHandshakePacket())
|
aacpManager.sendPacket(aacpManager.createHandshakePacket())
|
||||||
aacpManager.sendSetFeatureFlagsPacket()
|
aacpManager.sendSetFeatureFlagsPacket()
|
||||||
aacpManager.sendNotificationRequest()
|
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())
|
aacpManager.sendRequestProximityKeys((AACPManager.Companion.ProximityKeyType.IRK.value + AACPManager.Companion.ProximityKeyType.ENC_KEY.value).toByte())
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
aacpManager.sendPacket(aacpManager.createHandshakePacket())
|
aacpManager.sendPacket(aacpManager.createHandshakePacket())
|
||||||
@@ -2373,12 +2405,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
isConnectedLocally = false
|
isConnectedLocally = false
|
||||||
socket.close()
|
socket.close()
|
||||||
aacpManager.disconnected()
|
aacpManager.disconnected()
|
||||||
|
updateNotificationContent(false)
|
||||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
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}")
|
showSocketConnectionFailureNotification("Failed to establish connection: ${e.message}")
|
||||||
isConnectedLocally = false
|
isConnectedLocally = false
|
||||||
this@AirPodsService.device = device
|
this@AirPodsService.device = device
|
||||||
@@ -2391,7 +2424,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
if (!this::socket.isInitialized) return
|
if (!this::socket.isInitialized) return
|
||||||
socket.close()
|
socket.close()
|
||||||
MediaController.pausedWhileTakingOver = false
|
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!!),
|
showIsland(this, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!),
|
||||||
IslandType.MOVED_TO_REMOTE)
|
IslandType.MOVED_TO_REMOTE)
|
||||||
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
|
||||||
@@ -2447,7 +2480,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
if (profile == BluetoothProfile.A2DP) {
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
try {
|
try {
|
||||||
if (proxy.getConnectionState(device) == BluetoothProfile.STATE_DISCONNECTED) {
|
if (proxy.getConnectionState(device) == BluetoothProfile.STATE_DISCONNECTED) {
|
||||||
Log.d("AirPodsService", "Already disconnected from A2DP")
|
Log.d(TAG, "Already disconnected from A2DP")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val method =
|
val method =
|
||||||
@@ -2540,13 +2573,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateNotificationContent(true, name, batteryNotification.getBattery())
|
updateNotificationContent(true, name, batteryNotification.getBattery())
|
||||||
Log.d("AirPodsService", "setName: $name")
|
Log.d(TAG, "setName: $name")
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
clearPacketLogs()
|
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)
|
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
|
|
||||||
@@ -2588,9 +2621,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
val useAlternatePackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", false)
|
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) {
|
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)
|
takeOver("call", startHeadTrackingAgain = true)
|
||||||
Log.d("AirPodsService", "Taking over for head tracking")
|
Log.d(TAG, "Taking over for head tracking")
|
||||||
} else {
|
} 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) {
|
if (useAlternatePackets) {
|
||||||
aacpManager.sendDataPacket(aacpManager.createAlternateStartHeadTrackingPacket())
|
aacpManager.sendDataPacket(aacpManager.createAlternateStartHeadTrackingPacket())
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class BLEManager(private val context: Context) {
|
|||||||
fun onLidStateChanged(lidOpen: Boolean)
|
fun onLidStateChanged(lidOpen: Boolean)
|
||||||
fun onEarStateChanged(device: AirPodsStatus, leftInEar: Boolean, rightInEar: Boolean)
|
fun onEarStateChanged(device: AirPodsStatus, leftInEar: Boolean, rightInEar: Boolean)
|
||||||
fun onBatteryChanged(device: AirPodsStatus)
|
fun onBatteryChanged(device: AirPodsStatus)
|
||||||
|
fun onDeviceDisappeared()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mBluetoothLeScanner: BluetoothLeScanner? = null
|
private var mBluetoothLeScanner: BluetoothLeScanner? = null
|
||||||
@@ -335,7 +336,7 @@ class BLEManager(private val context: Context) {
|
|||||||
val model = modelNames[modelId] ?: "Unknown ($modelId)"
|
val model = modelNames[modelId] ?: "Unknown ($modelId)"
|
||||||
|
|
||||||
val status = data[5].toInt() and 0xFF
|
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 lid = data[8].toInt() and 0xFF
|
||||||
val color = colorNames[data[9].toInt()] ?: "Unknown"
|
val color = colorNames[data[9].toInt()] ?: "Unknown"
|
||||||
val conn = connStates[data[10].toInt()] ?: "Unknown (${data[10].toInt()})"
|
val conn = connStates[data[10].toInt()] ?: "Unknown (${data[10].toInt()})"
|
||||||
@@ -396,6 +397,10 @@ class BLEManager(private val context: Context) {
|
|||||||
deviceStatusMap.remove(device.key)
|
deviceStatusMap.remove(device.key)
|
||||||
Log.d(TAG, "Removed stale device from tracking: ${device.key}")
|
Log.d(TAG, "Removed stale device from tracking: ${device.key}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deviceStatusMap.isEmpty()) {
|
||||||
|
airPodsStatusListener?.onDeviceDisappeared()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkLidStateTimeout() {
|
private fun checkLidStateTimeout() {
|
||||||
@@ -483,8 +488,8 @@ class BLEManager(private val context: Context) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "AirPodsBLE"
|
private const val TAG = "AirPodsBLE"
|
||||||
private const val CLEANUP_INTERVAL_MS = 30000L
|
private const val CLEANUP_INTERVAL_MS = 10000L
|
||||||
private const val STALE_DEVICE_TIMEOUT_MS = 60000L
|
private const val STALE_DEVICE_TIMEOUT_MS = 15000L
|
||||||
private const val LID_CLOSE_TIMEOUT_MS = 2000L
|
private const val LID_CLOSE_TIMEOUT_MS = 2500L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -653,7 +653,7 @@ class IslandWindow(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
context.unregisterReceiver(batteryReceiver)
|
context.unregisterReceiver(batteryReceiver)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
// e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceManager.getService()?.islandOpen = false
|
ServiceManager.getService()?.islandOpen = false
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ object MediaController {
|
|||||||
|
|
||||||
var recentlyLostOwnership: Boolean = false
|
var recentlyLostOwnership: Boolean = false
|
||||||
|
|
||||||
|
private var lastPlayWithReplay: Boolean = false
|
||||||
|
private var lastPlayTime: Long = 0L
|
||||||
|
|
||||||
fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) {
|
fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) {
|
||||||
if (this::audioManager.isInitialized) {
|
if (this::audioManager.isInitialized) {
|
||||||
return
|
return
|
||||||
@@ -101,6 +104,14 @@ object MediaController {
|
|||||||
val isActive = audioManager.isMusicActive
|
val isActive = audioManager.isMusicActive
|
||||||
Log.d("MediaController", "Playback config changed, iPausedTheMedia: $iPausedTheMedia, isActive: $isActive, pausedForOtherDevice: $pausedForOtherDevice, lastKnownIsMusicActive: $lastKnownIsMusicActive")
|
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) {
|
if (now - lastPlaybackCallbackAt < PLAYBACK_DEBOUNCE_MS) {
|
||||||
Log.d("MediaController", "Ignoring playback callback due to debounce (${now - lastPlaybackCallbackAt}ms)")
|
Log.d("MediaController", "Ignoring playback callback due to debounce (${now - lastPlaybackCallbackAt}ms)")
|
||||||
lastPlaybackCallbackAt = now
|
lastPlaybackCallbackAt = now
|
||||||
@@ -114,20 +125,42 @@ object MediaController {
|
|||||||
return
|
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) {
|
if (pausedForOtherDevice) {
|
||||||
handler.removeCallbacks(clearPausedForOtherDeviceRunnable)
|
handler.removeCallbacks(clearPausedForOtherDeviceRunnable)
|
||||||
handler.postDelayed(clearPausedForOtherDeviceRunnable, PAUSED_FOR_OTHER_DEVICE_CLEAR_MS)
|
handler.postDelayed(clearPausedForOtherDeviceRunnable, PAUSED_FOR_OTHER_DEVICE_CLEAR_MS)
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
Log.d("MediaController", "Detected play while pausedForOtherDevice; attempting to take over")
|
Log.d("MediaController", "Detected play while pausedForOtherDevice; attempting to take over")
|
||||||
if (!recentlyLostOwnership) {
|
if (!recentlyLostOwnership && hasNewMusicOrMovie) {
|
||||||
pausedForOtherDevice = false
|
pausedForOtherDevice = false
|
||||||
userPlayedTheMedia = true
|
userPlayedTheMedia = true
|
||||||
if (!pausedWhileTakingOver) {
|
if (!pausedWhileTakingOver) {
|
||||||
ServiceManager.getService()?.takeOver("music")
|
ServiceManager.getService()?.takeOver("music")
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
Log.d("MediaController", "Still not active while pausedForOtherDevice; will clear state after timeout")
|
Log.d("MediaController", "Still not active while pausedForOtherDevice; will clear state after timeout")
|
||||||
@@ -152,10 +185,10 @@ object MediaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.d("MediaController", "pausedWhileTakingOver: $pausedWhileTakingOver")
|
Log.d("MediaController", "pausedWhileTakingOver: $pausedWhileTakingOver")
|
||||||
if (!pausedWhileTakingOver && isActive) {
|
if (!pausedWhileTakingOver && isActive && hasNewMusicOrMovie) {
|
||||||
if (lastKnownIsMusicActive != true) {
|
if (lastKnownIsMusicActive != true) {
|
||||||
if (!recentlyLostOwnership) {
|
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")
|
ServiceManager.getService()?.takeOver("music")
|
||||||
} else {
|
} else {
|
||||||
Log.d("MediaController", "Skipping take-over due to recent ownership loss")
|
Log.d("MediaController", "Skipping take-over due to recent ownership loss")
|
||||||
@@ -242,9 +275,13 @@ object MediaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun sendPlay() {
|
fun sendPlay(replayWhenPaused: Boolean = false, force: Boolean = false) {
|
||||||
Log.d("MediaController", "Sending play with iPausedTheMedia: $iPausedTheMedia")
|
Log.d("MediaController", "Sending play with iPausedTheMedia: $iPausedTheMedia, replayWhenPaused: $replayWhenPaused, force: $force")
|
||||||
if (iPausedTheMedia) {
|
if (replayWhenPaused) {
|
||||||
|
lastPlayWithReplay = true
|
||||||
|
lastPlayTime = SystemClock.uptimeMillis()
|
||||||
|
}
|
||||||
|
if (iPausedTheMedia || force) { // very creative, ik. thanks.
|
||||||
Log.d("MediaController", "Sending play and setting userPlayedTheMedia to false")
|
Log.d("MediaController", "Sending play and setting userPlayedTheMedia to false")
|
||||||
userPlayedTheMedia = false
|
userPlayedTheMedia = false
|
||||||
audioManager.dispatchMediaKeyEvent(
|
audioManager.dispatchMediaKeyEvent(
|
||||||
|
|||||||
Reference in New Issue
Block a user