mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-08 06:56:10 +00:00
fix: cancel in-flight wake connects when proximity wake is disabled
This commit is contained in:
parent
4eb2a35e2f
commit
ed13ba2625
@ -18,6 +18,10 @@ import Foundation
|
||||
enum BLEProximityWakeSettings {
|
||||
private static let enabledKey = "ble.proximityWakeEnabled"
|
||||
|
||||
/// Posted when the standard-store preference changes so BLE can cancel
|
||||
/// any in-flight pending connects that would still wake the app.
|
||||
static let didChangeNotification = Notification.Name("bitchat.bleProximityWakeSettingsDidChange")
|
||||
|
||||
/// When false, bitchat will not arm pending background connects.
|
||||
static var enabled: Bool {
|
||||
get { enabled(in: .standard) }
|
||||
@ -32,11 +36,18 @@ enum BLEProximityWakeSettings {
|
||||
|
||||
static func setEnabled(_ enabled: Bool, in defaults: UserDefaults) {
|
||||
defaults.set(enabled, forKey: enabledKey)
|
||||
// Only the live preference store drives BLE wake behaviour.
|
||||
if defaults === UserDefaults.standard {
|
||||
NotificationCenter.default.post(name: didChangeNotification, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-wipe hook. Removing the key restores the on-by-default
|
||||
/// mesh-reachability preference of a fresh install.
|
||||
static func reset(in defaults: UserDefaults = .standard) {
|
||||
defaults.removeObject(forKey: enabledKey)
|
||||
if defaults === UserDefaults.standard {
|
||||
NotificationCenter.default.post(name: didChangeNotification, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,12 +252,17 @@ final class BLERadioController {
|
||||
return
|
||||
}
|
||||
|
||||
if self.delegate?.radioIsAppActive() == false {
|
||||
// Backgrounded: leave the connect pending. iOS never expires
|
||||
// it — the controller completes it whenever the peer comes
|
||||
// back into range, waking the app (state restoration
|
||||
// relaunches us if we were terminated). Foreground return
|
||||
// cancels stale pendings via cancelStalePendingConnects().
|
||||
if self.delegate?.radioIsAppActive() == false,
|
||||
BLEProximityWakeSettings.enabled {
|
||||
// Backgrounded with wake-on-proximity on: leave the connect
|
||||
// pending. iOS never expires it — the controller completes it
|
||||
// whenever the peer comes back into range, waking the app
|
||||
// (state restoration relaunches us if we were terminated).
|
||||
// Foreground return cancels stale pendings via
|
||||
// cancelStalePendingConnects(). If the user opted out, fall
|
||||
// through and cancel so an in-flight foreground connect that
|
||||
// timed out after backgrounding cannot still trigger the
|
||||
// iOS 26 accessory-open prompt (#1512 Codex).
|
||||
SecureLogger.info("🌙 Connect timeout deferred while backgrounded, left pending for wake-on-proximity: \(candidate.name)", category: .session)
|
||||
return
|
||||
}
|
||||
@ -316,10 +321,8 @@ final class BLERadioController {
|
||||
) {
|
||||
// Opt-out for people who don't want iOS 26's "accessory would like
|
||||
// to open bitchat" wake prompt (#1427 / #1396). Default stays on.
|
||||
// Turning the setting off only prevents future arming — already
|
||||
// pending connects stay until the next foreground cancel/re-arm
|
||||
// cycle. That is fine in practice: the toggle lives in-app, so it
|
||||
// cannot change while we are backgrounded and actively waiting.
|
||||
// Disabling also cancels in-flight / pending connects via
|
||||
// `cancelPendingWakeConnects()` (settings notification).
|
||||
guard BLEProximityWakeSettings.enabled else { return }
|
||||
queue.async { [weak self] in
|
||||
guard let self,
|
||||
@ -368,20 +371,35 @@ final class BLERadioController {
|
||||
/// scheduler take over. Anything still nearby is rediscovered within
|
||||
/// seconds by the allow-duplicates foreground scan.
|
||||
func cancelStalePendingConnects() {
|
||||
cancelPendingConnects(olderThan: TransportConfig.bleConnectTimeoutSeconds, reason: "stale pending connect(s) on foreground")
|
||||
}
|
||||
|
||||
/// Drop every still-connecting peripheral immediately. Used when the
|
||||
/// user turns off wake-on-proximity so an already-armed or in-flight
|
||||
/// connect cannot still wake the app after opt-out (#1512).
|
||||
func cancelPendingWakeConnects() {
|
||||
cancelPendingConnects(olderThan: 0, reason: "pending connect(s) after wake-on-proximity opt-out")
|
||||
}
|
||||
|
||||
private func cancelPendingConnects(olderThan minAge: TimeInterval, reason: String) {
|
||||
queue.async { [weak self] in
|
||||
guard let self, let central = self.central else { return }
|
||||
let now = Date()
|
||||
var cancelled = 0
|
||||
for state in self.linkStateStore.peripheralStates where state.isConnecting && !state.isConnected {
|
||||
let age = state.lastConnectionAttempt.map { now.timeIntervalSince($0) } ?? .infinity
|
||||
guard age > TransportConfig.bleConnectTimeoutSeconds else { continue }
|
||||
// minAge == 0 means cancel every pending connect (opt-out);
|
||||
// otherwise match the prior stale-foreground threshold (`>`).
|
||||
if minAge > 0 {
|
||||
guard age > minAge else { continue }
|
||||
}
|
||||
let peripheralID = state.peripheral.identifier.uuidString
|
||||
central.cancelPeripheralConnection(state.peripheral)
|
||||
self.delegate?.radioTearDownPeripheralLink(peripheralID)
|
||||
cancelled += 1
|
||||
}
|
||||
if cancelled > 0 {
|
||||
SecureLogger.info("🌅 Cancelled \(cancelled) stale pending connect(s) on foreground", category: .session)
|
||||
SecureLogger.info("🌅 Cancelled \(cancelled) \(reason)", category: .session)
|
||||
self.tryConnectFromQueue()
|
||||
}
|
||||
}
|
||||
|
||||
@ -568,6 +568,12 @@ final class BLEService: NSObject {
|
||||
name: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(proximityWakeSettingDidChange),
|
||||
name: BLEProximityWakeSettings.didChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
#endif
|
||||
|
||||
// Tag BLE queue for re-entrancy detection
|
||||
@ -5230,6 +5236,14 @@ extension BLEService {
|
||||
scheduleBluetoothStatusSample(after: 15.0, context: "background-15s")
|
||||
// No Local Name; nothing to refresh for advertising policy
|
||||
}
|
||||
|
||||
@objc private func proximityWakeSettingDidChange() {
|
||||
// Opt-out must drop already-armed / in-flight wake connects so the
|
||||
// accessory-open prompt cannot fire after the user turned the setting
|
||||
// off in App Info (#1512 Codex).
|
||||
guard !BLEProximityWakeSettings.enabled else { return }
|
||||
radio.cancelPendingWakeConnects()
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Private Message Handling
|
||||
|
||||
@ -34,4 +34,22 @@ struct BLEProximityWakeSettingsTests {
|
||||
BLEProximityWakeSettings.reset(in: defaults)
|
||||
#expect(BLEProximityWakeSettings.enabled(in: defaults))
|
||||
}
|
||||
|
||||
@Test
|
||||
func standardStoreChangePostsNotification() async {
|
||||
let previous = BLEProximityWakeSettings.enabled
|
||||
defer { BLEProximityWakeSettings.enabled = previous }
|
||||
|
||||
await confirmation("didChangeNotification fires for standard store") { confirm in
|
||||
let observer = NotificationCenter.default.addObserver(
|
||||
forName: BLEProximityWakeSettings.didChangeNotification,
|
||||
object: nil,
|
||||
queue: nil
|
||||
) { _ in
|
||||
confirm()
|
||||
}
|
||||
defer { NotificationCenter.default.removeObserver(observer) }
|
||||
BLEProximityWakeSettings.enabled = !previous
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user