android: use pressandhold settings when cycling modes

This commit is contained in:
Kavish Devar
2026-04-28 20:27:32 +05:30
parent 4ef3e4d4da
commit 795bebc6ae
4 changed files with 65 additions and 127 deletions

View File

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

View File

@@ -20,7 +20,6 @@
package me.kavishdevar.librepods.presentation.screens package me.kavishdevar.librepods.presentation.screens
import android.content.Context
import android.util.Log import android.util.Log
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -34,13 +33,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font 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.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.navigation.NavController import androidx.navigation.NavController
import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R 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.SelectItem
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSelectList 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 me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.experimental.and import kotlin.experimental.and
import kotlin.io.encoding.ExperimentalEncodingApi 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", "Noise Cancellation mode: ${(modesByte and 0x02) != 0.toByte()}")
Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}") Log.d("PressAndHoldSettingsScreen", "Adaptive mode: ${(modesByte and 0x08) != 0.toByte()}")
val context = LocalContext.current val longPressAction = if (name.lowercase() == "left") state.leftAction else state.rightAction
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 backdrop = rememberLayerBackdrop() val backdrop = rememberLayerBackdrop()
StyledScaffold( StyledScaffold(
title = name title = name
@@ -105,16 +92,14 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
name = stringResource(R.string.noise_control), name = stringResource(R.string.noise_control),
selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES, selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES,
onClick = { onClick = {
longPressAction = StemAction.CYCLE_NOISE_CONTROL_MODES viewModel.setLongPressAction(name, StemAction.CYCLE_NOISE_CONTROL_MODES)
sharedPreferences.edit { putString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name) }
} }
), ),
SelectItem( SelectItem(
name = stringResource(R.string.digital_assistant), name = stringResource(R.string.digital_assistant),
selected = longPressAction == StemAction.DIGITAL_ASSISTANT, selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
onClick = { onClick = {
longPressAction = StemAction.DIGITAL_ASSISTANT viewModel.setLongPressAction(name, StemAction.DIGITAL_ASSISTANT)
sharedPreferences.edit { putString(prefKey, StemAction.DIGITAL_ASSISTANT.name) }
}, },
enabled = state.isPremium enabled = state.isPremium
) )
@@ -162,21 +147,10 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
val offListeningModeValue = ServiceManager.getService()!!.aacpManager.controlCommandStatusList.find { val currentByte = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS]?.get(0)?.toInt() ?: 0
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 listeningModeItems = mutableListOf<SelectItem>() val listeningModeItems = mutableListOf<SelectItem>()
if (allowOff) { if (state.offListeningMode) {
listeningModeItems.add( listeningModeItems.add(
SelectItem( SelectItem(
name = stringResource(R.string.off), name = stringResource(R.string.off),
@@ -184,21 +158,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
iconRes = R.drawable.noise_cancellation, iconRes = R.drawable.noise_cancellation,
selected = (currentByte and 0x01) != 0, selected = (currentByte and 0x01) != 0,
onClick = { onClick = {
val bit = 0x01 viewModel.toggleListeningMode(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
} }
) )
) )
@@ -210,21 +170,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
iconRes = R.drawable.transparency, iconRes = R.drawable.transparency,
selected = (currentByte and 0x04) != 0, selected = (currentByte and 0x04) != 0,
onClick = { onClick = {
val bit = 0x04 viewModel.toggleListeningMode(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
} }
), ),
SelectItem( SelectItem(
@@ -233,21 +179,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
iconRes = R.drawable.adaptive, iconRes = R.drawable.adaptive,
selected = (currentByte and 0x08) != 0, selected = (currentByte and 0x08) != 0,
onClick = { onClick = {
val bit = 0x08 viewModel.toggleListeningMode(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
} }
), ),
SelectItem( SelectItem(
@@ -256,21 +188,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
iconRes = R.drawable.noise_cancellation, iconRes = R.drawable.noise_cancellation,
selected = (currentByte and 0x02) != 0, selected = (currentByte and 0x02) != 0,
onClick = { onClick = {
val bit = 0x02 viewModel.toggleListeningMode(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
} }
) )
)) ))
@@ -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) 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() { fun disconnect() {
service.disconnectAirPods() service.disconnectAirPods()
if (appContext.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") != PackageManager.PERMISSION_GRANTED) { if (appContext.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") != PackageManager.PERMISSION_GRANTED) {

View File

@@ -539,28 +539,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
} else { } else {
val currentMode = ancNotification.status val currentMode = ancNotification.status
val configByte = sharedPreferences.getInt("long_press_byte", 0b0111)
val allowOffModeValue = val allowOffModeValue =
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION } aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION }
val allowOffMode = allowOffModeValue?.value?.takeIf { it.isNotEmpty() } val allowOffMode =
?.get(0) == 0x01.toByte() allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte() || sharedPreferences.getBoolean("off_listening_mode", true)
val nextMode = getNextMode(currentMode = currentMode, configByte = configByte, allowOffMode)
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
}
}
aacpManager.sendControlCommand( aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value, AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE.value,
@@ -568,7 +552,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
) )
Log.d( Log.d(
TAG, TAG,
"Cycling ANC mode from $currentMode to $nextMode (offListeningMode: $allowOffMode)" "Cycling ANC mode from $currentMode to $nextMode"
) )
} }
} }
@@ -1970,7 +1954,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val allowOffModeValue = val allowOffModeValue =
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION } aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION }
val allowOffMode = 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( it.setInt(
R.id.widget_off_button, R.id.widget_off_button,
"setBackgroundResource", "setBackgroundResource",
@@ -3185,3 +3169,20 @@ private fun Int.dpToPx(): Int {
val density = Resources.getSystem().displayMetrics.density val density = Resources.getSystem().displayMetrics.density
return (this * density).toInt() 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]
}