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

@@ -43,6 +43,17 @@
android:foregroundServiceType="connectedDevice"
android:permission="android.permission.BLUETOOTH_CONNECT" />
<service
android:name=".AirPodsQSService"
android:exported="true"
android:icon="@drawable/airpods"
android:label="ANC Mode"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<!-- <receiver android:name=".StartupReceiver"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->

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

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="43dp" android:viewportHeight="607.69" android:viewportWidth="902.34" android:width="63.849365dp">
<path android:fillAlpha="0" android:fillColor="#FF000000" android:pathData="M0,0h902.34v607.69h-902.34z" android:strokeAlpha="0"/>
<path android:fillAlpha="0.85" android:fillColor="#ffffff" android:pathData="M315.92,550.31C315.92,567.88 304.69,577.41 286.13,577.41L261.48,577.41C242.92,577.41 231.45,567.88 231.45,550.31L231.45,358.73C267.25,355.98 293.73,349.94 315.92,341.64ZM670.9,358.73L670.9,550.31C670.9,567.88 659.42,577.41 640.87,577.41L616.21,577.41C597.66,577.41 586.43,567.88 586.43,550.31L586.43,341.64C608.62,349.94 635.09,355.98 670.9,358.73Z"/>
<path android:fillAlpha="0.85" android:fillColor="#ffffff" android:pathData="M429.2,153.09C429.2,221.45 388.18,270.28 335.2,299.57C312.69,312 288.17,321.38 249.56,326.11C255.64,311.65 259.03,294.95 259.03,276.14C259.03,213.57 207.98,151 137.53,139.93C143.33,128.37 149.48,118.78 153.81,113.29C192.14,56.65 252.44,29.55 306.64,30.29C375.24,31.02 429.2,76.43 429.2,153.09ZM748.53,113.29C752.86,118.78 759.01,128.37 764.82,139.93C694.36,151 643.31,213.57 643.31,276.14C643.31,294.95 646.71,311.65 652.78,326.11C614.17,321.38 589.66,312 567.14,299.57C514.16,270.28 473.14,221.45 473.14,153.09C473.14,76.43 527.1,31.02 595.7,30.29C649.9,29.55 710.21,56.65 748.53,113.29ZM346.19,100.11L301.51,137.71C295.41,142.84 294.68,151.62 299.56,157.48C304.69,163.83 313.72,164.32 319.34,159.44L364.75,121.84C370.85,116.71 371.09,107.92 365.97,102.06C361.33,95.72 352.3,94.98 346.19,100.11ZM536.38,102.06C531.25,107.92 531.49,116.71 537.6,121.84L583.01,159.44C588.62,164.32 597.66,163.83 602.78,157.48C607.67,151.62 606.93,142.84 600.83,137.71L556.15,100.11C550.05,94.98 541.02,95.72 536.38,102.06Z"/>
<path android:fillAlpha="0.85" android:fillColor="#ffffff" android:pathData="M140.87,364.76C189.45,364.76 229.98,334.48 229.98,276.14C229.98,222.43 180.18,167.25 114.99,167.25C61.28,167.25 29.05,209.24 29.05,254.65C29.05,320.08 84.72,364.76 140.87,364.76ZM125.98,319.59C117.19,327.16 102.29,319.35 84.23,297.38C66.41,275.89 61.52,260.51 70.07,252.94C79.1,245.37 93.75,252.94 111.82,274.92C129.4,297.13 134.52,312.27 125.98,319.59ZM761.47,364.76C817.63,364.76 873.29,320.08 873.29,254.65C873.29,209.24 841.06,167.25 787.35,167.25C722.17,167.25 672.36,222.43 672.36,276.14C672.36,334.48 712.89,364.76 761.47,364.76ZM776.37,319.59C767.82,312.27 772.95,297.13 790.53,274.92C808.59,252.94 823.24,245.37 832.28,252.94C840.82,260.51 835.94,275.89 818.11,297.38C800.29,319.35 785.16,327.16 776.37,319.59Z"/>
</vector>