Преглед изворни кода

android: Implement amiibo reading from nfc tag

Narr the Reg пре 3 година
родитељ
комит
ca4be4283d

+ 14 - 1
src/android/app/src/main/AndroidManifest.xml

@@ -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"/>
 

+ 12 - 0
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.
      *

+ 20 - 0
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)

+ 32 - 0
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)

+ 6 - 1
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
     }
 }

+ 165 - 0
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<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()
+            )
+        )
+    }
+}

+ 10 - 1
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<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,

+ 3 - 0
src/android/app/src/main/jni/emu_window/emu_window.h

@@ -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 {

+ 20 - 0
src/android/app/src/main/jni/native.cpp

@@ -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) {

+ 7 - 1
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,

+ 6 - 0
src/android/app/src/main/res/menu/menu_game_grid.xml

@@ -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"

+ 2 - 0
src/android/app/src/main/res/values/strings.xml

@@ -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 - 0
src/android/app/src/main/res/xml/nfc_tech_filter.xml

@@ -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>

+ 22 - 4
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<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;
 }

+ 2 - 0
src/input_common/drivers/virtual_amiibo.h

@@ -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();