Phase 8.4:PhoneStateMonitor → phonestate 反向 callback

契约 §3.1 [14]phonestate 落地。msext 用已废弃的 CTCallCenter(iOS 10+
deprecated);新外壳改用 CallKit CXCallObserver,对 H5 输出字符串一致:
- "2" = 来电响铃中(hasConnected=false / isOutgoing=false / hasEnded=false)
       或已连接(hasConnected=true)
- "0" = 挂断(hasEnded=true)
- 其它(拨出未连接、保持)不发

新增:
- Source/Resource/PhoneStateMonitor.swift(@MainActor singleton,与 BatteryMonitor /
  NetworkMonitor 同款 pattern;onChange closure + start/stop 生命周期)

接入:
- SubGameViewController.setupExternalSubscriptions:start + onChange
- SubGameViewController.teardownExternalSubscriptions:onChange=nil + stop
- 大厅 WebContainerViewController 不订阅(msext gameController 同款边界,
  契约表注「已注释」表示大厅不发;H5 子游戏才依赖此事件做哑麦/暂停)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-23 02:01:29 +08:00
co-authored by Claude Opus 4.7
parent 8fc37b0827
commit ded6b5d86b
3 changed files with 93 additions and 4 deletions
@@ -0,0 +1,78 @@
//
// PhoneStateMonitor.swift
// ylgamehall
//
// 电话状态监听(Phase 8.4,契约 §3.1 [14] phonestate 反向 callback)。
//
// msext 用已废弃的 `CTCallCenter`(iOS 10+ deprecated);本项目用 CallKit
// `CXCallObserver`(iOS 10+ 推荐),语义等价,对 H5 输出的字符串一致:
// - "2" = 来电响铃中(hasConnected=false, isOutgoing=false, hasEnded=false)
// 或 已连接(hasConnected=true,无论 incoming/outgoing)
// - "0" = 挂断(hasEnded=true)
// - 其它(拨出中未连接、保持等)不发
//
// 仅子游戏注册,大厅不发(msext gameController.m 同款;契约表注「已注释」表示
// msext 在线版当前不主动触发,但 H5 子游戏仍按契约处理收到的事件)。
//
// 使用模式(与 BatteryMonitor / NetworkMonitor 一致):
// - viewDidLoad / setupExternalSubscriptions:`shared.start()` + `onChange = { ... }`
// - viewWillDisappear / teardownExternalSubscriptions:`onChange = nil` + `shared.stop()`
//
import Foundation
import CallKit
@MainActor
public final class PhoneStateMonitor: NSObject {
public static let shared = PhoneStateMonitor()
/// 状态变化回调,参数即给 H5 的 `phonestate` 字符串("2" / "0")。
/// closure 在 main actor 上调,调用方可直接拿 bridge 反向 callback。
public var onChange: ((String) -> Void)?
private var observer: CXCallObserver?
public override init() {
super.init()
}
/// 开始监听。重复调安全(已 start 则无操作)。
public func start() {
guard observer == nil else { return }
let o = CXCallObserver()
// queue: nil → main queue(callObserver:callChanged: 回调线程一致,
// 子游戏页 bridge.call 也走主线程,无并发问题)
o.setDelegate(self, queue: nil)
observer = o
}
/// 停止监听。stop 之后再 start 重新创建 observer。
public func stop() {
observer?.setDelegate(nil, queue: nil)
observer = nil
}
}
extension PhoneStateMonitor: CXCallObserverDelegate {
/// CXCallObserver 状态切换。本方法 `@objc` 由 CallKit 在主队列调(init 时传 nil)。
public nonisolated func callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) {
let state: String
if call.hasEnded {
state = "0"
} else if call.hasConnected {
// 已接通(任何方向)— 视频房 / 录音应静音
state = "2"
} else if !call.isOutgoing {
// 来电响铃中(hasEnded=false, hasConnected=false, isOutgoing=false)
state = "2"
} else {
// 拨出中未连接 / 保持等中间态 — H5 不关心,丢弃
return
}
Task { @MainActor in
Self.shared.onChange?(state)
}
}
}