diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 34bc80f..a9f9e93 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,6 +6,12 @@
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
index 0a924e0..9a5cf14 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
@@ -6,6 +6,9 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothHeadset
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothSocket
import android.content.BroadcastReceiver
import android.content.Context
@@ -23,6 +26,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.lsposed.hiddenapibypass.HiddenApiBypass
+private const val VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV = "+IPHONEACCEV"
+private const val VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL = 1
+private const val APPLE = 0x004C
+const val ACTION_BATTERY_LEVEL_CHANGED = "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED"
+const val EXTRA_BATTERY_LEVEL = "android.bluetooth.device.extra.BATTERY_LEVEL"
+private const val PACKAGE_ASI = "com.google.android.settings.intelligence"
+private const val ACTION_ASI_UPDATE_BLUETOOTH_DATA = "batterywidget.impl.action.update_bluetooth_data"
+//private const val COMPANION_TYPE_NONE = "COMPANION_NONE"
+//const val VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID"
+
class AirPodsService : Service() {
inner class LocalBinder : Binder() {
fun getService(): AirPodsService = this@AirPodsService
@@ -93,7 +106,7 @@ class AirPodsService : Service() {
fun getANC(): Int {
return ancNotification.status
}
-
+//
// private fun buildBatteryText(battery: List): String {
// val left = battery[0]
// val right = battery[1]
@@ -118,6 +131,165 @@ class AirPodsService : Service() {
return notificationBuilder.build()
}
+ fun disconnectAudio(context: Context, device: BluetoothDevice?) {
+ val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
+
+ bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.A2DP) {
+ try {
+ val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
+ method.invoke(proxy, device)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
+ }
+ }
+ }
+
+ override fun onServiceDisconnected(profile: Int) { }
+ }, BluetoothProfile.A2DP)
+
+ bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.HEADSET) {
+ try {
+ val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
+ method.invoke(proxy, device)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
+ }
+ }
+ }
+
+ override fun onServiceDisconnected(profile: Int) { }
+ }, BluetoothProfile.HEADSET)
+ }
+
+ fun connectAudio(context: Context, device: BluetoothDevice?) {
+ val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
+
+ bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.A2DP) {
+ try {
+ val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
+ method.invoke(proxy, device)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
+ }
+ }
+ }
+
+ override fun onServiceDisconnected(profile: Int) { }
+ }, BluetoothProfile.A2DP)
+
+ bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.HEADSET) {
+ try {
+ val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
+ method.invoke(proxy, device)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
+ }
+ }
+ }
+
+ override fun onServiceDisconnected(profile: Int) { }
+ }, BluetoothProfile.HEADSET)
+ }
+
+ fun updatePodsStatus(device: BluetoothDevice, batteryList: List) {
+ var batteryUnified = 0
+ var batteryUnifiedArg = 0
+
+ // Handle each Battery object from batteryList
+// batteryList.forEach { battery ->
+// when (battery.getComponentName()) {
+// "LEFT" -> {
+// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 10, battery.level.toString().toByteArray())
+// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 13, battery.getStatusName()?.uppercase()?.toByteArray())
+// }
+// "RIGHT" -> {
+// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 11, battery.level.toString().toByteArray())
+// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 14, battery.getStatusName()?.uppercase()?.toByteArray())
+// }
+// "CASE" -> {
+// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 12, battery.level.toString().toByteArray())
+// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 15, battery.getStatusName()?.uppercase()?.toByteArray())
+// }
+// }
+// }
+
+
+ // Sending broadcast for battery update
+ broadcastVendorSpecificEventIntent(
+ VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV,
+ APPLE,
+ BluetoothHeadset.AT_CMD_TYPE_SET,
+ batteryUnified,
+ batteryUnifiedArg,
+ device
+ )
+ }
+
+ @Suppress("SameParameterValue")
+ @SuppressLint("MissingPermission")
+ private fun broadcastVendorSpecificEventIntent(
+ command: String,
+ companyId: Int,
+ commandType: Int,
+ batteryUnified: Int,
+ batteryUnifiedArg: Int,
+ device: BluetoothDevice
+ ) {
+ val arguments = arrayOf(
+ 1, // Number of key(IndicatorType)/value pairs
+ VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL, // IndicatorType: Battery Level
+ batteryUnifiedArg // Battery Level
+ )
+
+ val intent = Intent(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT).apply {
+ putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, command)
+ putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, commandType)
+ putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arguments)
+ putExtra(BluetoothDevice.EXTRA_DEVICE, device)
+ putExtra(BluetoothDevice.EXTRA_NAME, device.name)
+ addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "." + companyId.toString())
+ }
+ sendBroadcast(intent)
+
+ val batteryIntent = Intent(ACTION_BATTERY_LEVEL_CHANGED).apply {
+ putExtra(BluetoothDevice.EXTRA_DEVICE, device)
+ putExtra(EXTRA_BATTERY_LEVEL, batteryUnified)
+ }
+ sendBroadcast(batteryIntent)
+
+ val statusIntent = Intent(ACTION_ASI_UPDATE_BLUETOOTH_DATA).setPackage(PACKAGE_ASI).apply {
+ putExtra(ACTION_BATTERY_LEVEL_CHANGED, intent)
+ }
+ sendBroadcast(statusIntent)
+ }
+
+
+ fun setName(name: String) {
+ val nameBytes = name.toByteArray()
+ val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
+ nameBytes.size.toByte(), 0x00) + nameBytes
+ socket?.outputStream?.write(bytes)
+ socket?.outputStream?.flush()
+ val hex = bytes.joinToString(" ") { "%02X".format(it) }
+ Log.d("AirPodsService", "setName: $name, sent packet: $hex")
+ }
+
@SuppressLint("MissingPermission", "InlinedApi")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -133,6 +305,7 @@ class AirPodsService : Service() {
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
+
socket = HiddenApiBypass.newInstance(BluetoothSocket::class.java, 3, true, true, device, 0x1001, uuid) as BluetoothSocket?
try {
socket?.connect()
@@ -150,8 +323,9 @@ class AirPodsService : Service() {
MediaController.initialize(audioManager)
val buffer = ByteArray(1024)
val bytesRead = it.inputStream.read(buffer)
- val data = buffer.copyOfRange(0, bytesRead)
+ var data: ByteArray = byteArrayOf()
if (bytesRead > 0) {
+ data = buffer.copyOfRange(0, bytesRead)
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
putExtra("data", buffer.copyOfRange(0, bytesRead))
})
@@ -159,6 +333,15 @@ class AirPodsService : Service() {
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
Log.d("AirPods Data", "Data received: $formattedHex")
}
+ else if (bytesRead == -1) {
+ Log.d("AirPods Service", "Socket closed (bytesRead = -1)")
+ this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
+ socket?.close()
+ sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
+ return@launch
+ }
+ var inEar = false
+ var inEarData = listOf()
if (earDetectionNotification.isEarDetectionData(data)) {
earDetectionNotification.setStatus(data)
sendBroadcast(Intent(AirPodsNotifications.EAR_DETECTION_DATA).apply {
@@ -169,7 +352,7 @@ class AirPodsService : Service() {
putExtra("data", bytes)
})
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
- var inEar = false
+ var justEnabledA2dp = false
val earReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val data = intent.getByteArrayExtra("data")
@@ -179,10 +362,52 @@ class AirPodsService : Service() {
} else {
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
}
- if (inEar) {
- MediaController.sendPlay()
+
+ val newInEarData = listOf(data[0] == 0x00.toByte(), data[1] == 0x00.toByte())
+ if (newInEarData.contains(true) && inEarData == listOf(false, false)) {
+ connectAudio(this@AirPodsService, device)
+ justEnabledA2dp = true
+ val bluetoothAdapter = this@AirPodsService.getSystemService(BluetoothManager::class.java).adapter
+ bluetoothAdapter.getProfileProxy(
+ this@AirPodsService, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(
+ profile: Int,
+ proxy: BluetoothProfile
+ ) {
+ if (profile == BluetoothProfile.A2DP) {
+ val connectedDevices =
+ proxy.connectedDevices
+ if (connectedDevices.isNotEmpty()) {
+ MediaController.sendPlay()
+ }
+ }
+ bluetoothAdapter.closeProfileProxy(
+ profile,
+ proxy
+ )
+ }
+
+ override fun onServiceDisconnected(
+ profile: Int
+ ) {
+ }
+ }
+ ,BluetoothProfile.A2DP
+ )
+
}
- else {
+ else if (newInEarData == listOf(false, false)){
+ disconnectAudio(this@AirPodsService, device)
+ }
+
+ inEarData = newInEarData
+
+ if (inEar == true) {
+ if (!justEnabledA2dp) {
+ justEnabledA2dp = false
+ MediaController.sendPlay()
+ }
+ } else {
MediaController.sendPause()
}
}
@@ -209,18 +434,21 @@ class AirPodsService : Service() {
for (battery in batteryNotification.getBattery()) {
Log.d("AirPods Parser", "${battery.getComponentName()}: ${battery.getStatusName()} at ${battery.level}% ")
}
+// updatePodsStatus(device!!, batteryNotification.getBattery())
}
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
conversationAwarenessNotification.setData(data)
sendBroadcast(Intent(AirPodsNotifications.CA_DATA).apply {
putExtra("data", conversationAwarenessNotification.status)
})
+
+
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
MediaController.startSpeaking()
- }
- else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
+ } else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
MediaController.stopSpeaking()
}
+
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
}
else { }
@@ -228,6 +456,9 @@ class AirPodsService : Service() {
}
Log.d("AirPods Service", "Socket closed")
isRunning = false
+ this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
+ socket?.close()
+ sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
}
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt
index e98d8c2..e815107 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt
@@ -59,6 +59,7 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.SolidColor
@@ -153,8 +154,9 @@ fun BatteryView() {
@Composable
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
navController: NavController) {
- var deviceName by remember { mutableStateOf(TextFieldValue(device?.name ?: "AirPods Pro (fallback, should never show up)")) }
-
+ val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ var deviceName by remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", device?.name ?: "") ?: "")) }
+// 4B 61 76 69 73 68 E2 80 99 73 20 41 69 72 50 6F 64 73 20 50 72 6F
val verticalScrollState = rememberScrollState()
Column(
modifier = Modifier
@@ -176,25 +178,29 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
})
}
}
- BatteryView()
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
if (service != null) {
+ BatteryView()
+
+ Spacer(modifier = Modifier.height(32.dp))
StyledTextField(
name = "Name",
value = deviceName.text,
- onValueChange = { deviceName = TextFieldValue(it) }
+ onValueChange = {
+ deviceName = TextFieldValue(it)
+ sharedPreferences.edit().putString("name", it).apply()
+ service.setName(it)
+ }
)
Spacer(modifier = Modifier.height(32.dp))
-
NoiseControlSettings(service = service)
Spacer(modifier = Modifier.height(16.dp))
AudioSettings(service = service, sharedPreferences = sharedPreferences)
Spacer(modifier = Modifier.height(16.dp))
-
IndependentToggle(name = "Automatic Ear Detection", service = service, functionName = "setEarDetection", sharedPreferences = sharedPreferences, true)
// Spacer(modifier = Modifier.height(16.dp))
@@ -217,7 +223,6 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
// }
Spacer(modifier = Modifier.height(16.dp))
-
Row (
modifier = Modifier
.background(if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp))
@@ -767,11 +772,17 @@ fun StyledTextField(
value: String,
onValueChange: (String) -> Unit
) {
+ var isFocused by remember { mutableStateOf(false) }
+
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
val backgroundColor = if (isDarkTheme) Color(0xFF1C1B20) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
- val cursorColor = if (isDarkTheme) Color.White else Color.Black
+ val cursorColor = if (isFocused) { // Show cursor only when focused
+ if (isDarkTheme) Color.White else Color.Black
+ } else {
+ Color.Transparent // Hide cursor when not focused
+ }
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -781,14 +792,14 @@ fun StyledTextField(
.background(
backgroundColor,
RoundedCornerShape(14.dp)
- ) // Dynamic background based on theme
+ )
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = name,
style = TextStyle(
fontSize = 16.sp,
- color = textColor // Text color based on theme
+ color = textColor
)
)
@@ -796,10 +807,10 @@ fun StyledTextField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(
- color = textColor, // Dynamic text color
+ color = textColor,
fontSize = 16.sp,
),
- cursorBrush = SolidColor(cursorColor), // Dynamic cursor color
+ cursorBrush = SolidColor(cursorColor), // Dynamic cursor color based on focus
decorationBox = { innerTextField ->
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -809,8 +820,11 @@ fun StyledTextField(
}
},
modifier = Modifier
- .fillMaxWidth() // Ensures text field takes remaining available space
- .padding(start = 8.dp), // Padding to adjust spacing between text field and icon,
+ .fillMaxWidth()
+ .padding(start = 8.dp)
+ .onFocusChanged { focusState ->
+ isFocused = focusState.isFocused // Update focus state
+ }
)
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt
index 4bd4995..bb4e936 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt
@@ -167,7 +167,7 @@ fun DebugScreen(navController: NavController) {
Row(
modifier = Modifier
.fillMaxWidth()
- .background(Color(0xFF1C1B20)),
+ .background(if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFF2F2F7)),
verticalAlignment = Alignment.CenterVertically
) {
val packet = remember { mutableStateOf(TextFieldValue("")) }
diff --git a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt
index fd3e609..bd76452 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt
@@ -1,13 +1,17 @@
package me.kavishdevar.aln
import android.annotation.SuppressLint
+import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
+import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
import android.content.ServiceConnection
+import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.os.ParcelUuid
@@ -30,6 +34,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -53,8 +58,8 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
-
setContent {
+ val topAppBarTitle = remember { mutableStateOf("AirPods Pro") }
ALNTheme {
Scaffold (
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
@@ -66,7 +71,7 @@ class MainActivity : ComponentActivity() {
CenterAlignedTopAppBar(
title = {
Text(
- text = "AirPods Pro Settings",
+ text = topAppBarTitle.value,
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black,
)
},
@@ -80,7 +85,7 @@ class MainActivity : ComponentActivity() {
)
}
) { innerPadding ->
- Main(innerPadding)
+ Main(innerPadding, topAppBarTitle)
}
}
}
@@ -90,7 +95,7 @@ class MainActivity : ComponentActivity() {
@SuppressLint("MissingPermission")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
-fun Main(paddingValues: PaddingValues) {
+fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState) {
val bluetoothConnectPermissionState = rememberPermissionState(
permission = "android.permission.BLUETOOTH_CONNECT"
)
@@ -100,38 +105,21 @@ fun Main(paddingValues: PaddingValues) {
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
val bluetoothManager = getSystemService(context, BluetoothManager::class.java)
val bluetoothAdapter = bluetoothManager?.adapter
- val devices = bluetoothAdapter?.bondedDevices
val airpodsDevice = remember { mutableStateOf(null) }
-
+ val airPodsService = remember { mutableStateOf(null) }
val navController = rememberNavController()
- if (devices != null) {
- for (device in devices) {
- if (device.uuids.contains(uuid)) {
- bluetoothAdapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
- override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
- if (profile == BluetoothProfile.A2DP) {
- val connectedDevices = proxy.connectedDevices
- if (connectedDevices.isNotEmpty()) {
- airpodsDevice.value = device
- if (context.getSystemService(AirPodsService::class.java) == null || context.getSystemService(AirPodsService::class.java)?.isRunning != true) {
- context.startService(Intent(context, AirPodsService::class.java).apply {
- putExtra("device", device)
- })
- }
- }
- }
- bluetoothAdapter.closeProfileProxy(profile, proxy)
- }
-
- override fun onServiceDisconnected(profile: Int) { }
- }, BluetoothProfile.A2DP)
- }
+ val disconnectReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ navController.navigate("notConnected")
}
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ context.registerReceiver(disconnectReceiver, IntentFilter(AirPodsNotifications.AIRPODS_DISCONNECTED),
+ Context.RECEIVER_NOT_EXPORTED)
+ }
- val airPodsService = remember { mutableStateOf(null) }
-
+ // Service connection for AirPodsService
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as AirPodsService.LocalBinder
@@ -144,16 +132,88 @@ fun Main(paddingValues: PaddingValues) {
}
}
- val intent = Intent(context, AirPodsService::class.java)
- context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
+ // Function to check if AirPods are connected
+ fun checkIfAirPodsConnected() {
+ val devices = bluetoothAdapter?.bondedDevices
+ devices?.forEach { device ->
+ if (device.uuids.contains(uuid)) {
+ bluetoothAdapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.A2DP) {
+ val connectedDevices = proxy.connectedDevices
+ if (connectedDevices.isNotEmpty()) {
+ airpodsDevice.value = device
+ val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ topAppBarTitle.value = sharedPreferences.getString("name", device.name) ?: device.name
+ // Start AirPods service if not running
+ if (context.getSystemService(AirPodsService::class.java)?.isRunning != true) {
+ context.startService(Intent(context, AirPodsService::class.java).apply {
+ putExtra("device", device)
+ })
+ context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+ } else {
+ airpodsDevice.value = null
+ }
+ }
+ bluetoothAdapter.closeProfileProxy(profile, proxy)
+ }
+
+ override fun onServiceDisconnected(profile: Int) {}
+ }, BluetoothProfile.A2DP)
+ }
+ }
+ }
+
+ // BroadcastReceiver to listen for connection state changes
+ val bluetoothReceiver = remember {
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val action = intent?.action
+ val device = intent?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ if (action == BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) {
+ when (intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1)) {
+ BluetoothAdapter.STATE_CONNECTED -> {
+ if (device?.uuids?.contains(uuid) == true) {
+ airpodsDevice.value = device
+ checkIfAirPodsConnected()
+ }
+ }
+ BluetoothAdapter.STATE_DISCONNECTED -> {
+ if (device?.uuids?.contains(uuid) == true) {
+ airpodsDevice.value = null
+ // Show not connected screen when AirPods disconnect
+ navController.navigate("notConnected")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Register the receiver in LaunchedEffect
+ LaunchedEffect(Unit) {
+ val filter = IntentFilter().apply {
+ addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ context.registerReceiver(bluetoothReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
+ }
+
+ // Initial check for AirPods connection
+ checkIfAirPodsConnected()
+ }
+
+ // UI logic
NavHost(
navController = navController,
startDestination = "notConnected",
- enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) }, // Slide in from the right
- exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) }, // Slide out to the left
- popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) }, // Slide in from the left
- popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) } // Slide out to the right
- ){
+ enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) },
+ exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) },
+ popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) },
+ popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) }
+ ) {
composable("notConnected") {
Text("Not Connected...")
}
@@ -169,17 +229,17 @@ fun Main(paddingValues: PaddingValues) {
DebugScreen(navController = navController)
}
}
+
+ // Automatically navigate to settings screen if AirPods are connected
if (airpodsDevice.value != null) {
LaunchedEffect(Unit) {
navController.navigate("settings") {
popUpTo("notConnected") { inclusive = true }
}
}
- }
- else {
+ } else {
Text("No AirPods connected")
}
- return
} else {
// Permission is not granted, request it
Column (
diff --git a/android/app/src/main/java/me/kavishdevar/aln/Packets.kt b/android/app/src/main/java/me/kavishdevar/aln/Packets.kt
index 62e7e62..82f3374 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/Packets.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/Packets.kt
@@ -67,6 +67,7 @@ class AirPodsNotifications {
const val ANC_DATA = "me.kavishdevar.aln.ANC_DATA"
const val BATTERY_DATA = "me.kavishdevar.aln.BATTERY_DATA"
const val CA_DATA = "me.kavishdevar.aln.CA_DATA"
+ const val AIRPODS_DISCONNECTED = "me.kavishdevar.aln.AIRPODS_DISCONNECTED"
}
class EarDetection {