feat(flow): on-demand host handoff with reliable return and a back-tap tip
Keyboard
- Only jump to the app when the voice surface is shown (tab tap / opened
there); typing tabs no longer leave the host on keyboard open.
- Wait up to 0.4 s for the arbiter to report the host app before leaving,
and late-bind an id that arrives within 6 s of the jump.
- Empty arbiter callbacks no longer wipe an already captured host id;
install the capture handler in every build configuration.
App
- Return to the previous app with up to four retries using iOS's real
open result, and honor the skip-app-switch preference.
- Keep the pending host id across cold-start zombie reconciliation, and
expire it after two minutes (legacy values without a timestamp stay valid).
- Show a light tip pointing at the system back control when we cannot
return automatically ("点击返回,立即使用").
Tests: TTL behaviour and return retry policy.
This commit is contained in:
@@ -263,6 +263,16 @@ final class FlowHandoffPolicyTests: XCTestCase {
|
||||
XCTAssertTrue(FlowHandoffPolicy.allowsProactiveHostAutoLaunch)
|
||||
}
|
||||
|
||||
/// Return-to-host retries: more than one chance (the first open can lose a
|
||||
/// race), spaced forward in time, and bounded so the tip appears promptly.
|
||||
func testHostReturnRetryDelaysAreMultipleIncreasingAndBounded() {
|
||||
let delays = FlowHandoffPolicy.hostReturnRetryDelays
|
||||
XCTAssertGreaterThan(delays.count, 1)
|
||||
XCTAssertTrue(delays.allSatisfy { $0 > 0 })
|
||||
XCTAssertEqual(delays, delays.sorted())
|
||||
XCTAssertLessThanOrEqual(delays.reduce(0, +), 6, "tip must not wait out the late-bind window")
|
||||
}
|
||||
|
||||
func testDebouncerIgnoresSingleDeadSample() {
|
||||
var debouncer = FlowColdStartDebouncer()
|
||||
XCTAssertFalse(debouncer.observe(hostTrulyDead: true))
|
||||
|
||||
@@ -52,4 +52,36 @@ final class FlowSessionPolicyTests: XCTestCase {
|
||||
FlowSessionBridge.clearPendingHostBundleId(defaults: defaults)
|
||||
XCTAssertNil(FlowSessionBridge.pendingHostBundleId(defaults: defaults))
|
||||
}
|
||||
|
||||
func testPendingHostBundleIdExpiresAfterTTL() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.setPendingHostBundleId("com.tencent.xin", defaults: defaults)
|
||||
|
||||
let justInside = Date().addingTimeInterval(FlowSessionKeys.pendingHostBundleIdTTL - 5)
|
||||
XCTAssertEqual(
|
||||
FlowSessionBridge.pendingHostBundleId(defaults: defaults, now: justInside),
|
||||
"com.tencent.xin"
|
||||
)
|
||||
let pastTTL = Date().addingTimeInterval(FlowSessionKeys.pendingHostBundleIdTTL + 5)
|
||||
XCTAssertNil(FlowSessionBridge.pendingHostBundleId(defaults: defaults, now: pastTTL))
|
||||
}
|
||||
|
||||
func testPendingHostBundleIdWithoutTimestampStaysValid() {
|
||||
// Written by an older build: no timestamp key. Must keep working.
|
||||
let defaults = makeDefaults()
|
||||
defaults.set("com.tencent.xin", forKey: FlowSessionKeys.pendingHostBundleId)
|
||||
|
||||
let farFuture = Date().addingTimeInterval(FlowSessionKeys.pendingHostBundleIdTTL * 10)
|
||||
XCTAssertEqual(
|
||||
FlowSessionBridge.pendingHostBundleId(defaults: defaults, now: farFuture),
|
||||
"com.tencent.xin"
|
||||
)
|
||||
}
|
||||
|
||||
func testClearingPendingHostBundleIdAlsoClearsTimestamp() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.setPendingHostBundleId("com.tencent.xin", defaults: defaults)
|
||||
FlowSessionBridge.clearPendingHostBundleId(defaults: defaults)
|
||||
XCTAssertNil(defaults.object(forKey: FlowSessionKeys.pendingHostBundleIdAt))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user