Browse Source

[Android] Finally add Amiibo load support to Android (#2845)

Co-authored-by: Ribbit <ribbit@placeholder.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2845
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: Ribbit <ribbit@eden-emu.dev>
Co-committed-by: Ribbit <ribbit@eden-emu.dev>
pull/2846/head
Ribbit 2 months ago
committed by crueter
parent
commit
683c2834aa
No known key found for this signature in database GPG Key ID: 425ACD2D4830EBC6
  1. 15
      src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
  2. 130
      src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
  3. 41
      src/android/app/src/main/jni/native.cpp
  4. 5
      src/android/app/src/main/res/menu/menu_in_game.xml
  5. 10
      src/android/app/src/main/res/values/strings.xml

15
src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt

@ -549,6 +549,21 @@ object NativeLibrary {
*/ */
external fun clearFilesystemProvider() external fun clearFilesystemProvider()
/**
* Gets the current virtual amiibo state reported by the core.
*
* @return Native enum value for the current amiibo state.
*/
external fun getVirtualAmiiboState(): Int
/**
* Loads amiibo data into the currently running emulation session.
*
* @param data Raw amiibo file contents.
* @return Native enum value representing the load result.
*/
external fun loadAmiibo(data: ByteArray): Int
/** /**
* Checks if all necessary keys are present for decryption * Checks if all necessary keys are present for decryption
*/ */

130
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt

@ -35,6 +35,8 @@ import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
@ -60,6 +62,7 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
@ -121,6 +124,39 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var perfStatsRunnable: Runnable? = null private var perfStatsRunnable: Runnable? = null
private var socRunnable: Runnable? = null private var socRunnable: Runnable? = null
private var isAmiiboPickerOpen = false
private val loadAmiiboLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
isAmiiboPickerOpen = false
val binding = _binding ?: return@registerForActivityResult
binding.inGameMenu.requestFocus()
if (!isAdded || uri == null) {
return@registerForActivityResult
}
val data = try {
requireContext().contentResolver.openInputStream(uri)?.use { it.readBytes() }
} catch (e: Exception) {
Log.error("[EmulationFragment] Failed to read amiibo: ${e.message}")
showAmiiboDialog(R.string.amiibo_unknown_error)
return@registerForActivityResult
}
val amiiboData = data ?: run {
showAmiiboDialog(R.string.amiibo_not_valid)
return@registerForActivityResult
}
if (amiiboData.isEmpty()) {
showAmiiboDialog(R.string.amiibo_not_valid)
return@registerForActivityResult
}
val result = NativeLibrary.loadAmiibo(amiiboData)
handleAmiiboLoadResult(result)
}
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@ -623,6 +659,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true true
} }
R.id.menu_load_amiibo -> handleLoadAmiiboSelection()
R.id.menu_controls -> { R.id.menu_controls -> {
val action = HomeNavigationDirections.actionGlobalSettingsActivity( val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null, null,
@ -893,6 +931,70 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
private fun handleLoadAmiiboSelection(): Boolean {
val binding = _binding ?: return true
binding.inGameMenu.requestFocus()
if (!NativeLibrary.isRunning()) {
showAmiiboDialog(R.string.amiibo_wrong_state)
return true
}
when (AmiiboState.fromValue(NativeLibrary.getVirtualAmiiboState())) {
AmiiboState.TagNearby -> {
NativeInput.onRemoveNfcTag()
showAmiiboDialog(R.string.amiibo_removed_message)
}
AmiiboState.WaitingForAmiibo -> {
if (isAmiiboPickerOpen) {
return true
}
isAmiiboPickerOpen = true
binding.drawerLayout.close()
loadAmiiboLauncher.launch(AMIIBO_MIME_TYPES)
}
else -> showAmiiboDialog(R.string.amiibo_wrong_state)
}
return true
}
private fun handleAmiiboLoadResult(result: Int) {
when (AmiiboLoadResult.fromValue(result)) {
AmiiboLoadResult.Success -> {
if (!isAdded) {
return
}
Toast.makeText(
requireContext(),
getString(R.string.amiibo_load_success),
Toast.LENGTH_SHORT
).show()
}
AmiiboLoadResult.UnableToLoad -> showAmiiboDialog(R.string.amiibo_in_use)
AmiiboLoadResult.NotAnAmiibo -> showAmiiboDialog(R.string.amiibo_not_valid)
AmiiboLoadResult.WrongDeviceState -> showAmiiboDialog(R.string.amiibo_wrong_state)
AmiiboLoadResult.Unknown -> showAmiiboDialog(R.string.amiibo_unknown_error)
}
}
private fun showAmiiboDialog(@StringRes messageRes: Int) {
if (!isAdded) {
return
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.amiibo_title)
.setMessage(messageRes)
.setPositiveButton(R.string.ok, null)
.show()
}
override fun onPause() { override fun onPause() {
if (this::emulationState.isInitialized) { if (this::emulationState.isInitialized) {
if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
@ -906,6 +1008,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
isAmiiboPickerOpen = false
} }
override fun onDetach() { override fun onDetach() {
@ -1739,7 +1842,34 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
private enum class AmiiboState(val value: Int) {
Disabled(0),
Initialized(1),
WaitingForAmiibo(2),
TagNearby(3);
companion object {
fun fromValue(value: Int): AmiiboState =
values().firstOrNull { it.value == value } ?: Disabled
}
}
private enum class AmiiboLoadResult(val value: Int) {
Success(0),
UnableToLoad(1),
NotAnAmiibo(2),
WrongDeviceState(3),
Unknown(4);
companion object {
fun fromValue(value: Int): AmiiboLoadResult =
values().firstOrNull { it.value == value } ?: Unknown
}
}
companion object { companion object {
private val AMIIBO_MIME_TYPES =
arrayOf("application/octet-stream", "application/x-binary", "*/*")
private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!) private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
private val socUpdateHandler = Handler(Looper.myLooper()!!) private val socUpdateHandler = Handler(Looper.myLooper()!!)
} }

41
src/android/app/src/main/jni/native.cpp

@ -7,8 +7,10 @@
#include <codecvt> #include <codecvt>
#include <locale> #include <locale>
#include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector>
#include <dlfcn.h> #include <dlfcn.h>
#ifdef ARCHITECTURE_arm64 #ifdef ARCHITECTURE_arm64
@ -67,6 +69,7 @@
#include "hid_core/frontend/emulated_controller.h" #include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h" #include "hid_core/hid_core.h"
#include "hid_core/hid_types.h" #include "hid_core/hid_types.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "jni/native.h" #include "jni/native.h"
#include "video_core/renderer_base.h" #include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h" #include "video_core/renderer_vulkan/renderer_vulkan.h"
@ -1005,6 +1008,44 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobje
return ContentManager::AreKeysPresent(); return ContentManager::AreKeysPresent();
} }
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getVirtualAmiiboState(JNIEnv* env, jobject jobj) {
if (!EmulationSession::GetInstance().IsRunning()) {
return static_cast<jint>(InputCommon::VirtualAmiibo::State::Disabled);
}
auto* virtual_amiibo =
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo();
if (virtual_amiibo == nullptr) {
return static_cast<jint>(InputCommon::VirtualAmiibo::State::Disabled);
}
return static_cast<jint>(virtual_amiibo->GetCurrentState());
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_loadAmiibo(JNIEnv* env, jobject jobj,
jbyteArray jdata) {
if (!EmulationSession::GetInstance().IsRunning() || jdata == nullptr) {
return static_cast<jint>(InputCommon::VirtualAmiibo::Info::WrongDeviceState);
}
auto* virtual_amiibo =
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo();
if (virtual_amiibo == nullptr) {
return static_cast<jint>(InputCommon::VirtualAmiibo::Info::Unknown);
}
const jsize length = env->GetArrayLength(jdata);
std::vector<u8> bytes(static_cast<std::size_t>(length));
if (length > 0) {
env->GetByteArrayRegion(jdata, 0, length,
reinterpret_cast<jbyte*>(bytes.data()));
}
const auto info =
virtual_amiibo->LoadAmiibo(std::span<u8>(bytes.data(), bytes.size()));
return static_cast<jint>(info);
}
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_NativeLibrary_initMultiplayer( Java_org_yuzu_yuzu_1emu_NativeLibrary_initMultiplayer(
JNIEnv* env, [[maybe_unused]] jobject obj) { JNIEnv* env, [[maybe_unused]] jobject obj) {

5
src/android/app/src/main/res/menu/menu_in_game.xml

@ -33,6 +33,11 @@
android:icon="@drawable/ic_two_users" android:icon="@drawable/ic_two_users"
android:title="@string/multiplayer" /> android:title="@string/multiplayer" />
<item
android:id="@+id/menu_load_amiibo"
android:icon="@drawable/ic_nfc"
android:title="@string/load_amiibo" />
<item <item
android:id="@+id/menu_overlay_controls" android:id="@+id/menu_overlay_controls"

10
src/android/app/src/main/res/values/strings.xml

@ -803,11 +803,21 @@
<string name="emulation_pause">Pause emulation</string> <string name="emulation_pause">Pause emulation</string>
<string name="emulation_unpause">Unpause emulation</string> <string name="emulation_unpause">Unpause emulation</string>
<string name="emulation_input_overlay">Overlay options</string> <string name="emulation_input_overlay">Overlay options</string>
<string name="load_amiibo">Load Amiibo</string>
<string name="touchscreen">Touchscreen</string> <string name="touchscreen">Touchscreen</string>
<string name="lock_drawer">Lock drawer</string> <string name="lock_drawer">Lock drawer</string>
<string name="unlock_drawer">Unlock drawer</string> <string name="unlock_drawer">Unlock drawer</string>
<string name="reset">Reset</string> <string name="reset">Reset</string>
<!-- Amiibo -->
<string name="amiibo_title">Amiibo</string>
<string name="amiibo_removed_message">The current amiibo has been removed</string>
<string name="amiibo_wrong_state">The current game is not looking for amiibo</string>
<string name="amiibo_not_valid">The selected file is not a valid amiibo</string>
<string name="amiibo_in_use">The selected file is already in use</string>
<string name="amiibo_unknown_error">An unknown error occurred</string>
<string name="amiibo_load_success">Amiibo loaded</string>
<!-- Software keyboard --> <!-- Software keyboard -->
<string name="software_keyboard">Software keyboard</string> <string name="software_keyboard">Software keyboard</string>

Loading…
Cancel
Save