95 lines
3.6 KiB
Objective-C
95 lines
3.6 KiB
Objective-C
//
|
||
// KBKeyboardPermissionManager.m
|
||
//
|
||
|
||
#import "KBKeyboardPermissionManager.h"
|
||
#import <Security/Security.h>
|
||
#import "KBConfig.h"
|
||
|
||
// Keychain 存储:记录上次扩展上报的“完全访问”状态
|
||
static NSString * const kKBPermService = @"com.loveKey.nyx.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
|