diff --git a/android/app/release/baselineProfiles/0/app-release.dm b/android/app/release/baselineProfiles/0/app-release.dm
index 7d4a3cc..8999145 100644
Binary files a/android/app/release/baselineProfiles/0/app-release.dm and b/android/app/release/baselineProfiles/0/app-release.dm differ
diff --git a/android/app/release/baselineProfiles/1/app-release.dm b/android/app/release/baselineProfiles/1/app-release.dm
index 9a2b1b2..4bf5ed1 100644
Binary files a/android/app/release/baselineProfiles/1/app-release.dm and b/android/app/release/baselineProfiles/1/app-release.dm differ
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a9f9e93..57599da 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -43,6 +43,17 @@
android:foregroundServiceType="connectedDevice"
android:permission="android.permission.BLUETOOTH_CONNECT" />
+
+
+
+
+
+
diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt
new file mode 100644
index 0000000..c7562fc
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt
@@ -0,0 +1,90 @@
+package me.kavishdevar.aln
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import android.util.Log
+
+class AirPodsQSService: TileService() {
+ private val ancModes = listOf(NoiseControlMode.OFF.name, NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
+ private var currentModeIndex = 3
+ private lateinit var ancStatusReceiver: BroadcastReceiver
+ private lateinit var availabilityReceiver: BroadcastReceiver
+
+ @SuppressLint("InlinedApi")
+ override fun onStartListening() {
+ super.onStartListening()
+ currentModeIndex = (ServiceManager.getService()?.getANC()?.minus(1)) ?: 3
+ if (currentModeIndex == -1) {
+ currentModeIndex = 3
+ }
+
+ if (ServiceManager.getService() == null) {
+ qsTile.state = Tile.STATE_UNAVAILABLE
+ qsTile.updateTile()
+ }
+ if (ServiceManager.getService()?.isConnected == true) {
+ qsTile.state = Tile.STATE_ACTIVE
+ qsTile.updateTile()
+ }
+ else {
+ qsTile.state = Tile.STATE_UNAVAILABLE
+ qsTile.updateTile()
+ }
+
+ ancStatusReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val ancStatus = intent.getIntExtra("data", 4)
+ currentModeIndex = ancStatus - 1
+ updateTile()
+ }
+ }
+
+ availabilityReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == AirPodsNotifications.AIRPODS_CONNECTED) {
+ qsTile.state = Tile.STATE_ACTIVE
+ qsTile.updateTile()
+ }
+ else if (intent.action == AirPodsNotifications.AIRPODS_DISCONNECTED) {
+ qsTile.state = Tile.STATE_UNAVAILABLE
+ qsTile.updateTile()
+ }
+ }
+ }
+
+ registerReceiver(ancStatusReceiver, IntentFilter(AirPodsNotifications.ANC_DATA), RECEIVER_EXPORTED)
+ updateTile()
+ }
+
+ override fun onStopListening() {
+ super.onStopListening()
+ unregisterReceiver(ancStatusReceiver)
+ unregisterReceiver(availabilityReceiver)
+ }
+
+ override fun onClick() {
+ super.onClick()
+ Log.d("QuickSettingTileService", "ANC tile clicked")
+ currentModeIndex = (currentModeIndex + 1) % ancModes.size
+ switchAncMode(currentModeIndex)
+ }
+
+ private fun updateTile() {
+ val currentMode = ancModes[currentModeIndex]
+ qsTile.label = currentMode.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() }
+ qsTile.state = Tile.STATE_ACTIVE
+ qsTile.updateTile()
+ }
+
+ private fun switchAncMode(modeIndex: Int) {
+ currentModeIndex = modeIndex
+ val airPodsService = ServiceManager.getService()
+ airPodsService?.setANCMode(currentModeIndex + 1)
+ updateTile()
+ }
+}
\ 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 9a5cf14..a7a51c8 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
@@ -36,6 +36,19 @@ private const val ACTION_ASI_UPDATE_BLUETOOTH_DATA = "batterywidget.impl.action.
//private const val COMPANION_TYPE_NONE = "COMPANION_NONE"
//const val VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID"
+
+object ServiceManager {
+ private var service: AirPodsService? = null
+ @Synchronized
+ fun getService(): AirPodsService? {
+ return service
+ }
+ @Synchronized
+ fun setService(service: AirPodsService?) {
+ this.service = service
+ }
+}
+
class AirPodsService : Service() {
inner class LocalBinder : Binder() {
fun getService(): AirPodsService = this@AirPodsService
@@ -45,7 +58,8 @@ class AirPodsService : Service() {
return LocalBinder()
}
- var isRunning: Boolean = false
+
+ var isConnected: Boolean = false
private var socket: BluetoothSocket? = null
fun sendPacket(packet: String) {
@@ -211,7 +225,7 @@ class AirPodsService : Service() {
var batteryUnified = 0
var batteryUnifiedArg = 0
- // Handle each Battery object from batteryList
+ // Handle each Battery object from batteryList
// batteryList.forEach { battery ->
// when (battery.getComponentName()) {
// "LEFT" -> {
@@ -296,10 +310,12 @@ class AirPodsService : Service() {
val notification = createNotification()
startForeground(1, notification)
- if (isRunning) {
+ ServiceManager.setService(this)
+
+ if (isConnected) {
return START_STICKY
}
- isRunning = true
+ isConnected = true
@Suppress("DEPRECATION") val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) intent?.getParcelableExtra("device", BluetoothDevice::class.java) else intent?.getParcelableExtra("device")
@@ -354,65 +370,65 @@ class AirPodsService : Service() {
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
var justEnabledA2dp = false
val earReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val data = intent.getByteArrayExtra("data")
- if (data != null && earDetectionEnabled) {
- inEar = if (data.find { it == 0x02.toByte() } != null || data.find { it == 0x03.toByte() } != null) {
- data[0] == 0x00.toByte() || data[1] == 0x00.toByte()
- } else {
- data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
- }
+ override fun onReceive(context: Context, intent: Intent) {
+ val data = intent.getByteArrayExtra("data")
+ if (data != null && earDetectionEnabled) {
+ inEar = if (data.find { it == 0x02.toByte() } != null || data.find { it == 0x03.toByte() } != null) {
+ data[0] == 0x00.toByte() || data[1] == 0x00.toByte()
+ } else {
+ data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
+ }
- 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()
- }
+ 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
- ) {
}
+ bluetoothAdapter.closeProfileProxy(
+ profile,
+ proxy
+ )
}
- ,BluetoothProfile.A2DP
- )
- }
- else if (newInEarData == listOf(false, false)){
- disconnectAudio(this@AirPodsService, device)
- }
-
- inEarData = newInEarData
-
- if (inEar == true) {
- if (!justEnabledA2dp) {
- justEnabledA2dp = false
- MediaController.sendPlay()
+ override fun onServiceDisconnected(
+ profile: Int
+ ) {
+ }
}
- } else {
- MediaController.sendPause()
+ ,BluetoothProfile.A2DP
+ )
+
+ }
+ else if (newInEarData == listOf(false, false)){
+ disconnectAudio(this@AirPodsService, device)
+ }
+
+ inEarData = newInEarData
+
+ if (inEar == true) {
+ if (!justEnabledA2dp) {
+ justEnabledA2dp = false
+ MediaController.sendPlay()
}
+ } else {
+ MediaController.sendPause()
}
}
}
+ }
val earIntentFilter = IntentFilter(AirPodsNotifications.EAR_DETECTION_DATA)
this@AirPodsService.registerReceiver(earReceiver, earIntentFilter,
@@ -455,7 +471,7 @@ class AirPodsService : Service() {
}
}
Log.d("AirPods Service", "Socket closed")
- isRunning = false
+ isConnected = false
this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
socket?.close()
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
@@ -471,6 +487,7 @@ class AirPodsService : Service() {
override fun onDestroy() {
super.onDestroy()
socket?.close()
- isRunning = false
+ isConnected = false
+ ServiceManager.setService(null)
}
}
\ No newline at end of file
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 e815107..4e01f6e 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt
@@ -729,10 +729,6 @@ fun NoiseControlButton(
}
}
-enum class NoiseControlMode {
- OFF, NOISE_CANCELLATION, TRANSPARENCY, ADAPTIVE
-}
-
@Composable
fun StyledSwitch(
checked: Boolean,
@@ -802,7 +798,6 @@ fun StyledTextField(
color = textColor
)
)
-
BasicTextField(
value = value,
onValueChange = onValueChange,
@@ -810,6 +805,7 @@ fun StyledTextField(
color = textColor,
fontSize = 16.sp,
),
+ singleLine = true,
cursorBrush = SolidColor(cursorColor), // Dynamic cursor color based on focus
decorationBox = { innerTextField ->
Row(
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 bd76452..153d7e6 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt
@@ -146,7 +146,7 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState) {
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) {
+ if (context.getSystemService(AirPodsService::class.java)?.isConnected != true) {
context.startService(Intent(context, AirPodsService::class.java).apply {
putExtra("device", device)
})
@@ -264,4 +264,4 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState) {
@Composable
fun PreviewAirPodsSettingsScreen() {
AirPodsSettingsScreen(paddingValues = PaddingValues(0.dp), device = null, service = null, navController = rememberNavController())
-}
+}
\ No newline at end of file
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 82f3374..d45ab0a 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/Packets.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/Packets.kt
@@ -59,6 +59,10 @@ data class Battery(val component: Int, val level: Int, val status: Int) : Parcel
}
}
+enum class NoiseControlMode {
+ OFF, NOISE_CANCELLATION, TRANSPARENCY, ADAPTIVE
+}
+
class AirPodsNotifications {
companion object {
const val AIRPODS_CONNECTED = "me.kavishdevar.aln.AIRPODS_CONNECTED"
diff --git a/android/app/src/main/java/me/kavishdevar/aln/StartupReceiver.kt b/android/app/src/main/java/me/kavishdevar/aln/StartupReceiver.kt
deleted file mode 100644
index 9cfe896..0000000
--- a/android/app/src/main/java/me/kavishdevar/aln/StartupReceiver.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package me.kavishdevar.aln
-
-import android.annotation.SuppressLint
-import android.bluetooth.BluetoothAdapter
-import android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothProfile
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.os.ParcelUuid
-
-class StartupReceiver : BroadcastReceiver() {
-
- companion object {
- val PodsUUIDS: Set = setOf(
- ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"),
- ParcelUuid.fromString("2a72e02b-7b99-778f-014d-ad0b7221ec74")
- )
-
- val btActions: Set = setOf(
- BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED,
- BluetoothDevice.ACTION_ACL_CONNECTED,
- BluetoothDevice.ACTION_ACL_DISCONNECTED,
- BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothDevice.ACTION_NAME_CHANGED
- )
- }
-
- override fun onReceive(context: Context?, intent: Intent?) {
- if (intent == null || context == null) return
-
- intent.action?.let { action ->
- if (btActions.contains(action)) {
- try {
- val state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR)
- val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
- device?.let {
- btProfileChanges(context, state, it)
- }
- } catch (e: NullPointerException) {
- }
- }
- }
- }
-
- @SuppressLint("MissingPermission")
- private fun isPods(device: BluetoothDevice): Boolean {
- device.uuids?.forEach { uuid ->
- if (PodsUUIDS.contains(uuid)) {
- return true
- }
- }
- return false
- }
-
- private fun startPodsService(context: Context, device: BluetoothDevice) {
- if (!isPods(device)) return
- val intent = Intent(context, AirPodsService::class.java).apply {
- putExtra(BluetoothDevice.EXTRA_DEVICE, device)
- }
- context.startService(intent)
- }
-
- private fun stopPodsService(context: Context) {
- context.stopService(Intent(context, AirPodsService::class.java))
- }
-
- private fun btProfileChanges(context: Context, state: Int, device: BluetoothDevice) {
- when (state) {
- BluetoothProfile.STATE_CONNECTED -> startPodsService(context, device)
- BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING -> stopPodsService(context)
- }
- }
-}
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/airpods.xml b/android/app/src/main/res/drawable/airpods.xml
new file mode 100644
index 0000000..8f3abdc
--- /dev/null
+++ b/android/app/src/main/res/drawable/airpods.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file