2026年2月16日月曜日

AndroidのHCEでオフラインスマート名刺

AndroidのHCEでオフラインスマート名刺

NFCを使ったスマート名刺と聞くと、ICカードにURLを埋め込むタイプのものを連想することが多いと思いますが、AndroidのHCE(Host Card based Emulation)を使えば受信側はアプリ不要、完全オフラインで画像つきのプロフィールを送信することができます。

仕組みは単純で、まず送信側のアプリでAIDにType 4 Tagを示す"D2760000850101"を設定します。
これで受信側は送信側をNFCタグと認識するので、OSを問わず、かつアプリ不要で通信できます。

画像を送信するためのキモになるのはvCard(.vcf)という名刺用のファイルフォーマットです。
iOSとAndroidはvCardで連絡先データを管理しています。vCardにはプロフィール画像と氏名、連絡先、会社などの情報を記載することができ、これをNDEFレコードにのせることで受信側は自動で連絡先データとして認識するというながれです。

NDEFレコード上ではvCardはMIMEタイプとして扱います。NDEFヘッダーのTNF(Type Name Format)に 0x02 (MIME Media) を指定し、タイプフィールドに text/vcard をセットすることで、受信側のOSがこれを連絡先データとして解釈します。

// NDEF Message Builder (vCard Record)
fun createVCardNdefMessage(vCardString: String): ByteArray {
    val vCardBytes = vCardString.toByteArray(StandardCharsets.UTF_8)
    val typeBytes = "text/vcard".toByteArray(StandardCharsets.UTF_8)
    val payloadLength = vCardBytes.size

    val output = ByteArrayOutputStream()
    val isShort = payloadLength <= 255
    var header = 0xC2 // MB=1, ME=1, TNF=0x02
    if (isShort) header = header or 0x10
    
    output.write(header)
    output.write(typeBytes.size)

    if (isShort) {
        output.write(payloadLength)
    } else {
        output.write((payloadLength shr 24) and 0xFF)
        output.write((payloadLength shr 16) and 0xFF)
        output.write((payloadLength shr 8) and 0xFF)
        output.write(payloadLength and 0xFF)
    }

    output.write(typeBytes)
    output.write(vCardBytes)
    return output.toByteArray()
}

HostApduService を継承し、以下のシーケンスを実装します。

  • SELECT AID: Type 4 Tag アプリケーションの選択
  • SELECT FILE: CCファイルまたはNDEFファイルの選択
  • READ_BINARY: 選択されたファイルのデータをオフセットに従って返却

特に READ_BINARY では、一度に送信できるバッファサイズ(Le)に制限があるため、大きなvCardデータ(画像付きなど)を送信する場合は、複数回の読み取りリクエストに対して正確にオフセット管理を行う必要があります。

// HostApduService (READ_BINARY Response)
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
    if (readBinaryApdu(commandApdu)) {
        val offset = getReadBinaryOffset(commandApdu)
        val le = getReadBinaryLe(commandApdu)
        
        val selectedFile = currentSelectedFile ?: return STATUS_FAILED
        if (offset >= selectedFile.size) return STATUS_FAILED
        
        val lenToRead = Math.min(le, selectedFile.size - offset)
        val response = ByteArray(lenToRead + 2)
        System.arraycopy(selectedFile, offset, response, 0, lenToRead)
        
        // Append Success Status (90 00)
        response[lenToRead] = 0x90.toByte()
        response[lenToRead + 1] = 0x00.toByte()
        return response
    }
    // ...
}

実際に動かしました。

vCardのファイルサイズが大きくなりすぎるとおそらくタイムアウトが発生してしまいます。
試したところ30KBあたりが送信できる限界で、20KBを超えると送信するのに5秒ほど時間がかかりました。実際に使うときは相手のスマホのNFCセンサーの位置を把握するためにさらに時間を要するので、10KB程度で収まるようにするのが現実的かと思いました。

Android Beamが廃止されたのもなんとなく頷けますが、異なるOS間で通信できるのは魅力的に思えます。
HCEでもURLのみを飛ばすことができます。そもそもオフラインで通信する必要もほとんどないうえ、URL単体のほうが速くて安定しているのでそのほうが実用性はあります。

URLバージョン

0 件のコメント:

コメントを投稿