android: Implement amiibo reading from nfc tag
This commit is contained in:
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.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
<application
|
||||
android:name="org.yuzu.yuzu_emu.YuzuApplication"
|
||||
|
@ -48,7 +49,19 @@
|
|||
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
|
||||
android:theme="@style/Theme.Yuzu.Main"
|
||||
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"/>
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 "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<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,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#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<u8> data);
|
||||
void OnRemoveNfcTag();
|
||||
void OnFrameDisplayed() 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);
|
||||
}
|
||||
|
||||
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,
|
||||
[[maybe_unused]] jclass clazz, jint id,
|
||||
jfloat x, jfloat y) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -22,6 +22,12 @@
|
|||
android:title="@string/install_keys"
|
||||
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
|
||||
android:id="@+id/button_select_gpu_driver"
|
||||
android:icon="@drawable/ic_settings"
|
||||
|
|
|
@ -52,8 +52,10 @@
|
|||
<!-- Add Directory Screen-->
|
||||
<string name="select_game_folder">Select game folder</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_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 -->
|
||||
<string name="select_gpu_driver">Select GPU driver</string>
|
||||
|
|
6
src/android/app/src/main/res/xml/nfc_tech_filter.xml
Normal file
6
src/android/app/src/main/res/xml/nfc_tech_filter.xml
Normal file
|
@ -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) {
|
||||
const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read,
|
||||
Common::FS::FileType::BinaryFile};
|
||||
|
||||
if (state != State::WaitingForAmiibo) {
|
||||
return Info::WrongDeviceState;
|
||||
}
|
||||
std::vector<u8> 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<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;
|
||||
memcpy(nfc_data.data(),data.data(),data.size_bytes());
|
||||
SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, nfc_data});
|
||||
return Info::Success;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
@ -47,6 +48,7 @@ public:
|
|||
State GetCurrentState() const;
|
||||
|
||||
Info LoadAmiibo(const std::string& amiibo_file);
|
||||
Info LoadAmiibo(std::span<u8> data);
|
||||
Info ReloadAmiibo();
|
||||
Info CloseAmiibo();
|
||||
|
||||
|
|
Loading…
Reference in a new issue