From 72f5d4e664bbc71f0fcca5c487a6fc5da2294385 Mon Sep 17 00:00:00 2001 From: bunnei Date: Sun, 26 Apr 2020 08:09:07 +0000 Subject: [PATCH] Merge branch 'amiibo' into 'master' android: Add Amiibo file support See merge request CitraInternal/citra-android!34 --- .../org/citra/citra_emu/NativeLibrary.java | 4 ++ .../activities/CustomFilePickerActivity.java | 4 ++ .../activities/EmulationActivity.java | 62 +++++++++++++++++++ .../fragments/CustomFilePickerFragment.java | 15 +++-- .../java/org/citra/citra_emu/model/Game.java | 1 - .../citra/citra_emu/ui/main/MainActivity.java | 4 +- .../citra_emu/utils/FileBrowserHelper.java | 20 +++++- .../org/citra/citra_emu/utils/FileUtil.java | 37 +++++++++++ src/android/app/src/main/jni/native.cpp | 27 ++++++++ src/android/app/src/main/jni/native.h | 4 ++ .../app/src/main/res/menu/menu_emulation.xml | 14 +++++ .../app/src/main/res/values/strings.xml | 6 ++ .../app/src/main/res/values/styles.xml | 1 + 13 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java index fd4c78451..c4a745df3 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java @@ -441,6 +441,10 @@ public final class NativeLibrary { /// Notifies that the activity is now in foreground and camera devices can now be reloaded public static native void ReloadCameraDevices(); + public static native boolean LoadAmiibo(byte[] bytes); + + public static native void RemoveAmiibo(); + /** * Button type for use in onTouchEvent */ diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java index 6d5d31d5a..3083286e2 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java @@ -14,6 +14,7 @@ import java.io.File; public class CustomFilePickerActivity extends FilePickerActivity { public static final String EXTRA_TITLE = "filepicker.intent.TITLE"; + public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS"; @Override protected AbstractFilePickerFragment getFragment( @@ -29,6 +30,9 @@ public class CustomFilePickerActivity extends FilePickerActivity { Intent intent = getIntent(); int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0); fragment.setTitle(title); + String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS); + fragment.setAllowedExtensions(allowedExtensions); + return fragment; } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java index e0abb339d..56050d39a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java @@ -34,10 +34,16 @@ import org.citra.citra_emu.features.settings.ui.SettingsActivity; import org.citra.citra_emu.features.settings.utils.SettingsFile; import org.citra.citra_emu.camera.StillImageCameraHelper; import org.citra.citra_emu.fragments.EmulationFragment; +import org.citra.citra_emu.ui.main.MainActivity; import org.citra.citra_emu.utils.ControllerMappingHelper; import org.citra.citra_emu.utils.EmulationMenuSettings; +import org.citra.citra_emu.utils.FileBrowserHelper; +import org.citra.citra_emu.utils.FileUtil; +import java.io.File; +import java.io.IOException; import java.lang.annotation.Retention; +import java.util.Collections; import java.util.List; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -58,7 +64,10 @@ public final class EmulationActivity extends AppCompatActivity { public static final int MENU_ACTION_RESET_OVERLAY = 10; public static final int MENU_ACTION_SHOW_OVERLAY = 11; public static final int MENU_ACTION_OPEN_SETTINGS = 12; + public static final int MENU_ACTION_LOAD_AMIIBO = 13; + public static final int MENU_ACTION_REMOVE_AMIIBO = 14; + public static final int REQUEST_SELECT_AMIIBO = 2; private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; private static SparseIntArray buttonsActionsMap = new SparseIntArray(); @@ -87,6 +96,10 @@ public final class EmulationActivity extends AppCompatActivity { .append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY); buttonsActionsMap .append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS); + buttonsActionsMap + .append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO); + buttonsActionsMap + .append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO); } private View mDecorView; @@ -375,6 +388,14 @@ public final class EmulationActivity extends AppCompatActivity { case MENU_ACTION_OPEN_SETTINGS: SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, ""); break; + + case MENU_ACTION_LOAD_AMIIBO: + FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO, R.string.select_amiibo, Collections.singletonList("bin")); + break; + + case MENU_ACTION_REMOVE_AMIIBO: + RemoveAmiibo(); + break; } return true; @@ -422,6 +443,47 @@ public final class EmulationActivity extends AppCompatActivity { return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent result) { + super.onActivityResult(requestCode, resultCode, result); + switch (requestCode) { + case REQUEST_SELECT_AMIIBO: + // If the user picked a file, as opposed to just backing out. + if (resultCode == MainActivity.RESULT_OK) { + String[] selectedFiles = FileBrowserHelper.getSelectedFiles(result); + if (selectedFiles == null) + return; + + onAmiiboSelected(selectedFiles[0]); + } + break; + } + } + + private void onAmiiboSelected(String selectedFile) { + File file = new File(selectedFile); + boolean success = false; + try { + byte[] bytes = FileUtil.getBytesFromFile(file); + success = NativeLibrary.LoadAmiibo(bytes); + } catch (IOException e) { + e.printStackTrace(); + } + + if (!success) { + new AlertDialog.Builder(this) + .setTitle(R.string.amiibo_load_error) + .setMessage(R.string.amiibo_load_error_message) + .setPositiveButton(android.R.string.ok, null) + .create() + .show(); + } + } + + private void RemoveAmiibo() { + NativeLibrary.RemoveAmiibo(); + } + private void toggleControls() { final SharedPreferences.Editor editor = mPreferences.edit(); boolean[] enabledButtons = new boolean[14]; diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java index 9047ed41c..e683918c7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java @@ -17,12 +17,13 @@ import org.citra.citra_emu.R; import java.io.File; import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; +import java.util.Collections; +import java.util.List; public class CustomFilePickerFragment extends FilePickerFragment { + private static String ALL_FILES = "*"; private int mTitle; - private static final Set extensions = new HashSet<>(Arrays.asList("elf", "axf", "cci", "3ds", "cxi", "app", "3dsx", "cia")); + private static List extensions = Collections.singletonList(ALL_FILES); @NonNull @Override @@ -66,6 +67,12 @@ public class CustomFilePickerFragment extends FilePickerFragment { mTitle = title; } + public void setAllowedExtensions(String allowedExtensions) { + if (allowedExtensions == null) + return; + + extensions = Arrays.asList(allowedExtensions.split(",")); + } @Override protected boolean isItemVisible(@NonNull final File file) { @@ -73,7 +80,7 @@ public class CustomFilePickerFragment extends FilePickerFragment { // files if the files don't show up in the file picker when mode == MODE_DIR. // To avoid this, show files even when the user needs to select a directory. return (showHiddenItems || !file.isHidden()) && - (file.isDirectory() || + (file.isDirectory() || extensions.contains(ALL_FILES) || extensions.contains(fileExtension(file.getName()).toLowerCase())); } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java index f929f8476..a4ffc59c7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java @@ -2,7 +2,6 @@ package org.citra.citra_emu.model; import android.content.ContentValues; import android.database.Cursor; -import android.os.Environment; import java.nio.file.Paths; diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java index 59d0f6ab7..a5e4d9489 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java @@ -26,6 +26,8 @@ import org.citra.citra_emu.utils.PicassoUtils; import org.citra.citra_emu.utils.StartupHandler; import org.citra.citra_emu.utils.ThemeUtil; +import java.util.Arrays; + /** * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which * individually display a grid of available games for each Fragment, in a tabbed layout. @@ -127,7 +129,7 @@ public final class MainActivity extends AppCompatActivity implements MainView { public void launchFileListActivity() { if (PermissionsHandler.hasWriteAccess(this)) { FileBrowserHelper.openDirectoryPicker(this, MainPresenter.REQUEST_ADD_DIRECTORY, - R.string.select_game_folder); + R.string.select_game_folder, Arrays.asList("elf", "axf", "cci", "3ds", "cxi", "app", "3dsx", "cia")); } else { PermissionsHandler.checkWritePermission(this); } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java index 9c733d1ad..791b0311b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java @@ -16,7 +16,7 @@ import java.io.File; import java.util.List; public final class FileBrowserHelper { - public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title) { + public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List extensions) { Intent i = new Intent(activity, CustomFilePickerActivity.class); i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); @@ -25,11 +25,12 @@ public final class FileBrowserHelper { i.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().getPath()); i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); + i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); activity.startActivityForResult(i, requestCode); } - public static void openFilePicker(FragmentActivity activity, int requestCode, int title) { + public static void openFilePicker(FragmentActivity activity, int requestCode, int title, List extensions) { Intent i = new Intent(activity, CustomFilePickerActivity.class); i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); @@ -38,6 +39,7 @@ public final class FileBrowserHelper { i.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().getPath()); i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); + i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); activity.startActivityForResult(i, requestCode); } @@ -53,4 +55,18 @@ public final class FileBrowserHelper { return null; } + + @Nullable + public static String[] getSelectedFiles(Intent result) { + // Use the provided utility method to parse the result + List files = Utils.getSelectedFilesFromResult(result); + if (!files.isEmpty()) { + String[] paths = new String[files.size()]; + for (int i = 0; i < files.size(); i++) + paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath(); + return paths; + } + + return null; + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java new file mode 100644 index 000000000..f9025171b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java @@ -0,0 +1,37 @@ +package org.citra.citra_emu.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class FileUtil { + public static byte[] getBytesFromFile(File file) throws IOException { + final long length = file.length(); + + // You cannot create an array using a long type. + if (length > Integer.MAX_VALUE) { + // File is too large + throw new IOException("File is too large!"); + } + + byte[] bytes = new byte[(int) length]; + + int offset = 0; + int numRead; + + try (InputStream is = new FileInputStream(file)) { + while (offset < bytes.length + && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { + offset += numRead; + } + } + + // Ensure all the bytes have been read in + if (offset < bytes.length) { + throw new IOException("Could not completely read file " + file.getName()); + } + + return bytes; + } +} diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 5d31440dc..b86717cc9 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -19,6 +19,7 @@ #include "core/frontend/camera/factory.h" #include "core/frontend/scope_acquire_context.h" #include "core/hle/service/am/am.h" +#include "core/hle/service/nfc/nfc.h" #include "core/settings.h" #include "jni/applets/swkbd.h" #include "jni/button_manager.h" @@ -481,4 +482,30 @@ void Java_org_citra_citra_1emu_NativeLibrary_ReloadCameraDevices(JNIEnv* env, jc } } +jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv *env, jclass clazz, jbyteArray bytes) { + Core::System& system{Core::System::GetInstance()}; + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (nfc == nullptr || env->GetArrayLength(bytes) != sizeof(Service::NFC::AmiiboData)) { + return static_cast(false); + } + + Service::NFC::AmiiboData amiibo_data{}; + env->GetByteArrayRegion(bytes, 0, sizeof(Service::NFC::AmiiboData), reinterpret_cast(&amiibo_data)); + + nfc->LoadAmiibo(amiibo_data); + return static_cast(true); +} + +void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv *env, jclass clazz) { + Core::System &system{Core::System::GetInstance()}; + Service::SM::ServiceManager &sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (nfc == nullptr) { + return; + } + + nfc->RemoveAmiibo(); +} + } // extern "C" diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 98b831965..0621d2eb0 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -134,6 +134,10 @@ Java_org_citra_citra_1emu_NativeLibrary_GetTextureFilterNames(JNIEnv* env, jclas JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_ReloadCameraDevices(JNIEnv* env, jclass clazz); +JNIEXPORT jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv *env, jclass clazz, jbyteArray bytes); + +JNIEXPORT void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv *env, jclass clazz); + #ifdef __cplusplus } #endif diff --git a/src/android/app/src/main/res/menu/menu_emulation.xml b/src/android/app/src/main/res/menu/menu_emulation.xml index e58a7ce54..e5c71fb86 100644 --- a/src/android/app/src/main/res/menu/menu_emulation.xml +++ b/src/android/app/src/main/res/menu/menu_emulation.xml @@ -25,6 +25,20 @@ + + + + + + + + Show Overlay Close Game Are you sure that you would like to close the current game? + Amiibo + Load + Remove + Select Amiibo file + Error loading Amiibo + While loading the specified Amiibo file, an error occurred. Please check that the file is correct. You need to allow write access to external storage for the emulator to work Loading Settings... diff --git a/src/android/app/src/main/res/values/styles.xml b/src/android/app/src/main/res/values/styles.xml index 94315b45c..e39620d72 100644 --- a/src/android/app/src/main/res/values/styles.xml +++ b/src/android/app/src/main/res/values/styles.xml @@ -29,6 +29,7 @@