2025-11-21 18:26:02 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBExtensionAppLauncher.m
|
|
|
|
|
|
// CustomKeyboard
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBExtensionAppLauncher.h"
|
2026-03-05 14:30:07 +08:00
|
|
|
|
|
|
|
|
|
|
#if KB_URL_BRIDGE_ENABLE
|
2025-11-21 18:26:02 +08:00
|
|
|
|
#import <objc/message.h>
|
2026-03-05 14:30:07 +08:00
|
|
|
|
#endif
|
2025-11-21 18:26:02 +08:00
|
|
|
|
|
|
|
|
|
|
@implementation KBExtensionAppLauncher
|
|
|
|
|
|
|
2026-03-05 14:30:07 +08:00
|
|
|
|
#if KB_URL_BRIDGE_ENABLE
|
|
|
|
|
|
+ (BOOL)kb_openURLViaResponderChain:(NSURL *)url
|
|
|
|
|
|
source:(nullable UIResponder *)source
|
|
|
|
|
|
completion:(void (^ _Nullable)(BOOL success))completion {
|
|
|
|
|
|
if (!url) {
|
|
|
|
|
|
if (completion) { completion(NO); }
|
|
|
|
|
|
return NO;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
UIResponder *responder = source;
|
|
|
|
|
|
|
|
|
|
|
|
// 优先尝试 openURL:options:completionHandler:
|
|
|
|
|
|
// 注意:在键盘扩展里走“响应链兜底”本身就存在不确定性;不同系统/宿主 App 的实现
|
|
|
|
|
|
// 可能对 options 参数的类型有不同假设。为避免类型不匹配导致崩溃,options 统一传 nil。
|
|
|
|
|
|
SEL openURLOptionsSel = NSSelectorFromString(@"openURL:options:completionHandler:");
|
|
|
|
|
|
while (responder) {
|
|
|
|
|
|
if ([responder respondsToSelector:openURLOptionsSel]) {
|
|
|
|
|
|
void (*msgSend)(id, SEL, NSURL *, id, void (^)(BOOL)) = (void *)objc_msgSend;
|
|
|
|
|
|
msgSend(responder, openURLOptionsSel, url, nil, ^(BOOL ok) {
|
|
|
|
|
|
if (completion) { completion(ok); }
|
|
|
|
|
|
});
|
|
|
|
|
|
return YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
responder = responder.nextResponder;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试 openURL:completionHandler:
|
|
|
|
|
|
responder = source;
|
|
|
|
|
|
SEL openURLCompletionSel = NSSelectorFromString(@"openURL:completionHandler:");
|
|
|
|
|
|
while (responder) {
|
|
|
|
|
|
if ([responder respondsToSelector:openURLCompletionSel]) {
|
|
|
|
|
|
void (*msgSend)(id, SEL, NSURL *, void (^)(BOOL)) = (void *)objc_msgSend;
|
|
|
|
|
|
msgSend(responder, openURLCompletionSel, url, ^(BOOL ok) {
|
|
|
|
|
|
if (completion) { completion(ok); }
|
|
|
|
|
|
});
|
|
|
|
|
|
return YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
responder = responder.nextResponder;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 兜底:openURL:
|
|
|
|
|
|
responder = source;
|
|
|
|
|
|
SEL openURLSel = NSSelectorFromString(@"openURL:");
|
|
|
|
|
|
while (responder) {
|
|
|
|
|
|
if ([responder respondsToSelector:openURLSel]) {
|
|
|
|
|
|
BOOL (*msgSend)(id, SEL, NSURL *) = (void *)objc_msgSend;
|
|
|
|
|
|
BOOL ok = msgSend(responder, openURLSel, url);
|
|
|
|
|
|
if (completion) { completion(ok); }
|
|
|
|
|
|
return YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
responder = responder.nextResponder;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (completion) { completion(NO); }
|
|
|
|
|
|
return NO;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2025-11-21 18:26:02 +08:00
|
|
|
|
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
|
|
|
|
|
|
fallbackURL:(NSURL * _Nullable)fallbackURL
|
|
|
|
|
|
usingInputController:(UIInputViewController *)ivc
|
2026-03-05 14:30:07 +08:00
|
|
|
|
source:(UIResponder * _Nullable)source
|
2025-11-21 18:26:02 +08:00
|
|
|
|
completion:(void (^ _Nullable)(BOOL success))completion {
|
2026-03-05 14:30:07 +08:00
|
|
|
|
if (![NSThread isMainThread]) {
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
|
[self openPrimaryURL:primaryURL
|
|
|
|
|
|
fallbackURL:fallbackURL
|
|
|
|
|
|
usingInputController:ivc
|
|
|
|
|
|
source:source
|
|
|
|
|
|
completion:completion];
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-11-21 18:26:02 +08:00
|
|
|
|
if (!ivc || (!primaryURL && !fallbackURL)) {
|
|
|
|
|
|
if (completion) { completion(NO); }
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保证在主线程回调,避免调用方再做一次 dispatch。
|
|
|
|
|
|
void (^finish)(BOOL) = ^(BOOL ok){
|
|
|
|
|
|
if (!completion) return;
|
|
|
|
|
|
if ([NSThread isMainThread]) {
|
|
|
|
|
|
completion(ok);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{ completion(ok); });
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
NSURL *first = primaryURL ?: fallbackURL;
|
|
|
|
|
|
NSURL *second = (first == primaryURL) ? fallbackURL : nil;
|
|
|
|
|
|
|
|
|
|
|
|
if (!first) {
|
|
|
|
|
|
finish(NO);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[ivc.extensionContext openURL:first completionHandler:^(BOOL ok) {
|
|
|
|
|
|
if (ok) {
|
|
|
|
|
|
finish(YES);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (second) {
|
|
|
|
|
|
[ivc.extensionContext openURL:second completionHandler:^(BOOL ok2) {
|
|
|
|
|
|
if (ok2) {
|
|
|
|
|
|
finish(YES);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-05 14:30:07 +08:00
|
|
|
|
|
|
|
|
|
|
#if KB_URL_BRIDGE_ENABLE
|
|
|
|
|
|
// 的场景且业务强依赖时才开启此兜底。
|
|
|
|
|
|
UIResponder *start = (source ?: (UIResponder *)ivc.view ?: (UIResponder *)ivc);
|
|
|
|
|
|
[self kb_openURLViaResponderChain:second
|
|
|
|
|
|
source:start
|
|
|
|
|
|
completion:^(BOOL ok3) {
|
|
|
|
|
|
finish(ok3);
|
|
|
|
|
|
}];
|
|
|
|
|
|
#else
|
|
|
|
|
|
finish(NO);
|
|
|
|
|
|
#endif
|
2025-11-21 18:26:02 +08:00
|
|
|
|
}];
|
|
|
|
|
|
} else {
|
2026-03-05 14:30:07 +08:00
|
|
|
|
#if KB_URL_BRIDGE_ENABLE
|
|
|
|
|
|
UIResponder *start = (source ?: (UIResponder *)ivc.view ?: (UIResponder *)ivc);
|
|
|
|
|
|
[self kb_openURLViaResponderChain:first
|
|
|
|
|
|
source:start
|
|
|
|
|
|
completion:^(BOOL ok3) {
|
|
|
|
|
|
finish(ok3);
|
|
|
|
|
|
}];
|
|
|
|
|
|
#else
|
|
|
|
|
|
finish(NO);
|
|
|
|
|
|
#endif
|
2025-11-21 18:26:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
+ (void)openScheme:(NSURL *)scheme
|
|
|
|
|
|
usingInputController:(UIInputViewController *)ivc
|
2026-03-05 14:30:07 +08:00
|
|
|
|
source:(UIResponder * _Nullable)source
|
2025-11-21 18:26:02 +08:00
|
|
|
|
completion:(void (^ _Nullable)(BOOL success))completion {
|
|
|
|
|
|
[self openPrimaryURL:scheme
|
|
|
|
|
|
fallbackURL:nil
|
|
|
|
|
|
usingInputController:ivc
|
|
|
|
|
|
source:source
|
|
|
|
|
|
completion:completion];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|