前回 は、久しぶりに Androidアプリ開発ということで、開発環境の構築と、簡単なアプリを作って、動かしてみました。
今回は、やりたかった NFC の読み込みをするアプリを動かしてみようと思います。良さそうな OSS を持ってきて動かしてみます。
それでは、やっていきます。
- 参考文献
- はじめに
- NFCリーダーをGitHubで探す
- Android Studioに既存アプリを読み込ませる
- NFCリーダーアプリをエミュレータで起動する
- NFCリーダーアプリを実機で起動する
- NFCリーダーのソースを解析する
- おわりに
参考文献
今回も、参考にさせて頂いた書籍です。
Kotlin は詳しく知らないので、文法などは以下の書籍を参考にしています。
はじめに
「Javaでデザインパターンを学ぶ」の記事一覧です。良かったら参考にしてください。
・第1回:Javaでデザインパターンを学ぶ:Singletonパターン
・第2回:Javaでデザインパターンを学ぶ:Template Methodパターン
・第3回:Javaでデザインパターンを学ぶ:Observerパターン
・第4回:Javaでデザインパターンを学ぶ:Iteratorパターン
・第5回:Javaでデザインパターンを学ぶ:Factory Methodパターン
・第6回:Javaでデザインパターンを学ぶ:Stateパターン
・第7回:Javaでデザインパターンを学ぶ:Visitorパターン
・第8回:Javaでデザインパターンを学ぶ:Adapterパターン
・第9回:Javaでデザインパターンを学ぶ:Prototypeパターン
・番外編:Javaのコンパイル方法(仕組み)をパッケージ含めていろいろ試してみる
・番外編2:Jarの作り方とJarを含んだコンパイル方法をパッケージ含めていろいろ試してみる
・番外編3:GradleでJavaプロジェクトを作ってみる(Ubuntu22.04)
・番外編4:IntelliJを使ってJavaのGradleプロジェクトでデバッグしてみる(Ubuntu22.04)
・番外編5:GradleプロジェクトでJGraphTを使う(Ubuntu22.04)
・番外編6:JGraphTのサンプルソースの解説と可視化の補足(Ubuntu22.04、IntelliJ、Gradle)
・番外編7:Androidアプリの開発環境の構築とHelloWorldアプリを作ってみる
・番外編8:Androidアプリ:NFCリーダーアプリを動かしてソースコードを確認する ← 今回
NFCリーダーをGitHubで探す
まず、今回の目的(NFCリーダー)に合った OSS を GitHub から探す必要がありますが、運よく、とても良さそうなものが見つかりました。最終更新日が 2024/2/20 と、そこまで古くなくて、Kotlin で書かれていて、機能がシンプルで扱いやすそうです。
今回は、ありがたくこちらのソースコードを使わせて頂こうと思います。
Android Studioに既存アプリを読み込ませる
まず、GitHub のソースコードをダウンロードします。Windows で動かすので、Git BASH で、適当なところにクローンします。
$ git clone https://github.com/abdelaz9z/NFC-Reader.git android-NFC-Reader Cloning into 'android-NFC-Reader'... remote: Enumerating objects: 96, done. remote: Counting objects: 100% (96/96), done. remote: Compressing objects: 100% (74/74), done. remote: Total 96 (delta 6), reused 88 (delta 4), pack-reused 0 (from 0) Receiving objects: 100% (96/96), 154.34 KiB | 3.35 MiB/s, done. Resolving deltas: 100% (6/6), done.
次に、Android Studio で読み込みます。前回の記事では New Project を選びましたが、ここでは、Open を選びます。Clone Repository というズバリのようなものがありますが、今回は Open の方を使います。

すると、ディレクトリを選択するダイアログが出るので、クローンしたディレクトリを選択して、OK をクリックします。読み込みに時間がかかる場合がありますが、待てば、ちゃんと表示されます。

Android Studio が起動し、バックグラウンドでビルドなどが始まります。時間がかかりますが、待ちます。いろいろ情報、警告が出ますが、いったんは無視します。「Gradle をアップグレードしますか?」というように推奨されますが、まずは、このまま(開発された方の環境を変更しない)にしておいた方がいいです。うまく動かない場合に、アップグレードをやってみる、というやり方でいいと思います。
しばらくすると、ビルドが完了します。
NFCリーダーアプリをエミュレータで起動する
早速、エミュレータで起動してみます。エミュレータが選択されていることを確認して、▷ボタンを押して起動します。無事に起動できました。

NFCリーダーアプリを実機で起動する
エミュレータでは、NFCタグを読ませることが出来なさそうなので、早速実機で動かしていきます。前回、HelloWorldアプリでやった方法と同じように準備して、Android Studio の方も、エミュレータから実機に切り替えて、▷ボタンを押して起動します。

うまく起動できたようなので、使わなくなったセブンイレブンの nanacoカードをスマホの裏面にピタッと接触させます。うまく読み込めました!

NFCリーダーのソースを解析する
NFCリーダーのソースコードを確認していきます。まずは、構成を見ていきます。
NFCリーダーのソースコードの構成
Android Studio でビルド、実行したことで、いくつかのソースが追加、変更されているようです。
$ git status On branch master Your branch is up to date with 'origin/master'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: .idea/compiler.xml modified: .idea/gradle.xml modified: .idea/misc.xml Untracked files: (use "git add <file>..." to include in what will be committed) .idea/deploymentTargetSelector.xml .idea/runConfigurations.xml .idea/vcs.xml
treeコマンドの結果です。.git や app/build などは非表示にしています。app/src/main/java/com/casecode/nfcreader が、メインのソースコードです。
$ tree -a . |-- .git (★非表示とする) |-- .gitignore |-- .gradle | |-- 8.2 | | |-- checksums | | | |-- checksums.lock | | | |-- md5-checksums.bin | | | `-- sha1-checksums.bin | | |-- dependencies-accessors | | | |-- dependencies-accessors.lock | | | `-- gc.properties | | |-- executionHistory | | | |-- executionHistory.bin | | | `-- executionHistory.lock | | |-- fileChanges | | | `-- last-build.bin | | |-- fileHashes | | | |-- fileHashes.bin | | | |-- fileHashes.lock | | | `-- resourceHashesCache.bin | | |-- gc.properties | | `-- vcsMetadata | |-- buildOutputCleanup | | |-- buildOutputCleanup.lock | | |-- cache.properties | | `-- outputFiles.bin | |-- config.properties | |-- file-system.probe | |-- kotlin | | |-- errors | | `-- sessions | `-- vcs-1 | `-- gc.properties |-- .idea | |-- .gitignore | |-- .name | |-- android-NFC-Reader.iml | |-- appInsightsSettings.xml | |-- caches | | `-- deviceStreaming.xml | |-- compiler.xml | |-- deploymentTargetDropDown.xml | |-- deploymentTargetSelector.xml | |-- gradle.xml | |-- kotlinc.xml | |-- migrations.xml | |-- misc.xml | |-- runConfigurations.xml | |-- vcs.xml | `-- workspace.xml |-- README.md |-- Screenshot_20240220_094138.png |-- Screenshot_20240220_094158.png |-- app | |-- .gitignore | |-- build (★GitHubソースに含まれないので割愛する) | |-- build.gradle.kts | |-- proguard-rules.pro | `-- src | |-- androidTest | | `-- java | | `-- com | | `-- casecode | | `-- nfcreader | | `-- ExampleInstrumentedTest.kt | |-- main | | |-- AndroidManifest.xml | | |-- ic_launcher-playstore.png | | |-- java | | | `-- com | | | `-- casecode | | | `-- nfcreader | | | |-- Coroutines.kt | | | |-- NFCManager.kt | | | |-- NFCStatus.kt | | | |-- ui | | | | |-- MainActivity.kt | | | | `-- MainFragment.kt | | | `-- viewmodel | | | `-- MainViewModel.kt | | `-- res | | |-- drawable | | | |-- baseline_nfc_24.xml | | | |-- ic_launcher_background.xml | | | `-- ic_launcher_foreground.xml | | |-- layout | | | |-- activity_main.xml | | | `-- fragment_main.xml | | |-- mipmap-anydpi-v26 | | | |-- ic_launcher.xml | | | `-- ic_launcher_round.xml | | |-- mipmap-hdpi | | | |-- ic_launcher.webp | | | `-- ic_launcher_round.webp | | |-- mipmap-mdpi | | | |-- ic_launcher.webp | | | `-- ic_launcher_round.webp | | |-- mipmap-xhdpi | | | |-- ic_launcher.webp | | | `-- ic_launcher_round.webp | | |-- mipmap-xxhdpi | | | |-- ic_launcher.webp | | | `-- ic_launcher_round.webp | | |-- mipmap-xxxhdpi | | | |-- ic_launcher.webp | | | `-- ic_launcher_round.webp | | |-- values | | | |-- colors.xml | | | |-- ic_launcher_background.xml | | | |-- strings.xml | | | `-- themes.xml | | |-- values-night | | | `-- themes.xml | | `-- xml | | |-- backup_rules.xml | | `-- data_extraction_rules.xml | `-- test | `-- java | `-- com | `-- casecode | `-- nfcreader | `-- ExampleUnitTest.kt |-- build.gradle.kts |-- gradle | `-- wrapper | |-- gradle-wrapper.jar | `-- gradle-wrapper.properties |-- gradle.properties |-- gradlew |-- gradlew.bat |-- local.properties (★GitHubソースに含まれてなかった→ビルドで新しく追加された) `-- settings.gradle.kts 458 directories, 889 files
NFCリーダーのソースコード
AndroidManifest.xml
まず、AndroidManifest.xml を見てみます。このファイルは、Androidアプリの構成や、使用するハードウェアのパーミッションなどの情報が書かれています。
パッと見たところは、NFC が許可されているぐらいで、それ以外は特別な記述は無さそうです。activity要素を見ると、intent-filter要素に、android.intent.action.MAIN とあるので、.ui.MainActivity がエントリポイントであることが分かります。また、android.intent.category.LAUNCHER とあるので、スマートフォンのホーム画面にアプリのアイコンが表示されます。このあたりは、HelloWorldアプリと同じでした。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.nfc" android:required="true" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.NFCReader" tools:targetApi="31"> <activity android:name=".ui.MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
ui/MainActivity.kt
エントリポイントの ui/MainActivity.kt を見ます。全ソースを貼ると長いので、一部と、メソッドだけを貼ります。特に、NFCタグを読み出すところを中心に見ていきます。
onCreate() は、アプリが起動して最初に呼び出されます。ここではメイン関数のような感じで、主に初期化処理を行っています。R.layout.activity_main は、Rクラスで管理されているリソースIDです。Kotlin では ? というのが多く出てきますが、これは、null許容型(null を代入することを許可する)という意味で、binder は null許容型の変数ということです。binder?.viewModel は、本来は null かどうかを確認する if文が入るところを、binder が null かもしれないことを考慮した書き方です。binder?.viewModel に、MainViewModelクラスのオブジェクトが入ってるという感じです。
onCheckedChanged() は、NFC の ON/OFF のボタンの処理として、ON になると、NFC を有効にして、OFF にすると NFC を無効にする処理を行っています。onTagDiscovered() は、NFCタグを検出したときに呼ばれる処理です。ここで、MainViewModelクラスの readTagメソッドを呼び出しています。launchMainFragment() は、二重起動の防止などの処理をしています。
package com.casecode.nfcreader.ui class MainActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener, NfcAdapter.ReaderCallback { private var binder: ActivityBinder? = null private val viewModel: MainViewModel by viewModels<MainViewModel>() override fun onCreate(savedInstanceState: Bundle?) { ... } override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { binder = DataBindingUtil.setContentView(this@MainActivity, R.layout.activity_main) binder?.viewModel = viewModel binder?.lifecycleOwner = this@MainActivity ... } override fun onTagDiscovered(tag: Tag?) { binder?.viewModel?.readTag(tag) } private fun launchMainFragment() { ... } }
viewmodel/MainViewModel.kt
readTagメソッドを持つ、MainViewModelクラスです。readTagメソッドを見ていきます。
stringBuilder変数に文字列を追加していってます。これが、上で実機で確認できた内容だと思います。id!! の !! は、null非許容型へ強制キャストらしいので、id は ByteArray? なので、null許容型で宣言されてますが、強制キャストされたので、id が null の場合は例外が発生します。
id を 3パターン(16進、10進、10進を逆順で表示?)で表示した後、Technologies ということで、TypeA、TypeB、TypeF(FeliCa)を表示しています。その後は、MifareClassic だった場合の詳細表示と、MifareUltralight だった場合の詳細表示を行っています。
class MainViewModel(application: Application) : AndroidViewModel(application) { fun readTag(tag: Tag?) { Coroutines.default(this@MainViewModel) { Log.d(TAG, "readTag(${tag} ${tag?.techList})") postNFCStatus(NFCStatus.Process) val stringBuilder: StringBuilder = StringBuilder() val id: ByteArray? = tag?.id stringBuilder.append("Tag ID (hex): ${getHex(id!!)} \n") stringBuilder.append("Tag ID (dec): ${getDec(id)} \n") stringBuilder.append("Tag ID (reversed): ${getReversed(id)} \n") stringBuilder.append("Technologies: ") tag.techList.forEach { tech -> stringBuilder.append(tech.substring(prefix.length)) stringBuilder.append(", ") } stringBuilder.delete(stringBuilder.length - 2, stringBuilder.length) tag.techList.forEach { tech -> if (tech.equals(MifareClassic::class.java.name)) { stringBuilder.append('\n') val mifareTag: MifareClassic = MifareClassic.get(tag) val type: String = when (mifareTag.type) { MifareClassic.TYPE_CLASSIC -> "Classic" MifareClassic.TYPE_PLUS -> "Plus" MifareClassic.TYPE_PRO -> "Pro" else -> "Unknown" } stringBuilder.append("Mifare Classic type: $type \n") stringBuilder.append("Mifare size: ${mifareTag.size} bytes \n") stringBuilder.append("Mifare sectors: ${mifareTag.sectorCount} \n") stringBuilder.append("Mifare blocks: ${mifareTag.blockCount}") } if (tech.equals(MifareUltralight::class.java.name)) { stringBuilder.append('\n'); val mifareUlTag: MifareUltralight = MifareUltralight.get(tag); val type: String = when (mifareUlTag.type) { MifareUltralight.TYPE_ULTRALIGHT -> "Ultralight" MifareUltralight.TYPE_ULTRALIGHT_C -> "Ultralight C" else -> "Unkown" } stringBuilder.append("Mifare Ultralight type: "); stringBuilder.append(type) } } Log.d(TAG, "Datum: $stringBuilder") Log.d(ContentValues.TAG, "dumpTagData Return \n $stringBuilder") postNFCStatus(NFCStatus.Read) liveTag.emit("${getDateTimeNow()} \n $stringBuilder") } }
別の NFCタグを読み込ませてみたのが以下です。TypeA で、MifareClassic の NFCタグのようです。NdefFormatable とあります。NDEF とは、NFC Data Exchange Format の略で、簡単に言うとフォーマットのこと(データをどのように格納するかのフォーマットのこと)です。NDEF の中身を解析してくれる機能は無いようです。

見たいところは確認できたので、ソースコードの解析は以上としたいと思います。
おわりに
今回は、NFCリーダーの Androidアプリを動かしてみました。うまく動きましたが、NDEF の内容も解析して表示してくれる機能があれば、もっと良かったです。次は、ライターの機能を持つ Androidアプリを探して見ようと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。