From ca4be4283d8eec9624019f4bca88f7d1aa467f01 Mon Sep 17 00:00:00 2001 From: Narr the Reg Date: Wed, 22 Mar 2023 11:09:12 -0600 Subject: [PATCH] android: Implement amiibo reading from nfc tag --- src/android/app/src/main/AndroidManifest.xml | 15 +- .../java/org/yuzu/yuzu_emu/NativeLibrary.java | 12 ++ .../yuzu_emu/activities/EmulationActivity.kt | 20 +++ .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 32 ++++ .../yuzu/yuzu_emu/ui/main/MainPresenter.kt | 7 +- .../java/org/yuzu/yuzu_emu/utils/NfcReader.kt | 165 ++++++++++++++++++ .../src/main/jni/emu_window/emu_window.cpp | 11 +- .../app/src/main/jni/emu_window/emu_window.h | 3 + src/android/app/src/main/jni/native.cpp | 20 +++ src/android/app/src/main/jni/native.h | 8 +- .../app/src/main/res/menu/menu_game_grid.xml | 6 + .../app/src/main/res/values/strings.xml | 2 + .../app/src/main/res/xml/nfc_tech_filter.xml | 6 + src/input_common/drivers/virtual_amiibo.cpp | 26 ++- src/input_common/drivers/virtual_amiibo.h | 2 + 15 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt create mode 100644 src/android/app/src/main/res/xml/nfc_tech_filter.xml diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index c8f1696c5..a5c063d52 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + + android:screenOrientation="userLandscape" + android:exported="true"> + + + + + + + + + diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java index c056b7d6d..5def17f2b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java @@ -123,6 +123,18 @@ public final class NativeLibrary { public static native boolean onGamePadMotionEvent(int Device, long delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); + /** + * Signals and load a nfc tag + * + * @param data Byte array containing all the data from a nfc tag + */ + public static native boolean onReadNfcTag(byte[] data); + + /** + * Removes current loaded nfc tag + */ + public static native boolean onRemoveNfcTag(); + /** * Handles touch press events. * diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 3589e7629..32d04ef31 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -24,6 +24,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.fragments.EmulationFragment import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.utils.ControllerMappingHelper +import org.yuzu.yuzu_emu.utils.NfcReader import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable import org.yuzu.yuzu_emu.utils.ThemeHelper import kotlin.math.roundToInt @@ -37,6 +38,7 @@ open class EmulationActivity : AppCompatActivity() { var isActivityRecreated = false private var menuVisible = false private var emulationFragment: EmulationFragment? = null + private lateinit var nfcReader: NfcReader private lateinit var game: Game @@ -76,6 +78,9 @@ open class EmulationActivity : AppCompatActivity() { } title = game.title + nfcReader = NfcReader(this) + nfcReader.initialize() + // Start a foreground service to prevent the app from getting killed in the background // TODO(bunnei): Disable notifications until we support app suspension. //foregroundService = new Intent(EmulationActivity.this, ForegroundService.class); @@ -104,6 +109,21 @@ open class EmulationActivity : AppCompatActivity() { } return super.onKeyDown(keyCode, event) } + override fun onResume() { + super.onResume() + nfcReader.startScanning() + } + + override fun onPause() { + super.onPause() + nfcReader.stopScanning() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + nfcReader.onNewIntent(intent) + } override fun onSaveInstanceState(outState: Bundle) { outState.putParcelable(EXTRA_SELECTED_GAME, game) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index b87125d1c..441c9da9c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -112,6 +112,7 @@ class MainActivity : AppCompatActivity(), MainView { when (request) { MainPresenter.REQUEST_ADD_DIRECTORY -> getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) MainPresenter.REQUEST_INSTALL_KEYS -> getProdKey.launch(arrayOf("*/*")) + MainPresenter.REQUEST_INSTALL_AMIIBO_KEYS -> getAmiiboKey.launch(arrayOf("*/*")) MainPresenter.REQUEST_SELECT_GPU_DRIVER -> { // Get the driver name for the dialog message. var driverName = GpuDriverHelper.customDriverName @@ -221,6 +222,37 @@ class MainActivity : AppCompatActivity(), MainView { } } + private val getAmiiboKey = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) + return@registerForActivityResult + + val takeFlags = + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + contentResolver.takePersistableUriPermission( + result, + takeFlags + ) + + val dstPath = DirectoryInitialization.userDirectory + "/keys/" + if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "key_retail.bin")) { + if (NativeLibrary.ReloadKeys()) { + Toast.makeText( + this, + R.string.install_keys_success, + Toast.LENGTH_SHORT + ).show() + refreshFragment() + } else { + Toast.makeText( + this, + R.string.install_amiibo_keys_failure, + Toast.LENGTH_LONG + ).show() + } + } + } + private val getDriver = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> if (result == null) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt index dbfda7be3..554542e05 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt @@ -36,6 +36,10 @@ class MainPresenter(private val view: MainView) { launchFileListActivity(REQUEST_INSTALL_KEYS) return true } + R.id.button_install_amiibo_keys -> { + launchFileListActivity(REQUEST_INSTALL_AMIIBO_KEYS) + return true + } R.id.button_select_gpu_driver -> { launchFileListActivity(REQUEST_SELECT_GPU_DRIVER) return true @@ -64,6 +68,7 @@ class MainPresenter(private val view: MainView) { companion object { const val REQUEST_ADD_DIRECTORY = 1 const val REQUEST_INSTALL_KEYS = 2 - const val REQUEST_SELECT_GPU_DRIVER = 3 + const val REQUEST_INSTALL_AMIIBO_KEYS = 3 + const val REQUEST_SELECT_GPU_DRIVER = 4 } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt new file mode 100644 index 000000000..1ce220d42 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt @@ -0,0 +1,165 @@ +package org.yuzu.yuzu_emu.utils + +import android.app.Activity +import android.app.PendingIntent +import android.content.Intent +import android.content.IntentFilter +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.NfcA +import android.os.Build +import android.os.Handler +import android.os.Looper +import org.yuzu.yuzu_emu.NativeLibrary +import java.io.IOException + +class NfcReader(private val activity: Activity) { + private var nfcAdapter: NfcAdapter? = null + private var pendingIntent: PendingIntent? = null + + fun initialize() { + nfcAdapter = NfcAdapter.getDefaultAdapter(activity) ?: return + + pendingIntent = PendingIntent.getActivity( + activity, + 0, Intent(activity, activity.javaClass), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + else PendingIntent.FLAG_UPDATE_CURRENT + ) + + val tagDetected = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED) + tagDetected.addCategory(Intent.CATEGORY_DEFAULT) + } + + fun startScanning() { + nfcAdapter?.enableForegroundDispatch(activity, pendingIntent, null, null) + } + + fun stopScanning() { + nfcAdapter?.disableForegroundDispatch(activity) + } + + fun onNewIntent(intent: Intent) { + val action = intent.action + if (NfcAdapter.ACTION_TAG_DISCOVERED != action + && NfcAdapter.ACTION_TECH_DISCOVERED != action + && NfcAdapter.ACTION_NDEF_DISCOVERED != action + ) { + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val tag = + intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java) ?: return + readTagData(tag) + return + } + + val tag = + intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) ?: return + readTagData(tag) + } + + private fun readTagData(tag: Tag) { + if (!tag.techList.contains("android.nfc.tech.NfcA")) { + return + } + + val amiibo = NfcA.get(tag) ?: return + amiibo.connect() + + val tagData = ntag215ReadAll(amiibo) ?: return + NativeLibrary.onReadNfcTag(tagData) + + nfcAdapter?.ignore( + tag, + 1000, + { NativeLibrary.onRemoveNfcTag() }, + Handler(Looper.getMainLooper()) + ) + } + + private fun ntag215ReadAll(amiibo: NfcA): ByteArray? { + val bufferSize = amiibo.maxTransceiveLength; + val tagSize = 0x21C + val pageSize = 4 + val lastPage = tagSize / pageSize - 1 + val tagData = ByteArray(tagSize) + + // We need to read the ntag in steps otherwise we overflow the buffer + for (i in 0..tagSize step bufferSize - 1) { + val dataStart = i / pageSize + var dataEnd = (i + bufferSize) / pageSize + + if (dataEnd > lastPage) { + dataEnd = lastPage + } + + try { + val data = ntag215FastRead(amiibo, dataStart, dataEnd - 1) + System.arraycopy(data, 0, tagData, i, (dataEnd - dataStart) * pageSize) + } catch (e: IOException) { + return null; + } + } + return tagData + } + + private fun ntag215Read(amiibo: NfcA, page: Int): ByteArray? { + return amiibo.transceive( + byteArrayOf( + 0x30.toByte(), + (page and 0xFF).toByte() + ) + ) + } + + private fun ntag215FastRead(amiibo: NfcA, start: Int, end: Int): ByteArray? { + return amiibo.transceive( + byteArrayOf( + 0x3A.toByte(), + (start and 0xFF).toByte(), + (end and 0xFF).toByte() + ) + ) + } + + private fun ntag215PWrite( + amiibo: NfcA, + page: Int, + data1: Int, + data2: Int, + data3: Int, + data4: Int + ): ByteArray? { + return amiibo.transceive( + byteArrayOf( + 0xA2.toByte(), + (page and 0xFF).toByte(), + (data1 and 0xFF).toByte(), + (data2 and 0xFF).toByte(), + (data3 and 0xFF).toByte(), + (data4 and 0xFF).toByte() + ) + ) + } + + private fun ntag215PwdAuth( + amiibo: NfcA, + data1: Int, + data2: Int, + data3: Int, + data4: Int + ): ByteArray? { + return amiibo.transceive( + byteArrayOf( + 0x1B.toByte(), + (data1 and 0xFF).toByte(), + (data2 and 0xFF).toByte(), + (data3 and 0xFF).toByte(), + (data4 and 0xFF).toByte() + ) + ) + } +} diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index ad17cf129..2fdb68823 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -2,6 +2,7 @@ #include "common/logging/log.h" #include "input_common/drivers/touch_screen.h" +#include "input_common/drivers/virtual_amiibo.h" #include "input_common/drivers/virtual_gamepad.h" #include "input_common/main.h" #include "jni/emu_window/emu_window.h" @@ -36,7 +37,15 @@ void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timesta float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z) { m_input_subsystem->GetVirtualGamepad()->SetMotionState( - player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z); + player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z); +} + +void EmuWindow_Android::OnReadNfcTag(std::span data) { + m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data); +} + +void EmuWindow_Android::OnRemoveNfcTag() { + m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo(); } EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h index 1c1edf62c..2a0a72077 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.h +++ b/src/android/app/src/main/jni/emu_window/emu_window.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "core/frontend/emu_window.h" #include "core/frontend/graphics_context.h" @@ -39,6 +40,8 @@ public: void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y); void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); + void OnReadNfcTag(std::span data); + void OnRemoveNfcTag(); void OnFrameDisplayed() override {} std::unique_ptr CreateSharedContext() const override { diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 10603c8fa..86994f734 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -451,6 +451,26 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent( return static_cast(true); } +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jbyteArray j_data) { + jboolean isCopy{false}; + std::span data(reinterpret_cast(env->GetByteArrayElements(j_data, &isCopy)), + static_cast(env->GetArrayLength(j_data))); + + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().Window().OnReadNfcTag(data); + } + return static_cast(true); +} + +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().Window().OnRemoveNfcTag(); + } + return static_cast(true); +} + void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint id, jfloat x, jfloat y) { diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index d30351c16..8336e525a 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -32,7 +32,13 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMoveEv JNIEnv* env, jclass clazz, jstring j_device, jint axis, jfloat x, jfloat y); JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadAxisEvent( - JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val); + JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val); + +JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag( + JNIEnv* env, jclass clazz, jbyteArray j_data); + +JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag( + JNIEnv* env, jclass clazz); JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env, jclass clazz, diff --git a/src/android/app/src/main/res/menu/menu_game_grid.xml b/src/android/app/src/main/res/menu/menu_game_grid.xml index b834a444e..73046de0e 100644 --- a/src/android/app/src/main/res/menu/menu_game_grid.xml +++ b/src/android/app/src/main/res/menu/menu_game_grid.xml @@ -22,6 +22,12 @@ android:title="@string/install_keys" app:showAsAction="ifRoom" /> + + Select game folder Install keys + Install amiibo keys Keys successfully installed Keys file (prod.keys) is invalid + Keys file (key_retail.bin) is invalid Select GPU driver diff --git a/src/android/app/src/main/res/xml/nfc_tech_filter.xml b/src/android/app/src/main/res/xml/nfc_tech_filter.xml new file mode 100644 index 000000000..eb4497446 --- /dev/null +++ b/src/android/app/src/main/res/xml/nfc_tech_filter.xml @@ -0,0 +1,6 @@ + + + + android.nfc.tech.NfcA + + diff --git a/src/input_common/drivers/virtual_amiibo.cpp b/src/input_common/drivers/virtual_amiibo.cpp index 304f4c70b..9505179c6 100644 --- a/src/input_common/drivers/virtual_amiibo.cpp +++ b/src/input_common/drivers/virtual_amiibo.cpp @@ -73,10 +73,7 @@ VirtualAmiibo::State VirtualAmiibo::GetCurrentState() const { VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) { const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read, Common::FS::FileType::BinaryFile}; - - if (state != State::WaitingForAmiibo) { - return Info::WrongDeviceState; - } + std::vector data{}; if (!nfc_file.IsOpen()) { return Info::UnableToLoad; @@ -101,7 +98,28 @@ VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) { } file_path = filename; + return LoadAmiibo(data); +} + +VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(std::span data) { + if (state != State::WaitingForAmiibo) { + return Info::WrongDeviceState; + } + + switch (data.size_bytes()) { + case AmiiboSize: + case AmiiboSizeWithoutPassword: + nfc_data.resize(AmiiboSize); + break; + case MifareSize: + nfc_data.resize(MifareSize); + break; + default: + return Info::NotAnAmiibo; + } + state = State::AmiiboIsOpen; + memcpy(nfc_data.data(),data.data(),data.size_bytes()); SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, nfc_data}); return Info::Success; } diff --git a/src/input_common/drivers/virtual_amiibo.h b/src/input_common/drivers/virtual_amiibo.h index 488d00b31..34e97cd91 100644 --- a/src/input_common/drivers/virtual_amiibo.h +++ b/src/input_common/drivers/virtual_amiibo.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include @@ -47,6 +48,7 @@ public: State GetCurrentState() const; Info LoadAmiibo(const std::string& amiibo_file); + Info LoadAmiibo(std::span data); Info ReloadAmiibo(); Info CloseAmiibo();