chore(flow): expand host-return whitelist and add a DEBUG host bundle probe
- Rebuild the return-to-host whitelist from the CN/US top charts plus Apple system apps; drop the canOpenURL pre-check (LSApplicationQueriesSchemes is capped at 50) and let open() report failure. - Never record an OSG bundle id as the host to return to. - DEBUG only: probe the host bundle id through the keyboard arbiter callback and surface the log on a Home test card. Release builds keep the previous behaviour (no host identification).
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// HostBundleProbe.swift
|
||||
// OSGKeyboard · Keyboard Extension
|
||||
//
|
||||
// 识别当前宿主 App 的 bundle ID,供冷启动跳转主 App 后自动跳回。
|
||||
// 目前依赖仅在 DEBUG 编译的私有 arbiter 探针(`OSGHostArbiterProbe`),
|
||||
// Release 构建里识别结果恒为 nil,行为与接入前一致。
|
||||
|
||||
import Foundation
|
||||
import OSGKeyboardShared
|
||||
|
||||
enum HostAppIdentity {
|
||||
/// 最近一次识别到的宿主;识别不到、或宿主是 OSG 自己时为 nil。
|
||||
static var currentBundleId: String? {
|
||||
#if DEBUG
|
||||
guard let bundleId = OSGHostArbiterProbe.lastCapturedBundleId,
|
||||
!HostAppURLRegistry.isOwnBundleId(bundleId) else {
|
||||
return nil
|
||||
}
|
||||
return bundleId
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 跳转主 App(`startflow`)前调用:把当前宿主写进 App Group,主 App 就绪后据此跳回。
|
||||
/// 识别不到时写 nil,清掉上一次残留,避免跳回错误的 App。
|
||||
static func publishReturnTarget() {
|
||||
let bundleId = currentBundleId
|
||||
FlowSessionBridge.setPendingHostBundleId(bundleId)
|
||||
#if DEBUG
|
||||
let returnable = HostAppURLRegistry.lookup(bundleId: bundleId) != nil
|
||||
HostBundleProbeLog.append(
|
||||
event: "startflow",
|
||||
bundleId: bundleId,
|
||||
detail: returnable ? "returnable" : "no-return-url"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 键盘收起时作废识别结果,下次弹出由新回调重新填充。
|
||||
static func invalidate() {
|
||||
#if DEBUG
|
||||
OSGHostArbiterProbe.invalidateCapturedBundleId()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// 临时调试:把探针回调写进 App Group,主 App 首页的测试卡片读取展示。
|
||||
enum HostBundleProbe {
|
||||
/// 只需安装一次;回调可能来自非主线程,闭包必须是 nonisolated。
|
||||
static func installLogging() {
|
||||
OSGHostArbiterProbe.setCaptureHandler { @Sendable bundleId, detail in
|
||||
HostBundleProbeLog.append(event: "arbiter", bundleId: bundleId, detail: detail)
|
||||
}
|
||||
}
|
||||
|
||||
/// 键盘弹出时记一条标记,再主动触发回调,便于把记录和每次弹出对应起来。
|
||||
static func recordPresentationAndPing() {
|
||||
HostBundleProbeLog.append(
|
||||
event: "appear",
|
||||
bundleId: nil,
|
||||
detail: OSGHostArbiterProbe.isInstalled ? "hook=installed" : "hook=missing"
|
||||
)
|
||||
OSGHostArbiterProbe.pingArbiter()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,38 @@
|
||||
// OSGHostArbiterProbe.h
|
||||
// OSGKeyboard · Keyboard Extension
|
||||
//
|
||||
// 临时调试探针(仅 DEBUG):iOS 26.4+ 通过 _UIKeyboardArbiterClient 回调
|
||||
// 读取宿主 App 的 bundle ID。原理参考 editorss/KeyboardHostBundleID(MIT)。
|
||||
// 必须在 +load 安装,否则可能错过键盘首次切换宿主时的回调。
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 回调可能在 arbiter 的私有队列上触发,不保证主线程。
|
||||
typedef void (^OSGHostArbiterCaptureHandler)(NSString *_Nullable bundleId, NSString *_Nullable detail);
|
||||
|
||||
@interface OSGHostArbiterProbe : NSObject
|
||||
|
||||
/// 钩子是否已成功安装(私有类或方法不存在时为 NO)。
|
||||
@property (class, nonatomic, readonly) BOOL isInstalled;
|
||||
|
||||
/// 最近一次回调读到的宿主 bundle ID;回调读不到或已作废时为 nil。线程安全。
|
||||
@property (class, nonatomic, readonly, nullable) NSString *lastCapturedBundleId;
|
||||
|
||||
/// 作废上一次的识别结果(键盘收起时调用,避免把上一个宿主当成当前宿主)。
|
||||
+ (void)invalidateCapturedBundleId;
|
||||
|
||||
/// 每次 arbiter 回调都会调用;设置为 nil 取消。
|
||||
+ (void)setCaptureHandler:(nullable OSGHostArbiterCaptureHandler)handler;
|
||||
|
||||
/// 主动让 arbiter 检查连接,可能触发一次回调,但不保证。
|
||||
+ (void)pingArbiter;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,133 @@
|
||||
// OSGHostArbiterProbe.m
|
||||
// OSGKeyboard · Keyboard Extension
|
||||
//
|
||||
// 临时调试探针(仅 DEBUG),见头文件说明。所有私有符号都在运行时按字符串查找,
|
||||
// 任何一步找不到都静默放弃,并保证原始实现照常执行。
|
||||
|
||||
#import "OSGHostArbiterProbe.h"
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
#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 不能再用。
|
||||
sLastCapturedBundleId = bundleId.length > 0 ? bundleId : nil;
|
||||
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
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user