以前、RubotoでNFCのIDmなどを読んでみたため、今度はSuicaやEdyの履歴などを読もうかと考えましたが、自分で実装するには時間がかかりそうでした。
そのため、良いものがないかを探したところ、Kazzzさんのライブラリ「nfc-felica」がありました。ありがたい限りです。
nfc-felica - android nfc access felica, ISO15693 raw command - Google Project Hosting
ライブラリに付属しているアプリを見てみると、Suicaの読み取りについては実装されていたものの、Edyなど他のFeliCaの読み取りについては実装されていませんでした。
そこで、ライブラリの使い方を学びがてら、Suica・Edyの履歴読み取りをするアプリをRubotoで作ってみることにしました。
環境
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.14.0 | 1.7.4 | Nexus7 2012 | android-17 |
2013.09.02追記
当初は、Rubotoのバージョン0.13.0で作成しましたが、0.14.0リリース後に動作の確認ができたため、rubotoの表記を0.14.0に修正しました。
2013.09.02追記 ここまで
生成
今回はライブラリを変更・使用しているため、パッケージ名を取得してみました。
あと、いつも通り、Windowsでは生成後にメモリを修正しておきます。
ruboto gen app --package jp.gr.java_conf.thinkami.felica_read --target android-17 --with-jruby
パッケージ名は以下の場所にて取得しました。
Package BOF Home Page
AndroidManifestの追記
以前NFCを読んだ時と同じく、
- permission
- GooglePlayでの表示
に関して追記しました。
なお、intent-filterについては、後述のForeground Dispatch Systemを使うことにしたため、記載していません。
Rubuto0.13.0では起動に時間がかかるために、起動中にタグをロストすることもあったことから、Foreground Dispatch Systemを使った方がいいのかなという印象です。
ライブラリ「nfc-felica」の準備
SubversionによるCheckout
ライブラリのプロジェクトページよりCheckoutします。
TortoiseSVNであれば、SVN Checkoutで「URL or repository」に以下を指定して実行します。
http://nfc-felica.googlecode.com/svn/
ライブラリのコピー
今回、Suicaの読み取りはプロジェクトに同梱されているJavaのソースコードを利用するため、以下の2つのフォルダを丸ごとコピーします。
本来なら、必要なものだけが良いはずですが、今後色々とやるかもしれないと考え、全部コピーしてから不要なものを削除することにしました。
コピー元
- <CheckOutフォルダ>\nfc-felica\trunk\nfc-felica\src\net
- <CheckOutフォルダ>\nfc-felica\trunk\nfc-felica-lib\src\net
コピー先
- felica_read\src
なお、Javaのパッケージに合わせて両方とも同じフォルダへと統合しました。
- 【改訂版】Eclipseではじめるプログラミング(10):Javaの実案件に必須のパッケージとインポートを知る (2/3) - @IT
- [Eclipse] パッケージ宣言と異なるディレクトリへのソース配置 − Java Solution − @IT
Suica用DBのコピー
Suica用のdbもコピーしておきます。
最新版ではないかもしれませんが、今回はこのまま進めます。
コピー元
コピー先
- felica_read\assets\StationCode.db
不要なjavaファイルの削除
ここまでの状態でビルドをしようとしても、いくつかエラーが出てビルドができません。
そのため、不要なものを削除し、ビルドできるようにします。
1. アプリやFragmentの削除
Support LibraryのFragmentを使っているため、「エラー:パッケージandroid.support.v4.appは存在しません」と表示されます。今回は、アプリやFragmentがなくても動作することから、以下を削除します。
- felica_read\src\net\kazzz\NFCTagReader.java
- felica_read\src\net\kazzz\NFCTagSample.java
- felica_read\src\net\kazzz\NFCTagWriter.java
- felica_read\src\net\kazzz\AbstractNfcTagFragment.java
- felica_read\src\net\kazzz\felica\NfcFeliCaTagFragment.java
- felica_read\src\net\kazzz\iso15693\ISO15693TagFragment.java
2. taskフォルダ以下
今回はFeliCaへの書き込みを行わないため、taskフォルダ自体を削除します。- felica_read\src\net\kazzz\task
ちなみに、今回使用しなかったものの、Support Libraryのインストール方法は、以下の通りです。
以前は「Android Compatibility Package」という名前でしたが、途中で「Support Package」に代わり、現在はAndroid SDK Managerに表示される「Support Library」になったようです。
また、ライブラリのFragmentを継承してActivityを作ろうかとも思いましたが、
class HogeActivity < Fragment end
としたところ、以下のエラーが出たため、今回はFragment継承の方向を諦めました。
(TypeError) superclass mismatch for class HogeActivity
ライブラリのパスの修正
Suicaのdbを使うため、felica_read\src\net\kazzz\felica\suica\DBUtil.javaのパスを変更します。
(変更しない場合、以下のエラーが発生します)
Failed to open database '/data/data/net.kazzz/databases/StationCode.db'
変更箇所は 45行目。
// private static final String DB_PATH = "/data/data/net.kazzz/databases/"; private static final String DB_PATH = "/data/data/jp.gr.java_conf.thinkami/databases/";
ライブラリ「nfc-felica」を使う上で参考にしたところ
実際にライブラリを使用していたり、使った時のソースコードが公開されていたので、そちらが参考になりました。
- AndroidのNFC機能でFeliCaの読み書きをする (ゆめ技:ゆめみスタッフブログ)
- android2.3のNFCを利用してSuicaの入退場記録を表示 - baroqueworksdevの日記
- http://blog.atrac613.io/2012/03/10/suica-balance/
- codebutler/farebot · GitHub
なお、ライブラリのアプリではFragmentを使っていますが、Suica側の書き直しなどを考えて、Fragmentは使わないようにしました。
NfcFeliCaTagFragmentを作る - Kazzzの日記
FeliCaの仕様など
フォーマットについて
FeliCa LibraryのWikiに詳しくまとめられています。今回使用するSuica・Edyの場合は、以下のページが該当します。
- suica - FeliCa Library Wiki - FeliCa Library - SourceForge.JP
- Edy - FeliCa Library Wiki - FeliCa Library - SourceForge.JP
ただし、今回のEdyについては残額情報ではなく利用履歴の取得なので、サービスコードは以下にある通り「170F」を使います*1。
なお、Edyから履歴を読むコード例が以下にありました。注意点などもあり、詳しくためになりました。
Android NFCでFelicaとたわむれてみる
FeliCaの技術情報やAndroidから扱う際の参考資料です。
- Sony Japan | FeliCa | 法人のお客様 | 技術情報
- AndroidでFeliCaの履歴を読もう
- カテゴリ:NFC - Android Memo Wiki
- IC SFCard Fan
Android関連で調べたこと
ScrollViewについて
今回、利用履歴を一つのTextViewへ入れるようにしました。
利用履歴が多い場合、TextViewの改行量も増え、その結果、画面内には収まらなくなりました。
そこで画面をスクロールする方法がないかを調べたところ、ScrollViewがあることが分かりました。
Rubotoではどこまで実装されているのか分かりませんでしたが、Googleグループに動作した情報がありました。
Google グループ
そこで、上記を参考に、以下のようなソースを書きました。
@balanceのTextViewはスクロールなしの固定、@historyのTextViewはスクロールありとなります。
@main = linear_layout orientation: :vertical do @balance = text_view text: 'Felicaを読んで下さい', text_size: 36 text_view text: '-' * 60 scroll_view do linear_layout orientation: :vertical do @history = text_view text: '' end end end
ちなみに、XMLでの記述方法は、以下が参考になりました。
画面全体を縦方向にスクロールできるようにする - Droidくん「JavaとXMLの魔境、Androidフレームワークの世界へようこそ!」 - Android 開発コミュニティ
フォアグラウンドディスパッチを使い、アプリが起動している時のみFeliCaに反応させる
intent-filterでFeliCaのみに反応させることも考えましたが、FeliCaを発見するたびにアプリの起動を迫られるのが面倒でした。
そこで、アプリが起動している時のみFeliCaに反応させる方法がないかを調べたところ、 Foreground Dispatch Systemという仕組みを使えば良さそうなことが分かりました。
- AndroidによるNFCスマートポスタータグの作成(3/5) - テクノロジーコラム:モバイル | NTTソフトウェア
- Advanced NFC | Android Developers
- アプリが起動して前面にいる場合のみ、NFCタグに反応する - hiro99ma site
Javaコードを参考に作ってみたところ、コンストラクタでIntent生成するところで、エラーになり生成できませんでした。
i = Intent.new(self, FelicaReadActivity.class.name)
NameError: no constructorfor arguments 'jp.gr.java_conf.thinkami.felica_read.FelicaReadActivity,org.jruby.RubyString) on Java::AndroidContent::Intent
available overloads:
(java.lang.String,android.net.Uri)
(android.content.Context, java.lang.Uri)
そのため、引数なしのコンストラクタ + setClassNameで生成するようにしました。
Intentの作り方 - プログラマってこんなかんじ??
p = getPackageName c = self.java_class.name i = Intent.new i.setClassName(p, c)
また、上記のリンク先では生成したIntentにFLAG_ACTIVITY_NEW_TASKフラグなどを設定していましたが、AndroidManifestにlaunchModeを指定しても有効だったため、今回は後者の方法で実装しました。
<activity android:label='@string/app_name' android:name='FelicaActivity' android:launchMode='singleInstance'>
launchModeについては、以下が参考になりました。
他、getDefaultAdapterには、「NfcManager」「NfcAdapter」の2クラスにありました。
ソースコードやドキュメントを読むと、NfcAdapterのものはNfcManagerを呼び出すようなHelperだったため、NfcAdapterのものを使用しました。
ちなみに、API16にてNfcAdapter.getDefaultAdapter()の引数なし版は削除されたようですが、引数あり版は有効なようです*2。
platform_frameworks_base/core/java/android/nfc/NfcAdapter.java at android-4.3_r2.2 · android/platform_frameworks_base · GitHub
FeliCaだけに反応させるため、TechListを指定
フォアグラウンドでのディスパッチ - enableForegroundDispatch - Android Memo Wiki
ただし、JRubyの場合、「NfcF.class.name」ではNfcFクラスの名前が取れなかったため、「NfcF.java_class.name」としてJavaのクラスを取得してからクラス名を取得しました。
CallingJavaFromJRuby · jruby/jruby Wiki · GitHub
JRuby関連で調べたこと
Javaクラスの動的な拡張
Javaのクラスを指定してメソッドを足すことで、拡張できました。
JRubyメモ(工事中)
java_import 'net.kazzz.felica.suica.Suica' class Suica::History def title 'Suica' end end
JavaとJRuby間のやりとり
メソッドの指定とか(java_send 等)
CallingJavaFromJRuby · jruby/jruby Wiki · GitHub
他にもいろいろとあったため、JRubyの本を買おうか悩んでいたりします。
The Pragmatic Bookshelf | Using JRuby
Ruby関連
よく忘れるので、残しておきます。
- リテラル
- 正規表現
- instance method String#%
- Rubyで使われる記号の意味(正規表現の複雑な記号は除く)
- Rubyで時刻日付を扱うまとめ - むかぁ~ どっと こむ
- Rubyの文字列リテラルの種類と使い分け方 - ぬいぐるみライフ(仮)
ヒアドキュメントでの前方空白の削除
今回は空白のインデントなどが不要だったため、stackoverflowに記載されているもののうち、一番簡易的なものを使用しました。
dump += <<-EOF.gsub /^\s+/, "" EOF
stackoverflowには他にもいろいろと方法が記載されています。
How do I remove leading whitespace chars from Ruby HEREDOC? - Stack Overflow
その他
NFCなどの購入情報
- [Android] RFID/NFC Real Touch Shop に行ってきました - adakoda
- NFCタグシール(NFC Tag・丸型・Circus・20枚セット)300-NFC001の販売商品 |通販ならサンワダイレクト
- Amazon.co.jp: NFC(FeliCa,Mifare) 開発スタートキット101-A-5: 家電・カメラ
ライブラリ以外での読み書きNFC
- Android NFCとNexusSで MifareClassic を読み書きする(前編) (ゆめ技:ゆめみスタッフブログ)
- ブリリアントサービス NFC技術ブログ: AndroidでRTD Textを書く
ExcelVBAでFelicaを読み書きしていそうなコードもありました。
AMAYUS Wiki - felica
ソース
Githubはこちら。
thinkAmi/RubotoFelicaRead · GitHub
ファイル自体は4つに分けました。なお、エラーハンドリングなどはかなり端折ってあります。
felica_read_activity.rb
メインのActivity。今回はサンプル的なものなので、Activityにいろいろと詰めています。felica_lib.rb
net.kazzz.felica.lib.FeliCaLibを拡張し、Edy用のサービスコードを追加suica.rb
net.kazzz.felica.suica.Suicaを拡張し、必要なメソッドを追加edy.rb
net.kazzz.felica.suica.Suicaを参考にして作成した、Edyからデータをダンプするクラス
felica_read_activity.rb
# coding: utf-8 require 'ruboto/widget' require 'ruboto/util/toast' require 'suica' require 'edy' require 'felica_lib' ruboto_import_widgets :LinearLayout, :ScrollView, :TextView, :Button java_import 'net.kazzz.felica.FeliCaTag' java_import 'net.kazzz.felica.lib.FeliCaLib' java_import 'net.kazzz.felica.suica.Suica' java_import 'android.nfc.NfcAdapter' java_import 'android.nfc.NfcManager' java_import 'android.nfc.tech.NfcF' java_import 'android.content.Intent' java_import 'android.app.PendingIntent' class FelicaReadActivity def onCreate(bundle) super @main = linear_layout orientation: :vertical do @balance = text_view text: 'Felicaを読んで下さい', text_size: 36 text_view text: '-' * 60 scroll_view do linear_layout orientation: :vertical do @history = text_view text: '' end end end self.content_view = @main # フォアグラウンド・ディスパッチの準備 @adapter = NfcAdapter.getDefaultAdapter(self) end def onNewIntent(intent) super a = intent.getAction return unless a == NfcAdapter::ACTION_TECH_DISCOVERED || a == NfcAdapter::ACTION_TAG_DISCOVERED toast '読み込みを開始します' # Suicaのシステムコードかどうかで、Suica/Edyの判定をしている(手抜き...) tag = intent.getParcelableExtra(NfcAdapter::EXTRA_TAG); case to_system_code(NfcF.get(tag).getSystemCode) when FeliCaLib::SYSTEMCODE_SUICA h, b = dump_history(tag, FeliCaLib::SYSTEMCODE_SUICA, FeliCaLib::SERVICE_SUICA_HISTORY, ->(block_data, activity){ Suica::History.new(block_data, activity) } ) else h, b = dump_history(tag, FeliCaLib::SYSTEMCODE_EDY, FeliCaLib::SERVICE_EDY_HISTORY, ->(block_data, activity){ Edy.new(block_data) } ) end @history.text = h @balance.text = '現在の残高: ¥' + b.to_s.gsub(/(?<=\d)(?=(?:\d{3})+(?!\d))/, ',') toast '読み込みが完了しました。' end def onResume super # 本来なら後者の条件だけで良いはずだが、なぜかインスタンス変数がnilになっているときもあるので、nilチェックを追加 if @adapter.nil? || !@adapter.isEnabled toast 'NFCが無効になっています。' finish return end pi = create_pending_intent # フォアグラウンド・ディスパッチで反応するのは、FeliCaだけにする t = [[NfcF.java_class.name]] @adapter.enableForegroundDispatch(self, pi, nil, t) end def onPause super @adapter.disableForegroundDispatch(self) if isFinishing end def create_pending_intent p = getPackageName c = self.java_class.name i = Intent.new # AndroidManifestにlaunchModeを記載したので、ここではフラグの指定は不要みたい i.setClassName(p, c) PendingIntent.getActivity(self, 0, i, 0) end def to_system_code(bytes) bytes.nil? ? '' : bytes.map{ |byte| "%02X" % (byte & 0xff) }.join.to_i end def dump_history(tag, system_code, service_code, func) felica_tag = FeliCaTag.new(tag) felica_tag.polling(system_code) sc = FeliCaLib::ServiceCode.new(service_code) addr = 0 result = felica_tag.readWithoutEncryption(sc, addr) dump = '' while !result.nil? && result.getStatusFlag1 == 0 target = func.call(result.getBlockData, self) dump += <<-EOF.gsub /^\s+/, "" #{target.title} 履歴 No. #{(addr + 1).to_s} --------- #{target.to_string} --------------------------------------- EOF balance = target.balance if balance.nil? addr += 1 begin result = felica_tag.readWithoutEncryption(sc, addr) rescue dump += <<-EOF.gsub /^\s+/, "" --------------------------------------- 読込が中断されました --------------------------------------- EOF return dump, balance end end return dump, balance end end
felica_lib.rb
java_import 'net.kazzz.felica.lib.FeliCaLib' class FeliCaLib # サービスコード # 参照: http://jennychan.web.fc2.com/format/edy.html SERVICE_EDY_HISTORY = '0x170F'.to_i(16) end
suica.rb
java_import 'net.kazzz.felica.suica.Suica' # Javaのクラス名を指定して、あとはメソッドを書いていけば、動的に拡張できる class Suica::History # toStringメソッドは、JRubyが自動的に to_string メソッドへと変換してくれるので、実装しなくてよい def title 'Suica' end def balance b = self.getBalance b.to_s.gsub(/(?<=\d)(?=(?:\d{3})+(?!\d))/, ',') end end
edy.rb
# coding: utf-8 require 'date' java_import 'net.kazzz.util.Util' class Edy attr_reader :title def initialize(data) @data = data @title = 'Edy' end def to_string <<-EOF.gsub /^\s+/, "" 区分: #{category} 連番: #{sequence} 日時: #{datetime} 金額: #{to_currency(amount)} 残高: #{to_currency(balance)} EOF end def category case to_hex_string(@data[0]) when '0x02'; 'チャージ' when '0x04'; 'Edyギフト' when '0x20'; '支払い' else; '???' end end def sequence Util.toInt(@data[1], @data[2], @data[3]).to_s end def datetime dt10 = Util.toInt(@data[4], @data[5], @data[6], @data[7]) # 2進数に直したときの、上位15bitが日付、下位17bitが時刻 # 上記は10進数なので、2進数へと変換する dt2 = dt10.to_s(2).to_i(2) # 日付の算出 # 先頭から15bit残すので、右にある17bitをシフトして消す elapsed_days = dt2 >> 17 # elapsed_days = dt2.to_i(2) >> 17 # 時刻の算出 # 後ろから17bitだけを残すので、不要なbitを消すために、論理積を取る elapsed_time = dt2 & 0b00000000000000011111111111111111 # 2000/01/01からの経過日時の算出 d = (Date.new 2000, 1, 1) + elapsed_days dt = Time.local(d.year, d.month, d.day, 0, 0, 0, 0) + elapsed_time dt.strftime('%Y/%m/%d %H:%m:%S') end def amount Util.toInt(@data[8], @data[9], @data[10], @data[11]) end def balance Util.toInt(@data[12], @data[13], @data[14], @data[15]) end def to_hex_string(byte) "%#02x" % (byte & 0xff) end def to_currency(value) '¥' + value.to_s.gsub(/(?<=\d)(?=(?:\d{3})+(?!\d))/, ',') end end