// // KBKeyboardPermissionManager.m // #import "KBKeyboardPermissionManager.h" #import #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