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