diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index c8f1696c54..a5c063d52d 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 c056b7d6da..5def17f2bd 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 3589e76298..32d04ef316 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 b87125d1cb..441c9da9c6 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 dbfda7be35..554542e05a 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 0000000000..1ce220d420
--- /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 ad17cf1296..2fdb688236 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 1c1edf62c6..2a0a720772 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 10603c8fad..86994f7349 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 d30351c164..8336e525a5 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 b834a444e8..73046de0e9 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 0000000000..eb4497446f
--- /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 304f4c70bf..9505179c6a 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 488d00b311..34e97cd916 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();