Merge branch 'main' into update-gitignore

This commit is contained in:
Kavish Devar
2025-02-14 23:38:55 +05:30
committed by GitHub
6 changed files with 158 additions and 274 deletions

13
.gitignore vendored
View File

@@ -1,5 +1,14 @@
# Created by https://www.toptal.com/developers/gitignore/api/qt,c++,clion,kotlin,python,android,pycharm,androidstudio,visualstudiocode,linux
# Edit at https://www.toptal.com/developers/gitignore?templates=qt,c++,clion,kotlin,python,android,pycharm,androidstudio,visualstudiocode,linux wak.toml
log.txt
btl2capfix.zip
root-module-manual
.vscode
testing.py
.DS_Store
CMakeLists.txt.user*
# Android Template
### Android ### ### Android ###
# Gradle files # Gradle files

View File

@@ -1,17 +1,17 @@
/* /*
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality! * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
* *
* Copyright (C) 2024 Kavish Devar * Copyright (C) 2024 Kavish Devar
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published * it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License. * by the Free Software Foundation, either version 3 of the License.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@@ -216,7 +216,7 @@ fun LongPressElement(name: String, checked: MutableState<Boolean>, id: String, e
sharedPreferences.getBoolean("long_press_adaptive", false) sharedPreferences.getBoolean("long_press_adaptive", false)
) )
ServiceManager.getService() ServiceManager.getService()
?.updateLongPress(originalLongPressArray, newLongPressArray, offListeningMode) ?.updateLongPress(originalLongPressArray, newLongPressArray)
} }
val shape = when { val shape = when {
isFirst -> RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp) isFirst -> RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
@@ -294,4 +294,4 @@ fun LongPressElement(name: String, checked: MutableState<Boolean>, id: String, e
) )
} }
} }
} }

View File

@@ -26,15 +26,10 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -62,12 +57,9 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.edit import androidx.core.content.edit
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.kavishdevar.aln.MainActivity import me.kavishdevar.aln.MainActivity
import me.kavishdevar.aln.R import me.kavishdevar.aln.R
@@ -80,9 +72,10 @@ import me.kavishdevar.aln.utils.CrossDevicePackets
import me.kavishdevar.aln.utils.Enums import me.kavishdevar.aln.utils.Enums
import me.kavishdevar.aln.utils.IslandType import me.kavishdevar.aln.utils.IslandType
import me.kavishdevar.aln.utils.IslandWindow import me.kavishdevar.aln.utils.IslandWindow
import me.kavishdevar.aln.utils.LongPressPackets import me.kavishdevar.aln.utils.LongPressMode
import me.kavishdevar.aln.utils.MediaController import me.kavishdevar.aln.utils.MediaController
import me.kavishdevar.aln.utils.PopupWindow import me.kavishdevar.aln.utils.PopupWindow
import me.kavishdevar.aln.utils.determinePacket
import me.kavishdevar.aln.widgets.BatteryWidget import me.kavishdevar.aln.widgets.BatteryWidget
import me.kavishdevar.aln.widgets.NoiseControlWidget import me.kavishdevar.aln.widgets.NoiseControlWidget
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
@@ -260,42 +253,6 @@ class AirPodsService : Service() {
updateBatteryWidget() updateBatteryWidget()
} }
@SuppressLint("MissingPermission")
fun scanForAirPods(bluetoothAdapter: BluetoothAdapter): Flow<List<ScanResult>> = callbackFlow {
val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
?: throw IllegalStateException("Bluetooth adapter unavailable")
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
if (result.device != null) {
trySend(listOf(result))
}
}
override fun onBatchScanResults(results: List<ScanResult>) {
trySend(results)
}
override fun onScanFailed(errorCode: Int) {
close(Exception("Scan failed with error: $errorCode"))
}
}
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val scanFilters = listOf<ScanFilter>(
ScanFilter.Builder()
.setManufacturerData(0x004C, byteArrayOf())
.build()
)
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback)
awaitClose { bluetoothLeScanner.stopScan(scanCallback) }
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
fun startForegroundNotification() { fun startForegroundNotification() {
val notificationChannel = NotificationChannel( val notificationChannel = NotificationChannel(
@@ -718,29 +675,6 @@ class AirPodsService : Service() {
} }
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
if (bluetoothAdapter.isEnabled) {
CoroutineScope(Dispatchers.IO).launch {
var lastData = byteArrayOf()
scanForAirPods(bluetoothAdapter).collect { scanResults ->
scanResults.forEach { scanResult ->
val device = scanResult.device
device.fetchUuidsWithSdp()
val manufacturerData =
scanResult.scanRecord?.manufacturerSpecificData?.get(0x004C)
if (manufacturerData != null && manufacturerData != lastData) {
lastData = manufacturerData
val formattedHex =
manufacturerData.joinToString(" ") { "%02X".format(it) }
val rssi = scanResult.rssi
Log.d(
"AirPodsBLEService",
"Received broadcast of size ${manufacturerData.size} from ${device.address} | $rssi | $formattedHex"
)
}
}
}
}
}
bluetoothAdapter.bondedDevices.forEach { device -> bluetoothAdapter.bondedDevices.forEach { device ->
device.fetchUuidsWithSdp() device.fetchUuidsWithSdp()
@@ -1457,136 +1391,27 @@ class AirPodsService : Service() {
fun updateLongPress( fun updateLongPress(
oldLongPressArray: BooleanArray, oldLongPressArray: BooleanArray,
newLongPressArray: BooleanArray, newLongPressArray: BooleanArray,
offListeningMode: Boolean
) { ) {
if (oldLongPressArray.contentEquals(newLongPressArray)) { if (oldLongPressArray.contentEquals(newLongPressArray)) {
return return
} }
val oldOffEnabled = oldLongPressArray[0] val oldModes = mutableSetOf<LongPressMode>()
val oldAncEnabled = oldLongPressArray[1] val newModes = mutableSetOf<LongPressMode>()
val oldTransparencyEnabled = oldLongPressArray[2]
val oldAdaptiveEnabled = oldLongPressArray[3]
val newOffEnabled = newLongPressArray[0] if (oldLongPressArray[0]) oldModes.add(LongPressMode.OFF)
val newAncEnabled = newLongPressArray[1] if (oldLongPressArray[1]) oldModes.add(LongPressMode.ANC)
val newTransparencyEnabled = newLongPressArray[2] if (oldLongPressArray[2]) oldModes.add(LongPressMode.TRANSPARENCY)
val newAdaptiveEnabled = newLongPressArray[3] if (oldLongPressArray[3]) oldModes.add(LongPressMode.ADAPTIVE)
if (newLongPressArray[0]) newModes.add(LongPressMode.OFF)
if (newLongPressArray[1]) newModes.add(LongPressMode.ANC)
if (newLongPressArray[2]) newModes.add(LongPressMode.TRANSPARENCY)
if (newLongPressArray[3]) newModes.add(LongPressMode.ADAPTIVE)
val changedIndex = findChangedIndex(oldLongPressArray, newLongPressArray) val changedIndex = findChangedIndex(oldLongPressArray, newLongPressArray)
Log.d("AirPodsService", "changedIndex: $changedIndex") val newEnabled = newLongPressArray[changedIndex]
var packet: ByteArray? = null
if (offListeningMode) {
packet = when (changedIndex) {
0 -> {
if (newOffEnabled) {
when {
oldAncEnabled && oldTransparencyEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_EVERYTHING.value
oldAncEnabled && oldTransparencyEnabled -> LongPressPackets.ENABLE_OFF_FROM_TRANSPARENCY_AND_ANC.value
oldAncEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_OFF_FROM_ADAPTIVE_AND_ANC.value
oldTransparencyEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE.value
else -> null
}
} else {
when {
oldAncEnabled && oldTransparencyEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_OFF_FROM_EVERYTHING.value
oldAncEnabled && oldTransparencyEnabled -> LongPressPackets.DISABLE_OFF_FROM_TRANSPARENCY_AND_ANC.value
oldAncEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_OFF_FROM_ADAPTIVE_AND_ANC.value
oldTransparencyEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE.value
else -> null
}
}
}
1 -> { val packet = determinePacket(changedIndex, newEnabled, oldModes, newModes)
if (newAncEnabled) {
when {
oldOffEnabled && oldTransparencyEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_EVERYTHING.value
oldOffEnabled && oldTransparencyEnabled -> LongPressPackets.ENABLE_ANC_FROM_OFF_AND_TRANSPARENCY.value
oldOffEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_ANC_FROM_OFF_AND_ADAPTIVE.value
oldTransparencyEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE.value
else -> null
}
} else {
when {
oldOffEnabled && oldTransparencyEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_ANC_FROM_EVERYTHING.value
oldOffEnabled && oldTransparencyEnabled -> LongPressPackets.DISABLE_ANC_FROM_OFF_AND_TRANSPARENCY.value
oldOffEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_ANC_FROM_OFF_AND_ADAPTIVE.value
oldTransparencyEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE.value
else -> null
}
}
}
2 -> {
if (newTransparencyEnabled) {
when {
oldOffEnabled && oldAncEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_EVERYTHING.value
oldOffEnabled && oldAncEnabled -> LongPressPackets.ENABLE_TRANSPARENCY_FROM_OFF_AND_ANC.value
oldOffEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE.value
oldAncEnabled && oldAdaptiveEnabled -> LongPressPackets.ENABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC.value
else -> null
}
} else {
when {
oldOffEnabled && oldAncEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_TRANSPARENCY_FROM_EVERYTHING.value
oldOffEnabled && oldAncEnabled -> LongPressPackets.DISABLE_TRANSPARENCY_FROM_OFF_AND_ANC.value
oldOffEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE.value
oldAncEnabled && oldAdaptiveEnabled -> LongPressPackets.DISABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC.value
else -> null
}
}
}
3 -> {
if (newAdaptiveEnabled) {
when {
oldOffEnabled && oldAncEnabled && oldTransparencyEnabled -> LongPressPackets.ENABLE_EVERYTHING.value
oldOffEnabled && oldAncEnabled -> LongPressPackets.ENABLE_ADAPTIVE_FROM_OFF_AND_ANC.value
oldOffEnabled && oldTransparencyEnabled -> LongPressPackets.ENABLE_ADAPTIVE_FROM_OFF_AND_TRANSPARENCY.value
oldAncEnabled && oldTransparencyEnabled -> LongPressPackets.ENABLE_ADAPTIVE_FROM_TRANSPARENCY_AND_ANC.value
else -> null
}
} else {
when {
oldOffEnabled && oldAncEnabled && oldTransparencyEnabled -> LongPressPackets.DISABLE_ADAPTIVE_FROM_EVERYTHING.value
oldOffEnabled && oldAncEnabled -> LongPressPackets.DISABLE_ADAPTIVE_FROM_OFF_AND_ANC.value
oldOffEnabled && oldTransparencyEnabled -> LongPressPackets.DISABLE_ADAPTIVE_FROM_OFF_AND_TRANSPARENCY.value
oldAncEnabled && oldTransparencyEnabled -> LongPressPackets.DISABLE_ADAPTIVE_FROM_TRANSPARENCY_AND_ANC.value
else -> null
}
}
}
else -> null
}
} else {
when (changedIndex) {
1 -> {
packet = if (newLongPressArray[1]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
} else {
LongPressPackets.DISABLE_ANC_OFF_DISABLED.value
}
}
2 -> {
packet = if (newLongPressArray[2]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
} else {
LongPressPackets.DISABLE_TRANSPARENCY_OFF_DISABLED.value
}
}
3 -> {
packet = if (newLongPressArray[3]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
} else {
LongPressPackets.DISABLE_ADAPTIVE_OFF_DISABLED.value
}
}
}
}
packet?.let { packet?.let {
Log.d("AirPodsService", "Sending packet: ${it.joinToString(" ") { "%02X".format(it) }}") Log.d("AirPodsService", "Sending packet: ${it.joinToString(" ") { "%02X".format(it) }}")
sendPacket(it) sendPacket(it)

View File

@@ -1,17 +1,17 @@
/* /*
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality! * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
* *
* Copyright (C) 2024 Kavish Devar * Copyright (C) 2024 Kavish Devar
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published * it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License. * by the Free Software Foundation, either version 3 of the License.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@@ -241,47 +241,36 @@ class Capabilities {
} }
} }
enum class LongPressPackets(val value: ByteArray) { enum class LongPressMode {
ENABLE_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0F, 0x00, 0x00, 0x00)), OFF, TRANSPARENCY, ADAPTIVE, ANC
}
DISABLE_OFF_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)), data class LongPressPacket(val modes: Set<LongPressMode>) {
DISABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0c, 0x00, 0x00, 0x00)), val value: ByteArray
DISABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)), get() {
DISABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)), val baseArray = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A)
val modeByte = calculateModeByte()
return baseArray + byteArrayOf(modeByte, 0x00, 0x00, 0x00)
}
ENABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)), private fun calculateModeByte(): Byte {
ENABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)), var modeByte: Byte = 0x00
ENABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)), modes.forEach { mode ->
modeByte = when (mode) {
LongPressMode.OFF -> (modeByte + 0x01).toByte()
LongPressMode.TRANSPARENCY -> (modeByte + 0x02).toByte()
LongPressMode.ADAPTIVE -> (modeByte + 0x04).toByte()
LongPressMode.ANC -> (modeByte + 0x08).toByte()
}
}
return modeByte
}
}
DISABLE_TRANSPARENCY_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)), fun determinePacket(changedIndex: Int, newEnabled: Boolean, oldModes: Set<LongPressMode>, newModes: Set<LongPressMode>): ByteArray? {
DISABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x03, 0x00, 0x00, 0x00)), return if (newEnabled) {
DISABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)), LongPressPacket(oldModes + newModes.elementAt(changedIndex)).value
DISABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x09, 0x00, 0x00, 0x00)), } else {
LongPressPacket(oldModes - newModes.elementAt(changedIndex)).value
ENABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)), }
ENABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)), }
ENABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)),
DISABLE_ANC_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0D, 0x00, 0x00, 0x00)),
DISABLE_ANC_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x05, 0x00, 0x00, 0x00)),
DISABLE_ANC_FROM_ADAPTIVE_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0c, 0x00, 0x00, 0x00)),
DISABLE_ANC_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x09, 0x00, 0x00, 0x00)),
ENABLE_ANC_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
ENABLE_ANC_FROM_ADAPTIVE_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
ENABLE_ANC_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x05, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x03, 0x00, 0x00, 0x00)),
ENABLE_ADAPTIVE_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)),
ENABLE_ADAPTIVE_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
ENABLE_ADAPTIVE_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
ENABLE_EVERYTHING_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0E, 0x00, 0x00, 0x00)),
DISABLE_TRANSPARENCY_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0A, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)),
DISABLE_ANC_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0C, 0x00, 0x00, 0x00)),
}

50
linux/README.md Normal file
View File

@@ -0,0 +1,50 @@
# AirPods Linux Native (ALN)
A native Linux application to control your AirPods, with support for:
- Noise Control modes (Off, Transparency, Adaptive, Noise Cancellation)
- Conversational Awareness
- Battery monitoring
- Auto play/pause on ear detection
- Seamless handoff between phone and PC
## Prerequisites
1. Your phone's Bluetooth MAC address (can be found in Settings > About Device)
2. Qt6 packages
```bash
sudo pacman -S qt6-base qt6-connectivity qt6-multimedia-ffmpeg qt6-multimedia # Arch Linux / EndeavourOS
```
## Setup
1. Edit `main.h` and update `PHONE_MAC_ADDRESS` with your phone's Bluetooth MAC address:
```cpp
#define PHONE_MAC_ADDRESS "XX:XX:XX:XX:XX:XX" // Replace with your phone's MAC
```
2. Build the application:
```bash
mkdir build
cd build
cmake ..
make -j $(nproc)
```
3. Run the application:
```bash
./applinux
```
## Usage
- Left-click the tray icon to view battery status
- Right-click to access the control menu:
- Toggle Conversational Awareness
- Switch between noise control modes
- View battery levels
- Control playback

View File

@@ -52,6 +52,18 @@ class AirPodsTrayApp : public QObject {
Q_OBJECT Q_OBJECT
public: public:
enum NoiseControlMode : quint8
{
Off = 0,
NoiseCancellation = 1,
Transparency = 2,
Adaptive = 3,
MinValue = Off,
MaxValue = Adaptive,
};
Q_ENUM(NoiseControlMode)
AirPodsTrayApp(bool debugMode) : debugMode(debugMode) { AirPodsTrayApp(bool debugMode) : debugMode(debugMode) {
if (debugMode) { if (debugMode) {
QLoggingCategory::setFilterRules("airpodsApp.debug=true"); QLoggingCategory::setFilterRules("airpodsApp.debug=true");
@@ -79,6 +91,11 @@ public:
QAction *adaptiveAction = new QAction("Adaptive", trayMenu); QAction *adaptiveAction = new QAction("Adaptive", trayMenu);
QAction *noiseCancellationAction = new QAction("Noise Cancellation", trayMenu); QAction *noiseCancellationAction = new QAction("Noise Cancellation", trayMenu);
offAction->setData(NoiseControlMode::Off);
transparencyAction->setData(NoiseControlMode::Transparency);
adaptiveAction->setData(NoiseControlMode::Adaptive);
noiseCancellationAction->setData(NoiseControlMode::NoiseCancellation);
offAction->setCheckable(true); offAction->setCheckable(true);
transparencyAction->setCheckable(true); transparencyAction->setCheckable(true);
adaptiveAction->setCheckable(true); adaptiveAction->setCheckable(true);
@@ -95,10 +112,14 @@ public:
noiseControlGroup->addAction(adaptiveAction); noiseControlGroup->addAction(adaptiveAction);
noiseControlGroup->addAction(noiseCancellationAction); noiseControlGroup->addAction(noiseCancellationAction);
connect(offAction, &QAction::triggered, this, [this]() { setNoiseControlMode(0); }); connect(offAction, &QAction::triggered, this, [this]()
connect(transparencyAction, &QAction::triggered, this, [this]() { setNoiseControlMode(2); }); { setNoiseControlMode(NoiseControlMode::Off); });
connect(adaptiveAction, &QAction::triggered, this, [this]() { setNoiseControlMode(3); }); connect(transparencyAction, &QAction::triggered, this, [this]()
connect(noiseCancellationAction, &QAction::triggered, this, [this]() { setNoiseControlMode(1); }); { setNoiseControlMode(NoiseControlMode::Transparency); });
connect(adaptiveAction, &QAction::triggered, this, [this]()
{ setNoiseControlMode(NoiseControlMode::Adaptive); });
connect(noiseCancellationAction, &QAction::triggered, this, [this]()
{ setNoiseControlMode(NoiseControlMode::NoiseCancellation); });
connect(this, &AirPodsTrayApp::noiseControlModeChanged, this, &AirPodsTrayApp::updateNoiseControlMenu); connect(this, &AirPodsTrayApp::noiseControlModeChanged, this, &AirPodsTrayApp::updateNoiseControlMenu);
connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateBatteryTooltip); connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateBatteryTooltip);
@@ -274,20 +295,20 @@ public slots:
} }
} }
void setNoiseControlMode(int mode) { void setNoiseControlMode(NoiseControlMode mode) {
LOG_INFO("Setting noise control mode to: " << mode); LOG_INFO("Setting noise control mode to: " << mode);
QByteArray packet; QByteArray packet;
switch (mode) { switch (mode) {
case 0: case Off:
packet = QByteArray::fromHex("0400040009000D01000000"); packet = QByteArray::fromHex("0400040009000D01000000");
break; break;
case 1: case NoiseCancellation:
packet = QByteArray::fromHex("0400040009000D02000000"); packet = QByteArray::fromHex("0400040009000D02000000");
break; break;
case 2: case Transparency:
packet = QByteArray::fromHex("0400040009000D03000000"); packet = QByteArray::fromHex("0400040009000D03000000");
break; break;
case 3: case Adaptive:
packet = QByteArray::fromHex("0400040009000D04000000"); packet = QByteArray::fromHex("0400040009000D04000000");
break; break;
} }
@@ -310,24 +331,10 @@ public slots:
} }
} }
void updateNoiseControlMenu(int mode) { void updateNoiseControlMenu(NoiseControlMode mode) {
QList<QAction *> actions = trayMenu->actions(); QList<QAction *> actions = trayMenu->actions();
for (QAction *action : actions) { for (QAction *action : actions) {
action->setChecked(false); action->setChecked(action->data().toInt() == mode);
}
switch (mode) {
case 0:
actions[0]->setChecked(true);
break;
case 1:
actions[3]->setChecked(true);
break;
case 2:
actions[1]->setChecked(true);
break;
case 3:
actions[2]->setChecked(true);
break;
} }
} }
@@ -591,12 +598,16 @@ public slots:
void parseData(const QByteArray &data) { void parseData(const QByteArray &data) {
LOG_DEBUG("Received: " << data.toHex()); LOG_DEBUG("Received: " << data.toHex());
if (data.size() == 11 && data.startsWith(QByteArray::fromHex("0400040009000D"))) { if (data.size() == 11 && data.startsWith(QByteArray::fromHex("0400040009000D"))) {
int mode = data[7] - 1; quint8 rawMode = data[7] - 1;
LOG_INFO("Noise control mode: " << mode); if (rawMode >= NoiseControlMode::MinValue && rawMode <= NoiseControlMode::MaxValue)
if (mode >= 0 && mode <= 3) { {
NoiseControlMode mode = static_cast<NoiseControlMode>(rawMode);
LOG_INFO("Noise control mode: " << rawMode);
emit noiseControlModeChanged(mode); emit noiseControlModeChanged(mode);
} else { }
LOG_ERROR("Invalid noise control mode value received: " << mode); else
{
LOG_ERROR("Invalid noise control mode value received: " << rawMode);
} }
} else if (data.size() == 8 && data.startsWith(QByteArray::fromHex("040004000600"))) { } else if (data.size() == 8 && data.startsWith(QByteArray::fromHex("040004000600"))) {
char primary = data[6]; char primary = data[6];
@@ -887,7 +898,7 @@ public slots:
} }
signals: signals:
void noiseControlModeChanged(int mode); void noiseControlModeChanged(NoiseControlMode mode);
void earDetectionStatusChanged(const QString &status); void earDetectionStatusChanged(const QString &status);
void batteryStatusChanged(const QString &status); void batteryStatusChanged(const QString &status);