fixUI
This commit is contained in:
@@ -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
|
||||
|
||||
37
Shared/KBKeyboardPermissionManager.h
Normal file
37
Shared/KBKeyboardPermissionManager.h
Normal 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
|
||||
|
||||
94
Shared/KBKeyboardPermissionManager.m
Normal file
94
Shared/KBKeyboardPermissionManager.m
Normal 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
|
||||
Reference in New Issue
Block a user