Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

NFCタグ読み取り機能を使ったイベント受付機能を開発した話

技術本部Mobile Applicationグループの森です。
EightのAndroidアプリを開発しています。

Eight主催のイベントでEightアプリを使って簡単に入場できる機能を開発したので、その紹介をしたいと思います。

はじめに

2024/1/11−12の日程でEight主催のイベントBIS2024を開催しました。
eight-event.8card.net

本イベントの詳細については割愛させていただきますが、Eightアプリのみでイベントの入場・セミナー/ブースでの受付・名刺交換が簡単に行える紙の名刺が要らないイベントになっております。

アプリの実装

イベントの受付でEightアプリをインストールしたスマートフォンをタッチするだけで入場受付が完了し、目の前のプリンターから受付票が印刷されるという機能となります。

この受付機能を実装するにあたりNFCタグの読み取り機能を実装しました。

NFCを利用した経緯

Eightアプリは2023年9月にリニューアルし、スマートフォンをかざすだけで名刺交換が行える「タッチ名刺交換」機能をリリースしました。
本イベントではアプリを開いてスマートフォンをタッチするという一貫した体験を提供したいという理由からタッチすることで入場できる機能を開発しました。

タッチ名刺交換ではBLE(Bluetooth Low Energy)を利用していますが、安定性や実現性の観点からNFCを使用することになりました。

NFCタグには、入場に必要な情報を付与したURLを書き込みました。

URLを利用した理由は以下です。

  • Eightアプリが未インストールの場合はアプリストアに遷移したい
  • NFC非対応のスマートフォンの場合代替手段としてQRコード撮影からの入場と処理を共通化したい
  • iOSとの互換性が高い


以下で、具体的な実装のサンプルを記載していきます。

NFCタグを読み取る

基本的にNFCに対応したAndroidスマートフォンはNFCタグにタッチするだけでタグを検出できます。

検出したNFCタグの情報をAndroidアプリで取得するためにはAndroidManifestにIntentFilterを設定する必要があります。
タッチでの入場処理ではACTION_NDEF_DISCOVERDというIntentをIntentFilterに設定することで実現しました。

NFCタグにEightのURLを書き込んでおき、そのURLをIntentFilterに指定します。

    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
        <category android:name="android.intent.category.DEFAULT"/>
       <data android:scheme="https"
                  android:host="example.com"
                  android:pathPattern="/home" />
    </intent-filter>

上記の例ではhttps://example.com/homeと一致するURLがNFCタグに書き込まれているときにアプリにIntentが通知されます。

NFCタグにタッチした時のスクリーンショット

実際にタッチした時の画像がこちらで、NFCタグに書き込まれたURLをハンドリングできるアプリの一覧が表示されます。

一覧画面で自分のアプリを選択することで、IntentFilterを設定したActivityが起動されます。
起動されたActivityでIntentからデータを取得することで特定の処理を行うことが可能です。

特定のActivityでのみNFCタグを読み込む

上記のAndroidManifestにIntentFIiterを記載する方法では、NFCタグにタッチしたときの起点が固定のActivityになってしまい、必ず画面遷移が発生してしまいます。
Eightアプリとしては、タッチ入場の機能を明示している画面上でタッチをした際はIntentFilterを通した遷移を行わずにそのまま画面上で入場処理を行いたいという要望があったため、上記の方法では対応が不十分でした。

そこで、利用したのがForegroundDispatchという方法です。

前の項目で記載しましたが、Androidスマートフォンは誰に言われるでもなく、NFCタグを検出し、タグに含まれる情報を処理できるIntentFilterを設定したアプリを起動しようとします。
ForegroundDispatchを利用することにより、Androidのシステムがタグを検出するよりも先にActivityでNFCタグの情報を処理することができます。

以下にサンプルコードを記載します。

AndroidManifestの変更

ForegroundDispatchを利用するために、NfcAdapterを使用します。
このクラスのメソッドを呼び出すにはNFCのパーミッションが必要になります。

パーミッションがない場合呼び出し時にSecurityExceptionが発生します。

    <uses-permission android:name="android.permission.NFC"/>
Activityの変更

まずはIntentFilterを定義します。

最初のサンプルと同じく、https://example.com/homeに反応するようにしています。

val intentFilter: IntentFilter =
            IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).apply {
                addDataScheme("https")
                addDataAuthority("example.com", null)
                addDataPath("/home", PatternMatcher.PATTERN_LITERAL)
            }

次は処理するNFCのテクノロジーのリストを定義します。

NFCにはさまざまな規格があり、どの規格のタグを検出可能かをホワイトリスト形式で記載します。
一覧は以下リンクから確認ください。
https://developer.android.com/guide/topics/connectivity/nfc/advanced-nfc?hl=ja#tag-tech

val techListsArray = arrayOf(arrayOf<String>(Ndef::class.java.name))

ForegroundDispatchによる受け取るNFCタグの情報はActivityのonNewIntentメソッドにて受け取ります。
自分自身を呼び出すためのPendingIntentを定義します。

    private val pendingIntent = PendingIntentCompat.getActivity(
            this,
            0,
            Intent(activity, activity.javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
            0,
            true)

ActivityのライフサイクルメソッドにNfcAdapterを呼び出すコードを記載します。

    val nfcAdapter: NfcAdapter? by lazy{ NfcAdapter.getDefaultAdapter(this) }

    override fun onResume() {
        super.onResume()
        try {
            adapter?.enableForegroundDispatch(this, pendingIntent, arrayOf(intentFilter), techListsArray)
        } catch (e: UnsupportedOperationException) {
            // このメソッドの呼び出しをサポートしていない場合の処理
        }
    }

    override fun onPause() {
        super.onPause()
        try {
            adapter?.disableForegroundDispatch(this)
        } catch (e: UnsupportedOperationException) {
            // このメソッドの呼び出しをサポートしていない場合の処理
        }
    }

enableForegroundDispatchですが、IntentFilterは配列で渡すことができますので、必要に応じて追加してください。

上記のコードでForegroundDispatchを受け取る準備は完了です。

NFCタグをタッチしたデータを受け取るためにonNewIntentをオーバーライドします

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        intent?.let {
            if (it.action == NfcAdapter.ACTION_NDEF_DISCOVERED) {
                 // TODO:フィルターに一致するか確認
                 // 一致したら何かしらの処理を行う
            }
        }
    }

このように、onNewIntentで受け取ったIntentからNFCタグの情報を取得し、目的のタグにタッチされた場合のみ処理を行います。

実装における注意点

サンプルが古い

サンプルコードはApiDemosという、いにしえのサンプルアプリに存在しています。

Android Developerのサイトには、サンプルへのリンクが貼られていますが、そこから飛ぶことはできません。
Javaかつ、実装が古いためコピペで動作するか確認したいと思っても、そのままでは動かすことは難しいです。
samples/ApiDemos/src/com/example/android/apis/nfc/ForegroundDispatch.java - platform/development - Git at Google

NFC非対応スマートフォンの場合の対応

NFC非対応の端末の場合、NfcAdapter.getDefaultAdapterはnullを返してきます。

NfcAdapter.enableForegroundDispatchがThrowする例外にUnsupportedOperationExceptionというものがあったため、Adapterは取得できるが、例外が投げられるのかと思っていましたが、そうではなくnullが帰ってきます。
nullであっても問題ないようにコーディングしておきましょう。

ちなみにNFC対応スマートフォンでAndroidの設定でNFCをオフにしているときは、nullが帰ってくることはなく、メソッドを呼び出しても例外は発生しません。
必須の機能である場合は、NfcAdapter.isEnabledにて設定が有効になっているか確認してください。

NFCのパーミッションが付与されない場合がある

日々の運用チェックでNfcAdapter.enableForegroundDispatchを呼び出した時にSecurityExceptionが稀に発生しているのを発見しました。

NFCのパーミッションはAndroidManifestに記載するだけで付与されるはずですが、権限が付与されないままアプリが起動してしまうことがあるようです。
SecurityExceptionが発生するのは予期していないためアプリがクラッシュします。

回避方法としては、SecurityExceptionをcatchするか、権限があるかを確認してから呼び出すことになります。

おわりに

共にSansan / Eightのモバイルアプリ開発していく仲間を募集中です!
選考評価無しで現場のエンジニアのリアルな声が聞けるカジュアル面談もあるので、ご興味ありましたらぜひ面談だけでもお越しいただけたら幸いです!

open.talentio.com

© Sansan, Inc.