mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-03 00:29:16 +00:00
android: add QS Tile to change Noise Control Mode
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user