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

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