mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-28 22:01:50 +00:00
try to add some cross-device stuff
This commit is contained in:
@@ -22,10 +22,12 @@ import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Context.RECEIVER_EXPORTED
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
@@ -147,6 +149,16 @@ fun Main() {
|
||||
}
|
||||
}
|
||||
|
||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val isAvailableChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == "CrossDeviceIsAvailable") {
|
||||
Log.d("MainActivity", "CrossDeviceIsAvailable changed")
|
||||
isRemotelyConnected.value = sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(isAvailableChangeListener)
|
||||
Log.d("MainActivity", "CrossDeviceIsAvailable: ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | isAvailable: ${CrossDevice.isAvailable}")
|
||||
isRemotelyConnected.value = sharedPreferences.getBoolean("CrossDeviceIsAvailable", false) || CrossDevice.isAvailable
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package me.kavishdevar.aln.services
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -47,6 +48,7 @@ import android.os.Looper
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.core.app.NotificationCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -64,6 +66,7 @@ import me.kavishdevar.aln.utils.Battery
|
||||
import me.kavishdevar.aln.utils.BatteryComponent
|
||||
import me.kavishdevar.aln.utils.BatteryStatus
|
||||
import me.kavishdevar.aln.utils.CrossDevice
|
||||
import me.kavishdevar.aln.utils.CrossDevicePackets
|
||||
import me.kavishdevar.aln.utils.Enums
|
||||
import me.kavishdevar.aln.utils.LongPressPackets
|
||||
import me.kavishdevar.aln.utils.MediaController
|
||||
@@ -163,11 +166,15 @@ class AirPodsService: Service() {
|
||||
return
|
||||
}
|
||||
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||
if (bluetoothDevice.uuids.contains(uuid)) {
|
||||
val intent = Intent(AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED)
|
||||
intent.putExtra("name", name)
|
||||
intent.putExtra("device", bluetoothDevice)
|
||||
context?.sendBroadcast(intent)
|
||||
bluetoothDevice.fetchUuidsWithSdp()
|
||||
if (bluetoothDevice.uuids != null) {
|
||||
if (bluetoothDevice.uuids.contains(uuid)) {
|
||||
val intent =
|
||||
Intent(AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED)
|
||||
intent.putExtra("name", name)
|
||||
intent.putExtra("device", bluetoothDevice)
|
||||
context?.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,6 +246,28 @@ class AirPodsService: Service() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun sendANCBroadcast() {
|
||||
sendBroadcast(Intent(AirPodsNotifications.Companion.ANC_DATA).apply {
|
||||
putExtra("data", ancNotification.status)
|
||||
})
|
||||
}
|
||||
|
||||
fun sendBatteryBroadcast() {
|
||||
sendBroadcast(Intent(AirPodsNotifications.Companion.BATTERY_DATA).apply {
|
||||
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
||||
})
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
fun sendBatteryNotification() {
|
||||
updateNotificationContent(
|
||||
true,
|
||||
getSharedPreferences("settings", MODE_PRIVATE).getString("name", device?.name),
|
||||
batteryNotification.getBattery()
|
||||
)
|
||||
}
|
||||
|
||||
fun updateBatteryWidget() {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(this)
|
||||
val componentName = ComponentName(this, BatteryWidget::class.java)
|
||||
@@ -791,20 +820,24 @@ class AirPodsService: Service() {
|
||||
fun sendPacket(packet: String) {
|
||||
val fromHex = packet.split(" ").map { it.toInt(16).toByte() }
|
||||
if (!isConnectedLocally && CrossDevice.isAvailable) {
|
||||
CrossDevice.sendRemotePacket(fromHex.toByteArray())
|
||||
CrossDevice.sendRemotePacket(CrossDevicePackets.AIRPODS_DATA_HEADER.packet + fromHex.toByteArray())
|
||||
return
|
||||
}
|
||||
socket.outputStream?.write(fromHex.toByteArray())
|
||||
socket.outputStream?.flush()
|
||||
if (this::socket.isInitialized) {
|
||||
socket.outputStream?.write(fromHex.toByteArray())
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun sendPacket(packet: ByteArray) {
|
||||
if (!isConnectedLocally && CrossDevice.isAvailable) {
|
||||
CrossDevice.sendRemotePacket(packet)
|
||||
CrossDevice.sendRemotePacket(CrossDevicePackets.AIRPODS_DATA_HEADER.packet + packet)
|
||||
return
|
||||
}
|
||||
socket.outputStream?.write(packet)
|
||||
socket.outputStream?.flush()
|
||||
if (this::socket.isInitialized) {
|
||||
socket.outputStream?.write(packet)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fun setANCMode(mode: Int) {
|
||||
@@ -823,37 +856,31 @@ class AirPodsService: Service() {
|
||||
sendPacket(Enums.NOISE_CANCELLATION_ADAPTIVE.value)
|
||||
}
|
||||
}
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setCAEnabled(enabled: Boolean) {
|
||||
sendPacket(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setOffListeningMode(enabled: Boolean) {
|
||||
sendPacket(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00))
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setAdaptiveStrength(strength: Int) {
|
||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
|
||||
sendPacket(bytes)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setPressSpeed(speed: Int) {
|
||||
// 0x00 = default, 0x01 = slower, 0x02 = slowest
|
||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00)
|
||||
sendPacket(bytes)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setPressAndHoldDuration(speed: Int) {
|
||||
// 0 - default, 1 - slower, 2 - slowest
|
||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00)
|
||||
sendPacket(bytes)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setVolumeSwipeSpeed(speed: Int) {
|
||||
@@ -861,25 +888,21 @@ class AirPodsService: Service() {
|
||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00)
|
||||
Log.d("AirPodsService", "Setting volume swipe speed to $speed by packet ${bytes.joinToString(" ") { "%02X".format(it) }}")
|
||||
sendPacket(bytes)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setNoiseCancellationWithOnePod(enabled: Boolean) {
|
||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1B, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)
|
||||
sendPacket(bytes)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setVolumeControl(enabled: Boolean) {
|
||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x25, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)
|
||||
sendPacket(bytes)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setToneVolume(volume: Int) {
|
||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00)
|
||||
sendPacket(bytes)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
val earDetectionNotification = AirPodsNotifications.EarDetection()
|
||||
@@ -892,7 +915,6 @@ class AirPodsService: Service() {
|
||||
fun setCaseChargingSounds(enabled: Boolean) {
|
||||
val bytes = byteArrayOf(0x12, 0x3a, 0x00, 0x01, 0x00, 0x08, if (enabled) 0x00 else 0x01)
|
||||
sendPacket(bytes)
|
||||
socket.outputStream?.flush()
|
||||
}
|
||||
|
||||
fun setEarDetection(enabled: Boolean) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothServerSocket
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -23,6 +24,7 @@ enum class CrossDevicePackets(val packet: ByteArray) {
|
||||
AIRPODS_DATA_HEADER(byteArrayOf(0x00, 0x04, 0x00, 0x01)),
|
||||
}
|
||||
|
||||
|
||||
object CrossDevice {
|
||||
private val uuid = UUID.fromString("1abbb9a4-10e4-4000-a75c-8953c5471342")
|
||||
private var serverSocket: BluetoothServerSocket? = null
|
||||
@@ -31,10 +33,12 @@ object CrossDevice {
|
||||
var isAvailable: Boolean = false // set to true when airpods are connected to another device
|
||||
var batteryBytes: ByteArray = byteArrayOf()
|
||||
var ancBytes: ByteArray = byteArrayOf()
|
||||
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
@SuppressLint("MissingPermission")
|
||||
fun init(context: Context) {
|
||||
Log.d("AirPodsQuickSwitchService", "Initializing CrossDevice")
|
||||
sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply()
|
||||
this.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||
startServer()
|
||||
}
|
||||
@@ -57,7 +61,8 @@ object CrossDevice {
|
||||
|
||||
fun setAirPodsConnected(connected: Boolean) {
|
||||
if (connected) {
|
||||
isAvailable = true
|
||||
isAvailable = false
|
||||
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply()
|
||||
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_CONNECTED.packet)
|
||||
} else {
|
||||
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)
|
||||
@@ -69,7 +74,9 @@ object CrossDevice {
|
||||
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DATA_HEADER.packet + packet)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun handleClientConnection(socket: BluetoothSocket) {
|
||||
Log.d("AirPodsQuickSwitchService", "Client connected")
|
||||
clientSocket = socket
|
||||
val inputStream = socket.inputStream
|
||||
val buffer = ByteArray(1024)
|
||||
@@ -78,14 +85,17 @@ object CrossDevice {
|
||||
while (true) {
|
||||
bytes = inputStream.read(buffer)
|
||||
val packet = buffer.copyOf(bytes)
|
||||
Log.d("AirPodsQuickSwitchService", "Received packet: ${packet.joinToString("") { "%02x".format(it) }}")
|
||||
if (bytes == -1) {
|
||||
break
|
||||
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_DISCONNECT.packet)) {
|
||||
ServiceManager.getService()?.disconnect()
|
||||
} else if (packet.contentEquals(CrossDevicePackets.AIRPODS_CONNECTED.packet)) {
|
||||
isAvailable = true
|
||||
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply()
|
||||
} else if (packet.contentEquals(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)) {
|
||||
isAvailable = false
|
||||
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply()
|
||||
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_BATTERY_BYTES.packet)) {
|
||||
Log.d("AirPodsQuickSwitchService", "Received battery request, battery data: ${batteryBytes.joinToString("") { "%02x".format(it) }}")
|
||||
sendRemotePacket(batteryBytes)
|
||||
@@ -94,16 +104,32 @@ object CrossDevice {
|
||||
sendRemotePacket(ancBytes)
|
||||
}
|
||||
else {
|
||||
if (ServiceManager.getService()?.isConnectedLocally == true) {
|
||||
val packetInHex = packet.joinToString("") { "%02x".format(it) }
|
||||
ServiceManager.getService()?.sendPacket(packetInHex)
|
||||
} else if (ServiceManager.getService()?.batteryNotification?.isBatteryData(packet) == true) {
|
||||
batteryBytes = packet
|
||||
ServiceManager.getService()?.batteryNotification?.setBattery(packet)
|
||||
ServiceManager.getService()?.updateBatteryWidget()
|
||||
} else if (ServiceManager.getService()?.ancNotification?.isANCData(packet) == true) {
|
||||
ServiceManager.getService()?.ancNotification?.setStatus(packet)
|
||||
ancBytes = packet
|
||||
if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) {
|
||||
// the AIRPODS_CONNECTED wasn't sent before
|
||||
isAvailable = true
|
||||
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply()
|
||||
val trimmedPacket =
|
||||
packet.drop(CrossDevicePackets.AIRPODS_DATA_HEADER.packet.size).toByteArray()
|
||||
Log.d("AirPodsQuickSwitchService", "Received relayed packet, with ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | ${ServiceManager.getService()?.batteryNotification?.isBatteryData(trimmedPacket)}")
|
||||
Log.d(
|
||||
"AirPodsQuickSwitchService",
|
||||
"Relayed packet: ${trimmedPacket.joinToString("") { "%02x".format(it) }}"
|
||||
)
|
||||
if (ServiceManager.getService()?.isConnectedLocally == true) {
|
||||
val packetInHex = trimmedPacket.joinToString("") { "%02x".format(it) }
|
||||
ServiceManager.getService()?.sendPacket(packetInHex)
|
||||
} else if (ServiceManager.getService()?.batteryNotification?.isBatteryData(trimmedPacket) == true) {
|
||||
batteryBytes = trimmedPacket
|
||||
ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket)
|
||||
Log.d("AirPodsQuickSwitchService", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}")
|
||||
ServiceManager.getService()?.updateBatteryWidget()
|
||||
ServiceManager.getService()?.sendBatteryBroadcast()
|
||||
ServiceManager.getService()?.sendBatteryNotification()
|
||||
} else if (ServiceManager.getService()?.ancNotification?.isANCData(trimmedPacket) == true) {
|
||||
ServiceManager.getService()?.ancNotification?.setStatus(trimmedPacket)
|
||||
ServiceManager.getService()?.sendANCBroadcast()
|
||||
ancBytes = trimmedPacket
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +138,7 @@ object CrossDevice {
|
||||
fun sendRemotePacket(byteArray: ByteArray) {
|
||||
if (clientSocket == null) {
|
||||
Log.d("AirPodsQuickSwitchService", "Client socket is null")
|
||||
return
|
||||
}
|
||||
clientSocket?.outputStream?.write(byteArray)
|
||||
clientSocket?.outputStream?.flush()
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
package me.kavishdevar.aln.utils
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class Enums(val value: ByteArray) {
|
||||
@@ -133,6 +134,9 @@ class AirPodsNotifications {
|
||||
}
|
||||
|
||||
fun setStatus(data: ByteArray) {
|
||||
if (data.size != 11) {
|
||||
return
|
||||
}
|
||||
status = data[7].toInt()
|
||||
}
|
||||
|
||||
@@ -154,13 +158,17 @@ class AirPodsNotifications {
|
||||
|
||||
fun isBatteryData(data: ByteArray): Boolean {
|
||||
if (data.size != 22) {
|
||||
Log.d("BatteryNotification", "Battery data size is not 22")
|
||||
return false
|
||||
}
|
||||
return data[0] == 0x04.toByte() && data[1] == 0x00.toByte() && data[2] == 0x04.toByte() &&
|
||||
data[3] == 0x00.toByte() && data[4] == 0x04.toByte() && data[5] == 0x00.toByte()
|
||||
Log.d("BatteryNotification", data.joinToString("") { "%02x".format(it) }.startsWith("040004000400").toString())
|
||||
return data.joinToString("") { "%02x".format(it) }.startsWith("040004000400")
|
||||
}
|
||||
|
||||
fun setBattery(data: ByteArray) {
|
||||
if (data.size != 22) {
|
||||
return
|
||||
}
|
||||
first = if (data[10].toInt() == BatteryStatus.DISCONNECTED) {
|
||||
Battery(first.component, first.level, data[10].toInt())
|
||||
} else {
|
||||
@@ -186,6 +194,7 @@ class AirPodsNotifications {
|
||||
}
|
||||
|
||||
class ConversationalAwarenessNotification {
|
||||
@Suppress("PrivatePropertyName")
|
||||
private val NOTIFICATION_PREFIX = Enums.CONVERSATION_AWARENESS_RECEIVE_PREFIX.value
|
||||
|
||||
var status: Byte = 0
|
||||
|
||||
Reference in New Issue
Block a user