mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-29 06:10:52 +00:00
android: use device name sent by the connected device in island
This commit is contained in:
@@ -872,16 +872,21 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
|
||||
override fun onOwnershipChangeReceived(owns: Boolean) {
|
||||
if (!owns) {
|
||||
MediaController.recentlyLostOwnership = true
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
MediaController.recentlyLostOwnership = false
|
||||
}, 3000)
|
||||
Log.d("AirPodsService", "ownership lost")
|
||||
MediaController.sendPause()
|
||||
MediaController.pausedForOtherDevice = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOwnershipToFalseRequest(reasonReverseTapped: Boolean) {
|
||||
override fun onOwnershipToFalseRequest(sender: String, reasonReverseTapped: Boolean) {
|
||||
// TODO: Show a reverse button, but that's a lot of effort -- i'd have to change the UI too, which i hate doing, and handle other device's reverses too, and disconnect audio etc... so for now, just pause the audio and show the island without asking to reverse.
|
||||
// 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")
|
||||
aacpManager.sendControlCommand(
|
||||
AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION.value,
|
||||
@@ -895,7 +900,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
this@AirPodsService,
|
||||
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
|
||||
IslandType.MOVED_TO_OTHER_DEVICE,
|
||||
reversed = true
|
||||
reversed = true,
|
||||
otherDeviceName = senderName
|
||||
)
|
||||
}
|
||||
if (!aacpManager.owns) {
|
||||
@@ -903,19 +909,22 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
this@AirPodsService,
|
||||
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
|
||||
IslandType.MOVED_TO_OTHER_DEVICE,
|
||||
reversed = reasonReverseTapped
|
||||
reversed = reasonReverseTapped,
|
||||
otherDeviceName = senderName
|
||||
)
|
||||
}
|
||||
MediaController.sendPause()
|
||||
}
|
||||
|
||||
override fun onShowNearbyUI() {
|
||||
showIsland(
|
||||
this@AirPodsService,
|
||||
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
|
||||
IslandType.MOVED_TO_OTHER_DEVICE,
|
||||
reversed = false
|
||||
)
|
||||
override fun onShowNearbyUI(sender: String) {
|
||||
val senderName = aacpManager.connectedDevices.find { it.mac == sender }?.type ?: "Other device"
|
||||
showIsland(
|
||||
this@AirPodsService,
|
||||
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
|
||||
IslandType.MOVED_TO_OTHER_DEVICE,
|
||||
reversed = false,
|
||||
otherDeviceName = senderName
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDeviceMetadataReceived(deviceMetadata: ByteArray) {
|
||||
@@ -1316,7 +1325,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
var islandOpen = false
|
||||
var islandWindow: IslandWindow? = null
|
||||
@SuppressLint("MissingPermission")
|
||||
fun showIsland(service: Service, batteryPercentage: Int, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false) {
|
||||
fun showIsland(service: Service, batteryPercentage: Int, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false, otherDeviceName: String? = null) {
|
||||
Log.d("AirPodsService", "Showing island window")
|
||||
if (!Settings.canDrawOverlays(service)) {
|
||||
Log.d("AirPodsService", "No permission for SYSTEM_ALERT_WINDOW")
|
||||
@@ -1324,7 +1333,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
islandWindow = IslandWindow(service.applicationContext)
|
||||
islandWindow!!.show(sharedPreferences.getString("name", "AirPods Pro").toString(), batteryPercentage, this@AirPodsService, type, reversed)
|
||||
islandWindow!!.show(sharedPreferences.getString("name", "AirPods Pro").toString(), batteryPercentage, this@AirPodsService, type, reversed, otherDeviceName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,8 @@ class AACPManager {
|
||||
data class ConnectedDevice(
|
||||
val mac: String,
|
||||
val info1: Byte,
|
||||
val info2: Byte
|
||||
val info2: Byte,
|
||||
var type: String?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -242,8 +243,8 @@ class AACPManager {
|
||||
fun onAudioSourceReceived(audioSource: ByteArray)
|
||||
fun onOwnershipChangeReceived(owns: Boolean)
|
||||
fun onConnectedDevicesReceived(connectedDevices: List<ConnectedDevice>)
|
||||
fun onOwnershipToFalseRequest(reasonReverseTapped: Boolean)
|
||||
fun onShowNearbyUI()
|
||||
fun onOwnershipToFalseRequest(sender: String, reasonReverseTapped: Boolean)
|
||||
fun onShowNearbyUI(sender: String)
|
||||
}
|
||||
|
||||
fun parseStemPressResponse(data: ByteArray): Pair<StemPressType, StemPressBudType> {
|
||||
@@ -521,11 +522,21 @@ class AACPManager {
|
||||
|
||||
Opcodes.SMART_ROUTING_RESP -> {
|
||||
val packetString = packet.decodeToString()
|
||||
val sender = packet.sliceArray(6..11).reversedArray().joinToString(":") { "%02X".format(it) }
|
||||
|
||||
if (connectedDevices.find { it.mac == sender }?.type == null && packetString.contains("btName")) {
|
||||
val nameStartIndex = packetString.indexOf("btName") + 7
|
||||
val nameEndIndex = if (packetString.contains("other")) (packetString.indexOf("otherDevice") - 2) else (packetString.indexOf("nearbyAudio") - 2)
|
||||
val name = packet.sliceArray(nameStartIndex..nameEndIndex).decodeToString()
|
||||
connectedDevices.find { it.mac == sender }?.type = name
|
||||
Log.d(TAG, "Device $sender is named $name")
|
||||
}
|
||||
Log.d(TAG, "Smart Routing Response from $sender: $packetString, type: ${connectedDevices.find { it.mac == sender }?.type}")
|
||||
if (packetString.contains("SetOwnershipToFalse")) {
|
||||
callback?.onOwnershipToFalseRequest(packetString.contains("ReverseBannerTapped"))
|
||||
callback?.onOwnershipToFalseRequest(sender, packetString.contains("ReverseBannerTapped"))
|
||||
}
|
||||
if (packetString.contains("ShowNearbyUI")) {
|
||||
callback?.onShowNearbyUI()
|
||||
callback?.onShowNearbyUI(sender)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +555,7 @@ class AACPManager {
|
||||
)
|
||||
return
|
||||
}
|
||||
// first 4 bytes AACP header, next two bytes opcode, next to bytes identifer
|
||||
|
||||
eqOnMedia = (packet[10] == 0x01.toByte())
|
||||
eqOnPhone = (packet[11] == 0x01.toByte())
|
||||
// there are 4 eqs. i am not sure what those are for, maybe all 4 listening modes, or maybe phone+media left+right, but then there shouldn't be another flag for phone/media enabled. just directly the EQ... weird.
|
||||
@@ -554,7 +565,7 @@ class AACPManager {
|
||||
val eq3 = ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||
val eq4 = ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||
|
||||
// for now, just take the first EQ
|
||||
// for now, taking just the first EQ
|
||||
eqData = FloatArray(8) { i -> eq1.get(i) }
|
||||
Log.d(TAG, "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia")
|
||||
}
|
||||
@@ -756,11 +767,11 @@ class AACPManager {
|
||||
|
||||
fun createMediaInformationNewDevicePacket(selfMacAddress: String, targetMacAddress: String): ByteArray {
|
||||
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
|
||||
val buffer = ByteBuffer.allocate(112)
|
||||
val buffer = ByteBuffer.allocate(116)
|
||||
buffer.put(
|
||||
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
|
||||
)
|
||||
buffer.put(byteArrayOf(0x68, 0x00))
|
||||
buffer.put(byteArrayOf(0x6C, 0x00))
|
||||
buffer.put(byteArrayOf(0x01, 0xE5.toByte(), 0x4A))
|
||||
buffer.put("playingApp".toByteArray())
|
||||
buffer.put(0x42)
|
||||
@@ -775,8 +786,8 @@ class AACPManager {
|
||||
buffer.put(selfMacAddress.toByteArray())
|
||||
buffer.put(0x46)
|
||||
buffer.put("btName".toByteArray())
|
||||
buffer.put(0x43)
|
||||
buffer.put("And".toByteArray())
|
||||
buffer.put(0x47)
|
||||
buffer.put("Android".toByteArray())
|
||||
buffer.put(0x58)
|
||||
buffer.put("otherDevice".toByteArray())
|
||||
buffer.put("AudioCategory".toByteArray())
|
||||
@@ -805,8 +816,6 @@ class AACPManager {
|
||||
buffer.put(
|
||||
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
|
||||
)
|
||||
// 620001E54A6C6F63616C73636F7265306446726561736F6E4848696A61636B763251617564696F526F7574696E6753636F7265312D015F617564696F526F7574696E675365744F776E657273686970546F46616C7365014B72656D6F746573636F7265A5
|
||||
|
||||
buffer.put(byteArrayOf(0x62, 0x00))
|
||||
buffer.put(byteArrayOf(0x01, 0xE5.toByte()))
|
||||
buffer.put(0x4A)
|
||||
@@ -854,16 +863,16 @@ class AACPManager {
|
||||
streamingState: Boolean = true
|
||||
): ByteArray {
|
||||
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
|
||||
val buffer = ByteBuffer.allocate(134)
|
||||
val buffer = ByteBuffer.allocate(138)
|
||||
buffer.put(
|
||||
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
|
||||
)
|
||||
buffer.put(
|
||||
byteArrayOf(
|
||||
0x7E,
|
||||
0x82.toByte(), // related to the length
|
||||
0x00
|
||||
)
|
||||
) // something to do with the length, can't confirm, but changing causes airpods to soft reset
|
||||
)
|
||||
buffer.put(byteArrayOf(0x01, 0xE5.toByte(), 0x4A)) // unknown, constant
|
||||
buffer.put("PlayingApp".toByteArray())
|
||||
buffer.put(byteArrayOf(0x56)) // 'V', seems like a identifier or a separator
|
||||
@@ -877,8 +886,8 @@ class AACPManager {
|
||||
buffer.put(0x51) // 'Q'
|
||||
buffer.put(selfMacAddress.toByteArray()) // self MAC
|
||||
buffer.put("btName".toByteArray()) // self name
|
||||
buffer.put(0x44) // 'D'
|
||||
buffer.put("iPho".toByteArray()) // if set to iPad, shows "Moved to iPad, but most likely we're running on a phone. setting to anything else of the same length will show iPhone instead.
|
||||
buffer.put(0x47) // 'D'
|
||||
buffer.put("Android".toByteArray()) // if set to iPad, shows "Moved to iPad", but most likely we're running on a phone. setting to anything else of the same length will show iPhone instead.
|
||||
buffer.put(0x58) // 'X'
|
||||
buffer.put("otherDevice".toByteArray())
|
||||
buffer.put("AudioCategory".toByteArray())
|
||||
@@ -973,11 +982,11 @@ class AACPManager {
|
||||
|
||||
fun createAddTiPiDevicePacket(selfMacAddress: String, targetMacAddress: String): ByteArray {
|
||||
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
|
||||
val buffer = ByteBuffer.allocate(86)
|
||||
val buffer = ByteBuffer.allocate(90)
|
||||
buffer.put(
|
||||
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
|
||||
)
|
||||
buffer.put(byteArrayOf(0x4E, 0x00))
|
||||
buffer.put(byteArrayOf(0x52, 0x00))
|
||||
buffer.put(byteArrayOf(0x01, 0xE5.toByte()))
|
||||
buffer.put(0x48) // 'H'
|
||||
buffer.put("idleTime".toByteArray())
|
||||
@@ -989,8 +998,8 @@ class AACPManager {
|
||||
buffer.put(selfMacAddress.toByteArray())
|
||||
buffer.put(0x46)
|
||||
buffer.put("btName".toByteArray())
|
||||
buffer.put(0x43)
|
||||
buffer.put("And".toByteArray())
|
||||
buffer.put(0x47)
|
||||
buffer.put("Android".toByteArray())
|
||||
buffer.put(0x50)
|
||||
buffer.put("nearbyAudioScore".toByteArray())
|
||||
buffer.put(byteArrayOf(0x0E))
|
||||
@@ -1164,13 +1173,13 @@ class AACPManager {
|
||||
val mac = macBytes.joinToString(":") { "%02X".format(it) }
|
||||
val info1 = data[offset + 6]
|
||||
val info2 = data[offset + 7]
|
||||
devices.add(ConnectedDevice(mac, info1, info2))
|
||||
val existingDevice = devices.find { it.mac == mac }
|
||||
devices.add(ConnectedDevice(mac, info1, info2, existingDevice?.type))
|
||||
offset += 8
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
|
||||
fun sendSomePacketIDontKnowWhatItIs() {
|
||||
// 2900 00ff ffff ffff ffff -- enables setting EQ
|
||||
sendDataPacket(
|
||||
|
||||
@@ -165,7 +165,7 @@ class IslandWindow(private val context: Context) {
|
||||
@SuppressLint("SetTextI18s", "ClickableViewAccessibility", "UnspecifiedRegisterReceiverFlag",
|
||||
"SetTextI18n"
|
||||
)
|
||||
fun show(name: String, batteryPercentage: Int, context: Context, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false) {
|
||||
fun show(name: String, batteryPercentage: Int, context: Context, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false, otherDeviceName: String? = null) {
|
||||
if (ServiceManager.getService()?.islandOpen == true) return
|
||||
else ServiceManager.getService()?.islandOpen = true
|
||||
|
||||
@@ -352,19 +352,22 @@ class IslandWindow(private val context: Context) {
|
||||
|
||||
when (type) {
|
||||
IslandType.CONNECTED -> {
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_connected_text)
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_connected_text)
|
||||
}
|
||||
IslandType.TAKING_OVER -> {
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_taking_over_text)
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_taking_over_text)
|
||||
}
|
||||
IslandType.MOVED_TO_REMOTE -> {
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_moved_to_remote_text)
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_remote_text)
|
||||
}
|
||||
IslandType.MOVED_TO_OTHER_DEVICE -> {
|
||||
if (otherDeviceName == null || otherDeviceName.isEmpty()) {
|
||||
e("IslandWindow", "Other device name is null or empty for MOVED_TO_OTHER_DEVICE type")
|
||||
}
|
||||
if (reversed) {
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_moved_to_other_device_reversed_text)
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_other_device_reversed_text)
|
||||
} else {
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_moved_to_other_device_text)
|
||||
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_other_device_text, otherDeviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ object MediaController {
|
||||
private var conversationalAwarenessVolume: Int = 2
|
||||
private var conversationalAwarenessPauseMusic: Boolean = false
|
||||
|
||||
var recentlyLostOwnership: Boolean = false
|
||||
|
||||
fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) {
|
||||
if (this::audioManager.isInitialized) {
|
||||
return
|
||||
@@ -118,10 +120,14 @@ object MediaController {
|
||||
|
||||
if (isActive) {
|
||||
Log.d("MediaController", "Detected play while pausedForOtherDevice; attempting to take over")
|
||||
pausedForOtherDevice = false
|
||||
userPlayedTheMedia = true
|
||||
if (!pausedWhileTakingOver) {
|
||||
ServiceManager.getService()?.takeOver("music")
|
||||
if (!recentlyLostOwnership) {
|
||||
pausedForOtherDevice = false
|
||||
userPlayedTheMedia = true
|
||||
if (!pausedWhileTakingOver) {
|
||||
ServiceManager.getService()?.takeOver("music")
|
||||
}
|
||||
} else {
|
||||
Log.d("MediaController", "Skipping take-over due to recent ownership loss")
|
||||
}
|
||||
} else {
|
||||
Log.d("MediaController", "Still not active while pausedForOtherDevice; will clear state after timeout")
|
||||
@@ -148,8 +154,12 @@ object MediaController {
|
||||
Log.d("MediaController", "pausedWhileTakingOver: $pausedWhileTakingOver")
|
||||
if (!pausedWhileTakingOver && isActive) {
|
||||
if (lastKnownIsMusicActive != true) {
|
||||
Log.d("MediaController", "Music is active and not pausedWhileTakingOver; requesting takeOver")
|
||||
ServiceManager.getService()?.takeOver("music")
|
||||
if (!recentlyLostOwnership) {
|
||||
Log.d("MediaController", "Music is active and not pausedWhileTakingOver; requesting takeOver")
|
||||
ServiceManager.getService()?.takeOver("music")
|
||||
} else {
|
||||
Log.d("MediaController", "Skipping take-over due to recent ownership loss")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<string name="island_connected_remote_text">Connected to Linux</string>
|
||||
<string name="island_taking_over_text">Connected</string>
|
||||
<string name="island_moved_to_remote_text">Moved to Linux</string>
|
||||
<string name="island_moved_to_other_device_text">Moved to other device</string>
|
||||
<string name="island_moved_to_other_device_text">Moved to %1$s</string>
|
||||
<string name="island_moved_to_other_device_reversed_text">Reconnect from notification</string>
|
||||
<string name="head_tracking">Head Tracking</string>
|
||||
<string name="head_gestures_details">Nod to answer calls, and shake your head to decline.</string>
|
||||
|
||||
Reference in New Issue
Block a user