diff --git a/.gitignore b/.gitignore index 243069b..384a986 100644 --- a/.gitignore +++ b/.gitignore @@ -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 ### # Gradle files diff --git a/android/app/src/main/java/me/kavishdevar/aln/screens/PressAndHoldSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/PressAndHoldSettingsScreen.kt index fb83f3a..f37fcc9 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/screens/PressAndHoldSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/PressAndHoldSettingsScreen.kt @@ -1,17 +1,17 @@ /* * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality! - * + * * Copyright (C) 2024 Kavish Devar - * + * * 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 * by the Free Software Foundation, either version 3 of the License. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -216,7 +216,7 @@ fun LongPressElement(name: String, checked: MutableState, id: String, e sharedPreferences.getBoolean("long_press_adaptive", false) ) ServiceManager.getService() - ?.updateLongPress(originalLongPressArray, newLongPressArray, offListeningMode) + ?.updateLongPress(originalLongPressArray, newLongPressArray) } val shape = when { isFirst -> RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp) @@ -294,4 +294,4 @@ fun LongPressElement(name: String, checked: MutableState, id: String, e ) } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt index 8d8d0e0..5a0b1d2 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt @@ -26,15 +26,10 @@ import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.appwidget.AppWidgetManager -import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile 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.ComponentName import android.content.Context @@ -62,12 +57,9 @@ import androidx.core.app.NotificationCompat import androidx.core.content.edit import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch import me.kavishdevar.aln.MainActivity 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.IslandType 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.PopupWindow +import me.kavishdevar.aln.utils.determinePacket import me.kavishdevar.aln.widgets.BatteryWidget import me.kavishdevar.aln.widgets.NoiseControlWidget import org.lsposed.hiddenapibypass.HiddenApiBypass @@ -260,42 +253,6 @@ class AirPodsService : Service() { updateBatteryWidget() } - @SuppressLint("MissingPermission") - fun scanForAirPods(bluetoothAdapter: BluetoothAdapter): Flow> = 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) { - 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.Builder() - .setManufacturerData(0x004C, byteArrayOf()) - .build() - ) - - bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback) - awaitClose { bluetoothLeScanner.stopScan(scanCallback) } - } - - @OptIn(ExperimentalMaterial3Api::class) fun startForegroundNotification() { val notificationChannel = NotificationChannel( @@ -718,29 +675,6 @@ class AirPodsService : Service() { } 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 -> device.fetchUuidsWithSdp() @@ -1457,136 +1391,27 @@ class AirPodsService : Service() { fun updateLongPress( oldLongPressArray: BooleanArray, newLongPressArray: BooleanArray, - offListeningMode: Boolean ) { if (oldLongPressArray.contentEquals(newLongPressArray)) { return } - val oldOffEnabled = oldLongPressArray[0] - val oldAncEnabled = oldLongPressArray[1] - val oldTransparencyEnabled = oldLongPressArray[2] - val oldAdaptiveEnabled = oldLongPressArray[3] + val oldModes = mutableSetOf() + val newModes = mutableSetOf() - val newOffEnabled = newLongPressArray[0] - val newAncEnabled = newLongPressArray[1] - val newTransparencyEnabled = newLongPressArray[2] - val newAdaptiveEnabled = newLongPressArray[3] + if (oldLongPressArray[0]) oldModes.add(LongPressMode.OFF) + if (oldLongPressArray[1]) oldModes.add(LongPressMode.ANC) + if (oldLongPressArray[2]) oldModes.add(LongPressMode.TRANSPARENCY) + 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) - Log.d("AirPodsService", "changedIndex: $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 - } - } - } + val newEnabled = newLongPressArray[changedIndex] - 1 -> { - 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 - } - } - } - - } + val packet = determinePacket(changedIndex, newEnabled, oldModes, newModes) packet?.let { Log.d("AirPodsService", "Sending packet: ${it.joinToString(" ") { "%02X".format(it) }}") sendPacket(it) diff --git a/android/app/src/main/java/me/kavishdevar/aln/utils/Packets.kt b/android/app/src/main/java/me/kavishdevar/aln/utils/Packets.kt index 6a3d845..61891d9 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/utils/Packets.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/utils/Packets.kt @@ -1,17 +1,17 @@ /* * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality! - * + * * Copyright (C) 2024 Kavish Devar - * + * * 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 * by the Free Software Foundation, either version 3 of the License. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -241,47 +241,36 @@ class Capabilities { } } -enum class LongPressPackets(val value: ByteArray) { - ENABLE_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0F, 0x00, 0x00, 0x00)), +enum class LongPressMode { + 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)), - DISABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)), - DISABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)), +data class LongPressPacket(val modes: Set) { + val value: ByteArray + get() { + 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)), - ENABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)), - ENABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)), + private fun calculateModeByte(): Byte { + var modeByte: Byte = 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)), - DISABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x03, 0x00, 0x00, 0x00)), - DISABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)), - DISABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x09, 0x00, 0x00, 0x00)), - - 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)), -} \ No newline at end of file +fun determinePacket(changedIndex: Int, newEnabled: Boolean, oldModes: Set, newModes: Set): ByteArray? { + return if (newEnabled) { + LongPressPacket(oldModes + newModes.elementAt(changedIndex)).value + } else { + LongPressPacket(oldModes - newModes.elementAt(changedIndex)).value + } +} diff --git a/linux/README.md b/linux/README.md new file mode 100644 index 0000000..2b23c38 --- /dev/null +++ b/linux/README.md @@ -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 diff --git a/linux/main.cpp b/linux/main.cpp index e906095..749bcb7 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -52,6 +52,18 @@ class AirPodsTrayApp : public QObject { Q_OBJECT public: + enum NoiseControlMode : quint8 + { + Off = 0, + NoiseCancellation = 1, + Transparency = 2, + Adaptive = 3, + + MinValue = Off, + MaxValue = Adaptive, + }; + Q_ENUM(NoiseControlMode) + AirPodsTrayApp(bool debugMode) : debugMode(debugMode) { if (debugMode) { QLoggingCategory::setFilterRules("airpodsApp.debug=true"); @@ -79,6 +91,11 @@ public: QAction *adaptiveAction = new QAction("Adaptive", 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); transparencyAction->setCheckable(true); adaptiveAction->setCheckable(true); @@ -95,10 +112,14 @@ public: noiseControlGroup->addAction(adaptiveAction); noiseControlGroup->addAction(noiseCancellationAction); - connect(offAction, &QAction::triggered, this, [this]() { setNoiseControlMode(0); }); - connect(transparencyAction, &QAction::triggered, this, [this]() { setNoiseControlMode(2); }); - connect(adaptiveAction, &QAction::triggered, this, [this]() { setNoiseControlMode(3); }); - connect(noiseCancellationAction, &QAction::triggered, this, [this]() { setNoiseControlMode(1); }); + connect(offAction, &QAction::triggered, this, [this]() + { setNoiseControlMode(NoiseControlMode::Off); }); + connect(transparencyAction, &QAction::triggered, this, [this]() + { 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::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); QByteArray packet; switch (mode) { - case 0: + case Off: packet = QByteArray::fromHex("0400040009000D01000000"); break; - case 1: + case NoiseCancellation: packet = QByteArray::fromHex("0400040009000D02000000"); break; - case 2: + case Transparency: packet = QByteArray::fromHex("0400040009000D03000000"); break; - case 3: + case Adaptive: packet = QByteArray::fromHex("0400040009000D04000000"); break; } @@ -310,24 +331,10 @@ public slots: } } - void updateNoiseControlMenu(int mode) { + void updateNoiseControlMenu(NoiseControlMode mode) { QList actions = trayMenu->actions(); for (QAction *action : actions) { - action->setChecked(false); - } - 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; + action->setChecked(action->data().toInt() == mode); } } @@ -591,12 +598,16 @@ public slots: void parseData(const QByteArray &data) { LOG_DEBUG("Received: " << data.toHex()); if (data.size() == 11 && data.startsWith(QByteArray::fromHex("0400040009000D"))) { - int mode = data[7] - 1; - LOG_INFO("Noise control mode: " << mode); - if (mode >= 0 && mode <= 3) { + quint8 rawMode = data[7] - 1; + if (rawMode >= NoiseControlMode::MinValue && rawMode <= NoiseControlMode::MaxValue) + { + NoiseControlMode mode = static_cast(rawMode); + LOG_INFO("Noise control mode: " << rawMode); 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"))) { char primary = data[6]; @@ -887,7 +898,7 @@ public slots: } signals: - void noiseControlModeChanged(int mode); + void noiseControlModeChanged(NoiseControlMode mode); void earDetectionStatusChanged(const QString &status); void batteryStatusChanged(const QString &status);