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.
134 lines
4.8 KiB
Objective-C
134 lines
4.8 KiB
Objective-C
// OSGHostArbiterProbe.m
|
||
// OSGKeyboard · Keyboard Extension
|
||
//
|
||
// 宿主识别探针(所有构建配置均编译),见头文件说明。所有私有符号都在运行时按字符串查找,
|
||
// 任何一步找不到都静默放弃,并保证原始实现照常执行。
|
||
|
||
#import "OSGHostArbiterProbe.h"
|
||
|
||
#import <UIKit/UIKit.h>
|
||
#import <objc/message.h>
|
||
#import <objc/runtime.h>
|
||
#import <os/lock.h>
|
||
|
||
static NSString *const kArbiterClientClassName = @"_UIKeyboardArbiterClient";
|
||
static NSString *const kDestinationClassName = @"_UIKeyboardArbiterClientInputDestination";
|
||
static NSString *const kEnabledSelectorName = @"enabled";
|
||
static NSString *const kSharedClientSelectorName = @"automaticSharedArbiterClient";
|
||
static NSString *const kCheckConnectionSelectorName = @"checkConnection";
|
||
static NSString *const kKeyboardChangedSelectorName = @"queue_keyboardChanged:onComplete:";
|
||
static NSString *const kSourceBundleIdKey = @"_sourceBundleIdentifier";
|
||
|
||
static os_unfair_lock sLock = OS_UNFAIR_LOCK_INIT;
|
||
static OSGHostArbiterCaptureHandler _Nullable sHandler = nil;
|
||
static NSString *_Nullable sLastCapturedBundleId = nil;
|
||
static IMP _Nullable sOriginalKeyboardChangedIMP = NULL;
|
||
static BOOL sInstalled = NO;
|
||
|
||
#pragma mark - Trampolines
|
||
|
||
// 替换 -[_UIKeyboardArbiterClientInputDestination queue_keyboardChanged:onComplete:]。
|
||
// bundle ID 在参数 change 上,不在 self 上。
|
||
static void OSGSwizzledKeyboardChanged(id self, SEL _cmd, id change, id completion) {
|
||
NSString *bundleId = nil;
|
||
if (change) {
|
||
@try {
|
||
id value = [change valueForKey:kSourceBundleIdKey];
|
||
if ([value isKindOfClass:[NSString class]]) {
|
||
bundleId = [(NSString *)value copy];
|
||
}
|
||
} @catch (__unused NSException *exception) {
|
||
// 字段改名时 KVC 会抛异常;吞掉,保证原始实现继续执行。
|
||
}
|
||
}
|
||
NSString *detail = change ? NSStringFromClass([change class]) : @"change=nil";
|
||
|
||
os_unfair_lock_lock(&sLock);
|
||
// 只在读到有效 ID 时更新。读不到 ID 的回调(焦点切换、空事件)不再清掉已拿到的值:
|
||
// 之前“最后一个事件为准”会让一次空回调把正确的宿主抹掉,是识别忽有忽无的来源之一。
|
||
// 宿主真的变了,由键盘收起时的 invalidateCapturedBundleId 作废旧值。
|
||
if (bundleId.length > 0) {
|
||
sLastCapturedBundleId = bundleId;
|
||
}
|
||
OSGHostArbiterCaptureHandler handler = sHandler;
|
||
os_unfair_lock_unlock(&sLock);
|
||
if (handler) {
|
||
handler(bundleId, detail);
|
||
}
|
||
|
||
if (sOriginalKeyboardChangedIMP) {
|
||
((void (*)(id, SEL, id, id))sOriginalKeyboardChangedIMP)(self, _cmd, change, completion);
|
||
}
|
||
}
|
||
|
||
// 替换 +[_UIKeyboardArbiterClient enabled]:iOS 26.4 用它控制是否派发上面的回调。
|
||
static BOOL OSGAlwaysEnabled(__unused id self, __unused SEL _cmd) {
|
||
return YES;
|
||
}
|
||
|
||
@implementation OSGHostArbiterProbe
|
||
|
||
+ (void)load {
|
||
if (@available(iOS 26.4, *)) {
|
||
[self installHooks];
|
||
}
|
||
}
|
||
|
||
+ (void)installHooks {
|
||
Class clientClass = NSClassFromString(kArbiterClientClassName);
|
||
if (clientClass) {
|
||
Method enabled = class_getClassMethod(clientClass, NSSelectorFromString(kEnabledSelectorName));
|
||
if (enabled) {
|
||
method_setImplementation(enabled, (IMP)&OSGAlwaysEnabled);
|
||
}
|
||
}
|
||
|
||
Class destinationClass = NSClassFromString(kDestinationClassName);
|
||
if (!destinationClass) return;
|
||
Method keyboardChanged = class_getInstanceMethod(
|
||
destinationClass,
|
||
NSSelectorFromString(kKeyboardChangedSelectorName)
|
||
);
|
||
if (!keyboardChanged) return;
|
||
sOriginalKeyboardChangedIMP = method_getImplementation(keyboardChanged);
|
||
method_setImplementation(keyboardChanged, (IMP)&OSGSwizzledKeyboardChanged);
|
||
sInstalled = YES;
|
||
}
|
||
|
||
+ (BOOL)isInstalled {
|
||
return sInstalled;
|
||
}
|
||
|
||
+ (NSString *)lastCapturedBundleId {
|
||
os_unfair_lock_lock(&sLock);
|
||
NSString *bundleId = [sLastCapturedBundleId copy];
|
||
os_unfair_lock_unlock(&sLock);
|
||
return bundleId;
|
||
}
|
||
|
||
+ (void)invalidateCapturedBundleId {
|
||
os_unfair_lock_lock(&sLock);
|
||
sLastCapturedBundleId = nil;
|
||
os_unfair_lock_unlock(&sLock);
|
||
}
|
||
|
||
+ (void)setCaptureHandler:(OSGHostArbiterCaptureHandler)handler {
|
||
os_unfair_lock_lock(&sLock);
|
||
sHandler = [handler copy];
|
||
os_unfair_lock_unlock(&sLock);
|
||
}
|
||
|
||
+ (void)pingArbiter {
|
||
Class clientClass = NSClassFromString(kArbiterClientClassName);
|
||
SEL sharedSelector = NSSelectorFromString(kSharedClientSelectorName);
|
||
if (!clientClass || ![clientClass respondsToSelector:sharedSelector]) return;
|
||
|
||
id client = ((id (*)(id, SEL))objc_msgSend)(clientClass, sharedSelector);
|
||
SEL checkSelector = NSSelectorFromString(kCheckConnectionSelectorName);
|
||
if (client && [client respondsToSelector:checkSelector]) {
|
||
((void (*)(id, SEL))objc_msgSend)(client, checkSelector);
|
||
}
|
||
}
|
||
|
||
@end
|