This commit is contained in:
2025-11-03 13:25:41 +08:00
parent ffea9d2022
commit c7021e382e
12 changed files with 384 additions and 27 deletions

View File

@@ -31,3 +31,8 @@
#ifndef KB_KEYCHAIN_ACCESS_GROUP
#define KB_KEYCHAIN_ACCESS_GROUP @"TN6HHV45BB.com.keyBoardst.shared"
#endif
// 键盘扩展的 Bundle Identifier用于 App 侧检测是否已添加该键盘)
#ifndef KB_KEYBOARD_EXTENSION_BUNDLE_ID
#define KB_KEYBOARD_EXTENSION_BUNDLE_ID @"com.keyBoardst.CustomKeyboard"
#endif

View File

@@ -0,0 +1,37 @@
//
// KBKeyboardPermissionManager.h
// 主 App/键盘扩展 共用的“键盘启用 + 完全访问”权限管理
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, KBFARecord) {
KBFARecordUnknown = 0,
KBFARecordDenied = 1,
KBFARecordGranted = 2,
};
/// 统一权限管理App 与扩展均可使用)
@interface KBKeyboardPermissionManager : NSObject
+ (instancetype)shared;
/// App 侧:是否已添加并启用了自定义键盘(通过遍历 activeInputModes 粗略判断)
- (BOOL)isKeyboardEnabled;
/// 最后一次由扩展上报的“完全访问”状态(来源:扩展运行后写入共享钥匙串)
- (KBFARecord)lastKnownFullAccess;
/// 扩展侧:上报“完全访问”状态(写入共享钥匙串,以便 App 读取)
- (void)reportFullAccessFromExtension:(BOOL)granted;
/// App 侧:若未满足“已启用键盘 + 完全访问(或未知)”则展示引导页KBPermissionViewController
- (void)presentPermissionIfNeededFrom:(UIViewController *)presenting;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,94 @@
//
// KBKeyboardPermissionManager.m
//
#import "KBKeyboardPermissionManager.h"
#import <Security/Security.h>
#import "KBConfig.h"
// Keychain 访
static NSString * const kKBPermService = @"com.keyBoardst.perm";
static NSString * const kKBPermAccount = @"full_access"; // /0/1/2
@implementation KBKeyboardPermissionManager
+ (instancetype)shared {
static KBKeyboardPermissionManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBKeyboardPermissionManager new]; });
return m;
}
#pragma mark - App side
- (BOOL)isKeyboardEnabled {
// AppDelegate activeInputModes bundle id
for (UITextInputMode *mode in [UITextInputMode activeInputModes]) {
NSString *identifier = nil;
@try { identifier = [mode valueForKey:@"identifier"]; } @catch (__unused NSException *e) { identifier = nil; }
if ([identifier isKindOfClass:NSString.class] && [identifier rangeOfString:KB_KEYBOARD_EXTENSION_BUNDLE_ID].location != NSNotFound) {
return YES;
}
}
return NO;
}
- (KBFARecord)lastKnownFullAccess {
NSData *data = [self keychainRead];
if (data.length == 0) return KBFARecordUnknown;
uint8_t v = 0; [data getBytes:&v length:1];
if (v > KBFARecordGranted) v = KBFARecordUnknown;
return (KBFARecord)v;
}
- (void)presentPermissionIfNeededFrom:(UIViewController *)presenting {
BOOL enabled = [self isKeyboardEnabled];
KBFARecord fa = [self lastKnownFullAccess];
//
// -
// - 访Unknown
BOOL needGuide = (!enabled) || (enabled && fa == KBFARecordDenied);
if (!needGuide || !presenting) return;
Class cls = NSClassFromString(@"KBPermissionViewController");
if (!cls) return; // App
UIViewController *guide = [cls new];
guide.modalPresentationStyle = UIModalPresentationFullScreen;
[presenting presentViewController:guide animated:YES completion:nil];
}
#pragma mark - Extension side
- (void)reportFullAccessFromExtension:(BOOL)granted {
uint8_t v = granted ? KBFARecordGranted : KBFARecordDenied;
NSData *data = [NSData dataWithBytes:&v length:1];
[self keychainWrite:data];
}
#pragma mark - Keychain shared blob
- (NSMutableDictionary *)baseKCQuery {
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: kKBPermService,
(__bridge id)kSecAttrAccount: kKBPermAccount } mutableCopy];
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
return q;
}
- (BOOL)keychainWrite:(NSData *)data {
NSMutableDictionary *query = [self baseKCQuery];
SecItemDelete((__bridge CFDictionaryRef)query);
query[(__bridge id)kSecValueData] = data ?: [NSData data];
query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
OSStatus st = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
return (st == errSecSuccess);
}
- (NSData *)keychainRead {
NSMutableDictionary *query = [self baseKCQuery];
query[(__bridge id)kSecReturnData] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
CFTypeRef dataRef = NULL; OSStatus st = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef);
if (st != errSecSuccess || !dataRef) return nil;
return (__bridge_transfer NSData *)dataRef;
}
@end