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

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

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

@@ -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)),
DISABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0c, 0x00, 0x00, 0x00)), data class LongPressPacket(val modes: Set<LongPressMode>) {
DISABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)), val value: ByteArray
DISABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)), get() {
val baseArray = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A)
ENABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)), val modeByte = calculateModeByte()
ENABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)), return baseArray + byteArrayOf(modeByte, 0x00, 0x00, 0x00)
ENABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)), }
DISABLE_TRANSPARENCY_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)), private fun calculateModeByte(): Byte {
DISABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x03, 0x00, 0x00, 0x00)), var modeByte: Byte = 0x00
DISABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)), modes.forEach { mode ->
DISABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x09, 0x00, 0x00, 0x00)), modeByte = when (mode) {
LongPressMode.OFF -> (modeByte + 0x01).toByte()
ENABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)), LongPressMode.TRANSPARENCY -> (modeByte + 0x02).toByte()
ENABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)), LongPressMode.ADAPTIVE -> (modeByte + 0x04).toByte()
ENABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)), LongPressMode.ANC -> (modeByte + 0x08).toByte()
}
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)), return modeByte
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)), fun determinePacket(changedIndex: Int, newEnabled: Boolean, oldModes: Set<LongPressMode>, newModes: Set<LongPressMode>): ByteArray? {
ENABLE_ANC_FROM_ADAPTIVE_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)), return if (newEnabled) {
ENABLE_ANC_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)), LongPressPacket(oldModes + newModes.elementAt(changedIndex)).value
} else {
DISABLE_ADAPTIVE_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)), LongPressPacket(oldModes - newModes.elementAt(changedIndex)).value
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);