前回 は、久しぶりに Androidアプリ開発ということで、開発環境の構築と、簡単なアプリを作って、動かしてみました。
今回は、やりたかった NFC の読み込みをするアプリを動かしてみようと思います。良さそうな OSS を持ってきて動かしてみます。
それでは、やっていきます。
参考文献
今回も、参考にさせて頂いた書籍です。
Kotlin は詳しく知らないので、文法などは以下の書籍を参考にしています。
はじめに
「Javaでデザインパターンを学ぶ」の記事一覧です。良かったら参考にしてください。
Javaでデザインパターンの記事一覧
NFCリーダーをGitHubで探す
まず、今回の目的(NFCリーダー)に合った OSS を GitHub から探す必要がありますが、運よく、とても良さそうなものが見つかりました。最終更新日が 2024/2/20 と、そこまで古くなくて、Kotlin で書かれていて、機能がシンプルで扱いやすそうです。
今回は、ありがたくこちらのソースコードを使わせて頂こうと思います。
github.com
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 の方を使います。
Welcome to Android Studio
すると、ディレクトリを選択するダイアログが出るので、クローンしたディレクトリを選択して、OK をクリックします。読み込みに時間がかかる場合がありますが、待てば、ちゃんと表示されます。
Open File or Project
Android Studio が起動し、バックグラウンドでビルドなどが始まります。時間がかかりますが、待ちます。いろいろ情報、警告が出ますが、いったんは無視します。「Gradle をアップグレードしますか?」というように推奨されますが、まずは、このまま(開発された方の環境を変更しない)にしておいた方がいいです。うまく動かない場合に、アップグレードをやってみる、というやり方でいいと思います。
しばらくすると、ビルドが完了します。
NFCリーダーアプリをエミュレータで起動する
早速、エミュレータで起動してみます。エミュレータが選択されていることを確認して、▷ボタンを押して起動します。無事に起動できました。
エミュレータで起動できた
NFCリーダーアプリを実機で起動する
エミュレータでは、NFCタグを読ませることが出来なさそうなので、早速実機で動かしていきます。前回、HelloWorldアプリでやった方法と同じように準備して、Android Studio の方も、エミュレータから実機に切り替えて、▷ボタンを押して起動します。
実機でNFCリーダーが起動できた
うまく起動できたようなので、使わなくなったセブンイレブンの nanacoカードをスマホの裏面にピタッと接触させます。うまく読み込めました!
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 xmlnsandroid="http://schemas.android.com/apk/res/android"
xmlnstools="http://schemas.android.com/tools">
<uses-permission androidname="android.permission.NFC" />
<uses-feature
androidname="android.hardware.nfc"
androidrequired="true" />
<application
androidallowBackup="true"
androiddataExtractionRules="@xml/data_extraction_rules"
androidfullBackupContent="@xml/backup_rules"
androidicon="@mipmap/ic_launcher"
androidlabel="@string/app_name"
androidroundIcon="@mipmap/ic_launcher_round"
androidsupportsRtl="true"
androidtheme="@style/Theme.NFCReader"
toolstargetApi="31">
<activity
androidname=".ui.MainActivity"
androidexported="true">
<intent-filter>
<action androidname="android.intent.action.MAIN" />
<category androidname="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タグ(MifareClassic)
見たいところは確認できたので、ソースコードの解析は以上としたいと思います。
おわりに
今回は、NFCリーダーの Androidアプリを動かしてみました。うまく動きましたが、NDEF の内容も解析して表示してくれる機能があれば、もっと良かったです。次は、ライターの機能を持つ Androidアプリを探して見ようと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。