android: add QS Tile to change Noise Control Mode

This commit is contained in:
Kavish Devar
2024-10-19 19:48:12 +05:30
parent a8de72f190
commit 745040be2b
10 changed files with 190 additions and 136 deletions

View File

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

View File

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

View File

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

View File

@@ -146,7 +146,7 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
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<String>) {
@Composable
fun PreviewAirPodsSettingsScreen() {
AirPodsSettingsScreen(paddingValues = PaddingValues(0.dp), device = null, service = null, navController = rememberNavController())
}
}

View File

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

View File

@@ -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<ParcelUuid> = setOf(
ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"),
ParcelUuid.fromString("2a72e02b-7b99-778f-014d-ad0b7221ec74")
)
val btActions: Set<String> = 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)
}
}
}