diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 632f408..63baf77 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + + + + + diff --git a/app/src/main/java/com/github/brokenithm/activity/MainActivity.kt b/app/src/main/java/com/github/brokenithm/activity/MainActivity.kt index 10d5ec8..45d2c61 100644 --- a/app/src/main/java/com/github/brokenithm/activity/MainActivity.kt +++ b/app/src/main/java/com/github/brokenithm/activity/MainActivity.kt @@ -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 = 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" + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/brokenithm/util/AsyncTaskUtils.kt b/app/src/main/java/com/github/brokenithm/util/AsyncTaskUtil.kt similarity index 97% rename from app/src/main/java/com/github/brokenithm/util/AsyncTaskUtils.kt rename to app/src/main/java/com/github/brokenithm/util/AsyncTaskUtil.kt index f77989f..981c5e1 100644 --- a/app/src/main/java/com/github/brokenithm/util/AsyncTaskUtils.kt +++ b/app/src/main/java/com/github/brokenithm/util/AsyncTaskUtil.kt @@ -1,138 +1,138 @@ -package com.github.brokenithm.util - -import kotlinx.coroutines.* - -@Suppress("unused") -object AsyncTaskUtil { - fun 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 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 CoroutineScope.executeAsyncTask(task: AsyncTask, 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 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 make( - onPreExecute: () -> Unit = {}, - doInBackground: (Array) -> R, - onPostExecute: (R) -> Unit = {}, - onCancelled: () -> Unit = {} - ): AsyncTask { - return object : AsyncTask() { - 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 make( - onPreExecute: () -> Unit = {}, - doInBackground: (Array) -> R, - onPostExecute: (R) -> Unit = {}, - onProgressUpdate: (P) -> Unit = {}, - onCancelled: () -> Unit = {} - ): AsyncTask { - return object : AsyncTask() { - 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 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 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 CoroutineScope.executeAsyncTask(task: AsyncTask, 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 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 make( + onPreExecute: () -> Unit = {}, + doInBackground: (Array) -> R, + onPostExecute: (R) -> Unit = {}, + onCancelled: () -> Unit = {} + ): AsyncTask { + return object : AsyncTask() { + 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 make( + onPreExecute: () -> Unit = {}, + doInBackground: (Array) -> R, + onPostExecute: (R) -> Unit = {}, + onProgressUpdate: (P) -> Unit = {}, + onCancelled: () -> Unit = {} + ): AsyncTask { + return object : AsyncTask() { + 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() } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/brokenithm/util/FeliCa.kt b/app/src/main/java/com/github/brokenithm/util/FeliCa.kt new file mode 100644 index 0000000..1c3cd05 --- /dev/null +++ b/app/src/main/java/com/github/brokenithm/util/FeliCa.kt @@ -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 } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/xml/nfc_tech_filters.xml b/app/src/main/res/xml/nfc_tech_filters.xml new file mode 100644 index 0000000..4401923 --- /dev/null +++ b/app/src/main/res/xml/nfc_tech_filters.xml @@ -0,0 +1,9 @@ + + + + android.nfc.tech.NfcA + android.nfc.tech.NfcF + android.nfc.tech.NdefFormatable + android.nfc.tech.MifareClassic + + \ No newline at end of file