iOS用CallKit实现来电识别、来电拦截


前言

最近需要实现一个新需求,用iOS 10出的CallKit实现将APP的通讯录的信息同步到系统中,可以不把人员信息加到通讯录中,实现来电号码识别。这个功能在xx安全卫士、xx管家中很早就实现了,但是网上相关的资料较少,而且官方的文档写的太简单了,很多坑还要自己去摸索。于是记录一下和各位分享,如有错误之处请各位指出!

PS: 先说个题外话吧,CallKit功能在iOS 10的时候还不太稳定,iOS 10刚出来的时候为了体验骚扰拦截功能,手贱装了两个不同的拦截APP,然后就悲剧了。盗一张网上的图:

1.png

然后各种重启、重装APP都没有用,写的Demo也跑不起来,唯一的办法只有重置系统。说多了都是泪!

一、Call Directory app extension

实现来电识别、来电拦截功能需要使用CallKit当中的Call Directory app extension,首先,需要了解extension。关于extension网上有很多教程,这里就不细说了。推荐两篇文章,英文好的推荐看官方文档,还有一篇中文博客

使用Call Directory Extension主要需要和3个类打交道,分别是
CXCallDirectoryProviderCXCallDirectoryExtensionContextCXCallDirectoryManager

2.png

本文涉及的Demo

CXCallDirectoryProvider

官方文档:The principal object for a Call Directory app extension for a host app.

正如官方文档所说,这是Call Directory app extension最重要的一个类。
用系统模板新建Call Directory Extension之后会自动生成一个类,继承自CXCallDirectoryProvider。入口方法:

// 有两种情况改方法会被调用
// 1.第一次打开设置-电话-来电阻止与身份识别开关时,系统自动调用
// 2.调用CXCallDirectoryManager的reloadExtensionWithIdentifier方法会调用
- (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context {
    context.delegate = self;
    // 添加号码识别信息与号码拦截列表
    [self addIdentificationPhoneNumbersToContext:context];
    [context completeRequestWithCompletionHandler:nil];
}

CXCallDirectoryExtensionContext

官方文档:A programmatic interface for adding identification and blocking entries to a Call Directory app extension.
CXCallDirectoryExtensionContext objects are not initialized directly, but are instead passed as arguments to the CXCallDirectoryProvider instance method beginRequestWithExtensionContext:.

大致意思就是说,这是一个为Call Directory app extension添加号码识别、号码拦截的入口。CXCallDirectoryExtensionContext不需要自己初始化,它会作为CXCallDirectoryProviderbeginRequestWithExtensionContext函数的参数传递给使用者。
它的主要方法有两个:

// 设置号码识别信息
- (void)addIdentificationEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber label:(NSString *)label;

// 设置号码拦截列表
- (void)addBlockingEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber;

在设置时候要注意:

  1. 号码不能重复,不然会报错CXErrorCodeCallDirectoryManagerErrorDuplicateEntries
  2. 号码必须按照升序写入,不然会报错CXErrorCodeCallDirectoryManagerErrorEntriesOutOfOrder
  3. 号码必须格式化后传入,手机号码必须加上国家码,例如18012341234就不行,需要加上86,构造成8618012341234;固话需要格式为:国家码+区号(去掉第一个0)+号码,例如010-61001234格式化之后为,861061001234。如果号码格式错误,会导致识别不出来。
  4. 上限数据是200万(在其它文章里看到的,然后自己测试了下,构造了200万条数据写入的时候会报错CXErrorCodeCallDirectoryManagerErrorMaximumEntriesExceeded,150万条数据是OK的,所以这个数据上限一定要注意。实测安装了XX安全卫士、XX管家实现骚扰电话拦截用了3个extension,可能数据量太大就是一个原因。)
  5. 在用户第一次打开设置时,会调用beginRequestWithExtensionContext,这时候不宜写太多数据,不然会卡在设置那里转圈,用户体验很差。可以先写部分数据,然后回到主APP了调用reloadExtensionWithIdentifier去刷新。

CXCallDirectoryManager

官方文档:The programmatic interface to an object that manages a Call Directory app extension.

CXCallDirectoryManager主要作用是管理Call Directory app extension。
有两个方法:

// 重新设置号码识别、电话拦截列表
// 调用该方法后会重置之前设置的列表,然后调用beginRequestWithExtensionContext:
- (void)reloadExtensionWithIdentifier:(NSString *)identifier completionHandler:(nullable void (^)(NSError *_Nullable error))completion;

// 获取extension是否可用,需要在“设置-电话-来电阻止与身份识别"中开启权限
- (void)getEnabledStatusForExtensionWithIdentifier:(NSString *)identifier completionHandler:(void (^)(CXCallDirectoryEnabledStatus enabledStatus, NSError *_Nullable error))completion;

二、实战

先上Demo地址。下面会一步步讲解。

创建extension

新建一个Target(File-New-Target)。

3.png

会自动建立一个目录,默认有三个文件。在.m文件中有系统给出的示例代码

4.png

我们来看看系统的模板代码,首先是入口函数

- (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context {
    context.delegate = self;
    if (context.isIncremental) {
        [self addOrRemoveIncrementalBlockingPhoneNumbersToContext:context];

        [self addOrRemoveIncrementalIdentificationPhoneNumbersToContext:context];
    } else {
        [self addAllBlockingPhoneNumbersToContext:context];

        [self addAllIdentificationPhoneNumbersToContext:context];
    }
    
    [context completeRequestWithCompletionHandler:nil];
}

CallDirectoryHandler

我在Xcode 9生成的代码,context.isIncremental是iOS 11才增加的,还有所有的remove的方法也是iOS 11才有的,为了适配iOS 10,还是不推荐使用。
系统模板代码大致逻辑就是,先添加号码识别、号码拦截记录,添加完成后调用completeRequestWithCompletionHandler:完成整个过程。
由于号码拦截比较简单,只是写入一个号码的数组,本文就以号码识别为例,号码识别方法系统模板这么写的:

- (void)addAllIdentificationPhoneNumbersToContext:(CXCallDirectoryExtensionContext *)context {
    CXCallDirectoryPhoneNumber allPhoneNumbers[] = { 8618788888888, 8618885555555 };
    NSArray<NSString *> *labels = @[ @"送餐电话", @"诈骗电话" ];
    NSUInteger count = (sizeof(allPhoneNumbers) / sizeof(CXCallDirectoryPhoneNumber));
    for (NSUInteger i = 0; i < count; i += 1) {
        CXCallDirectoryPhoneNumber phoneNumber = allPhoneNumbers[i];
        NSString *label = labels[i];
        [context addIdentificationEntryWithNextSequentialPhoneNumber:phoneNumber label:label];
    }
}

这么多代码,核心就是一行[context addIdentificationEntryWithNextSequentialPhoneNumber:phoneNumber label:label];,注意phoneNumber是CXCallDirectoryPhoneNumber类型,其实就是long long类型。
在这个函数里,需要把需要识别的号码和识别信息,一条一条的写入

检查授权

开启extension功能需要在“设置-电话-来电阻止与身份识别”中开启,我们在写入数据时第一步是引导用户给我们的extension授权。

    CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance];
    [manage
     getEnabledStatusForExtensionWithIdentifier:self.externsionIdentifier
     completionHandler:^(CXCallDirectoryEnabledStatus enabledStatus, NSError * _Nullable error) {
         // 根据error,enabledStatus判断授权情况
         // error == nil && enabledStatus == CXCallDirectoryEnabledStatusEnabled 说明可用
         // error 见 CXErrorCodeCallDirectoryManagerError
         // enabledStatus 见 CXCallDirectoryEnabledStatus
     }];

写入数据

用户在设置开启后,调用reloadExtensionWithIdentifier即可触发CallDirectoryHandler更新数据逻辑。

    CXCallDirectoryManager *manager = [CXCallDirectoryManager sharedInstance];
    [manager reloadExtensionWithIdentifier:self.externsionIdentifier completionHandler:^(NSError * _Nullable error) {
        // error 见 CXErrorCodeCallDirectoryManagerError
    }];

验证

接下来在真机下跑下(一定要在插了电话卡的iPhone上调试,模拟器不行!),写入成功后,打开电话,拨号18788888888,提示”送餐电话”。说明写入成功!

5.png

三、extension和containing app数据共享

上面的步骤中,号码信息是写死在代码中的,在实际应用中这些号码信息肯定不是写死的,一般需要从服务器获取。这就需要我们的APP与extension进行通信,需要用到APP Groups,怎么用网上有很多文章了,我就不多说了,推荐一篇
其实本质就是通过APP Groups,开辟一片空间,extension和containing app都可以访问,然后我们的APP就可以通过NSUserDefaults、文件、数据库等方式共享数据给extension了。前期我使用过NSUserDefaults,效率很低,大概在5万数据的时候就爆内存了,使用extension一定要注意内存,不然很容易被系统干掉,所以不推荐使用这种方式。
Demo中采用的是读写文件的方式,大致思路(具体实现看Demo):

  1. 在APP中把数据序列化之后写到一个文件中
  2. 在extension中读取这个文件,读取一行,调用一次addIdentificationEntryWithNextSequentialPhoneNumber,然后及时释放
    这种方式理论上是可以达到最大限制200w条的(实际测试150万没有问题)。

获取APP Groups文件路径

NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:self.groupIdentifier];
    containerURL = [containerURL URLByAppendingPathComponent:@"CallDirectoryData"];
    NSString* filePath = containerURL.path;

进度监控

在xx安全卫士中,开启骚扰电话拦截功能有一个进度条,非常的直观。但是在extension中是没法更新UI的,有一种实现方式,可以用开源框架MMWormhole来实现APP与extension通信,然后把进度从extension传到APP中,在APP中更新进度条。理论上该方案是可行的,感兴趣的同学可以尝试下。

关于上架

在加上CallKit第一次上架时,收到了苹果的拒信,说CallKit在中国区被禁止了。

fileUpload.jpeg

后来观察了下xx安全卫士一直在正常更新,猜想苹果只是禁止了CallKit关于VOIP这部分功能。
遇到苹果的拒信不要慌,录制一个来电识别的功能演示视频,放到Youtube,然后在备注里面解释一下,就可以通过审核了。

给一个审核备注模板:

关于CallKit,我们遵守中国的法律,没有使用VOIP,我们仅使用了Call Directory。我们应用内有一个公司内部通讯录,为了增加用户员工沟通效率,我们在电话页面自动识别内部通讯录的人员 ,并展示姓名与岗位。我们录制了一个演示视频:视频地址


博客地址


欢迎关注我的博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容