Rubotoを使い、AndroidでNFCのIDmやNFC規格ごとの情報を読み取る

RubotoではどのようにNFCを読み取ればよいかを調べ、Androidのメソッドを利用して情報を読み取ったときのメモ。

環境

Platform JDK ant Ruby ruboto jruby-jars Device API level
Windows7 x64 1.7.0_25 1.9.1 RubyInstaller 1.9.3-p448 0.13.0 1.7.4 Nexus7 2012 android-17

アプリの動き

Nexus7にNFCを近づけると、IDmなどを表示します。
手元には以下のものがあったため、それぞれで試してみました。

規格名 実際のもの
NFC Type A 週刊アスキー(2013/9/10増刊号) の付録
NFC Type B 運転免許証
Felica (Type F) Edy


なお、身近なものでの規格例は、以下にありました。
http://yumewaza.yumemi.co.jp/2011/03/nexuss_nfc_gettechlist.html



アプリの生成

ruboto gen app --package com.example.nfc_read --target android-17 --with-jruby

全体を通した参考

以下のページと関連したGithubがとても参考になりました。ありがとうございます。

AndroidManifest.xmlへの追記

NFCを使うパーミッションの設定
<uses-permission android:name="android.permission.NFC" />
  • GooglePlayで公開する時に、ハードウェアにNFCが無い場合は表示されない設定
<uses-feature android:name="android.hardware.nfc" android:required="true" />
  • intent filterの設定

外部からintentを投げられた時(=NFCを読み取った時)に反応できるよう、intent filterを設定します。

<intent-filter>
    <action android:name="android.nfc.action.TECH_DISCOVERED"/>
    <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter"/>


category.DEFAULTについては、以下の記事が参考になりました。
Yukiの枝折: Android:IntentFilterにDEFAULT_CATEGORYが必要な理由


また、実際のfilterの内容は、meta-dataタグの中で、「@xml/nfc_tech_filter」と拡張子無しのファイル名を指定します。



intent filter用xmlファイルの作成

nfc_read\res\xml ディレクトリを作成し、以下のファイルを保存します。

nfc_tech_filter.xml
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcA</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcB</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcF</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcV</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NdefFormatable</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.MifareClassic</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

Activityまわり

IDmの取得について

intent.getParcelableExtra()とintent.getByteArrayExtra()を使うサンプルがありましたが、今回はAPI10以降かつタグの情報を色々と使うこともあり、前者を使うことにしました。
AndroidアプリでNFCタグを読み書きするための基礎知識:Androidで動く携帯Javaアプリ作成入門(38) - @IT



バイナリデータの16進数化について

Tag.getIdでIDmを取得するとバイト配列(JRubyではFixnum配列)が返ってきますが、Java同様マイナス値となることがありました。
JRubyなのでJavaと同じくマイナス値になるのだろうと考え、以下を参考に0xffの論理積を考慮しました。

getTechListの戻り値について

String配列で戻ってくるので

tech_list = tag.getTechList().join(',')

のように連結しようとしたところ、

undefined method 'join' for java.lang.String.[android.nfc.tech.NfcF]

とエラーになりました。


そのため、

getTechList.to_ary.join(',')

と、to_aryメソッドを経由して配列として扱えるようにしました。


to_aryとto_aのどちらを使うべきなのかで悩みましたが、以下を読んで、配列として扱えればよく、配列への変換までは不要と思い、前者を利用しました*1

ソース
# encoding: utf-8

require 'ruboto/widget'

ruboto_import_widgets :LinearLayout, :TextView

# NFCまわり
java_import 'android.nfc.NfcAdapter'
java_import 'android.nfc.tech.NfcA'
java_import 'android.nfc.tech.NfcB'
java_import 'android.nfc.tech.NfcF'
java_import 'android.nfc.tech.IsoDep'
java_import 'android.nfc.tech.MifareUltralight'



class NfcReadActivity
  def onCreate(bundle)
    super
    set_title 'NFC Reader Sample'


    intent = getIntent
    case intent.getAction
    when NfcAdapter::ACTION_TECH_DISCOVERED || NfcAdapter::ACTION_TAG_DISCOVERED

      tag = intent.getParcelableExtra(NfcAdapter::EXTRA_TAG)

      view =
        linear_layout orientation: :vertical do
          @idm_view = text_view text: tag.nil? ? 'No IDm' :   to_hex_string(tag.getId),
                                width: :match_parent,
                                gravity: :center,
                                text_size: 48.0

          @tag_info_view = text_view text: tag.nil? ? '' : tag.toString

          # getTechList.joinだとエラーになるため、to_aryを経由する
          @tech_list_view = text_view text: tag.nil? ? '' : tag.getTechList.to_ary.join(',')
        end
      self.content_view = view

       tech_view = create_tech_view(@tech_list_view.text, tag)
       view.addView tech_view unless tech_view.nil?
    end
  end


  def create_tech_view(tech_list, tag)
    view = linear_layout orientation: :vertical do
             @option_view = text_view text: add_delimiter + '以降はNFCタイプごとの項目' + add_delimiter 
           end

    tech_list.split(',').each do |tech|
      # いろいろ簡略化したことによりActivityへ集約するサンプル
      # その分、条件ごとにメソッドを作るという、あまり良くない書き方になってしまった
      case tech
      when 'android.nfc.tech.NfcA'
        tech_view = create_nfca_view(tag)

      when 'android.nfc.tech.NfcB'
        tech_view = create_nfcb_view(tag)

      when 'android.nfc.tech.NfcF'
        tech_view = create_nfcf_view(tag)

      when 'android.nfc.tech.IsoDep'
        tech_view = create_isodep_view(tag)

      when 'android.nfc.tech.MifareUltralight'
        tech_view = create_mifare_ultralight_view(tag)

      else
        next
      end

      view.addView(tech_view)
    end

    view
  end


  def create_nfca_view(tag)
    nfc = NfcA.get(tag)
    view = linear_layout orientation: :vertical do
      @atqa = text_view text: '[NFC-A] atqa: ' + to_hex_string(nfc.getAtqa)
      @sak = text_view text: '[NFC-A] sak: ' + nfc.getSak.to_s
    end
  end


  def create_nfcb_view(tag)
    nfc = NfcB.get(tag)
    view = linear_layout orientation: :vertical do
      @application_data = text_view text: '[NFC-B] application_data: ' + to_hex_string(nfc.getApplicationData)
      @protocol_info = text_view text: '[NFC-B] protocol_info: ' + to_hex_string(nfc.getProtocolInfo)
    end
  end


  def create_nfcf_view(tag)
    nfc = NfcF.get(tag)
    view = linear_layout orientation: :vertical do
      @manufacturer = text_view text: '[NFC-F] manufacturer: ' + to_hex_string(nfc.getManufacturer)
      @system_code = text_view text: '[NFC-F] system_code: ' + to_hex_string(nfc.getSystemCode)
    end
  end


  def create_isodep_view(tag)
    nfc = IsoDep.get(tag)
    view = linear_layout orientation: :vertical do
      @hi_layer_response = text_view text: '[ISO Dep] hi_layer_response: ' + to_hex_string(nfc.getHiLayerResponse)
      @historical_bytes = text_view text: '[ISO Dep] historical_bytes: ' + to_hex_string(nfc.getHistoricalBytes)
    end
  end


  def create_mifare_ultralight_view(tag)
    nfc = MifareUltralight.get(tag)
    view = linear_layout orientation: :vertical do
      @mifare_ultralight = text_view text: '[Mifare Ultralight] mifare_ultralight_type: ' + nfc.getType.to_s
    end
  end


  def to_hex_string(bytes)
    bytes.nil? ? '' : bytes.map{ |byte| "%02X" % (byte & 0xff) }.join
  end


  def add_delimiter
    '-' * 6
  end
end


AndroidManifest.xmlnfc_tech_filter.xmlnfc_read_activity.rbについては、Gistにも上げてあります。
RubotoでNFCを読み込むサンプル · GitHub



*1:認識が誤っていたら、すみません