From 29841a3fb2801e5fba8408c5304fb065c4cfd24c Mon Sep 17 00:00:00 2001 From: SSimco <37044560+SSimco@users.noreply.github.com> Date: Sun, 2 Jul 2023 17:01:23 +0300 Subject: [PATCH] Add support for controller on Android --- src/android/app/src/main/cpp/native-lib.cpp | 170 ++++++++++++++++- .../info/cemu/Cemu/ButtonListAdapter.java | 77 ++++++++ .../info/cemu/Cemu/ControllerHeaderItem.java | 22 +++ .../info/cemu/Cemu/ControllerInputItem.java | 34 ++++ .../cemu/Cemu/ControllerInputListener.java | 41 +++++ .../Cemu/ControllerInputsDataProvider.java | 164 +++++++++++++++++ .../cemu/Cemu/ControllerInputsFragment.java | 101 +++++++++++ .../java/info/cemu/Cemu/ControllerItem.java | 8 + .../info/cemu/Cemu/ControllerListAdapter.java | 122 +++++++++++++ .../Cemu/EmulatedControllerTypeAdapter.java | 71 ++++++++ .../info/cemu/Cemu/EmulationActivity.java | 48 +++-- .../info/cemu/Cemu/EmulationFragment.java | 41 ++--- .../java/info/cemu/Cemu/InputManager.java | 122 +++++++++++++ .../info/cemu/Cemu/InputSettingsFragment.java | 37 ++++ .../java/info/cemu/Cemu/MotionDialog.java | 33 ++++ .../java/info/cemu/Cemu/NativeLibrary.java | 171 ++++++++++++++++++ .../java/info/cemu/Cemu/SettingsActivity.java | 26 +-- .../java/info/cemu/Cemu/SettingsFragment.java | 2 +- .../src/main/res/layout/activity_settings.xml | 42 +++-- .../res/layout/fragment_controller_inputs.xml | 36 ++++ .../res/layout/fragment_input_settings.xml | 16 ++ .../app/src/main/res/layout/layout_button.xml | 33 ++++ .../app/src/main/res/layout/layout_choice.xml | 13 ++ .../res/layout/layout_controller_header.xml | 9 + .../res/layout/layout_controller_input.xml | 23 +++ .../src/main/res/layout/settings_header.xml | 2 +- .../res/navigation/nav_settings_graph.xml | 19 +- .../app/src/main/res/values-night/themes.xml | 8 +- src/android/app/src/main/res/values/refs.xml | 2 + .../app/src/main/res/values/strings.xml | 100 +++++----- .../app/src/main/res/values/themes.xml | 8 +- .../AndroidGui/AndroidEmulatedController.cpp | 3 + .../AndroidGui/AndroidEmulatedController.h | 112 ++++++++++++ src/gui/AndroidGui/CMakeLists.txt | 2 + src/input/CMakeLists.txt | 10 +- src/input/ControllerFactory.cpp | 12 ++ src/input/InputManager.cpp | 3 + src/input/InputManager.h | 4 + src/input/api/Android/AndroidController.cpp | 52 ++++++ src/input/api/Android/AndroidController.h | 30 +++ .../api/Android/AndroidControllerProvider.cpp | 80 ++++++++ .../api/Android/AndroidControllerProvider.h | 21 +++ src/input/api/InputAPI.h | 6 + 43 files changed, 1801 insertions(+), 135 deletions(-) create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ButtonListAdapter.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ControllerHeaderItem.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ControllerInputItem.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ControllerInputListener.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ControllerInputsDataProvider.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ControllerInputsFragment.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ControllerItem.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ControllerListAdapter.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/EmulatedControllerTypeAdapter.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/InputManager.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/InputSettingsFragment.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/MotionDialog.java create mode 100644 src/android/app/src/main/res/layout/fragment_controller_inputs.xml create mode 100644 src/android/app/src/main/res/layout/fragment_input_settings.xml create mode 100644 src/android/app/src/main/res/layout/layout_button.xml create mode 100644 src/android/app/src/main/res/layout/layout_choice.xml create mode 100644 src/android/app/src/main/res/layout/layout_controller_header.xml create mode 100644 src/android/app/src/main/res/layout/layout_controller_input.xml create mode 100644 src/android/app/src/main/res/values/refs.xml create mode 100644 src/gui/AndroidGui/AndroidEmulatedController.cpp create mode 100644 src/gui/AndroidGui/AndroidEmulatedController.h create mode 100644 src/input/api/Android/AndroidController.cpp create mode 100644 src/input/api/Android/AndroidController.h create mode 100644 src/input/api/Android/AndroidControllerProvider.cpp create mode 100644 src/input/api/Android/AndroidControllerProvider.h diff --git a/src/android/app/src/main/cpp/native-lib.cpp b/src/android/app/src/main/cpp/native-lib.cpp index b6a316b8..53b28452 100644 --- a/src/android/app/src/main/cpp/native-lib.cpp +++ b/src/android/app/src/main/cpp/native-lib.cpp @@ -14,7 +14,10 @@ #include "input/InputManager.h" #include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" #include "AndroidGui/AndroidAudio.h" - +#include "input/api/Android/AndroidControllerProvider.h" +#include "input/api/Android/AndroidController.h" +#include "input/ControllerFactory.h" +#include "AndroidGui/AndroidEmulatedController.h" #include "JNIUtils.h" extern "C" @@ -345,4 +348,167 @@ extern "C" JNIEXPORT void JNICALL Java_info_cemu_Cemu_NativeLibrary_addGamePath(JNIEnv *env, jclass clazz, jstring uri) { gui_addGamePath(JNIUtils::JStringToString(env, uri)); -} \ No newline at end of file +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_onNativeKey(JNIEnv *env, jclass clazz, jstring device_descriptor, + jstring device_name, jint key, jboolean is_pressed) { + auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android); + auto androidControllerProvider = dynamic_cast(apiProvider.get()); + if (androidControllerProvider) + androidControllerProvider->on_key_event(JNIUtils::JStringToString(env, device_descriptor), + JNIUtils::JStringToString(env, device_name), + key, + is_pressed); + +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_onNativeAxis(JNIEnv *env, jclass clazz, jstring device_descriptor, + jstring device_name, jint axis, jfloat value) { + auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android); + auto androidControllerProvider = dynamic_cast(apiProvider.get()); + androidControllerProvider->on_axis_event(JNIUtils::JStringToString(env, device_descriptor), + JNIUtils::JStringToString(env, device_name), + axis, + value); +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setControllerType(JNIEnv *env, jclass clazz, jint index, + jint emulated_controller_type) { + EmulatedController::Type type = static_cast(emulated_controller_type); + auto &androidEmulatedController = AndroidEmulatedController::getAndroidEmulatedController( + index); + if (EmulatedController::Type::VPAD <= type && type < EmulatedController::Type::MAX) + androidEmulatedController.setType(type); + else + androidEmulatedController.setDisabled(); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_info_cemu_Cemu_NativeLibrary_getControllerType(JNIEnv *env, jclass clazz, jint index) { + + auto emulatedController = AndroidEmulatedController::getAndroidEmulatedController( + index).getEmulatedController(); + if (emulatedController) + return emulatedController->type(); + return -1; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_info_cemu_Cemu_NativeLibrary_getWPADControllersCount(JNIEnv *env, jclass clazz) { + jint wpadCount = 0; + for (int i = 0; i < InputManager::kMaxController; i++) { + auto emulatedController = AndroidEmulatedController::getAndroidEmulatedController( + i).getEmulatedController(); + if (!emulatedController) + continue; + if (emulatedController->type() != EmulatedController::Type::VPAD) + ++wpadCount; + } + return wpadCount; +} +extern "C" +JNIEXPORT jint JNICALL +Java_info_cemu_Cemu_NativeLibrary_getVPADControllersCount(JNIEnv *env, jclass clazz) { + jint vpadCount = 0; + for (int i = 0; i < InputManager::kMaxController; i++) { + auto emulatedController = AndroidEmulatedController::getAndroidEmulatedController( + i).getEmulatedController(); + if (!emulatedController) + continue; + if (emulatedController->type() == EmulatedController::Type::VPAD) + ++vpadCount; + } + return vpadCount; +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_info_cemu_Cemu_NativeLibrary_isControllerDisabled(JNIEnv *env, jclass clazz, jint index) { + return AndroidEmulatedController::getAndroidEmulatedController(index).getEmulatedController() == + nullptr; +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setControllerMapping(JNIEnv *env, jclass clazz, + jstring device_descriptor, + jstring device_name, jint index, + jint mapping_id, jint button_id) { + auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android); + auto androidControllerProvider = dynamic_cast(apiProvider.get()); + auto deviceName = JNIUtils::JStringToString(env, device_name); + auto deviceDescriptor = JNIUtils::JStringToString(env, device_descriptor); + auto controller = ControllerFactory::CreateController(InputAPI::Android, deviceDescriptor, + deviceName); + AndroidEmulatedController::getAndroidEmulatedController(index).setMapping(mapping_id, + controller, + button_id); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_info_cemu_Cemu_NativeLibrary_getControllerMapping(JNIEnv *env, jclass clazz, jint index, + jint mapping_id) { + auto mapping = AndroidEmulatedController::getAndroidEmulatedController(index) + .getMapping(mapping_id); + return env->NewStringUTF(mapping.value_or("").c_str()); +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_clearControllerMapping(JNIEnv *env, jclass clazz, jint index, + jint mapping_id) { + AndroidEmulatedController::getAndroidEmulatedController(index).clearMapping(mapping_id); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_info_cemu_Cemu_NativeLibrary_getControllerMappings(JNIEnv *env, jclass clazz, jint index) { + jclass hashMapClass = env->FindClass("java/util/HashMap"); + jmethodID hashMapConstructor = env->GetMethodID(hashMapClass, "", "()V"); + jmethodID hashMapPut = env->GetMethodID(hashMapClass, "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); + jclass integerClass = env->FindClass("java/lang/Integer"); + jmethodID integerConstructor = env->GetMethodID(integerClass, "", "(I)V"); + jobject hashMapObj = env->NewObject(hashMapClass, hashMapConstructor); + auto mappings = AndroidEmulatedController::getAndroidEmulatedController(index).getMappings(); + for (const auto &pair: mappings) { + jint key = pair.first; + jstring buttonName = env->NewStringUTF(pair.second.c_str()); + jobject mappingId = env->NewObject(integerClass, integerConstructor, key); + env->CallObjectMethod(hashMapObj, hashMapPut, mappingId, buttonName); + } + return hashMapObj; +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_onKeyEvent(JNIEnv *env, jclass clazz, jstring device_descriptor, + jstring device_name, jint key_code, + jboolean is_pressed) { + auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android); + auto androidControllerProvider = dynamic_cast(apiProvider.get()); + auto deviceDescriptor = JNIUtils::JStringToString(env, device_descriptor); + auto deviceName = JNIUtils::JStringToString(env, device_name); + androidControllerProvider->on_key_event(deviceDescriptor, deviceName, key_code, is_pressed); +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_onAxisEvent(JNIEnv *env, jclass clazz, jstring device_descriptor, + jstring device_name, jint axis_code, + jfloat value) { + auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android); + auto androidControllerProvider = dynamic_cast(apiProvider.get()); + auto deviceDescriptor = JNIUtils::JStringToString(env, device_descriptor); + auto deviceName = JNIUtils::JStringToString(env, device_name); + androidControllerProvider->on_axis_event(deviceDescriptor, deviceName, axis_code, value); +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ButtonListAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/ButtonListAdapter.java new file mode 100644 index 00000000..f72902a1 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ButtonListAdapter.java @@ -0,0 +1,77 @@ +package info.cemu.Cemu; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +public class ButtonListAdapter extends RecyclerView.Adapter { + + public interface OnButtonClickListener { + void onButtonClicked(); + } + + private static class ButtonInfo { + public ButtonInfo(String text, String description, OnButtonClickListener onButtonClickListener) { + this.text = text; + this.description = description; + this.onButtonClickListener = onButtonClickListener; + } + + String text; + String description; + OnButtonClickListener onButtonClickListener; + } + + private final List buttonInfoList = new ArrayList<>(); + + public void addButtonInfo(String text, String description, OnButtonClickListener onButtonClickListener) { + buttonInfoList.add(new ButtonInfo(text, description, onButtonClickListener)); + notifyItemInserted(buttonInfoList.size()); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ButtonViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_button, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + ((ButtonViewHolder) holder).bind(buttonInfoList.get(position)); + holder.itemView.setOnClickListener(view -> { + ButtonInfo buttonInfo = buttonInfoList.get(holder.getAdapterPosition()); + if (buttonInfo.onButtonClickListener != null) { + buttonInfo.onButtonClickListener.onButtonClicked(); + } + }); + } + + @Override + public int getItemCount() { + return buttonInfoList.size(); + } + + private static class ButtonViewHolder extends RecyclerView.ViewHolder { + TextView text; + TextView description; + + void bind(ButtonInfo buttonInfo) { + text.setText(buttonInfo.text); + description.setText(buttonInfo.description); + } + + public ButtonViewHolder(View itemView) { + super(itemView); + text = itemView.findViewById(R.id.button_text); + description = itemView.findViewById(R.id.button_description); + } + } + +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ControllerHeaderItem.java b/src/android/app/src/main/java/info/cemu/Cemu/ControllerHeaderItem.java new file mode 100644 index 00000000..7b9ee893 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ControllerHeaderItem.java @@ -0,0 +1,22 @@ +package info.cemu.Cemu; + +public class ControllerHeaderItem implements ControllerItem { + private String header; + + public ControllerHeaderItem(String header) { + this.header = header; + } + + @Override + public int getType() { + return TYPE_HEADER; + } + + public String getHeader() { + return header; + } + + public void setHeader(String header) { + this.header = header; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputItem.java b/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputItem.java new file mode 100644 index 00000000..33ab3a6d --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputItem.java @@ -0,0 +1,34 @@ +package info.cemu.Cemu; + +public class ControllerInputItem implements ControllerItem { + private final int nameResourceId; + private final int id; + private String boundInput; + + public ControllerInputItem(int nameResourceId, int id, String boundInput) { + this.nameResourceId = nameResourceId; + this.id = id; + this.boundInput = boundInput; + } + + @Override + public int getType() { + return TYPE_INPUT; + } + + public int getNameResourceId() { + return nameResourceId; + } + + public int getId() { + return id; + } + + public String getBoundInput() { + return boundInput; + } + + public void setBoundInput(String boundInput) { + this.boundInput = boundInput; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputListener.java b/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputListener.java new file mode 100644 index 00000000..21746dfc --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputListener.java @@ -0,0 +1,41 @@ +package info.cemu.Cemu; + +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +public class ControllerInputListener { + private static boolean isController(int sources) { + return ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) || + ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) || + ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD); + } + + public static boolean onKeyEvent(KeyEvent keyEvent) { + InputDevice inputDevice = keyEvent.getDevice(); + if (isController(inputDevice.getSources())) { + String descriptor = inputDevice.getDescriptor(); + String name = inputDevice.getName(); + NativeLibrary.onNativeKey(descriptor == null ? name : descriptor, name, keyEvent.getKeyCode(), keyEvent.getAction() == KeyEvent.ACTION_DOWN); + return true; + } + return false; + } + + public static boolean onMotionEvent(MotionEvent motionEvent) { + int actionPointerIndex = motionEvent.getActionIndex(); + int action = motionEvent.getActionMasked(); + if (action == MotionEvent.ACTION_MOVE) { + InputDevice inputDevice = motionEvent.getDevice(); + String descriptor = inputDevice.getDescriptor(); + String name = inputDevice.getName(); + for (InputDevice.MotionRange range : inputDevice.getMotionRanges()) { + int axis = range.getAxis(); + float value = (motionEvent.getAxisValue(axis, actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f; + NativeLibrary.onNativeAxis(descriptor == null ? name : descriptor, name, axis, value); + } + return true; + } + return false; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputsDataProvider.java b/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputsDataProvider.java new file mode 100644 index 00000000..058763c7 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputsDataProvider.java @@ -0,0 +1,164 @@ +package info.cemu.Cemu; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ControllerInputsDataProvider { + private int getButtonResourceIdName(int controllerType, int buttonId) { + if (controllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD) { + return switch (buttonId) { + case NativeLibrary.VPAD_BUTTON_A -> R.string.button_a; + case NativeLibrary.VPAD_BUTTON_B -> R.string.button_b; + case NativeLibrary.VPAD_BUTTON_X -> R.string.button_x; + case NativeLibrary.VPAD_BUTTON_Y -> R.string.button_y; + case NativeLibrary.VPAD_BUTTON_L -> R.string.button_l; + case NativeLibrary.VPAD_BUTTON_R -> R.string.button_r; + case NativeLibrary.VPAD_BUTTON_ZL -> R.string.button_zl; + case NativeLibrary.VPAD_BUTTON_ZR -> R.string.button_zr; + case NativeLibrary.VPAD_BUTTON_PLUS -> R.string.button_plus; + case NativeLibrary.VPAD_BUTTON_MINUS -> R.string.button_minus; + case NativeLibrary.VPAD_BUTTON_UP -> R.string.button_up; + case NativeLibrary.VPAD_BUTTON_DOWN -> R.string.button_down; + case NativeLibrary.VPAD_BUTTON_LEFT -> R.string.button_left; + case NativeLibrary.VPAD_BUTTON_RIGHT -> R.string.button_right; + case NativeLibrary.VPAD_BUTTON_STICKL -> R.string.button_stickl; + case NativeLibrary.VPAD_BUTTON_STICKR -> R.string.button_stickr; + case NativeLibrary.VPAD_BUTTON_STICKL_UP -> R.string.button_stickl_up; + case NativeLibrary.VPAD_BUTTON_STICKL_DOWN -> R.string.button_stickl_down; + case NativeLibrary.VPAD_BUTTON_STICKL_LEFT -> R.string.button_stickl_left; + case NativeLibrary.VPAD_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right; + case NativeLibrary.VPAD_BUTTON_STICKR_UP -> R.string.button_stickr_up; + case NativeLibrary.VPAD_BUTTON_STICKR_DOWN -> R.string.button_stickr_down; + case NativeLibrary.VPAD_BUTTON_STICKR_LEFT -> R.string.button_stickr_left; + case NativeLibrary.VPAD_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right; + case NativeLibrary.VPAD_BUTTON_MIC -> R.string.button_mic; + case NativeLibrary.VPAD_BUTTON_SCREEN -> R.string.button_screen; + case NativeLibrary.VPAD_BUTTON_HOME -> R.string.button_home; + default -> + throw new RuntimeException("Invalid buttonId " + buttonId + " for controllerType " + controllerType); + }; + } + if (controllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_PRO) { + return switch (buttonId) { + case NativeLibrary.PRO_BUTTON_A -> R.string.button_a; + case NativeLibrary.PRO_BUTTON_B -> R.string.button_b; + case NativeLibrary.PRO_BUTTON_X -> R.string.button_x; + case NativeLibrary.PRO_BUTTON_Y -> R.string.button_y; + case NativeLibrary.PRO_BUTTON_L -> R.string.button_l; + case NativeLibrary.PRO_BUTTON_R -> R.string.button_r; + case NativeLibrary.PRO_BUTTON_ZL -> R.string.button_zl; + case NativeLibrary.PRO_BUTTON_ZR -> R.string.button_zr; + case NativeLibrary.PRO_BUTTON_PLUS -> R.string.button_plus; + case NativeLibrary.PRO_BUTTON_MINUS -> R.string.button_minus; + case NativeLibrary.PRO_BUTTON_HOME -> R.string.button_home; + case NativeLibrary.PRO_BUTTON_UP -> R.string.button_up; + case NativeLibrary.PRO_BUTTON_DOWN -> R.string.button_down; + case NativeLibrary.PRO_BUTTON_LEFT -> R.string.button_left; + case NativeLibrary.PRO_BUTTON_RIGHT -> R.string.button_right; + case NativeLibrary.PRO_BUTTON_STICKL -> R.string.button_stickl; + case NativeLibrary.PRO_BUTTON_STICKR -> R.string.button_stickr; + case NativeLibrary.PRO_BUTTON_STICKL_UP -> R.string.button_stickl_up; + case NativeLibrary.PRO_BUTTON_STICKL_DOWN -> R.string.button_stickl_down; + case NativeLibrary.PRO_BUTTON_STICKL_LEFT -> R.string.button_stickl_left; + case NativeLibrary.PRO_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right; + case NativeLibrary.PRO_BUTTON_STICKR_UP -> R.string.button_stickr_up; + case NativeLibrary.PRO_BUTTON_STICKR_DOWN -> R.string.button_stickr_down; + case NativeLibrary.PRO_BUTTON_STICKR_LEFT -> R.string.button_stickr_left; + case NativeLibrary.PRO_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right; + default -> + throw new RuntimeException("Invalid buttonId " + buttonId + " for controllerType " + controllerType); + }; + } + if (controllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_CLASSIC) { + return switch (buttonId) { + case NativeLibrary.CLASSIC_BUTTON_A -> R.string.button_a; + case NativeLibrary.CLASSIC_BUTTON_B -> R.string.button_b; + case NativeLibrary.CLASSIC_BUTTON_X -> R.string.button_x; + case NativeLibrary.CLASSIC_BUTTON_Y -> R.string.button_y; + case NativeLibrary.CLASSIC_BUTTON_L -> R.string.button_l; + case NativeLibrary.CLASSIC_BUTTON_R -> R.string.button_r; + case NativeLibrary.CLASSIC_BUTTON_ZL -> R.string.button_zl; + case NativeLibrary.CLASSIC_BUTTON_ZR -> R.string.button_zr; + case NativeLibrary.CLASSIC_BUTTON_PLUS -> R.string.button_plus; + case NativeLibrary.CLASSIC_BUTTON_MINUS -> R.string.button_minus; + case NativeLibrary.CLASSIC_BUTTON_HOME -> R.string.button_home; + case NativeLibrary.CLASSIC_BUTTON_UP -> R.string.button_up; + case NativeLibrary.CLASSIC_BUTTON_DOWN -> R.string.button_down; + case NativeLibrary.CLASSIC_BUTTON_LEFT -> R.string.button_left; + case NativeLibrary.CLASSIC_BUTTON_RIGHT -> R.string.button_right; + case NativeLibrary.CLASSIC_BUTTON_STICKL_UP -> R.string.button_stickl_up; + case NativeLibrary.CLASSIC_BUTTON_STICKL_DOWN -> R.string.button_stickl_down; + case NativeLibrary.CLASSIC_BUTTON_STICKL_LEFT -> R.string.button_stickl_left; + case NativeLibrary.CLASSIC_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right; + case NativeLibrary.CLASSIC_BUTTON_STICKR_UP -> R.string.button_stickr_up; + case NativeLibrary.CLASSIC_BUTTON_STICKR_DOWN -> R.string.button_stickr_down; + case NativeLibrary.CLASSIC_BUTTON_STICKR_LEFT -> R.string.button_stickr_left; + case NativeLibrary.CLASSIC_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right; + default -> + throw new RuntimeException("Invalid buttonId " + buttonId + " for controllerType " + controllerType); + }; + } + if (controllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE) { + return switch (buttonId) { + case NativeLibrary.WIIMOTE_BUTTON_A -> R.string.button_a; + case NativeLibrary.WIIMOTE_BUTTON_B -> R.string.button_b; + case NativeLibrary.WIIMOTE_BUTTON_1 -> R.string.button_1; + case NativeLibrary.WIIMOTE_BUTTON_2 -> R.string.button_2; + case NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_Z -> R.string.button_nunchuck_z; + case NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_C -> R.string.button_nunchuck_c; + case NativeLibrary.WIIMOTE_BUTTON_PLUS -> R.string.button_plus; + case NativeLibrary.WIIMOTE_BUTTON_MINUS -> R.string.button_minus; + case NativeLibrary.WIIMOTE_BUTTON_UP -> R.string.button_up; + case NativeLibrary.WIIMOTE_BUTTON_DOWN -> R.string.button_down; + case NativeLibrary.WIIMOTE_BUTTON_LEFT -> R.string.button_left; + case NativeLibrary.WIIMOTE_BUTTON_RIGHT -> R.string.button_right; + case NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_UP -> R.string.button_nunchuck_up; + case NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_DOWN -> R.string.button_nunchuck_down; + case NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_LEFT -> R.string.button_nunchuck_left; + case NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_RIGHT -> R.string.button_nunchuck_right; + case NativeLibrary.WIIMOTE_BUTTON_HOME -> R.string.button_home; + default -> + throw new RuntimeException("Invalid buttonId " + buttonId + " for controllerType " + controllerType); + }; + } + throw new RuntimeException("Invalid controllerType " + controllerType); + } + + private void addControllerInputsGroup(List controllerInputItems, String groupName, int controllerType, int[] buttons, Map inputs) { + controllerInputItems.add(new ControllerHeaderItem(groupName)); + for (int button : buttons) { + controllerInputItems.add(new ControllerInputItem(getButtonResourceIdName(controllerType, button), button, inputs.getOrDefault(button, ""))); + } + } + + public List getControllerInputs(int controllerType, Map inputs) { + List controllerInputItems = new ArrayList<>(); + if (controllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD) { + addControllerInputsGroup(controllerInputItems, "Buttons", controllerType, new int[]{NativeLibrary.VPAD_BUTTON_A, NativeLibrary.VPAD_BUTTON_B, NativeLibrary.VPAD_BUTTON_X, NativeLibrary.VPAD_BUTTON_Y, NativeLibrary.VPAD_BUTTON_L, NativeLibrary.VPAD_BUTTON_R, NativeLibrary.VPAD_BUTTON_ZL, NativeLibrary.VPAD_BUTTON_ZR, NativeLibrary.VPAD_BUTTON_PLUS, NativeLibrary.VPAD_BUTTON_MINUS}, inputs); + addControllerInputsGroup(controllerInputItems, "D-Pad", controllerType, new int[]{NativeLibrary.VPAD_BUTTON_UP, NativeLibrary.VPAD_BUTTON_DOWN, NativeLibrary.VPAD_BUTTON_LEFT, NativeLibrary.VPAD_BUTTON_RIGHT}, inputs); + addControllerInputsGroup(controllerInputItems, "Left Axis", controllerType, new int[]{NativeLibrary.VPAD_BUTTON_STICKL, NativeLibrary.VPAD_BUTTON_STICKL_UP, NativeLibrary.VPAD_BUTTON_STICKL_DOWN, NativeLibrary.VPAD_BUTTON_STICKL_LEFT, NativeLibrary.VPAD_BUTTON_STICKL_RIGHT}, inputs); + addControllerInputsGroup(controllerInputItems, "Right Axis", controllerType, new int[]{NativeLibrary.VPAD_BUTTON_STICKR, NativeLibrary.VPAD_BUTTON_STICKR_UP, NativeLibrary.VPAD_BUTTON_STICKR_DOWN, NativeLibrary.VPAD_BUTTON_STICKR_LEFT, NativeLibrary.VPAD_BUTTON_STICKR_RIGHT}, inputs); + addControllerInputsGroup(controllerInputItems, "Extra", controllerType, new int[]{NativeLibrary.VPAD_BUTTON_MIC, NativeLibrary.VPAD_BUTTON_SCREEN, NativeLibrary.VPAD_BUTTON_HOME}, inputs); + } + if (controllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_PRO) { + addControllerInputsGroup(controllerInputItems, "Buttons", controllerType, new int[]{NativeLibrary.PRO_BUTTON_A, NativeLibrary.PRO_BUTTON_B, NativeLibrary.PRO_BUTTON_X, NativeLibrary.PRO_BUTTON_Y, NativeLibrary.PRO_BUTTON_L, NativeLibrary.PRO_BUTTON_R, NativeLibrary.PRO_BUTTON_ZL, NativeLibrary.PRO_BUTTON_ZR, NativeLibrary.PRO_BUTTON_PLUS, NativeLibrary.PRO_BUTTON_MINUS, NativeLibrary.PRO_BUTTON_HOME}, inputs); + addControllerInputsGroup(controllerInputItems, "Left Axis", controllerType, new int[]{NativeLibrary.PRO_BUTTON_STICKL, NativeLibrary.PRO_BUTTON_STICKL_UP, NativeLibrary.PRO_BUTTON_STICKL_DOWN, NativeLibrary.PRO_BUTTON_STICKL_LEFT, NativeLibrary.PRO_BUTTON_STICKL_RIGHT}, inputs); + addControllerInputsGroup(controllerInputItems, "Right Axis", controllerType, new int[]{NativeLibrary.PRO_BUTTON_STICKR, NativeLibrary.PRO_BUTTON_STICKR_UP, NativeLibrary.PRO_BUTTON_STICKR_DOWN, NativeLibrary.PRO_BUTTON_STICKR_LEFT, NativeLibrary.PRO_BUTTON_STICKR_RIGHT}, inputs); + addControllerInputsGroup(controllerInputItems, "D-Pad", controllerType, new int[]{NativeLibrary.PRO_BUTTON_UP, NativeLibrary.PRO_BUTTON_DOWN, NativeLibrary.PRO_BUTTON_LEFT, NativeLibrary.PRO_BUTTON_RIGHT}, inputs); + } + if (controllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_CLASSIC) { + addControllerInputsGroup(controllerInputItems, "Buttons", controllerType, new int[]{NativeLibrary.CLASSIC_BUTTON_A, NativeLibrary.CLASSIC_BUTTON_B, NativeLibrary.CLASSIC_BUTTON_X, NativeLibrary.CLASSIC_BUTTON_Y, NativeLibrary.CLASSIC_BUTTON_L, NativeLibrary.CLASSIC_BUTTON_R, NativeLibrary.CLASSIC_BUTTON_ZL, NativeLibrary.CLASSIC_BUTTON_ZR, NativeLibrary.CLASSIC_BUTTON_PLUS, NativeLibrary.CLASSIC_BUTTON_MINUS, NativeLibrary.CLASSIC_BUTTON_HOME}, inputs); + addControllerInputsGroup(controllerInputItems, "Left Axis", controllerType, new int[]{NativeLibrary.CLASSIC_BUTTON_STICKL_UP, NativeLibrary.CLASSIC_BUTTON_STICKL_DOWN, NativeLibrary.CLASSIC_BUTTON_STICKL_LEFT, NativeLibrary.CLASSIC_BUTTON_STICKL_RIGHT}, inputs); + addControllerInputsGroup(controllerInputItems, "Right Axis", controllerType, new int[]{NativeLibrary.CLASSIC_BUTTON_STICKR_UP, NativeLibrary.CLASSIC_BUTTON_STICKR_DOWN, NativeLibrary.CLASSIC_BUTTON_STICKR_LEFT, NativeLibrary.CLASSIC_BUTTON_STICKR_RIGHT}, inputs); + addControllerInputsGroup(controllerInputItems, "D-Pad", controllerType, new int[]{NativeLibrary.CLASSIC_BUTTON_UP, NativeLibrary.CLASSIC_BUTTON_DOWN, NativeLibrary.CLASSIC_BUTTON_LEFT, NativeLibrary.CLASSIC_BUTTON_RIGHT}, inputs); + } + if (controllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE) { + addControllerInputsGroup(controllerInputItems, "Buttons", controllerType, new int[]{NativeLibrary.WIIMOTE_BUTTON_A, NativeLibrary.WIIMOTE_BUTTON_B, NativeLibrary.WIIMOTE_BUTTON_1, NativeLibrary.WIIMOTE_BUTTON_2, NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_Z, NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_C, NativeLibrary.WIIMOTE_BUTTON_PLUS, NativeLibrary.WIIMOTE_BUTTON_MINUS, NativeLibrary.WIIMOTE_BUTTON_HOME}, inputs); + addControllerInputsGroup(controllerInputItems, "D-Pad", controllerType, new int[]{NativeLibrary.WIIMOTE_BUTTON_UP, NativeLibrary.WIIMOTE_BUTTON_DOWN, NativeLibrary.WIIMOTE_BUTTON_LEFT, NativeLibrary.WIIMOTE_BUTTON_RIGHT}, inputs); + addControllerInputsGroup(controllerInputItems, "Nunchuck", controllerType, new int[]{NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_UP, NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_DOWN, NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_LEFT, NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_RIGHT}, inputs); + } + return controllerInputItems; + } + +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputsFragment.java new file mode 100644 index 00000000..95fcfc0d --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ControllerInputsFragment.java @@ -0,0 +1,101 @@ +package info.cemu.Cemu; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; + +import info.cemu.Cemu.databinding.FragmentControllerInputsBinding; + +import java.util.HashMap; +import java.util.Objects; + +public class ControllerInputsFragment extends Fragment { + public static final String CONTROLLER_INDEX = "ControllerIndex"; + private int controllerIndex; + private int controllerType; + private final InputManager inputManager = new InputManager(); + private EmulatedControllerTypeAdapter emulatedControllerTypeAdapter; + private ControllerListAdapter controllerListAdapter; + + private void onTypeChanged(int controllerType) { + if (this.controllerType == controllerType) return; + this.controllerType = controllerType; + NativeLibrary.setControllerType(controllerIndex, controllerType); + emulatedControllerTypeAdapter.setCurrentControllerType(controllerType); + controllerListAdapter.setEmulatedControllerListItems(controllerType, new HashMap<>()); + emulatedControllerTypeAdapter.setControllerTypeCounts(NativeLibrary.getVPADControllersCount(), NativeLibrary.getWPADControllersCount()); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + controllerIndex = requireArguments().getInt(CONTROLLER_INDEX); + ActionBar actionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar(); + if (actionBar != null) + actionBar.setTitle(getString(R.string.controller_numbered, controllerIndex + 1)); + if (NativeLibrary.isControllerDisabled(controllerIndex)) + controllerType = NativeLibrary.EMULATED_CONTROLLER_TYPE_DISABLED; + else controllerType = NativeLibrary.getControllerType(controllerIndex); + controllerListAdapter = new ControllerListAdapter(); + emulatedControllerTypeAdapter = new EmulatedControllerTypeAdapter(); + emulatedControllerTypeAdapter.setCurrentControllerType(controllerType); + emulatedControllerTypeAdapter.setControllerTypeCounts(NativeLibrary.getVPADControllersCount(), NativeLibrary.getWPADControllersCount()); + + FragmentControllerInputsBinding binding = FragmentControllerInputsBinding.inflate(inflater, container, false); + + binding.emulatedControllerSpinner.setAdapter(emulatedControllerTypeAdapter); + binding.emulatedControllerSpinner.setSelection(emulatedControllerTypeAdapter.getPosition(controllerType)); + binding.emulatedControllerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, long l) { + onTypeChanged(emulatedControllerTypeAdapter.getItem(position)); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + } + }); + + controllerListAdapter.setEmulatedControllerListItems(controllerType, NativeLibrary.getControllerMappings(controllerIndex)); + controllerListAdapter.setOnInputClickListener((nameId, id, position) -> { + MotionDialog inputDialog = new MotionDialog(getContext()); + inputDialog.setOnKeyListener((dialog, keyCode, keyEvent) -> { + if (inputManager.mapKeyEventToMappingId(controllerIndex, id, keyEvent)) { + controllerListAdapter.setBoundInput(position, NativeLibrary.getControllerMapping(controllerIndex, id)); + inputDialog.dismiss(); + } + return true; + }); + inputDialog.setOnMotionListener(motionEvent -> { + if (inputManager.mapMotionEventToMappingId(controllerIndex, id, motionEvent)) { + controllerListAdapter.setBoundInput(position, NativeLibrary.getControllerMapping(controllerIndex, id)); + inputDialog.dismiss(); + } + return true; + }); + inputDialog.setMessage(getString(R.string.inputBindingDialogMessage, getString(nameId))); + inputDialog.setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.clear), (dialogInterface, i) -> { + NativeLibrary.clearControllerMapping(controllerIndex, id); + controllerListAdapter.clearBoundInput(position); + dialogInterface.dismiss(); + }); + inputDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel), (dialogInterface, i) -> dialogInterface.dismiss()); + inputDialog.setTitle(R.string.inputBindingDialogTitle); + inputDialog.show(); + }); + binding.controllerInputsRecyclerView.setAdapter(controllerListAdapter); + + return binding.getRoot(); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ControllerItem.java b/src/android/app/src/main/java/info/cemu/Cemu/ControllerItem.java new file mode 100644 index 00000000..89879e43 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ControllerItem.java @@ -0,0 +1,8 @@ +package info.cemu.Cemu; + +public interface ControllerItem { + int TYPE_HEADER = 0; + int TYPE_INPUT = 1; + + int getType(); +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ControllerListAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/ControllerListAdapter.java new file mode 100644 index 00000000..10ebe1ab --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ControllerListAdapter.java @@ -0,0 +1,122 @@ +package info.cemu.Cemu; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ControllerListAdapter extends RecyclerView.Adapter { + private List controllerItemList = new ArrayList<>(); + private final ControllerInputsDataProvider controllerInputs = new ControllerInputsDataProvider(); + private OnInputClickListener onInputClickListener; + + + public void setEmulatedControllerListItems(int controllerType, Map inputsMapping) { + controllerItemList = controllerInputs.getControllerInputs(controllerType, inputsMapping); + notifyDataSetChanged(); + } + + public void clearBoundInput(int position) { + ControllerItem listItem = controllerItemList.get(position); + if (listItem.getType() == ControllerItem.TYPE_INPUT) { + ControllerInputItem controllerInputItem = (ControllerInputItem) listItem; + controllerInputItem.setBoundInput(""); + controllerItemList.set(position, controllerInputItem); + notifyItemChanged(position); + } + } + + public interface OnInputClickListener { + void onInputClicked(int nameId, int id, int position); + } + + void setOnInputClickListener(OnInputClickListener onInputClickListener) { + this.onInputClickListener = onInputClickListener; + } + + @Override + public int getItemViewType(int position) { + return controllerItemList.get(position).getType(); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == ControllerItem.TYPE_HEADER) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_controller_header, parent, false); + return new HeaderViewHolder(view); + } else if (viewType == ControllerItem.TYPE_INPUT) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_controller_input, parent, false); + return new InputViewHolder(view); + } + throw new RuntimeException("Invalid viewType: " + viewType); + } + + public void setBoundInput(int inputItemPosition, String boundInput) { + ControllerItem listItem = controllerItemList.get(inputItemPosition); + if (listItem.getType() == ControllerItem.TYPE_INPUT) { + ControllerInputItem controllerInputItem = (ControllerInputItem) listItem; + controllerInputItem.setBoundInput(boundInput); + controllerItemList.set(inputItemPosition, controllerInputItem); + notifyItemChanged(inputItemPosition); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof HeaderViewHolder) { + ((HeaderViewHolder) holder).bind((ControllerHeaderItem) controllerItemList.get(position)); + } else if (holder instanceof InputViewHolder inputViewHolder) { + ControllerInputItem inputListItem = (ControllerInputItem) controllerItemList.get(position); + holder.itemView.setOnClickListener(v -> { + if (onInputClickListener != null) { + onInputClickListener.onInputClicked(inputListItem.getNameResourceId(), inputListItem.getId(), holder.getAdapterPosition()); + } + }); + inputViewHolder.bind(inputListItem); + } + } + + @Override + public int getItemCount() { + return controllerItemList.size(); + } + + public static class InputViewHolder extends RecyclerView.ViewHolder { + TextView inputName; + TextView boundInput; + + void bind(ControllerInputItem inputListItem) { + inputName.setText(inputListItem.getNameResourceId()); + boundInput.setText(inputListItem.getBoundInput()); + } + + public InputViewHolder(View itemView) { + super(itemView); + inputName = itemView.findViewById(R.id.controller_input_name); + boundInput = itemView.findViewById(R.id.controller_button_name); + } + } + + public static class HeaderViewHolder extends RecyclerView.ViewHolder { + TextView header; + + void bind(ControllerHeaderItem headerListItem) { + header.setText(headerListItem.getHeader()); + } + + public HeaderViewHolder(View itemView) { + super(itemView); + header = itemView.findViewById(R.id.textview_controller_header); + } + } + +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/EmulatedControllerTypeAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/EmulatedControllerTypeAdapter.java new file mode 100644 index 00000000..e20cc034 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/EmulatedControllerTypeAdapter.java @@ -0,0 +1,71 @@ +package info.cemu.Cemu; + + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import java.util.List; + +public class EmulatedControllerTypeAdapter extends BaseAdapter { + private final List controllerTypes = List.of(NativeLibrary.EMULATED_CONTROLLER_TYPE_DISABLED, NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD, NativeLibrary.EMULATED_CONTROLLER_TYPE_PRO, NativeLibrary.EMULATED_CONTROLLER_TYPE_CLASSIC, NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE); + private int vpadCount = 0; + private int wpadCount = 0; + private int currentControllerType = NativeLibrary.EMULATED_CONTROLLER_TYPE_DISABLED; + + public void setCurrentControllerType(int currentControllerType) { + this.currentControllerType = currentControllerType; + notifyDataSetChanged(); + } + + public void setControllerTypeCounts(int vpadCount, int wpadCount) { + this.vpadCount = vpadCount; + this.wpadCount = wpadCount; + notifyDataSetChanged(); + } + + @Override + public boolean isEnabled(int position) { + int type = controllerTypes.get(position); + if (type == NativeLibrary.EMULATED_CONTROLLER_TYPE_DISABLED) + return true; + if (type == NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD) + return currentControllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD || vpadCount < NativeLibrary.MAX_VPAD_CONTROLLERS; + boolean isWPAD = currentControllerType != NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD && currentControllerType != NativeLibrary.EMULATED_CONTROLLER_TYPE_DISABLED; + return isWPAD || wpadCount < NativeLibrary.MAX_WPAD_CONTROLLERS; + } + + @Override + public int getCount() { + return controllerTypes.size(); + } + + @Override + public Integer getItem(int position) { + return controllerTypes.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View view, ViewGroup viewGroup) { + int type = controllerTypes.get(position); + if (view == null) + view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.layout_choice, viewGroup, false); + TextView textView = view.findViewById(R.id.textview_choice); + textView.setText(NativeLibrary.controllerTypeToResourceNameId(type)); + float DISABLED_ALPHA = 0.6f; + if (!isEnabled(position)) + view.setAlpha(DISABLED_ALPHA); + return view; + } + + public int getPosition(int controllerType) { + return controllerTypes.indexOf(controllerType); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/EmulationActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/EmulationActivity.java index 91392daf..d0a5e1a5 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/EmulationActivity.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/EmulationActivity.java @@ -9,6 +9,10 @@ import androidx.core.view.WindowInsetsControllerCompat; import android.content.DialogInterface; import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import info.cemu.Cemu.databinding.ActivityEmulationBinding; import info.cemu.Cemu.databinding.ActivityMainBinding; @@ -16,9 +20,24 @@ import info.cemu.Cemu.databinding.ActivityMainBinding; public class EmulationActivity extends AppCompatActivity { public static final String GAME_TITLE_ID = "GameTitleId"; - long gameTitleId; - ActivityEmulationBinding binding; - EmulationFragment emulationFragment; + private long gameTitleId; + private ActivityEmulationBinding binding; + private EmulationFragment emulationFragment; + private final InputManager inputManager = new InputManager(); + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if (inputManager.onMotionEvent(event)) + return true; + return super.onGenericMotionEvent(event); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (inputManager.onKeyEvent(event)) + return true; + return super.dispatchKeyEvent(event); + } @Override protected void onCreate(Bundle savedInstanceState) { @@ -32,7 +51,7 @@ public class EmulationActivity extends AppCompatActivity { setContentView(binding.getRoot()); emulationFragment = (EmulationFragment) getSupportFragmentManager().findFragmentById(R.id.emulation_frame); if (emulationFragment == null) { - emulationFragment = EmulationFragment.newInstance(gameTitleId); + emulationFragment = new EmulationFragment(gameTitleId); getSupportFragmentManager() .beginTransaction() .add(R.id.emulation_frame, emulationFragment) @@ -46,19 +65,18 @@ public class EmulationActivity extends AppCompatActivity { } private void showExitConfirmationDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("Exit Confirmation"); - builder.setMessage("Are you sure you want to exit?"); - builder.setPositiveButton("Yes", (dialog, which) -> { - finishAffinity(); - System.exit(0); - }); - builder.setNegativeButton("No", (dialog, which) -> dialog.cancel()); - AlertDialog dialog = builder.create(); - dialog.show(); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.exit_confirmation_title) + .setMessage(R.string.exit_confirm_message) + .setPositiveButton(R.string.yes, (dialog, which) -> { + finishAffinity(); + System.exit(0); + }).setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) + .create() + .show(); } - void setFullscreen() { + private void setFullscreen() { WindowCompat.setDecorFitsSystemWindows(getWindow(), false); WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView()); controller.hide(WindowInsetsCompat.Type.systemBars()); diff --git a/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java index 19a0adb9..ed81ef3e 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java @@ -1,6 +1,7 @@ package info.cemu.Cemu; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -13,35 +14,17 @@ import androidx.fragment.app.Fragment; import info.cemu.Cemu.databinding.FragmentEmulationBinding; public class EmulationFragment extends Fragment implements SurfaceHolder.Callback { - FragmentEmulationBinding binding; - - public EmulationFragment() { - } - - static EmulationFragment newInstance(long gameTitleId) { - EmulationFragment emulationFragment = new EmulationFragment(); - Bundle arguments = new Bundle(); - arguments.putLong(EmulationActivity.GAME_TITLE_ID, gameTitleId); - emulationFragment.setArguments(arguments); - return emulationFragment; - } - - long gameTitleId; - boolean isGameRunning; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Bundle arguments = getArguments(); - if (arguments != null) { - gameTitleId = arguments.getLong(EmulationActivity.GAME_TITLE_ID); - } + private final long gameTitleId; + private boolean isGameRunning; + private SurfaceHolder mainCanvasSurfaceHolder; + public EmulationFragment(long gameTitleId) { + this.gameTitleId = gameTitleId; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentEmulationBinding.inflate(inflater, container, false); + FragmentEmulationBinding binding = FragmentEmulationBinding.inflate(inflater, container, false); SurfaceView mainCanvas = binding.mainCanvas; mainCanvas.getHolder().addCallback(this); return binding.getRoot(); @@ -49,25 +32,29 @@ public class EmulationFragment extends Fragment implements SurfaceHolder.Callbac @Override public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { - + mainCanvasSurfaceHolder = surfaceHolder; } @Override public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int format, int width, int height) { - NativeLibrary.setSurface(surfaceHolder.getSurface(), true); NativeLibrary.setSurfaceSize(width, height, true); if (!isGameRunning) { isGameRunning = true; + NativeLibrary.setSurface(surfaceHolder.getSurface(), true); NativeLibrary.initializerRenderer(); NativeLibrary.initializeRendererSurface(true); NativeLibrary.startGame(gameTitleId); - }else{ + } else { + if (mainCanvasSurfaceHolder == surfaceHolder) + return; + NativeLibrary.setSurface(surfaceHolder.getSurface(), true); NativeLibrary.recreateRenderSurface(true); } } @Override public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) { + mainCanvasSurfaceHolder = null; NativeLibrary.clearSurface(true); } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/InputManager.java b/src/android/app/src/main/java/info/cemu/Cemu/InputManager.java new file mode 100644 index 00000000..8c34daee --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/InputManager.java @@ -0,0 +1,122 @@ +package info.cemu.Cemu; + +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +public class InputManager { + private static class InvalidAxisException extends Exception { + public InvalidAxisException(int axis) { + super("Invalid axis " + axis); + } + } + + private int getNativeAxisKey(int axis, boolean isPositive) throws InvalidAxisException { + if (isPositive) { + return switch (axis) { + case MotionEvent.AXIS_X -> NativeLibrary.AXIS_X_POS; + case MotionEvent.AXIS_Y -> NativeLibrary.AXIS_Y_POS; + case MotionEvent.AXIS_RX, MotionEvent.AXIS_Z -> NativeLibrary.ROTATION_X_POS; + case MotionEvent.AXIS_RY, MotionEvent.AXIS_RZ -> NativeLibrary.ROTATION_Y_POS; + case MotionEvent.AXIS_LTRIGGER -> NativeLibrary.TRIGGER_X_POS; + case MotionEvent.AXIS_RTRIGGER -> NativeLibrary.TRIGGER_Y_POS; + case MotionEvent.AXIS_HAT_X -> NativeLibrary.DPAD_RIGHT; + case MotionEvent.AXIS_HAT_Y -> NativeLibrary.DPAD_DOWN; + default -> throw new InvalidAxisException(axis); + }; + } else { + return switch (axis) { + case MotionEvent.AXIS_X -> NativeLibrary.AXIS_X_NEG; + case MotionEvent.AXIS_Y -> NativeLibrary.AXIS_Y_NEG; + case MotionEvent.AXIS_RX, MotionEvent.AXIS_Z -> NativeLibrary.ROTATION_X_NEG; + case MotionEvent.AXIS_RY, MotionEvent.AXIS_RZ -> NativeLibrary.ROTATION_Y_NEG; + case MotionEvent.AXIS_HAT_X -> NativeLibrary.DPAD_LEFT; + case MotionEvent.AXIS_HAT_Y -> NativeLibrary.DPAD_UP; + default -> throw new InvalidAxisException(axis); + }; + } + } + + private boolean isMotionEventFromJoystick(MotionEvent event) { + return (event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE; + } + + public boolean mapMotionEventToMappingId(int controllerIndex, int mappingId, MotionEvent event) { + if (isMotionEventFromJoystick(event)) { + InputDevice device = event.getDevice(); + float maxAbsAxisValue = 0.0f; + int maxAxis = -1; + int actionPointerIndex = event.getActionIndex(); + for (InputDevice.MotionRange motionRange : device.getMotionRanges()) { + float axisValue = event.getAxisValue(motionRange.getAxis(), actionPointerIndex); + int axis; + try { + axis = getNativeAxisKey(motionRange.getAxis(), axisValue > 0); + } catch (InvalidAxisException e) { + continue; + } + if (Math.abs(axisValue) > maxAbsAxisValue) { + maxAxis = axis; + maxAbsAxisValue = Math.abs(axisValue); + } + } + float MIN_ABS_AXIS_VALUE = 0.33f; + if (maxAbsAxisValue > MIN_ABS_AXIS_VALUE) { + NativeLibrary.setControllerMapping(device.getDescriptor(), device.getName(), controllerIndex, mappingId, maxAxis); + return true; + } + } + return false; + } + + private boolean isSpecialKey(KeyEvent event) { + int keyCode = event.getKeyCode(); + return keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_UP || + keyCode == KeyEvent.KEYCODE_CAMERA || + keyCode == KeyEvent.KEYCODE_ZOOM_IN || + keyCode == KeyEvent.KEYCODE_ZOOM_OUT; + } + + private boolean isController(InputDevice inputDevice) { + + int sources = inputDevice.getSources(); + return (sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 || + ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) || + ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD); + } + + public boolean onKeyEvent(KeyEvent event) { + if (isSpecialKey(event)) + return false; + if (event.getDeviceId() < 0) + return false; + InputDevice device = event.getDevice(); + if (!isController(device)) + return false; + NativeLibrary.onNativeKey(device.getDescriptor(), device.getName(), event.getKeyCode(), event.getAction() == KeyEvent.ACTION_DOWN); + return true; + } + + public boolean onMotionEvent(MotionEvent event) { + if (!isMotionEventFromJoystick(event)) + return false; + InputDevice device = event.getDevice(); + int actionPointerIndex = event.getActionIndex(); + for (InputDevice.MotionRange motionRange : device.getMotionRanges()) { + float axisValue = event.getAxisValue(motionRange.getAxis(), actionPointerIndex); + int axis = motionRange.getAxis(); + NativeLibrary.onNativeAxis(device.getDescriptor(), device.getName(), axis, axisValue); + Log.i(getClass().getName(), "Axis: " + axis + " value: " + axisValue); + } + return true; + } + + public boolean mapKeyEventToMappingId(int controllerIndex, int mappingId, KeyEvent event) { + Log.i(getClass().getName(), event.toString()); + InputDevice device = event.getDevice(); + NativeLibrary.setControllerMapping(device.getDescriptor(), device.getName(), controllerIndex, mappingId, event.getKeyCode()); + return true; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/InputSettingsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/InputSettingsFragment.java new file mode 100644 index 00000000..2d5746ed --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/InputSettingsFragment.java @@ -0,0 +1,37 @@ +package info.cemu.Cemu; + + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + +import info.cemu.Cemu.databinding.FragmentInputSettingsBinding; + +public class InputSettingsFragment extends Fragment { + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + + FragmentInputSettingsBinding binding = FragmentInputSettingsBinding.inflate(inflater, container, false); + ButtonListAdapter buttonListAdapter = new ButtonListAdapter(); + for (int index = 0; index < NativeLibrary.MAX_CONTROLLERS; index++) { + int controllerIndex = index; + String controllerType = getString(NativeLibrary.controllerTypeToResourceNameId(NativeLibrary.getControllerType(controllerIndex))); + buttonListAdapter.addButtonInfo(getString(R.string.controller_numbered, controllerIndex + 1), getString(R.string.emulated_controller_with_type, controllerType), + () -> { + Bundle bundle = new Bundle(); + bundle.putInt(ControllerInputsFragment.CONTROLLER_INDEX, controllerIndex); + NavHostFragment.findNavController(InputSettingsFragment.this).navigate(R.id.action_inputSettingsFragment_to_controllerInputsFragment, bundle); + }); + } + binding.inputSettingsRecyclerView.setAdapter(buttonListAdapter); + return binding.getRoot(); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/MotionDialog.java b/src/android/app/src/main/java/info/cemu/Cemu/MotionDialog.java new file mode 100644 index 00000000..a20bc4c4 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/MotionDialog.java @@ -0,0 +1,33 @@ +package info.cemu.Cemu; + +import android.app.AlertDialog; +import android.content.Context; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; + +public class MotionDialog extends AlertDialog { + public static interface OnMotionListener { + boolean onMotion(MotionEvent ev); + } + + private OnMotionListener onMotionListener; + + protected MotionDialog(Context context) { + super(context); + } + + public void setOnMotionListener(OnMotionListener onMotionListener) { + this.onMotionListener = onMotionListener; + } + + @Override + public boolean onGenericMotionEvent(@NonNull MotionEvent event) { + if (onMotionListener != null && onMotionListener.onMotion(event)) { + return true; + } + return super.onGenericMotionEvent(event); + } +} + diff --git a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java index 2654ffb3..849f7a9e 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java @@ -1,8 +1,10 @@ package info.cemu.Cemu; +import android.view.KeyEvent; import android.view.Surface; import java.util.ArrayList; +import java.util.Map; public class NativeLibrary { static { @@ -61,4 +63,173 @@ public class NativeLibrary { public static native void initializeEmulation(); public static native void addGamePath(String uri); + + public static native void onNativeKey(String deviceDescriptor, String deviceName, int key, boolean isPressed); + + public static native void onNativeAxis(String deviceDescriptor, String deviceName, int axis, float value); + + static final int VPAD_BUTTON_NONE = 0; + static final int VPAD_BUTTON_A = 1; + static final int VPAD_BUTTON_B = 2; + static final int VPAD_BUTTON_X = 3; + static final int VPAD_BUTTON_Y = 4; + static final int VPAD_BUTTON_L = 5; + static final int VPAD_BUTTON_R = 6; + static final int VPAD_BUTTON_ZL = 7; + static final int VPAD_BUTTON_ZR = 8; + static final int VPAD_BUTTON_PLUS = 9; + static final int VPAD_BUTTON_MINUS = 10; + static final int VPAD_BUTTON_UP = 11; + static final int VPAD_BUTTON_DOWN = 12; + static final int VPAD_BUTTON_LEFT = 13; + static final int VPAD_BUTTON_RIGHT = 14; + static final int VPAD_BUTTON_STICKL = 15; + static final int VPAD_BUTTON_STICKR = 16; + static final int VPAD_BUTTON_STICKL_UP = 17; + static final int VPAD_BUTTON_STICKL_DOWN = 18; + static final int VPAD_BUTTON_STICKL_LEFT = 19; + static final int VPAD_BUTTON_STICKL_RIGHT = 20; + static final int VPAD_BUTTON_STICKR_UP = 21; + static final int VPAD_BUTTON_STICKR_DOWN = 22; + static final int VPAD_BUTTON_STICKR_LEFT = 23; + static final int VPAD_BUTTON_STICKR_RIGHT = 24; + static final int VPAD_BUTTON_MIC = 25; + static final int VPAD_BUTTON_SCREEN = 26; + static final int VPAD_BUTTON_HOME = 27; + static final int VPAD_BUTTON_MAX = 28; + + static final int PRO_BUTTON_NONE = 0; + static final int PRO_BUTTON_A = 1; + static final int PRO_BUTTON_B = 2; + static final int PRO_BUTTON_X = 3; + static final int PRO_BUTTON_Y = 4; + static final int PRO_BUTTON_L = 5; + static final int PRO_BUTTON_R = 6; + static final int PRO_BUTTON_ZL = 7; + static final int PRO_BUTTON_ZR = 8; + static final int PRO_BUTTON_PLUS = 9; + static final int PRO_BUTTON_MINUS = 10; + static final int PRO_BUTTON_HOME = 11; + static final int PRO_BUTTON_UP = 12; + static final int PRO_BUTTON_DOWN = 13; + static final int PRO_BUTTON_LEFT = 14; + static final int PRO_BUTTON_RIGHT = 15; + static final int PRO_BUTTON_STICKL = 16; + static final int PRO_BUTTON_STICKR = 17; + static final int PRO_BUTTON_STICKL_UP = 18; + static final int PRO_BUTTON_STICKL_DOWN = 19; + static final int PRO_BUTTON_STICKL_LEFT = 20; + static final int PRO_BUTTON_STICKL_RIGHT = 21; + static final int PRO_BUTTON_STICKR_UP = 22; + static final int PRO_BUTTON_STICKR_DOWN = 23; + static final int PRO_BUTTON_STICKR_LEFT = 24; + static final int PRO_BUTTON_STICKR_RIGHT = 25; + static final int PRO_BUTTON_MAX = 26; + + static final int CLASSIC_BUTTON_NONE = 0; + static final int CLASSIC_BUTTON_A = 1; + static final int CLASSIC_BUTTON_B = 2; + static final int CLASSIC_BUTTON_X = 3; + static final int CLASSIC_BUTTON_Y = 4; + static final int CLASSIC_BUTTON_L = 5; + static final int CLASSIC_BUTTON_R = 6; + static final int CLASSIC_BUTTON_ZL = 7; + static final int CLASSIC_BUTTON_ZR = 8; + static final int CLASSIC_BUTTON_PLUS = 9; + static final int CLASSIC_BUTTON_MINUS = 10; + static final int CLASSIC_BUTTON_HOME = 11; + static final int CLASSIC_BUTTON_UP = 12; + static final int CLASSIC_BUTTON_DOWN = 13; + static final int CLASSIC_BUTTON_LEFT = 14; + static final int CLASSIC_BUTTON_RIGHT = 15; + static final int CLASSIC_BUTTON_STICKL_UP = 16; + static final int CLASSIC_BUTTON_STICKL_DOWN = 17; + static final int CLASSIC_BUTTON_STICKL_LEFT = 18; + static final int CLASSIC_BUTTON_STICKL_RIGHT = 19; + static final int CLASSIC_BUTTON_STICKR_UP = 20; + static final int CLASSIC_BUTTON_STICKR_DOWN = 21; + static final int CLASSIC_BUTTON_STICKR_LEFT = 22; + static final int CLASSIC_BUTTON_STICKR_RIGHT = 23; + static final int CLASSIC_BUTTON_MAX = 24; + + static final int WIIMOTE_BUTTON_NONE = 0; + static final int WIIMOTE_BUTTON_A = 1; + static final int WIIMOTE_BUTTON_B = 2; + static final int WIIMOTE_BUTTON_1 = 3; + static final int WIIMOTE_BUTTON_2 = 4; + static final int WIIMOTE_BUTTON_NUNCHUCK_Z = 5; + static final int WIIMOTE_BUTTON_NUNCHUCK_C = 6; + static final int WIIMOTE_BUTTON_PLUS = 7; + static final int WIIMOTE_BUTTON_MINUS = 8; + static final int WIIMOTE_BUTTON_UP = 9; + static final int WIIMOTE_BUTTON_DOWN = 10; + static final int WIIMOTE_BUTTON_LEFT = 11; + static final int WIIMOTE_BUTTON_RIGHT = 12; + static final int WIIMOTE_BUTTON_NUNCHUCK_UP = 13; + static final int WIIMOTE_BUTTON_NUNCHUCK_DOWN = 14; + static final int WIIMOTE_BUTTON_NUNCHUCK_LEFT = 15; + static final int WIIMOTE_BUTTON_NUNCHUCK_RIGHT = 16; + static final int WIIMOTE_BUTTON_HOME = 17; + static final int WIIMOTE_BUTTON_MAX = 18; + + static final int EMULATED_CONTROLLER_TYPE_VPAD = 0; + static final int EMULATED_CONTROLLER_TYPE_PRO = 1; + static final int EMULATED_CONTROLLER_TYPE_CLASSIC = 2; + static final int EMULATED_CONTROLLER_TYPE_WIIMOTE = 3; + static final int EMULATED_CONTROLLER_TYPE_DISABLED = -1; + + static final int DPAD_UP = 34; + static final int DPAD_DOWN = 35; + static final int DPAD_LEFT = 36; + static final int DPAD_RIGHT = 37; + static final int AXIS_X_POS = 38; + static final int AXIS_Y_POS = 39; + static final int ROTATION_X_POS = 40; + static final int ROTATION_Y_POS = 41; + static final int TRIGGER_X_POS = 42; + static final int TRIGGER_Y_POS = 43; + static final int AXIS_X_NEG = 44; + static final int AXIS_Y_NEG = 45; + static final int ROTATION_X_NEG = 46; + static final int ROTATION_Y_NEG = 47; + + static final int MAX_CONTROLLERS = 8; + static final int MAX_VPAD_CONTROLLERS = 2; + static final int MAX_WPAD_CONTROLLERS = 7; + + public static int controllerTypeToResourceNameId(int type) { + if (type == NativeLibrary.EMULATED_CONTROLLER_TYPE_DISABLED) + return R.string.disabled; + else if (type == NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD) + return R.string.vpad_controller; + else if (type == NativeLibrary.EMULATED_CONTROLLER_TYPE_PRO) + return R.string.pro_controller; + else if (type == NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE) + return R.string.wiimote_controller; + else if (type == NativeLibrary.EMULATED_CONTROLLER_TYPE_CLASSIC) + return R.string.classic_controller; + throw new RuntimeException("Invalid controller type: " + type); + } + + public static native void setControllerType(int index, int emulatedControllerType); + + public static native boolean isControllerDisabled(int index); + + public static native int getControllerType(int index); + + public static native int getWPADControllersCount(); + + public static native int getVPADControllersCount(); + + public static native void setControllerMapping(String deviceDescriptor, String deviceName, int index, int mappingId, int buttonId); + + public static native void clearControllerMapping(int index, int mappingId); + + public static native String getControllerMapping(int index, int mappingId); + + public static native Map getControllerMappings(int index); + + public static native void onKeyEvent(String deviceDescriptor, String deviceName, int keyCode, boolean isPressed); + + public static native void onAxisEvent(String deviceDescriptor, String deviceName, int axisCode, float value); } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java index 2c935d9d..c28c595f 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java @@ -1,35 +1,35 @@ package info.cemu.Cemu; -import androidx.appcompat.app.AppCompatActivity; - -import android.content.Intent; import android.os.Bundle; -import java.util.Objects; +import androidx.appcompat.app.AppCompatActivity; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.AppBarConfiguration; +import androidx.navigation.ui.NavigationUI; import info.cemu.Cemu.databinding.ActivitySettingsBinding; public class SettingsActivity extends AppCompatActivity { - + private AppBarConfiguration appBarConfiguration; private ActivitySettingsBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - binding = ActivitySettingsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbarSettings); - Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_settings); + appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build(); + NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); } @Override public boolean onSupportNavigateUp() { - if (getSupportFragmentManager().getBackStackEntryCount() > 0) { - getSupportFragmentManager().popBackStack(); - } else { - finish(); - } - return true; + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_settings); + return NavigationUI.navigateUp(navController, appBarConfiguration) + || super.onSupportNavigateUp(); } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/SettingsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/SettingsFragment.java index e9bf7043..578eea7f 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/SettingsFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/SettingsFragment.java @@ -62,7 +62,7 @@ public class SettingsFragment extends Fragment { View headerLayout = getLayoutInflater().inflate(R.layout.settings_header, null); binding.settingsLayout.addView(headerLayout); SettingsHeaderBinding headerBinding = SettingsHeaderBinding.bind(headerLayout); - headerBinding.textviewHeaderName.setText(title); + headerBinding.settingsTextviewHeaderName.setText(title); } protected void addSpinner(String title, List items, int initialPosition, OnItemSelectedPositionListener itemSelectedPositionListener) { diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml index 81b281d7..c8f1f585 100644 --- a/src/android/app/src/main/res/layout/activity_settings.xml +++ b/src/android/app/src/main/res/layout/activity_settings.xml @@ -1,32 +1,40 @@ - - + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> - + android:layout_height="?attr/actionBarSize" /> + - - + - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_controller_inputs.xml b/src/android/app/src/main/res/layout/fragment_controller_inputs.xml new file mode 100644 index 00000000..26028267 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_controller_inputs.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_input_settings.xml b/src/android/app/src/main/res/layout/fragment_input_settings.xml new file mode 100644 index 00000000..fe660172 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_input_settings.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_button.xml b/src/android/app/src/main/res/layout/layout_button.xml new file mode 100644 index 00000000..4ef6dbfd --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_button.xml @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout/layout_choice.xml b/src/android/app/src/main/res/layout/layout_choice.xml new file mode 100644 index 00000000..605b86e8 --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_choice.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_controller_header.xml b/src/android/app/src/main/res/layout/layout_controller_header.xml new file mode 100644 index 00000000..6a2809b2 --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_controller_header.xml @@ -0,0 +1,9 @@ + diff --git a/src/android/app/src/main/res/layout/layout_controller_input.xml b/src/android/app/src/main/res/layout/layout_controller_input.xml new file mode 100644 index 00000000..455aef79 --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_controller_input.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/src/android/app/src/main/res/layout/settings_header.xml b/src/android/app/src/main/res/layout/settings_header.xml index b541bfc4..4799ea07 100644 --- a/src/android/app/src/main/res/layout/settings_header.xml +++ b/src/android/app/src/main/res/layout/settings_header.xml @@ -4,7 +4,7 @@ android:orientation="vertical"> diff --git a/src/android/app/src/main/res/navigation/nav_settings_graph.xml b/src/android/app/src/main/res/navigation/nav_settings_graph.xml index e7197238..5028a870 100644 --- a/src/android/app/src/main/res/navigation/nav_settings_graph.xml +++ b/src/android/app/src/main/res/navigation/nav_settings_graph.xml @@ -3,11 +3,18 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_settings_graph" - app:startDestination="@id/settingsFragment"> - + app:startDestination="@id/inputSettingsFragment"> + android:id="@+id/inputSettingsFragment" + android:name="info.cemu.Cemu.InputSettingsFragment" + android:label="@string/input_settings" + tools:layout="@layout/fragment_input_settings" > + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/values-night/themes.xml b/src/android/app/src/main/res/values-night/themes.xml index d25b0f2f..642d9d7c 100644 --- a/src/android/app/src/main/res/values-night/themes.xml +++ b/src/android/app/src/main/res/values-night/themes.xml @@ -1,7 +1,7 @@ - - \ No newline at end of file diff --git a/src/android/app/src/main/res/values/refs.xml b/src/android/app/src/main/res/values/refs.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/src/android/app/src/main/res/values/refs.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 57b78d8a..bd652bfa 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -1,50 +1,62 @@ Cemu Settings - - First Fragment - Second Fragment - Next - Previous - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris - volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus - dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad - litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend - diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a, - ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n - Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus - egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed - neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada - fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae, - molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor - bibendum, vel congue leo egestas.\n\n - Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit - amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel, - molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer - interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at - lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula, - in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque - est.\n\n - Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh. - Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui - non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In - eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc, - quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra - ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a - placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus - convallis.\n\n - Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et - malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa - gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper, - libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper - sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus - libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus - vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim. - Settings - - Hello blank fragment Game Icon + Select controller + Controller\'s settings + Emulated controller + Clear + Disabled + Wii U GamePad + Wii U Pro Controller + Wiimote + Wii U Classic Controller + A + B + X + Y + L + R + ZL + ZR + Plus + Minus + Up + Down + Left + Right + Click + Up + Down + Left + Right + Click + Up + Down + Left + Right + Blow Mic + Show Screen + Home + 1 + 2 + Z + C + Up + Down + Left + Right + Input binding + Trigger an input to bind it to %1$s + Clear + Cancel + Controller %1d + Input Settings + Emulated controller: + Emulated controller: %1s + Are you sure you want to exit? + Exit Confirmation + Yes + No \ No newline at end of file diff --git a/src/android/app/src/main/res/values/themes.xml b/src/android/app/src/main/res/values/themes.xml index 2d664e10..4356ebeb 100644 --- a/src/android/app/src/main/res/values/themes.xml +++ b/src/android/app/src/main/res/values/themes.xml @@ -1,8 +1,8 @@ - -