SINKCAPITAL
SINKCAPITAL
Comapny Blog
iosのcallkit周りでできること
tech

背景

 弊社ではITコンサル事業を行う傍iosゲームなど様々な新規事業を検討しては開発を行っています。 その中で今回案としてiosの電話機能周りを用いた新規プロダクトを検討していたのですが、 そもそもiosの電話周りが技術的に何ができるのかがわからなかったため調査を行いました。 なお今回電話受信時を検知することを目的としていたため、 アプリ内から電話を発信する側の知見については調査していません。

調査結果(電話受信時にできること)

 電話周りで使えるものとしては以下の2つが存在し、 それぞれ電話受信時にできることは以下の表の内容となっていました。 (厳密にはCallDirectoryはextensionのためCallKitの一部ですがわかりやすくするため分けています)

項目callkitCall Directory Extension
内容電話周りのiosフレームワークcallkitのextension
受信時所得できる情報「着信」「受話」「終話」タイミングなし
受信時にできること「着信」「受話」「終話」を検知しプログラムを実行辞書登録された電話番号に沿って、ラベル表示か受信拒否

Call Directory Extensionについて補足説明をしておくと、 こちらは「辞書登録された電話番号に沿って、ラベル表示か受信拒否」する機能を使用する際に使用する辞書の登録が行える機能であり、 そのため辞書への登録有無に関わらず受信時に電話番号やラベルの情報取得はできないようになっていました。
 当初ラベル登録さえしておけば誰からかかってきたのかをプログラム上で検知できると思っていたのですが、 調査の結果それはできずプログラム上から確認できるのは「着信」「受話」「終話」時刻程度ということがわかりました。

調査内容

 以下調査時の詳細内容となります。

web記事を漁る

 まず参考リンクにあるようなcallkitに関する記事を読み漁りました。 その中で出てきた情報をまとめると以下のようなものがありました

  • 個人情報保護の観点から受信電話番号自体の取得は不可能
  • 事前に電話番号とラベルを登録することで、受信電話番号がリストにあればラベルを返せる
  • 電話番号の受信時や保留時、切ったタイミングをプログラム上で検知できる

この時点では「電話番号は取得できないが、リストとして登録したものは検知できるのか」といった理解をしていました。

実際にコードを書いてみる

 callkitについては、 【Swift4】【CallKit】CXCall、電話の発着信等を取得する。 の内容を参考にすれば30分程度でサンプルアプリを作成することができました。 その結果「着信」「受話」「終話」の情報は取得できることがわかりました。 次にweb記事でもよくでていたCall Directory Extensionについては、 CallKit 思わぬハマリどころも!Call Directory Extensionを使って着信時に発信者名を表示する の内容を参考に上記で作成したアプリに追加を行いました。 ここが一番の勘違いしていたところだったのですが、 Call Directory Extensionのサンプルコードに辞書登録・編集のコードしかないように、 本機能が実際の受信時に呼ばれるものではないことを知りました。

実際のコード

※ほぼ参考リンクの内容ままです

import UIKit
import CallKit

class HomeViewController: UIViewController {

    var callObserver : CallKitController!
    @IBOutlet weak var stateLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        callObserver = CallKitController()

        /// NotificationCenterに登録する
        NotificationCenter.default.addObserver(self,  //オブザーバとして登録
            selector: #selector(catchNotification(notification:)), //通知を受け取った時に投げるメソッド
            name: Notification.Name("myNotificationName"), //通知の名前
            object: nil)

        // Call Directory Extensionの辞書を更新するコード
        CXCallDirectoryManager
            .sharedInstance
            .getEnabledStatusForExtension(withIdentifier: <Call Directory ExtensionのバンドルID>) {
                (status, errorOrNil) in
                switch status {
                case .enabled:
                // 有効
                    print("Call Directory enabled")
                case .disabled:
                // 無効
                    print("Call Directory disabled")
                case .unknown:
                    // 不明
                    print("Call Directory unknown")
                default:
                    fatalError()
                }
        }

        CXCallDirectoryManager
            .sharedInstance
            .reloadExtension(withIdentifier: <Call Directory ExtensionのバンドルID>) { errorOrNil in
                if let error = errorOrNil as? CXErrorCodeCallDirectoryManagerError {
                    print("reload failed")
                    switch error.code {
                    case .unknown:
                        print("error is unknown")
                    case .noExtensionFound:
                        print("error is noExtensionFound")
                    case .loadingInterrupted:
                        print("error is loadingInterrupted")
                    case .entriesOutOfOrder:
                        print("error is entriesOutOfOrder")
                    case .duplicateEntries:
                        print("error is duplicateEntries")
                    case .maximumEntriesExceeded:
                        print("maximumEntriesExceeded")
                    case .extensionDisabled:
                        print("extensionDisabled")
                    case .currentlyLoading:
                        print("currentlyLoading")
                    case .unexpectedIncrementalRemoval:
                        print("unexpectedIncrementalRemoval")
                    default:
                        fatalError()
                    }
                } else {
                    print("reload succeeded")
                }
        }
    }

    @objc func catchNotification(notification: Notification) -> Void {
        print("Catch notification")
        // 通知を受け取ったことにより行いたい処理を書く
        stateLabel.text = "通話を受けたよ!"
    }
}

※ XXXXXXXXの箇所は任意の番号を入れて使用してください

import Foundation
import CallKit

class CallDirectoryHandler: CXCallDirectoryProvider {

    override func beginRequest(with context: CXCallDirectoryExtensionContext) {
        context.delegate = self

        // Check whether this is an "incremental" data request. If so, only provide the set of phone number blocking
        // and identification entries which have been added or removed since the last time this extension's data was loaded.
        // But the extension must still be prepared to provide the full set of data at any time, so add all blocking
        // and identification phone numbers if the request is not incremental.
        print(context)
        print(context.isIncremental)
        if context.isIncremental {
            addOrRemoveIncrementalBlockingPhoneNumbers(to: context)
            addOrRemoveIncrementalIdentificationPhoneNumbers(to: context)
        } else {
            print("addAllBlockingPhoneNumbers")
            addAllBlockingPhoneNumbers(to: context)
            print("addAllIdentificationPhoneNumbers")
            addAllIdentificationPhoneNumbers(to: context)
        }

        context.completeRequest()
    }

    private func addAllBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
        // Retrieve all phone numbers to block from data store. For optimal performance and memory usage when there are many phone numbers,
        // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
        //
        // Numbers must be provided in numerically ascending order.
        let allPhoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1_408_555_5555, 1_800_555_5555 ]
        for phoneNumber in allPhoneNumbers {
            context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
        }
    }

    private func addOrRemoveIncrementalBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
        // Retrieve any changes to the set of phone numbers to block from data store. For optimal performance and memory usage when there are many phone numbers,
        // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
        let phoneNumbersToAdd: [CXCallDirectoryPhoneNumber] = [ 1_408_555_1234 ]
        for phoneNumber in phoneNumbersToAdd {
            context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
        }

        let phoneNumbersToRemove: [CXCallDirectoryPhoneNumber] = [ 1_800_555_5555 ]
        for phoneNumber in phoneNumbersToRemove {
            context.removeBlockingEntry(withPhoneNumber: phoneNumber)
        }

        // Record the most-recently loaded set of blocking entries in data store for the next incremental load...
    }

    private func addAllIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
        // Retrieve phone numbers to identify and their identification labels from data store. For optimal performance and memory usage when there are many phone numbers,
        // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
        //
        // Numbers must be provided in numerically ascending order.
        let allPhoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1_877_555_5555, 1_888_555_5555, 81_80XXXXXXXX, 81_80XXXXXXXX ]
        let labels = [ "Telemarketer", "Local business", "LABEL1" , "LABEL2"]
        for (phoneNumber, label) in zip(allPhoneNumbers, labels) {
            context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
        }
    }

    private func addOrRemoveIncrementalIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
        // Retrieve any changes to the set of phone numbers to identify (and their identification labels) from data store. For optimal performance and memory usage when there are many phone numbers,
        // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
        let phoneNumbersToAdd: [CXCallDirectoryPhoneNumber] = [ 1_408_555_5678, 81_80XXXXXXXX, 81_80XXXXXXXX ]
        let labelsToAdd = [ "New local business", "LABEL1" , "LABEL2" ]

        for (phoneNumber, label) in zip(phoneNumbersToAdd, labelsToAdd) {
            context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
        }

        let phoneNumbersToRemove: [CXCallDirectoryPhoneNumber] = [ 1_888_555_5555 ]

        for phoneNumber in phoneNumbersToRemove {
            context.removeIdentificationEntry(withPhoneNumber: phoneNumber)
        }

        // Record the most-recently loaded set of identification entries in data store for the next incremental load...
    }
}

extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {
    func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) {
        // An error occurred while adding blocking or identification entries, check the NSError for details.
        // For Call Directory error codes, see the CXErrorCodeCallDirectoryManagerError enum in <CallKit/CXError.h>.
        //
        // This may be used to store the error details in a location accessible by the extension's containing app, so that the
        // app may be notified about errors which occured while loading data even if the request to load data was initiated by
        // the user in Settings instead of via the app itself.
    }
}

感想

 今回電話受信周りの技術検証を実施してみたのですが、 appleの個人情報保護を身を以て感じることができました。 ラベルに電話番号を入れればどこからかかってきたのかも取れてしまうことを考えると、 さすがしっかりとした対策をされているといった印象です。 今後もこういった技術検証結果を知見として残せていければと思います。
こちらは弊社の新規サービス検討の中で検証した内容ではございますが、iOSのcall kitに関することでお悩みの際は ぜひお気軽にお問い合わせください。  

参考リンク

Nuxt上でのd3を利用した散布図の作成方法
櫻井 裕司
2021/10/29 櫻井 裕司
techdataAnalytics
クリック可能な散布図をNuxtjs上で作成する場合にd3.jsが汎用性が高く便利でした。利用するにあたって難しかった点などを備考録としてまとめています。
アクセスログを可視化しGAのデータを直感的に理解できる型態にする試み(ネットワーク型)
櫻井 裕司
2021/09/05 櫻井 裕司
techdataAnalytics
ビジネスに活きる分析を進める上で弊社では「理解できる」ことを重要と考えており、特に直感的理解は可視化を進める上で特に重要だと考える内容の一つです。弊社では様々なお客様のデータ分析を進める上で常により示唆の大きい可視化を追求しており、今回はその中で最近試みているネットワーク側の可視化についてまとめたいと思います。
代表櫻井による特別講演会が白陵高等学校で開かれました
櫻井 裕司
2021/07/31 櫻井 裕司
eventpersonal
2021年の夏に兵庫県の私立白陵高等学校において、代表櫻井による特別講演会を開催いたしました。今振り返って高校の頃の自分に伝えたいことについてお話しました。
Nuxtで動的ページを随時追加する場合にNot Foundとなる
櫻井 裕司
2021/05/31 櫻井 裕司
tech
Nuxtで動的ページを登録する方法はありますが、登録後に随時コンテンツが追加される際はNot Foundとなってしまうので、そう言った際の対処方法について
GKEをやめてCloud Runを始めてみました
櫻井 裕司
2021/04/19 櫻井 裕司
tech
firebaseで構築したシステムの裏で動かすバッチの負荷が大きく、cloud functionsで終わらなかったためCloud Runを利用してみました。動作確認までの知見等を雑多にまとめてみました。
AWSをやめてfirebaseを使い始めて感じたメリットやデメリットとそれの対応策(LT登壇内容)
櫻井 裕司
2021/03/26 櫻井 裕司
techeventpersonal
みそかつウェブ・GDG Nagoya主催の「around firebase」とCloud Native Nagoya主演の「Cloud Native Nagoya」にてfirebaseのLTをさせていただきました。そこで会話させていただいたfirebaseを使い始めて感じたメリット・デメリットについてまとめています。
PWA+SPAのwebアプリ作成にnuxtjs+firebaseがめちゃ便利だった
櫻井 裕司
2021/01/16 櫻井 裕司
tech
PWA+SPAのwebアプリを作る際にnuxt.js+firebaseを合わせて利用すると便利だったので知見を書き留めています
s3のhostingでPWAを導入する方法
櫻井 裕司
2020/12/19 櫻井 裕司
tech
アプリ作成時にpwaが比較されることが多かったですが、実際にpwaを実装した経験がなかったため今回自社サイトをPWA化してみました。
dockerでseleniumを動かしてみる(chrome_headless)
櫻井 裕司
2020/12/06 櫻井 裕司
tech
seleniumの相談をいただくことが増えたため、seleniumの勉強もかねてdockerでの実行テストを行いました
THE DECKのイベントにお邪魔させていただきました
本林 秀和
2020/12/05 本林 秀和
eventpersonal
大学コンソーシアム大阪のイベント@The DECK にお邪魔してきました
flutter(dart)を触ってみた感想
櫻井 裕司
2020/11/18 櫻井 裕司
tech
android向けアプリへの対応も考慮してflutter(dart)を触ってみたので、感想をまとめておこうと思います。理解が深まっていく中で定期的にまとめていければと思います。
代表本林による特別講演会が滝高校で開かれました
本林 秀和
2020/11/07 本林 秀和
eventpersonal
2020年11月7日(土)愛知県の私立滝高校において、代表本林による特別講演会を開催いたしました。IT業界やデータサイエンスについてお話しました。
AWS・GCPを選ぶ際の観点
櫻井 裕司
2020/10/28 櫻井 裕司
tech
AWSかGCPを選ぶ際の観点について書き留めておこうと思います
CloudFormationとterraformの比較
櫻井 裕司
2020/10/04 櫻井 裕司
tech
AWS CloudFormationとterraformの両方を使ってみて感じた違いをまとめてみました。
【個人ブログ】CTOの株運用ブログ_順調な滑り出し
櫻井 裕司
2020/07/19 櫻井 裕司
personalstock
長年放置してた株に少し手を出してみました。自分なりに少し情報整理と分析と予想をしたので記事にしてみます。
総務省特定サービス産業実態調査のデータ分析
櫻井 裕司
2020/07/18 櫻井 裕司
techdataAnalytics
総務省がAPIで市場データを公開しており、分析技術向上と市場感を養うことを目的に定期的に分析を行なっていこうと思います。今回は「特定サービス産業実態調査」について見ていこうと思います。
「お絵かきつみ木バトル」をリリースしました
櫻井 裕司
2020/07/12 櫻井 裕司
techapp
タスク管理を二次元的に行うアプリ「お絵かきつみ木バトル」をリリースしました。SinkCapitalはデータコンサルですが、知見蓄積のため様々な媒体での実験的開発を行っています
総務省工業統計調査のデータ分析
櫻井 裕司
2020/07/11 櫻井 裕司
techdataAnalytics
総務省がAPIで市場データを公開しており、分析技術向上と市場感を養うことを目的に定期的に分析を行なっていこうと思います。今回は「工業統計調査」について見ていこうと思います。
【個人ブログ】CTOが個人的に株をはじめました
櫻井 裕司
2020/07/08 櫻井 裕司
personalstock
長年放置してた株に少し手を出してみました。自分なりに少し情報整理と分析と予想をしたので記事にしてみます。
総務省サービス産業動向調査のデータ分析
櫻井 裕司
2020/07/08 櫻井 裕司
techdataAnalytics
総務省がAPIで市場データを公開しており、分析技術向上と市場感を養うことを目的に定期的に分析を行なっていこうと思います。初回は「サービス産業動向調査」について見ていこうと思います。
タスク管理アプリ「タスククロス」をリリースしました
櫻井 裕司
2020/04/08 櫻井 裕司
techapp
タスク管理を二次元的に行うアプリ「タスククロス」をリリースしました。SinkCapitalはデータコンサルですが、知見蓄積のため様々な媒体での実験的開発を行っています
【terraform】gcpでcicd環境を構築する方法
櫻井 裕司
2020/01/04 櫻井 裕司
tech
企業サイトはAWSを利用しているのですが、要件によってはGCPの方が適している場合もあるため、GCPでのcicd構築も行いました。AWSと比較しつつ説明しているため是非ご参考にしてみてください。
【合格体験記】GCP_Cloud_Archtectに受かりました
櫻井 裕司
2019/12/23 櫻井 裕司
personalqualification
Google Professional Cloud Architectに合格したので、勉強法別のコスパをまとめてみました。
AWSでサブドメインなし(wwwなし)からサブドメインあり(wwwあり)へのリダイレクト設定
櫻井 裕司
2019/12/23 櫻井 裕司
tech
もともと企業サイトがサブドメインありで公開していたが、サブドメインなしでもエラーなく接続できるように設計。terraformで作成しているので是非ご参考ください。
マークダウンで記事を書けるようにしてみた
櫻井 裕司
2019/12/16 櫻井 裕司
tech
ホームページのブログをマークダウンを使用してかけるようにしました。gatsbyなどもありますが、今回はお手製cicd+pythonを使用してライトに作成しました。