Compare commits

..

9 Commits

Author SHA1 Message Date
thisisAcidic
fb44f01ac0 android: allow non-premium users to disable head gestures (#564) 2026-05-03 01:41:23 +05:30
thisisAcidic
93a93cbe68 fix: sync magisk update json with current release URLs (#563) 2026-05-03 00:59:43 +05:30
Nikhil Maddirala
a4898293b8 docs: update readme root requirements (#557)
* Update readme root requirements

Clarified root requirements for LibrePods depending on device/OS and features needed.

* Revise Xposed workaround note in README

Updated warning about Xposed/LSPosed workaround for compatibility.
2026-05-01 20:06:42 +05:30
Kavish Devar
845f26192c android: make head tracking screen scrollable 2026-04-30 12:53:02 +05:30
Kavish Devar
3321bb1c43 android: bump version 2026-04-30 01:07:43 +05:30
Kavish Devar
c7a5cb2d8c android: fix crash in listening mode widget when service is null 2026-04-30 01:03:51 +05:30
Kavish Devar
7b81411417 android: fix media not resuming when using single AirPod 2026-04-30 01:00:15 +05:30
Kavish Devar
d80f2275a1 android: remove NativeBridge calls from app settings 2026-04-30 00:58:42 +05:30
Kavish Devar
795bebc6ae android: use pressandhold settings when cycling modes 2026-04-28 20:29:00 +05:30
11 changed files with 143 additions and 195 deletions

View File

@@ -76,19 +76,15 @@ https://github.com/user-attachments/assets/43911243-0576-4093-8c55-89c1db5ea533
### Root Requirement
The app needs root because of a bug in the Android Bluetooth stack Fluoride/non-compliance of Apple with Bluetooth standards. You must have Xposed installed for the app to workaround this bug and connect to AirPods.
LibrePods **may** require root depending on your device/OS and what features you want access to:
[https://issuetracker.google.com/issues/371713238](https://issuetracker.google.com/issues/371713238)
Please do not comment in the thread. The issue has already been resolved and should be available in Android 17 for all devices.
However, if you are using ColorOS/OxygenOS 16, Android 16 QPR3 on Pixel (ensure you're on the latest Play system update), you don't need root for most features.
- Features requiring the VendorID hook ([the features marked with an asterisk here](https://github.com/kavishdevar/librepods#key-features)) will always require root regardless of your device/OS.
- On **ColorOS/OxygenOS 16** and **Pixel devices on Android 16 QPR3** (with the latest Google Play system update), LibrePods does not need root for most features (except those requiring the VendorID hook mentioned above).
- On other devices, LibrePods needs root because of a bug in the Android Bluetooth stack Fluoride/non-compliance of Apple with Bluetooth standards. You must have Xposed installed for the app to workaround this bug and connect to AirPods. [This issue is being tracked here](https://issuetracker.google.com/issues/371713238). Please do not comment on the issue thread. The issue has already been resolved and should be available in **Android 17** for all devices.
> [!IMPORTANT]
> This workaround with Xposed is not guaranteed to work on all devices.
Features requiring the VendorID hook will still require root. These features include customizing transparency mode, setting up hearing aid, and use Bluetooth Multipoint.
### Troubleshooting steps for common errors
- Ensure the correct scope is set in LSPosed/Vector.
- Ensure there is no root-hiding module preventing the hook from loading on the Bluetooth app.

View File

@@ -1,6 +1,6 @@
import java.util.Properties
val appVersionName = "0.2.6"
val appVersionName = "0.2.8"
plugins {
alias(libs.plugins.android.application)
@@ -30,7 +30,7 @@ android {
applicationId = "me.kavishdevar.librepods"
minSdk = 33
targetSdk = 37
versionCode = 46
versionCode = 49
versionName = appVersionName
}
buildTypes {

View File

@@ -99,9 +99,9 @@ import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledIconButton
import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledToggle
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.HeadTracking
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.abs
import kotlin.math.cos
@@ -151,9 +151,13 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController
var lastClickTime by remember { mutableLongStateOf(0L) }
var shouldExplode by remember { mutableStateOf(false) }
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth(),
.fillMaxWidth()
.verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column (
@@ -163,7 +167,6 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController
.layerBackdrop(backdrop)
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
.verticalScroll(scrollState)
) {
Spacer(modifier = Modifier.height(topPadding))
@@ -194,7 +197,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController
label = "Head Gestures",
checked = state.headGesturesEnabled,
onCheckedChange = { viewModel.setHeadGesturesEnabled(it) },
enabled = state.isPremium,
enabled = state.isPremium || state.headGesturesEnabled,
description = stringResource(R.string.head_gestures_details)
)

View File

@@ -20,7 +20,6 @@
package me.kavishdevar.librepods.presentation.screens
import android.content.Context
import android.util.Log
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
@@ -34,13 +33,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
@@ -48,19 +42,17 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.navigation.NavController
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.AACPManager
import me.kavishdevar.librepods.data.StemAction
import me.kavishdevar.librepods.presentation.components.SelectItem
import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSelectList
import me.kavishdevar.librepods.data.StemAction
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.bluetooth.AACPManager
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.experimental.and
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -82,12 +74,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
Log.d("PressAndHoldSettingsScreen", "Noise Cancellation mode: ${(modesByte and 0x02) != 0.toByte()}")
Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}")
val context = LocalContext.current
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val prefKey = if (name.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
val longPressActionPref = sharedPreferences.getString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name)
Log.d("PressAndHoldSettingsScreen", "Long press action preference ($prefKey): $longPressActionPref")
var longPressAction by remember { mutableStateOf(StemAction.valueOf(longPressActionPref ?: StemAction.CYCLE_NOISE_CONTROL_MODES.name)) }
val longPressAction = if (name.lowercase() == "left") state.leftAction else state.rightAction
val backdrop = rememberLayerBackdrop()
StyledScaffold(
title = name
@@ -105,16 +92,14 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
name = stringResource(R.string.noise_control),
selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES,
onClick = {
longPressAction = StemAction.CYCLE_NOISE_CONTROL_MODES
sharedPreferences.edit { putString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name) }
viewModel.setLongPressAction(name, StemAction.CYCLE_NOISE_CONTROL_MODES)
}
),
SelectItem(
name = stringResource(R.string.digital_assistant),
selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
onClick = {
longPressAction = StemAction.DIGITAL_ASSISTANT
sharedPreferences.edit { putString(prefKey, StemAction.DIGITAL_ASSISTANT.name) }
viewModel.setLongPressAction(name, StemAction.DIGITAL_ASSISTANT)
},
enabled = state.isPremium
)
@@ -162,21 +147,10 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
Spacer(modifier = Modifier.height(8.dp))
val offListeningModeValue = ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find {
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
Log.d("PressAndHoldSettingsScreen", "Allow Off state: $offListeningModeValue")
val allowOff = offListeningModeValue == 1.toByte()
Log.d("PressAndHoldSettingsScreen", "Allow Off option: $allowOff")
val initialByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]
?.get(0)?.toInt()
?: sharedPreferences.getInt("long_press_byte", 0b0101)
var currentByte by remember { mutableIntStateOf(initialByte) }
val currentByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]?.get(0)?.toInt() ?: 0
val listeningModeItems = mutableListOf<SelectItem>()
if (allowOff) {
if (state.offListeningMode) {
listeningModeItems.add(
SelectItem(
name = stringResource(R.string.off),
@@ -184,21 +158,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
iconRes = R.drawable.noise_cancellation,
selected = (currentByte and 0x01) != 0,
onClick = {
val bit = 0x01
val newValue = if ((currentByte and bit) != 0) {
val temp = currentByte and bit.inv()
if (countEnabledModes(temp) >= 2) temp else currentByte
} else {
currentByte or bit
}
viewModel.setControlCommandByte(
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
newValue.toByte()
)
sharedPreferences.edit {
putInt("long_press_byte", newValue)
}
currentByte = newValue
viewModel.toggleListeningMode(0x01)
}
)
)
@@ -210,21 +170,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
iconRes = R.drawable.transparency,
selected = (currentByte and 0x04) != 0,
onClick = {
val bit = 0x04
val newValue = if ((currentByte and bit) != 0) {
val temp = currentByte and bit.inv()
if (countEnabledModes(temp) >= 2) temp else currentByte
} else {
currentByte or bit
}
viewModel.setControlCommandByte(
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
newValue.toByte()
)
sharedPreferences.edit {
putInt("long_press_byte", newValue)
}
currentByte = newValue
viewModel.toggleListeningMode(0x04)
}
),
SelectItem(
@@ -233,21 +179,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
iconRes = R.drawable.adaptive,
selected = (currentByte and 0x08) != 0,
onClick = {
val bit = 0x08
val newValue = if ((currentByte and bit) != 0) {
val temp = currentByte and bit.inv()
if (countEnabledModes(temp) >= 2) temp else currentByte
} else {
currentByte or bit
}
viewModel.setControlCommandByte(
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
newValue.toByte()
)
sharedPreferences.edit {
putInt("long_press_byte", newValue)
}
currentByte = newValue
viewModel.toggleListeningMode(0x08)
}
),
SelectItem(
@@ -256,21 +188,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
iconRes = R.drawable.noise_cancellation,
selected = (currentByte and 0x02) != 0,
onClick = {
val bit = 0x02
val newValue = if ((currentByte and bit) != 0) {
val temp = currentByte and bit.inv()
if (countEnabledModes(temp) >= 2) temp else currentByte
} else {
currentByte or bit
}
viewModel.setControlCommandByte(
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS,
newValue.toByte()
)
sharedPreferences.edit {
putInt("long_press_byte", newValue)
}
currentByte = newValue
viewModel.toggleListeningMode(0x02)
}
)
))
@@ -290,14 +208,4 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
}
}
}
Log.d("PressAndHoldSettingsScreen", "Current byte: ${modesByte.toString(2)}")
}
fun countEnabledModes(byteValue: Int): Int {
var count = 0
if ((byteValue and 0x01) != 0) count++
if ((byteValue and 0x02) != 0) count++
if ((byteValue and 0x04) != 0) count++
if ((byteValue and 0x08) != 0) count++
return count
}

View File

@@ -540,6 +540,35 @@ class AirPodsViewModel(
service.aacpManager.sendPhoneMediaEQ(eq, phoneByte, mediaByte)
}
fun setLongPressAction(side: String, action: StemAction) {
val prefKey = if (side.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
sharedPreferences.edit { putString(prefKey, action.name) }
_uiState.update {
if (side.lowercase() == "left") it.copy(leftAction = action) else it.copy(rightAction = action)
}
}
private fun countEnabledModes(byteValue: Int): Int {
var count = 0
if ((byteValue and 0x01) != 0) count++
if ((byteValue and 0x02) != 0) count++
if ((byteValue and 0x04) != 0) count++
if ((byteValue and 0x08) != 0) count++
return count
}
fun toggleListeningMode(modeBit: Int) {
val currentByte = uiState.value.controlStates[ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]?.get(0)?.toInt() ?: 0
val newValue = if ((currentByte and modeBit) != 0) {
val temp = currentByte and modeBit.inv()
if (countEnabledModes(temp) >= 2) temp else currentByte
} else {
currentByte or modeBit
}
setControlCommandByte(ControlCommandIdentifiers.LISTENING_MODE_CONFIGS, newValue.toByte())
sharedPreferences.edit { putInt("long_press_byte", newValue) }
}
fun disconnect() {
service.disconnectAirPods()
if (appContext.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") != PackageManager.PERMISSION_GRANTED) {

View File

@@ -12,8 +12,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import me.kavishdevar.librepods.billing.BillingManager
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
import me.kavishdevar.librepods.utils.NativeBridge
import me.kavishdevar.librepods.utils.XposedState
import kotlin.math.roundToInt
data class AppSettingsUiState(
@@ -91,9 +89,6 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
)
}
if (XposedState.isAvailable && XposedState.bluetoothScopeEnabled) {
NativeBridge.setSdpHook(_uiState.value.vendorIdHook)
}
}
fun setShowPhoneBatteryInWidget(enabled: Boolean) {
@@ -178,7 +173,6 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
}
fun setVendorIdHook(enabled: Boolean) {
NativeBridge.setSdpHook(enabled)
xposedRemotePref.putBoolean("vendor_id_hook", enabled)
_uiState.update { it.copy(vendorIdHook = enabled) }
}

View File

@@ -28,8 +28,8 @@ import android.content.Intent
import android.util.Log
import android.widget.RemoteViews
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.bluetooth.AACPManager
import me.kavishdevar.librepods.services.ServiceManager
import kotlin.io.encoding.ExperimentalEncodingApi
class NoiseControlWidget : AppWidgetProvider() {
@@ -82,8 +82,14 @@ class NoiseControlWidget : AppWidgetProvider() {
if (intent.action == "ACTION_SET_ANC_MODE") {
val mode = intent.getIntExtra("ANC_MODE", 1)
Log.d("NoiseControlWidget", "Setting ANC mode to $mode")
ServiceManager.getService()!!
.aacpManager
val service = ServiceManager.getService()
if (service == null) {
Log.w("NoiseControlWidget", "Service unavailable")
return
}
service.aacpManager
.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value,
mode.toByte()

View File

@@ -539,28 +539,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
} else {
val currentMode = ancNotification.status
val configByte = sharedPreferences.getInt("long_press_byte", 0b0111)
val allowOffModeValue =
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION }
val allowOffMode = allowOffModeValue?.value?.takeIf { it.isNotEmpty() }
?.get(0) == 0x01.toByte()
val nextMode = if (allowOffMode) {
when (currentMode) {
1 -> 2
2 -> 3
3 -> 4
4 -> 1
else -> 1
}
} else {
when (currentMode) {
1 -> 2
2 -> 3
3 -> 4
4 -> 2
else -> 2
}
}
val allowOffMode =
allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte() || sharedPreferences.getBoolean("off_listening_mode", true)
val nextMode = getNextMode(currentMode = currentMode, configByte = configByte, allowOffMode)
aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value,
@@ -568,7 +552,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
)
Log.d(
TAG,
"Cycling ANC mode from $currentMode to $nextMode (offListeningMode: $allowOffMode)"
"Cycling ANC mode from $currentMode to $nextMode"
)
}
}
@@ -1116,7 +1100,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
"AirPodsParser",
"Audio source changed mac: ${aacpManager.audioSource?.mac}, type: ${aacpManager.audioSource?.type?.name}"
)
if (aacpManager.audioSource?.type != AACPManager.Companion.AudioSourceType.NONE && aacpManager.audioSource?.mac != localMac) {
if (localMac!="" && (aacpManager.audioSource?.type != AACPManager.Companion.AudioSourceType.NONE && aacpManager.audioSource?.mac != localMac)) {
Log.d(
"AirPodsParser",
"Audio source is another device, better to give up aacp control"
@@ -1272,6 +1256,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
disconnectAudio(this@AirPodsService, device)
}
}
val wasNone = inEarData == listOf(false, false)
val nowSingle = newInEarData.count { it } == 1
if (wasNone && nowSingle) {
MediaController.sendPlay()
MediaController.iPausedTheMedia = false
return
}
if (inEarData.contains(false) && newInEarData == listOf(true, true)) {
Log.d("AirPodsParser", "User put in both AirPods from just one.")
@@ -1970,7 +1962,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val allowOffModeValue =
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION }
val allowOffMode =
allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte()
allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte() || sharedPreferences.getBoolean("off_listening_mode", true)
it.setInt(
R.id.widget_off_button,
"setBackgroundResource",
@@ -3005,22 +2997,20 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
fun connectAudio(context: Context, device: BluetoothDevice?) {
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
try {
if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
val policyMethod = proxy.javaClass.getMethod(
"setConnectionPolicy",
BluetoothDevice::class.java,
Int::class.java
)
Log.d(TAG, "calling A2DP.setConnectionPolicy for ${device?.address} to 100")
policyMethod.invoke(proxy, device, 100)
}
else {
Log.d(TAG, "not setting connection policy for A2DP, no BLUETOOTH_PRIVILEGED permission")
}
val policyMethod = proxy.javaClass.getMethod(
"setConnectionPolicy",
BluetoothDevice::class.java,
Int::class.java
)
Log.d(TAG, "calling A2DP.setConnectionPolicy for ${device?.address} to 100")
policyMethod.invoke(proxy, device, 100)
val connectMethod =
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
connectMethod.invoke(
@@ -3035,30 +3025,35 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
}
else {
val connectMethod =
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
connectMethod.invoke(
proxy, device
)
Log.d(TAG, "not setting connection policy for A2DP, no BLUETOOTH_PRIVILEGED permission. just called connect")
}
}
}
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP)
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP)
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
if (checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
try {
if (checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
val policyMethod = proxy.javaClass.getMethod(
"setConnectionPolicy",
BluetoothDevice::class.java,
Int::class.java
)
Log.d(
TAG,
"calling HEADSET.setConnectionPolicy for ${device?.address} to 100"
)
policyMethod.invoke(proxy, device, 100)
} else {
Log.d(TAG, "not setting connection policy for HEADSET, no MODIFIY_PHONE_STATE permission")
}
val policyMethod = proxy.javaClass.getMethod(
"setConnectionPolicy",
BluetoothDevice::class.java,
Int::class.java
)
Log.d(
TAG,
"calling HEADSET.setConnectionPolicy for ${device?.address} to 100"
)
policyMethod.invoke(proxy, device, 100)
val connectMethod =
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
connectMethod.invoke(proxy, device)
@@ -3067,11 +3062,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} finally {
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
}
} else {
Log.d(TAG, "not setting connection policy for HEADSET, no MODIFIY_PHONE_STATE permission")
}
}
}
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.HEADSET)
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.HEADSET)
}
fun setName(name: String) {
@@ -3185,3 +3183,20 @@ private fun Int.dpToPx(): Int {
val density = Resources.getSystem().displayMetrics.density
return (this * density).toInt()
}
fun getNextMode(currentMode: Int, configByte: Int, offmodeEnabled: Boolean): Int {
val enabledModes = buildList {
if ((configByte and 0x01) != 0 && offmodeEnabled) add(1)
if ((configByte and 0x04) != 0) add(3)
if ((configByte and 0x08) != 0) add(4)
if ((configByte and 0x02) != 0) add(2)
}
Log.d(TAG, "currentMode: $currentMode, config: ${configByte.toString(2)}")
if (enabledModes.isEmpty()) return currentMode
val currentIndex = enabledModes.indexOf(currentMode)
val nextIndex = if (currentIndex == -1) 0 else (currentIndex + 1) % enabledModes.size
return enabledModes[nextIndex]
}

View File

@@ -171,8 +171,10 @@ object MediaController {
}
if (configs != null && !iPausedTheMedia) {
val localMac = ServiceManager.getService()?.localMac ?: return
if (localMac == "") return
ServiceManager.getService()?.aacpManager?.sendMediaInformataion(
ServiceManager.getService()?.localMac ?: return,
localMac,
isActive
)
Log.d("MediaController", "User changed media state themselves; will wait for ear detection pause before auto-play")

View File

@@ -1,5 +0,0 @@
package me.kavishdevar.librepods.utils
object NativeBridge {
fun setSdpHook(enabled: Boolean) { }
}

View File

@@ -1,6 +1,6 @@
{
"version": "v0.2.6",
"versionCode": 46,
"zipUrl": "https://github.com/kavishdevar/librepods/releases/download/v0.2.3/LibrePods-FOSS-v0.2.3-release.zip",
"changelog": "https://raw.githubusercontent.com/kavishdevar/librepods/main/CHANGELOG.md"
}
"zipUrl": "https://github.com/kavishdevar/librepods/releases/download/v0.2.6/LibrePods-FOSS-v0.2.6-release.zip",
"changelog": "https://raw.githubusercontent.com/kavishdevar/librepods/main/extras/CHANGELOG.md"
}