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