android: Implement amiibo reading from nfc tag

master
Narr the Reg 2023-03-22 11:09:12 +07:00 committed by bunnei
parent b2aeb50229
commit ca4be4283d
15 changed files with 327 additions and 8 deletions

@ -13,6 +13,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.NFC" />
<application <application
android:name="org.yuzu.yuzu_emu.YuzuApplication" android:name="org.yuzu.yuzu_emu.YuzuApplication"
@ -48,7 +49,19 @@
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:theme="@style/Theme.Yuzu.Main" android:theme="@style/Theme.Yuzu.Main"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="userLandscape" /> android:screenOrientation="userLandscape"
android:exported="true">
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
<service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/> <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/>

@ -123,6 +123,18 @@ public final class NativeLibrary {
public static native boolean onGamePadMotionEvent(int Device, long delta_timestamp, float gyro_x, float gyro_y, 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); 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. * Handles touch press events.
* *

@ -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.fragments.EmulationFragment
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper 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.SerializableHelper.parcelable
import org.yuzu.yuzu_emu.utils.ThemeHelper import org.yuzu.yuzu_emu.utils.ThemeHelper
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -37,6 +38,7 @@ open class EmulationActivity : AppCompatActivity() {
var isActivityRecreated = false var isActivityRecreated = false
private var menuVisible = false private var menuVisible = false
private var emulationFragment: EmulationFragment? = null private var emulationFragment: EmulationFragment? = null
private lateinit var nfcReader: NfcReader
private lateinit var game: Game private lateinit var game: Game
@ -76,6 +78,9 @@ open class EmulationActivity : AppCompatActivity() {
} }
title = game.title title = game.title
nfcReader = NfcReader(this)
nfcReader.initialize()
// Start a foreground service to prevent the app from getting killed in the background // Start a foreground service to prevent the app from getting killed in the background
// TODO(bunnei): Disable notifications until we support app suspension. // TODO(bunnei): Disable notifications until we support app suspension.
//foregroundService = new Intent(EmulationActivity.this, ForegroundService.class); //foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
@ -104,6 +109,21 @@ open class EmulationActivity : AppCompatActivity() {
} }
return super.onKeyDown(keyCode, event) 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) { override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(EXTRA_SELECTED_GAME, game) outState.putParcelable(EXTRA_SELECTED_GAME, game)

@ -112,6 +112,7 @@ class MainActivity : AppCompatActivity(), MainView {
when (request) { when (request) {
MainPresenter.REQUEST_ADD_DIRECTORY -> getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) MainPresenter.REQUEST_ADD_DIRECTORY -> getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
MainPresenter.REQUEST_INSTALL_KEYS -> getProdKey.launch(arrayOf("*/*")) MainPresenter.REQUEST_INSTALL_KEYS -> getProdKey.launch(arrayOf("*/*"))
MainPresenter.REQUEST_INSTALL_AMIIBO_KEYS -> getAmiiboKey.launch(arrayOf("*/*"))
MainPresenter.REQUEST_SELECT_GPU_DRIVER -> { MainPresenter.REQUEST_SELECT_GPU_DRIVER -> {
// Get the driver name for the dialog message. // Get the driver name for the dialog message.
var driverName = GpuDriverHelper.customDriverName 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 = private val getDriver =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) if (result == null)

@ -36,6 +36,10 @@ class MainPresenter(private val view: MainView) {
launchFileListActivity(REQUEST_INSTALL_KEYS) launchFileListActivity(REQUEST_INSTALL_KEYS)
return true return true
} }
R.id.button_install_amiibo_keys -> {
launchFileListActivity(REQUEST_INSTALL_AMIIBO_KEYS)
return true
}
R.id.button_select_gpu_driver -> { R.id.button_select_gpu_driver -> {
launchFileListActivity(REQUEST_SELECT_GPU_DRIVER) launchFileListActivity(REQUEST_SELECT_GPU_DRIVER)
return true return true
@ -64,6 +68,7 @@ class MainPresenter(private val view: MainView) {
companion object { companion object {
const val REQUEST_ADD_DIRECTORY = 1 const val REQUEST_ADD_DIRECTORY = 1
const val REQUEST_INSTALL_KEYS = 2 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
} }
} }

@ -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<Tag>(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()
)
)
}
}

@ -2,6 +2,7 @@
#include "common/logging/log.h" #include "common/logging/log.h"
#include "input_common/drivers/touch_screen.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/drivers/virtual_gamepad.h"
#include "input_common/main.h" #include "input_common/main.h"
#include "jni/emu_window/emu_window.h" #include "jni/emu_window/emu_window.h"
@ -39,6 +40,14 @@ void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timesta
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<u8> data) {
m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data);
}
void EmuWindow_Android::OnRemoveNfcTag() {
m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo();
}
EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem,
ANativeWindow* surface, ANativeWindow* surface,
std::shared_ptr<Common::DynamicLibrary> driver_library) std::shared_ptr<Common::DynamicLibrary> driver_library)

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <span>
#include "core/frontend/emu_window.h" #include "core/frontend/emu_window.h"
#include "core/frontend/graphics_context.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 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, 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); float gyro_z, float accel_x, float accel_y, float accel_z);
void OnReadNfcTag(std::span<u8> data);
void OnRemoveNfcTag();
void OnFrameDisplayed() override {} void OnFrameDisplayed() override {}
std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override { std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {

@ -451,6 +451,26 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent(
return static_cast<jboolean>(true); return static_cast<jboolean>(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<u8> data(reinterpret_cast<u8 *>(env->GetByteArrayElements(j_data, &isCopy)),
static_cast<size_t>(env->GetArrayLength(j_data)));
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnReadNfcTag(data);
}
return static_cast<jboolean>(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<jboolean>(true);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed([[maybe_unused]] JNIEnv* env, void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz, jint id, [[maybe_unused]] jclass clazz, jint id,
jfloat x, jfloat y) { jfloat x, jfloat y) {

@ -34,6 +34,12 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMoveEv
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadAxisEvent( 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, JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env,
jclass clazz, jclass clazz,
jfloat x, jfloat y, jfloat x, jfloat y,

@ -22,6 +22,12 @@
android:title="@string/install_keys" android:title="@string/install_keys"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/button_install_amiibo_keys"
android:icon="@drawable/ic_install"
android:title="@string/install_amiibo_keys"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/button_select_gpu_driver" android:id="@+id/button_select_gpu_driver"
android:icon="@drawable/ic_settings" android:icon="@drawable/ic_settings"

@ -52,8 +52,10 @@
<!-- Add Directory Screen--> <!-- Add Directory Screen-->
<string name="select_game_folder">Select game folder</string> <string name="select_game_folder">Select game folder</string>
<string name="install_keys">Install keys</string> <string name="install_keys">Install keys</string>
<string name="install_amiibo_keys">Install amiibo keys</string>
<string name="install_keys_success">Keys successfully installed</string> <string name="install_keys_success">Keys successfully installed</string>
<string name="install_keys_failure">Keys file (prod.keys) is invalid</string> <string name="install_keys_failure">Keys file (prod.keys) is invalid</string>
<string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string>
<!-- GPU driver installation --> <!-- GPU driver installation -->
<string name="select_gpu_driver">Select GPU driver</string> <string name="select_gpu_driver">Select GPU driver</string>

@ -0,0 +1,6 @@
<?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-list>
</resources>

@ -73,10 +73,7 @@ VirtualAmiibo::State VirtualAmiibo::GetCurrentState() const {
VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) { VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read, const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read,
Common::FS::FileType::BinaryFile}; Common::FS::FileType::BinaryFile};
std::vector<u8> data{};
if (state != State::WaitingForAmiibo) {
return Info::WrongDeviceState;
}
if (!nfc_file.IsOpen()) { if (!nfc_file.IsOpen()) {
return Info::UnableToLoad; return Info::UnableToLoad;
@ -101,7 +98,28 @@ VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
} }
file_path = filename; file_path = filename;
return LoadAmiibo(data);
}
VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(std::span<u8> 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; state = State::AmiiboIsOpen;
memcpy(nfc_data.data(),data.data(),data.size_bytes());
SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, nfc_data}); SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, nfc_data});
return Info::Success; return Info::Success;
} }

@ -4,6 +4,7 @@
#pragma once #pragma once
#include <array> #include <array>
#include <span>
#include <string> #include <string>
#include <vector> #include <vector>
@ -47,6 +48,7 @@ public:
State GetCurrentState() const; State GetCurrentState() const;
Info LoadAmiibo(const std::string& amiibo_file); Info LoadAmiibo(const std::string& amiibo_file);
Info LoadAmiibo(std::span<u8> data);
Info ReloadAmiibo(); Info ReloadAmiibo();
Info CloseAmiibo(); Info CloseAmiibo();