Add support for scanning Aime and FeliCa card

master
Tindy X 2021-07-20 13:31:37 +08:00
parent 234fcda4f2
commit 7a53265d8d
No known key found for this signature in database
GPG Key ID: C6AD413169968D58
5 changed files with 354 additions and 141 deletions

View File

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC"/>
<application
android:name=".BrokenithmApplication"
@ -16,11 +17,15 @@
<activity android:name=".activity.MainActivity"
android:screenOrientation="landscape"
android:configChanges="uiMode|orientation">
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filters" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
</activity>
</application>

View File

@ -1,14 +1,20 @@
package com.github.brokenithm.activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.nfc.NfcAdapter
import android.nfc.NfcManager
import android.nfc.Tag
import android.nfc.tech.MifareClassic
import android.os.*
import android.util.DisplayMetrics
import android.util.Log
import android.view.*
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
@ -16,6 +22,7 @@ import androidx.lifecycle.lifecycleScope
import com.github.brokenithm.BrokenithmApplication
import com.github.brokenithm.R
import com.github.brokenithm.util.AsyncTaskUtil
import com.github.brokenithm.util.FeliCa
import net.cachapa.expandablelayout.ExpandableLayout
import java.net.*
import java.nio.ByteBuffer
@ -125,6 +132,90 @@ class MainActivity : AppCompatActivity() {
}
}
// nfc
private fun Byte.getBit(bit: Int) = (toInt() ushr bit) and 0x1
private fun MifareClassic.authenticateBlock(blockIndex: Int, keyA: ByteArray, keyB: ByteArray, write: Boolean = false): Boolean {
// check access bits
val sectorIndex = blockToSector(blockIndex)
val accessBitsBlock = sectorToBlock(sectorIndex) + 3
if (!authenticateSectorWithKeyA(sectorIndex, keyA)) return false
val accessBits = readBlock(accessBitsBlock)
val targetBit = blockIndex % 4
val bitC1 = accessBits[7].getBit(targetBit + 4)
val bitC2 = accessBits[8].getBit(targetBit)
val bitC3 = accessBits[8].getBit(targetBit + 4)
val allBits = (bitC1 shl 2) or (bitC2 shl 1) or bitC3
return if (write) {
when (allBits) {
0 -> true
3, 4, 6 -> authenticateSectorWithKeyB(sectorIndex, keyB)
else -> false
}
} else {
when (allBits) {
7 -> false
3, 5 -> authenticateSectorWithKeyB(sectorIndex, keyB)
else -> true
}
}
}
private fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
enum class CardType {
CARD_AIME, CARD_FELICA
}
private var adapter: NfcAdapter? = null
private val mAimeKey = byteArrayOf(0x57, 0x43, 0x43, 0x46, 0x76, 0x32)
private var enableNFC = true
private var hasCard = false
private var cardType = CardType.CARD_AIME
private val cardId = ByteArray(10)
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val tag: Tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) ?: return
val felica = FeliCa.get(tag)
if (felica != null) {
thread {
try {
felica.connect()
felica.poll()
felica.IDm?.copyInto(cardId) ?: throw IllegalStateException("Failed to fetch IDm from FeliCa")
cardId[8] = 0
cardId[9] = 0
cardType = CardType.CARD_FELICA
hasCard = true
Log.d(TAG, "found felica card: ${cardId.toHexString().removeRange(16..19)}")
while (felica.isConnected) Thread.sleep(50)
hasCard = false
felica.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
return
}
val mifare = MifareClassic.get(tag) ?: return
thread {
try {
mifare.connect()
if (mifare.authenticateBlock(2, keyA = mAimeKey, keyB = mAimeKey)) {
Thread.sleep(100)
val block = mifare.readBlock(2)
block.copyInto(cardId, 0, 6, 16)
cardType = CardType.CARD_AIME
hasCard = true
Log.d(TAG, "found aime card: ${cardId.toHexString()}")
while (mifare.isConnected) Thread.sleep(50)
hasCard = false
} else {
Log.d(TAG, "nfc auth failed")
}
mifare.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@ -132,6 +223,8 @@ class MainActivity : AppCompatActivity() {
setImmersive()
app = application as BrokenithmApplication
vibrator = applicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val nfcManager = getSystemService(Context.NFC_SERVICE) as NfcManager
adapter = nfcManager.defaultAdapter
vibrateMethod = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
{
@ -488,11 +581,31 @@ class MainActivity : AppCompatActivity() {
mSensorManager?.registerListener(listener, gyro, 10000)
val accel = mSensorManager?.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)
mSensorManager?.registerListener(listener, accel, 10000)
enableNfcForegroundDispatch()
}
private fun enableNfcForegroundDispatch() {
try {
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val nfcPendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
adapter?.enableForegroundDispatch(this, nfcPendingIntent, null, null)
} catch (ex: IllegalStateException) {
Log.e(TAG, "Error enabling NFC foreground dispatch", ex)
}
}
private fun disableNfcForegroundDispatch() {
try {
adapter?.disableForegroundDispatch(this)
} catch (ex: IllegalStateException) {
Log.e(TAG, "Error disabling NFC foreground dispatch", ex)
}
}
override fun onPause() {
super.onPause()
disableNfcForegroundDispatch()
mSensorManager?.unregisterListener(listener)
super.onPause()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@ -634,6 +747,8 @@ class MainActivity : AppCompatActivity() {
val buffer = applyKeys(buttons, IoBuffer())
try {
mTCPSocket.getOutputStream().write(constructBuffer(buffer))
if (enableNFC)
mTCPSocket.getOutputStream().write(constructCardData())
} catch (e: Exception) {
e.printStackTrace()
continue
@ -665,6 +780,8 @@ class MainActivity : AppCompatActivity() {
val packet = constructPacket(buffer)
try {
socket.send(packet)
if (enableNFC)
socket.send(constructCardPacket())
} catch (e: Exception) {
e.printStackTrace()
Thread.sleep(100)
@ -720,7 +837,7 @@ class MainActivity : AppCompatActivity() {
var serviceBtn = false
}
private fun getLocalIPAddress(useIPv4: Boolean): ByteArray {
private fun getLocalIPAddress(useIPv4: Boolean = true): ByteArray {
try {
val interfaces: List<NetworkInterface> = Collections.list(NetworkInterface.getNetworkInterfaces())
for (intf in interfaces) {
@ -744,7 +861,7 @@ class MainActivity : AppCompatActivity() {
private fun sendConnect(address: InetSocketAddress?) {
address ?: return
thread {
val selfAddress = getLocalIPAddress(true)
val selfAddress = getLocalIPAddress()
if (selfAddress.isEmpty()) return@thread
val buffer = ByteArray(21)
byteArrayOf('C'.toByte(), 'O'.toByte(), 'N'.toByte()).copyInto(buffer, 1)
@ -885,6 +1002,21 @@ class MainActivity : AppCompatActivity() {
return DatagramPacket(realBuf, buffer.length + 1)
}
private fun constructCardData(): ByteArray {
val buf = ByteArray(24)
byteArrayOf(15, 'C'.toByte(), 'R'.toByte(), 'D'.toByte()).copyInto(buf)
buf[4] = if (hasCard) 1 else 0
buf[5] = cardType.ordinal.toByte()
if (hasCard)
cardId.copyInto(buf, 6)
return buf
}
private fun constructCardPacket(): DatagramPacket {
val buf = constructCardData()
return DatagramPacket(buf, buf[0] + 1)
}
private val airUpdateInterval = 10L
private var mLastAirHeight = 6
private var mLastAirUpdateTime = 0L
@ -952,4 +1084,8 @@ class MainActivity : AppCompatActivity() {
private fun makePaint(color: Int): Paint {
return Paint().apply { this.color = color }
}
companion object {
private const val TAG = "Brokenithm"
}
}

View File

@ -1,138 +1,138 @@
package com.github.brokenithm.util
import kotlinx.coroutines.*
@Suppress("unused")
object AsyncTaskUtil {
fun <R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit = {},
doInBackground: () -> R,
onPostExecute: (R) -> Unit = {},
onCancelled: () -> Unit = {}
) = launch {
try {
onPreExecute() // runs in Main Thread
val result = withContext(Dispatchers.IO) {
doInBackground() // runs in background thread without blocking the Main Thread
}
onPostExecute(result) // runs in Main Thread
} catch (e: CancellationException) {
onCancelled()
}
}
fun <P, R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit = {},
doInBackground: suspend (suspend (P) -> Unit) -> R,
onPostExecute: (R) -> Unit = {},
onProgressUpdate: (P) -> Unit,
onCancelled: () -> Unit = {}
) = launch {
try {
onPreExecute()
val result = withContext(Dispatchers.IO) {
doInBackground {
withContext(Dispatchers.Main) { onProgressUpdate(it) }
}
}
onPostExecute(result)
} catch (e: CancellationException) {
onCancelled()
}
}
fun CoroutineScope.executeDelayedTask(
task: () -> Unit,
delayMillis: Long,
onCancelled: () -> Unit = {}
) = launch {
try {
delay(delayMillis)
withContext(Dispatchers.Main) { task() }
} catch (e: CancellationException) {
onCancelled()
}
}
fun <A, P, R> CoroutineScope.executeAsyncTask(task: AsyncTask<A, P, R>, vararg arguments: A) = launch {
try {
task.onPreExecute()
val result = withContext(Dispatchers.IO) {
task.doInBackground(*arguments)
}
task.onPostExecute(result)
} catch (e: CancellationException) {
task.onCancelled()
}
}
abstract class AsyncTask<A, P, R> private constructor() {
open fun onPreExecute() {}
abstract suspend fun doInBackground(vararg argument: A): R
open fun onPostExecute(result: R) {}
open fun onCancelled() {}
open fun onProgressUpdate(progress: P) {}
protected fun publishProgress(progress: P) { onProgressUpdate(progress) }
private var mJob: Job? = null
val isCompleted: Boolean
get() = mJob?.isCompleted ?: false
val isActive: Boolean
get() = mJob?.isActive ?: false
val isCancelled: Boolean
get() = mJob?.isCancelled ?: false
fun execute(scope: CoroutineScope, vararg argument: A) {
if (mJob != null && (!mJob!!.isCompleted || mJob!!.isActive)) {
mJob!!.cancel()
mJob = null
}
mJob = scope.executeAsyncTask(this, *argument)
}
fun cancel(exception: CancellationException? = null) {
if (mJob != null && !mJob!!.isCancelled) {
mJob!!.cancel(exception)
}
}
fun cancel(message: String, cause: Throwable? = null) {
if (mJob != null && !mJob!!.isCancelled) {
mJob!!.cancel(message, cause)
}
}
companion object {
fun <A, R> make(
onPreExecute: () -> Unit = {},
doInBackground: (Array<out A>) -> R,
onPostExecute: (R) -> Unit = {},
onCancelled: () -> Unit = {}
): AsyncTask<A, Unit, R> {
return object : AsyncTask<A, Unit, R>() {
override fun onPreExecute() { onPreExecute() }
override suspend fun doInBackground(vararg argument: A): R { return doInBackground(argument) }
override fun onPostExecute(result: R) { onPostExecute(result) }
override fun onCancelled() { onCancelled() }
}
}
fun <A, P, R> make(
onPreExecute: () -> Unit = {},
doInBackground: (Array<out A>) -> R,
onPostExecute: (R) -> Unit = {},
onProgressUpdate: (P) -> Unit = {},
onCancelled: () -> Unit = {}
): AsyncTask<A, P, R> {
return object : AsyncTask<A, P, R>() {
override fun onPreExecute() { onPreExecute() }
override suspend fun doInBackground(vararg argument: A): R { return doInBackground(argument) }
override fun onPostExecute(result: R) { onPostExecute(result) }
override fun onProgressUpdate(progress: P) { onProgressUpdate(progress) }
override fun onCancelled() { onCancelled() }
}
}
}
}
package com.github.brokenithm.util
import kotlinx.coroutines.*
@Suppress("unused")
object AsyncTaskUtil {
fun <R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit = {},
doInBackground: () -> R,
onPostExecute: (R) -> Unit = {},
onCancelled: () -> Unit = {}
) = launch {
try {
onPreExecute() // runs in Main Thread
val result = withContext(Dispatchers.IO) {
doInBackground() // runs in background thread without blocking the Main Thread
}
onPostExecute(result) // runs in Main Thread
} catch (e: CancellationException) {
onCancelled()
}
}
fun <P, R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit = {},
doInBackground: suspend (suspend (P) -> Unit) -> R,
onPostExecute: (R) -> Unit = {},
onProgressUpdate: (P) -> Unit,
onCancelled: () -> Unit = {}
) = launch {
try {
onPreExecute()
val result = withContext(Dispatchers.IO) {
doInBackground {
withContext(Dispatchers.Main) { onProgressUpdate(it) }
}
}
onPostExecute(result)
} catch (e: CancellationException) {
onCancelled()
}
}
fun CoroutineScope.executeDelayedTask(
task: () -> Unit,
delayMillis: Long,
onCancelled: () -> Unit = {}
) = launch {
try {
delay(delayMillis)
withContext(Dispatchers.Main) { task() }
} catch (e: CancellationException) {
onCancelled()
}
}
fun <A, P, R> CoroutineScope.executeAsyncTask(task: AsyncTask<A, P, R>, vararg arguments: A) = launch {
try {
task.onPreExecute()
val result = withContext(Dispatchers.IO) {
task.doInBackground(*arguments)
}
task.onPostExecute(result)
} catch (e: CancellationException) {
task.onCancelled()
}
}
abstract class AsyncTask<A, P, R> private constructor() {
open fun onPreExecute() {}
abstract suspend fun doInBackground(vararg argument: A): R
open fun onPostExecute(result: R) {}
open fun onCancelled() {}
open fun onProgressUpdate(progress: P) {}
protected fun publishProgress(progress: P) { onProgressUpdate(progress) }
private var mJob: Job? = null
val isCompleted: Boolean
get() = mJob?.isCompleted ?: false
val isActive: Boolean
get() = mJob?.isActive ?: false
val isCancelled: Boolean
get() = mJob?.isCancelled ?: false
fun execute(scope: CoroutineScope, vararg argument: A) {
if (mJob != null && (!mJob!!.isCompleted || mJob!!.isActive)) {
mJob!!.cancel()
mJob = null
}
mJob = scope.executeAsyncTask(this, *argument)
}
fun cancel(exception: CancellationException? = null) {
if (mJob != null && !mJob!!.isCancelled) {
mJob!!.cancel(exception)
}
}
fun cancel(message: String, cause: Throwable? = null) {
if (mJob != null && !mJob!!.isCancelled) {
mJob!!.cancel(message, cause)
}
}
companion object {
fun <A, R> make(
onPreExecute: () -> Unit = {},
doInBackground: (Array<out A>) -> R,
onPostExecute: (R) -> Unit = {},
onCancelled: () -> Unit = {}
): AsyncTask<A, Unit, R> {
return object : AsyncTask<A, Unit, R>() {
override fun onPreExecute() { onPreExecute() }
override suspend fun doInBackground(vararg argument: A): R { return doInBackground(argument) }
override fun onPostExecute(result: R) { onPostExecute(result) }
override fun onCancelled() { onCancelled() }
}
}
fun <A, P, R> make(
onPreExecute: () -> Unit = {},
doInBackground: (Array<out A>) -> R,
onPostExecute: (R) -> Unit = {},
onProgressUpdate: (P) -> Unit = {},
onCancelled: () -> Unit = {}
): AsyncTask<A, P, R> {
return object : AsyncTask<A, P, R>() {
override fun onPreExecute() { onPreExecute() }
override suspend fun doInBackground(vararg argument: A): R { return doInBackground(argument) }
override fun onPostExecute(result: R) { onPostExecute(result) }
override fun onProgressUpdate(progress: P) { onProgressUpdate(progress) }
override fun onCancelled() { onCancelled() }
}
}
}
}
}

View File

@ -0,0 +1,63 @@
package com.github.brokenithm.util
import android.nfc.Tag
import android.nfc.tech.NfcF
import android.nfc.tech.TagTechnology
@Suppress("Unused", "MemberVisibilityCanBePrivate", "SpellCheckingInspection", "PropertyName")
class FeliCa private constructor(private val nfcF: NfcF) : TagTechnology {
private lateinit var mTag: Tag
var IDm: ByteArray? = null
private set
var PMm: ByteArray? = null
private set
val systemCode: ByteArray
get() = nfcF.systemCode
override fun connect() = nfcF.connect()
override fun isConnected() = nfcF.isConnected
override fun close() {
IDm = null
PMm = null
nfcF.close()
}
override fun getTag() = mTag
fun getMaxTransceiveLength() = nfcF.maxTransceiveLength
fun transceive(data: ByteArray): ByteArray = nfcF.transceive(data)
var timeout: Int
get() = nfcF.timeout
set(value) { nfcF.timeout = value }
private fun checkConnected() {
if (!nfcF.isConnected)
throw IllegalStateException("Call connect() first!")
}
fun poll(systemCode: Int = 0xFFFF, requestCode: Int = 0x01) {
checkConnected()
val buffer = ByteArray(6)
buffer[0] = 6
buffer[1] = FELICA_CMD_POLLING
buffer[2] = ((systemCode shr 8) and 0xff).toByte()
buffer[3] = (systemCode and 0xff).toByte()
buffer[4] = requestCode.toByte()
buffer[5] = 0
val result = nfcF.transceive(buffer)
if (result.size != 18 && result.size != 20)
throw IllegalStateException("Poll FeliCa response incorrect")
IDm = result.copyOfRange(2, 10)
PMm = result.copyOfRange(10, 18)
}
companion object {
private const val FELICA_CMD_POLLING: Byte = 0x00
fun get(tag: Tag): FeliCa? {
val realTag = NfcF.get(tag) ?: return null
return FeliCa(realTag).apply { mTag = tag }
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
<tech>android.nfc.tech.NfcF</tech>
<tech>android.nfc.tech.NdefFormatable</tech>
<tech>android.nfc.tech.MifareClassic</tech>
</tech-list>
</resources>