Skip to content

Commit 032b94e

Browse files
committed
android: use device name sent by the connected device in island
1 parent 5c9beeb commit 032b94e

File tree

5 files changed

+80
-49
lines changed

5 files changed

+80
-49
lines changed

android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -872,16 +872,21 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
872872

873873
override fun onOwnershipChangeReceived(owns: Boolean) {
874874
if (!owns) {
875+
MediaController.recentlyLostOwnership = true
876+
Handler(Looper.getMainLooper()).postDelayed({
877+
MediaController.recentlyLostOwnership = false
878+
}, 3000)
875879
Log.d("AirPodsService", "ownership lost")
876880
MediaController.sendPause()
877881
MediaController.pausedForOtherDevice = true
878882
}
879883
}
880884

881-
override fun onOwnershipToFalseRequest(reasonReverseTapped: Boolean) {
885+
override fun onOwnershipToFalseRequest(sender: String, reasonReverseTapped: Boolean) {
882886
// 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.
883887
// 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".
884888
// (20 minutes later) i've done it nonetheless :]
889+
val senderName = aacpManager.connectedDevices.find { it.mac == sender }?.type ?: "Other device"
885890
Log.d("AirPodsService", "other device has hijacked the connection, reasonReverseTapped: $reasonReverseTapped")
886891
aacpManager.sendControlCommand(
887892
AACPManager.Companion.ControlCommandIdentifiers.OWNS_CONNECTION.value,
@@ -895,27 +900,31 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
895900
this@AirPodsService,
896901
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
897902
IslandType.MOVED_TO_OTHER_DEVICE,
898-
reversed = true
903+
reversed = true,
904+
otherDeviceName = senderName
899905
)
900906
}
901907
if (!aacpManager.owns) {
902908
showIsland(
903909
this@AirPodsService,
904910
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
905911
IslandType.MOVED_TO_OTHER_DEVICE,
906-
reversed = reasonReverseTapped
912+
reversed = reasonReverseTapped,
913+
otherDeviceName = senderName
907914
)
908915
}
909916
MediaController.sendPause()
910917
}
911918

912-
override fun onShowNearbyUI() {
913-
showIsland(
914-
this@AirPodsService,
915-
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
916-
IslandType.MOVED_TO_OTHER_DEVICE,
917-
reversed = false
918-
)
919+
override fun onShowNearbyUI(sender: String) {
920+
val senderName = aacpManager.connectedDevices.find { it.mac == sender }?.type ?: "Other device"
921+
showIsland(
922+
this@AirPodsService,
923+
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
924+
IslandType.MOVED_TO_OTHER_DEVICE,
925+
reversed = false,
926+
otherDeviceName = senderName
927+
)
919928
}
920929

921930
override fun onDeviceMetadataReceived(deviceMetadata: ByteArray) {
@@ -1316,15 +1325,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
13161325
var islandOpen = false
13171326
var islandWindow: IslandWindow? = null
13181327
@SuppressLint("MissingPermission")
1319-
fun showIsland(service: Service, batteryPercentage: Int, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false) {
1328+
fun showIsland(service: Service, batteryPercentage: Int, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false, otherDeviceName: String? = null) {
13201329
Log.d("AirPodsService", "Showing island window")
13211330
if (!Settings.canDrawOverlays(service)) {
13221331
Log.d("AirPodsService", "No permission for SYSTEM_ALERT_WINDOW")
13231332
return
13241333
}
13251334
CoroutineScope(Dispatchers.Main).launch {
13261335
islandWindow = IslandWindow(service.applicationContext)
1327-
islandWindow!!.show(sharedPreferences.getString("name", "AirPods Pro").toString(), batteryPercentage, this@AirPodsService, type, reversed)
1336+
islandWindow!!.show(sharedPreferences.getString("name", "AirPods Pro").toString(), batteryPercentage, this@AirPodsService, type, reversed, otherDeviceName)
13281337
}
13291338
}
13301339

android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ class AACPManager {
174174
data class ConnectedDevice(
175175
val mac: String,
176176
val info1: Byte,
177-
val info2: Byte
177+
val info2: Byte,
178+
var type: String?
178179
)
179180
}
180181

@@ -242,8 +243,8 @@ class AACPManager {
242243
fun onAudioSourceReceived(audioSource: ByteArray)
243244
fun onOwnershipChangeReceived(owns: Boolean)
244245
fun onConnectedDevicesReceived(connectedDevices: List<ConnectedDevice>)
245-
fun onOwnershipToFalseRequest(reasonReverseTapped: Boolean)
246-
fun onShowNearbyUI()
246+
fun onOwnershipToFalseRequest(sender: String, reasonReverseTapped: Boolean)
247+
fun onShowNearbyUI(sender: String)
247248
}
248249

249250
fun parseStemPressResponse(data: ByteArray): Pair<StemPressType, StemPressBudType> {
@@ -521,11 +522,21 @@ class AACPManager {
521522

522523
Opcodes.SMART_ROUTING_RESP -> {
523524
val packetString = packet.decodeToString()
525+
val sender = packet.sliceArray(6..11).reversedArray().joinToString(":") { "%02X".format(it) }
526+
527+
if (connectedDevices.find { it.mac == sender }?.type == null && packetString.contains("btName")) {
528+
val nameStartIndex = packetString.indexOf("btName") + 7
529+
val nameEndIndex = if (packetString.contains("other")) (packetString.indexOf("otherDevice") - 2) else (packetString.indexOf("nearbyAudio") - 2)
530+
val name = packet.sliceArray(nameStartIndex..nameEndIndex).decodeToString()
531+
connectedDevices.find { it.mac == sender }?.type = name
532+
Log.d(TAG, "Device $sender is named $name")
533+
}
534+
Log.d(TAG, "Smart Routing Response from $sender: $packetString, type: ${connectedDevices.find { it.mac == sender }?.type}")
524535
if (packetString.contains("SetOwnershipToFalse")) {
525-
callback?.onOwnershipToFalseRequest(packetString.contains("ReverseBannerTapped"))
536+
callback?.onOwnershipToFalseRequest(sender, packetString.contains("ReverseBannerTapped"))
526537
}
527538
if (packetString.contains("ShowNearbyUI")) {
528-
callback?.onShowNearbyUI()
539+
callback?.onShowNearbyUI(sender)
529540
}
530541
}
531542

@@ -544,7 +555,7 @@ class AACPManager {
544555
)
545556
return
546557
}
547-
// first 4 bytes AACP header, next two bytes opcode, next to bytes identifer
558+
548559
eqOnMedia = (packet[10] == 0x01.toByte())
549560
eqOnPhone = (packet[11] == 0x01.toByte())
550561
// 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 {
554565
val eq3 = ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
555566
val eq4 = ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
556567

557-
// for now, just take the first EQ
568+
// for now, taking just the first EQ
558569
eqData = FloatArray(8) { i -> eq1.get(i) }
559570
Log.d(TAG, "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia")
560571
}
@@ -756,11 +767,11 @@ class AACPManager {
756767

757768
fun createMediaInformationNewDevicePacket(selfMacAddress: String, targetMacAddress: String): ByteArray {
758769
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
759-
val buffer = ByteBuffer.allocate(112)
770+
val buffer = ByteBuffer.allocate(116)
760771
buffer.put(
761772
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
762773
)
763-
buffer.put(byteArrayOf(0x68, 0x00))
774+
buffer.put(byteArrayOf(0x6C, 0x00))
764775
buffer.put(byteArrayOf(0x01, 0xE5.toByte(), 0x4A))
765776
buffer.put("playingApp".toByteArray())
766777
buffer.put(0x42)
@@ -775,8 +786,8 @@ class AACPManager {
775786
buffer.put(selfMacAddress.toByteArray())
776787
buffer.put(0x46)
777788
buffer.put("btName".toByteArray())
778-
buffer.put(0x43)
779-
buffer.put("And".toByteArray())
789+
buffer.put(0x47)
790+
buffer.put("Android".toByteArray())
780791
buffer.put(0x58)
781792
buffer.put("otherDevice".toByteArray())
782793
buffer.put("AudioCategory".toByteArray())
@@ -805,8 +816,6 @@ class AACPManager {
805816
buffer.put(
806817
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
807818
)
808-
// 620001E54A6C6F63616C73636F7265306446726561736F6E4848696A61636B763251617564696F526F7574696E6753636F7265312D015F617564696F526F7574696E675365744F776E657273686970546F46616C7365014B72656D6F746573636F7265A5
809-
810819
buffer.put(byteArrayOf(0x62, 0x00))
811820
buffer.put(byteArrayOf(0x01, 0xE5.toByte()))
812821
buffer.put(0x4A)
@@ -854,16 +863,16 @@ class AACPManager {
854863
streamingState: Boolean = true
855864
): ByteArray {
856865
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
857-
val buffer = ByteBuffer.allocate(134)
866+
val buffer = ByteBuffer.allocate(138)
858867
buffer.put(
859868
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
860869
)
861870
buffer.put(
862871
byteArrayOf(
863-
0x7E,
872+
0x82.toByte(), // related to the length
864873
0x00
865874
)
866-
) // something to do with the length, can't confirm, but changing causes airpods to soft reset
875+
)
867876
buffer.put(byteArrayOf(0x01, 0xE5.toByte(), 0x4A)) // unknown, constant
868877
buffer.put("PlayingApp".toByteArray())
869878
buffer.put(byteArrayOf(0x56)) // 'V', seems like a identifier or a separator
@@ -877,8 +886,8 @@ class AACPManager {
877886
buffer.put(0x51) // 'Q'
878887
buffer.put(selfMacAddress.toByteArray()) // self MAC
879888
buffer.put("btName".toByteArray()) // self name
880-
buffer.put(0x44) // 'D'
881-
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.
889+
buffer.put(0x47) // 'D'
890+
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.
882891
buffer.put(0x58) // 'X'
883892
buffer.put("otherDevice".toByteArray())
884893
buffer.put("AudioCategory".toByteArray())
@@ -973,11 +982,11 @@ class AACPManager {
973982

974983
fun createAddTiPiDevicePacket(selfMacAddress: String, targetMacAddress: String): ByteArray {
975984
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
976-
val buffer = ByteBuffer.allocate(86)
985+
val buffer = ByteBuffer.allocate(90)
977986
buffer.put(
978987
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
979988
)
980-
buffer.put(byteArrayOf(0x4E, 0x00))
989+
buffer.put(byteArrayOf(0x52, 0x00))
981990
buffer.put(byteArrayOf(0x01, 0xE5.toByte()))
982991
buffer.put(0x48) // 'H'
983992
buffer.put("idleTime".toByteArray())
@@ -989,8 +998,8 @@ class AACPManager {
989998
buffer.put(selfMacAddress.toByteArray())
990999
buffer.put(0x46)
9911000
buffer.put("btName".toByteArray())
992-
buffer.put(0x43)
993-
buffer.put("And".toByteArray())
1001+
buffer.put(0x47)
1002+
buffer.put("Android".toByteArray())
9941003
buffer.put(0x50)
9951004
buffer.put("nearbyAudioScore".toByteArray())
9961005
buffer.put(byteArrayOf(0x0E))
@@ -1164,13 +1173,13 @@ class AACPManager {
11641173
val mac = macBytes.joinToString(":") { "%02X".format(it) }
11651174
val info1 = data[offset + 6]
11661175
val info2 = data[offset + 7]
1167-
devices.add(ConnectedDevice(mac, info1, info2))
1176+
val existingDevice = devices.find { it.mac == mac }
1177+
devices.add(ConnectedDevice(mac, info1, info2, existingDevice?.type))
11681178
offset += 8
11691179
}
11701180

11711181
return devices
11721182
}
1173-
11741183
fun sendSomePacketIDontKnowWhatItIs() {
11751184
// 2900 00ff ffff ffff ffff -- enables setting EQ
11761185
sendDataPacket(

android/app/src/main/java/me/kavishdevar/librepods/utils/IslandWindow.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ class IslandWindow(private val context: Context) {
165165
@SuppressLint("SetTextI18s", "ClickableViewAccessibility", "UnspecifiedRegisterReceiverFlag",
166166
"SetTextI18n"
167167
)
168-
fun show(name: String, batteryPercentage: Int, context: Context, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false) {
168+
fun show(name: String, batteryPercentage: Int, context: Context, type: IslandType = IslandType.CONNECTED, reversed: Boolean = false, otherDeviceName: String? = null) {
169169
if (ServiceManager.getService()?.islandOpen == true) return
170170
else ServiceManager.getService()?.islandOpen = true
171171

@@ -352,19 +352,22 @@ class IslandWindow(private val context: Context) {
352352

353353
when (type) {
354354
IslandType.CONNECTED -> {
355-
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_connected_text)
355+
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_connected_text)
356356
}
357357
IslandType.TAKING_OVER -> {
358-
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_taking_over_text)
358+
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_taking_over_text)
359359
}
360360
IslandType.MOVED_TO_REMOTE -> {
361-
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_moved_to_remote_text)
361+
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_remote_text)
362362
}
363363
IslandType.MOVED_TO_OTHER_DEVICE -> {
364+
if (otherDeviceName == null || otherDeviceName.isEmpty()) {
365+
e("IslandWindow", "Other device name is null or empty for MOVED_TO_OTHER_DEVICE type")
366+
}
364367
if (reversed) {
365-
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_moved_to_other_device_reversed_text)
368+
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_other_device_reversed_text)
366369
} else {
367-
islandView.findViewById<TextView>(R.id.island_connected_text).text = getString(context, R.string.island_moved_to_other_device_text)
370+
islandView.findViewById<TextView>(R.id.island_connected_text).text = context.getString(R.string.island_moved_to_other_device_text, otherDeviceName)
368371
}
369372
}
370373
}

android/app/src/main/java/me/kavishdevar/librepods/utils/MediaController.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ object MediaController {
6161
private var conversationalAwarenessVolume: Int = 2
6262
private var conversationalAwarenessPauseMusic: Boolean = false
6363

64+
var recentlyLostOwnership: Boolean = false
65+
6466
fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) {
6567
if (this::audioManager.isInitialized) {
6668
return
@@ -118,10 +120,14 @@ object MediaController {
118120

119121
if (isActive) {
120122
Log.d("MediaController", "Detected play while pausedForOtherDevice; attempting to take over")
121-
pausedForOtherDevice = false
122-
userPlayedTheMedia = true
123-
if (!pausedWhileTakingOver) {
124-
ServiceManager.getService()?.takeOver("music")
123+
if (!recentlyLostOwnership) {
124+
pausedForOtherDevice = false
125+
userPlayedTheMedia = true
126+
if (!pausedWhileTakingOver) {
127+
ServiceManager.getService()?.takeOver("music")
128+
}
129+
} else {
130+
Log.d("MediaController", "Skipping take-over due to recent ownership loss")
125131
}
126132
} else {
127133
Log.d("MediaController", "Still not active while pausedForOtherDevice; will clear state after timeout")
@@ -148,8 +154,12 @@ object MediaController {
148154
Log.d("MediaController", "pausedWhileTakingOver: $pausedWhileTakingOver")
149155
if (!pausedWhileTakingOver && isActive) {
150156
if (lastKnownIsMusicActive != true) {
151-
Log.d("MediaController", "Music is active and not pausedWhileTakingOver; requesting takeOver")
152-
ServiceManager.getService()?.takeOver("music")
157+
if (!recentlyLostOwnership) {
158+
Log.d("MediaController", "Music is active and not pausedWhileTakingOver; requesting takeOver")
159+
ServiceManager.getService()?.takeOver("music")
160+
} else {
161+
Log.d("MediaController", "Skipping take-over due to recent ownership loss")
162+
}
153163
}
154164
}
155165

android/app/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
<string name="island_connected_remote_text">Connected to Linux</string>
4848
<string name="island_taking_over_text">Connected</string>
4949
<string name="island_moved_to_remote_text">Moved to Linux</string>
50-
<string name="island_moved_to_other_device_text">Moved to other device</string>
50+
<string name="island_moved_to_other_device_text">Moved to %1$s</string>
5151
<string name="island_moved_to_other_device_reversed_text">Reconnect from notification</string>
5252
<string name="head_tracking">Head Tracking</string>
5353
<string name="head_gestures_details">Nod to answer calls, and shake your head to decline.</string>

0 commit comments

Comments
 (0)