android: disable audio profiles when not in ear; add a debug screen

This commit is contained in:
Kavish Devar
2024-10-15 11:50:20 +05:30
parent f0c8a4965a
commit 4fd2717413
6 changed files with 388 additions and 64 deletions

View File

@@ -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<Battery>): 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<Battery>) {
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<Boolean>()
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))
}
}
}

View File

@@ -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
}
)
}
}

View File

@@ -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("")) }

View File

@@ -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<String>) {
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<BluetoothDevice?>(null) }
val airPodsService = remember { mutableStateOf<AirPodsService?>(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<AirPodsService?>(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>(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 (

View File

@@ -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 {