以下の内容はhttps://daisuke20240310.hatenablog.com/entry/android_nfcreaderより取得しました。


Androidアプリ:NFCリーダーアプリを動かしてソースコードを確認する

前回 は、久しぶりに Androidアプリ開発ということで、開発環境の構築と、簡単なアプリを作って、動かしてみました。

今回は、やりたかった NFC の読み込みをするアプリを動かしてみようと思います。良さそうな OSS を持ってきて動かしてみます。

それでは、やっていきます。

参考文献

今回も、参考にさせて頂いた書籍です。

Kotlin は詳しく知らないので、文法などは以下の書籍を参考にしています。

はじめに

「Javaでデザインパターンを学ぶ」の記事一覧です。良かったら参考にしてください。

Javaでデザインパターンの記事一覧
・準備編:Java開発環境構築(JDK+Visual Studio Code)
・第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 で書かれていて、機能がシンプルで扱いやすそうです。

今回は、ありがたくこちらのソースコードを使わせて頂こうと思います。

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
Welcome to Android Studio

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

Open File or Project
Open File or Project

Android Studio が起動し、バックグラウンドでビルドなどが始まります。時間がかかりますが、待ちます。いろいろ情報、警告が出ますが、いったんは無視します。「Gradle をアップグレードしますか?」というように推奨されますが、まずは、このまま(開発された方の環境を変更しない)にしておいた方がいいです。うまく動かない場合に、アップグレードをやってみる、というやり方でいいと思います。

しばらくすると、ビルドが完了します。

NFCリーダーアプリをエミュレータで起動する

早速、エミュレータで起動してみます。エミュレータが選択されていることを確認して、▷ボタンを押して起動します。無事に起動できました。

エミュレータで起動できた
エミュレータで起動できた

NFCリーダーアプリを実機で起動する

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

実機でNFCリーダーが起動できた
実機でNFCリーダーが起動できた

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

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 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タグ(MifareClassic)
NFCタグ(MifareClassic)

見たいところは確認できたので、ソースコードの解析は以上としたいと思います。

おわりに

今回は、NFCリーダーの Androidアプリを動かしてみました。うまく動きましたが、NDEF の内容も解析して表示してくれる機能があれば、もっと良かったです。次は、ライターの機能を持つ Androidアプリを探して見ようと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/android_nfcreaderより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14