mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-31 07:10:45 +00:00
Merge branch 'main' into update-gitignore
This commit is contained in:
13
.gitignore
vendored
13
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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 <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)
|
||||
)
|
||||
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<Boolean>, id: String, e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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)
|
||||
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<LongPressMode>()
|
||||
val newModes = mutableSetOf<LongPressMode>()
|
||||
|
||||
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)
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -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<LongPressMode>) {
|
||||
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)),
|
||||
}
|
||||
fun determinePacket(changedIndex: Int, newEnabled: Boolean, oldModes: Set<LongPressMode>, newModes: Set<LongPressMode>): ByteArray? {
|
||||
return if (newEnabled) {
|
||||
LongPressPacket(oldModes + newModes.elementAt(changedIndex)).value
|
||||
} else {
|
||||
LongPressPacket(oldModes - newModes.elementAt(changedIndex)).value
|
||||
}
|
||||
}
|
||||
|
||||
50
linux/README.md
Normal file
50
linux/README.md
Normal 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
|
||||
@@ -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<QAction *> 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<NoiseControlMode>(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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user