mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
fix(popup): Prevent false-positive popups from BLE signal oscillation
Use profile-based identity matching and cooldown keys so two BLE signals for the same AirPods share one cooldown timer. Revert connection monitor to show-once-per-connection behavior.
This commit is contained in:
@@ -33,7 +33,7 @@ class PopUpReaction @Inject constructor(
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
) {
|
||||
|
||||
private val caseCoolDowns = mutableMapOf<PodDevice.Id, Instant>()
|
||||
private val caseCoolDowns = mutableMapOf<String, Instant>()
|
||||
|
||||
private fun monitorCase(): Flow<Event> = reactionSettings.showPopUpOnCaseOpen.flow
|
||||
.flatMapLatest { isEnabled ->
|
||||
@@ -56,10 +56,10 @@ class PopUpReaction @Inject constructor(
|
||||
}
|
||||
log(TAG, VERBOSE) { "previous-id=${previous?.identifier}, current-id=${current.identifier}" }
|
||||
|
||||
val isSameDeviceWithCaseNowOpen =
|
||||
previous?.identifier == current.identifier && previous.caseLidState != current.caseLidState
|
||||
val isNewDeviceWithJustOpenedCase =
|
||||
previous?.identifier != current.identifier && previous?.caseLidState != current.caseLidState
|
||||
val isSameDeviceOrProfile = previous?.identifier == current.identifier ||
|
||||
(previous?.meta?.profile?.id != null && previous.meta.profile?.id == current.meta.profile?.id)
|
||||
val isSameDeviceWithCaseNowOpen = isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState
|
||||
val isNewDeviceWithJustOpenedCase = !isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState
|
||||
|
||||
if (!isSameDeviceWithCaseNowOpen && !isNewDeviceWithJustOpenedCase) {
|
||||
return@mapNotNull null
|
||||
@@ -69,43 +69,46 @@ class PopUpReaction @Inject constructor(
|
||||
throttleCasePopUps(current)
|
||||
}
|
||||
|
||||
private fun throttleCasePopUps(current: DualApplePods): Event? = when {
|
||||
current.caseLidState == DualApplePods.LidState.OPEN -> {
|
||||
log(TAG, INFO) { "Show popup" }
|
||||
private fun throttleCasePopUps(current: DualApplePods): Event? {
|
||||
val cooldownKey = current.meta.profile?.id ?: current.identifier.toString()
|
||||
return when {
|
||||
current.caseLidState == DualApplePods.LidState.OPEN -> {
|
||||
log(TAG, INFO) { "Show popup" }
|
||||
|
||||
val now = Instant.now()
|
||||
val lastShown = caseCoolDowns[current.identifier] ?: Instant.MIN
|
||||
val sinceLastPop = Duration.between(lastShown, now)
|
||||
log(TAG) { "Time since last case popup: $sinceLastPop" }
|
||||
val now = Instant.now()
|
||||
val lastShown = caseCoolDowns[cooldownKey] ?: Instant.MIN
|
||||
val sinceLastPop = Duration.between(lastShown, now)
|
||||
log(TAG) { "Time since last case popup: $sinceLastPop" }
|
||||
|
||||
if (sinceLastPop >= Duration.ofSeconds(10)) {
|
||||
caseCoolDowns[current.identifier] = Instant.now()
|
||||
Event.PopupShow(device = current)
|
||||
} else {
|
||||
log(TAG, INFO) { "Case popup is still on cooldown: $sinceLastPop" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
current.caseLidState != DualApplePods.LidState.OPEN -> {
|
||||
when (current.caseLidState) {
|
||||
DualApplePods.LidState.CLOSED -> {
|
||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||
caseCoolDowns.remove(current.identifier)
|
||||
}
|
||||
|
||||
else -> {
|
||||
log(TAG, WARN) { "Lid was was not actively closed, refreshing cooldown." }
|
||||
caseCoolDowns[current.identifier] = Instant.now()
|
||||
if (sinceLastPop >= Duration.ofSeconds(10)) {
|
||||
caseCoolDowns[cooldownKey] = Instant.now()
|
||||
Event.PopupShow(device = current)
|
||||
} else {
|
||||
log(TAG, INFO) { "Case popup is still on cooldown: $sinceLastPop" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
log(TAG, INFO) { "Hide popup" }
|
||||
current.caseLidState != DualApplePods.LidState.OPEN -> {
|
||||
when (current.caseLidState) {
|
||||
DualApplePods.LidState.CLOSED -> {
|
||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||
caseCoolDowns.remove(cooldownKey)
|
||||
}
|
||||
|
||||
Event.PopupHide()
|
||||
else -> {
|
||||
log(TAG, WARN) { "Lid was was not actively closed, refreshing cooldown." }
|
||||
caseCoolDowns[cooldownKey] = Instant.now()
|
||||
}
|
||||
}
|
||||
|
||||
log(TAG, INFO) { "Hide popup" }
|
||||
|
||||
Event.PopupHide()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val connectionCoolDowns = mutableMapOf<BluetoothAddress, Instant>()
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.darken.capod.reaction.ui.popup
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -49,65 +50,67 @@ fun PopUpContent(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(top = 20.dp, bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
Box(modifier = modifier.padding(start = 12.dp, top = 12.dp, end = 12.dp, bottom = 16.dp)) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
|
||||
) {
|
||||
// Header: label + signal quality
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(top = 20.dp, bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = device.getLabel(context),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
|
||||
val signalText = device.getSignalQuality(context)
|
||||
if (signalText.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.SignalCellularAlt,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
// Header: label + signal quality
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = signalText,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
text = device.getLabel(context),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
|
||||
val signalText = device.getSignalQuality(context)
|
||||
if (signalText.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.SignalCellularAlt,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = signalText,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Device-specific content
|
||||
when (device) {
|
||||
is DualPodDevice -> DualPodContent(device)
|
||||
is SinglePodDevice -> SinglePodContent(device)
|
||||
}
|
||||
// Device-specific content
|
||||
when (device) {
|
||||
is DualPodDevice -> DualPodContent(device)
|
||||
is SinglePodDevice -> SinglePodContent(device)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
// Close button
|
||||
Button(
|
||||
onClick = onClose,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
) {
|
||||
Text(text = stringResource(R.string.general_close_action))
|
||||
// Close button
|
||||
Button(
|
||||
onClick = onClose,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
) {
|
||||
Text(text = stringResource(R.string.general_close_action))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ class PopUpWindow @Inject constructor(
|
||||
val dm = appContext.resources.displayMetrics
|
||||
val margin = (24 * dm.density).toInt()
|
||||
width = minOf(dm.widthPixels - margin * 2, (400 * dm.density).toInt())
|
||||
y = (8 * dm.density).toInt()
|
||||
}
|
||||
|
||||
private var composeView: ComposeView? = null
|
||||
|
||||
Reference in New Issue
Block a user