diff --git a/.gitmodules b/.gitmodules index 90edb5822..11845060a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,7 @@ [submodule "client/3rd/QSimpleCrypto"] path = client/3rd/QSimpleCrypto url = https://github.com/amnezia-vpn/QSimpleCrypto.git +[submodule "client/3rd/qtgamepad"] + path = client/3rd/qtgamepad + url = https://github.com/amnezia-vpn/qtgamepad.git + branch = 6.6 diff --git a/client/3rd/qtgamepad b/client/3rd/qtgamepad new file mode 160000 index 000000000..f72b3e0c6 --- /dev/null +++ b/client/3rd/qtgamepad @@ -0,0 +1 @@ +Subproject commit f72b3e0c6229d1622b6bc7f6a5ec5ccff83460eb diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 3a9ffb27a..d5c5d3ad1 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -39,6 +39,11 @@ endif() find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES}) +# Android: Qt private modules (like CorePrivate) are needed by qtgamepad +if(ANDROID) + find_package(Qt6CorePrivate CONFIG REQUIRED) +endif() + set(LIBS ${LIBS} Qt6::Core Qt6::Gui Qt6::Network Qt6::Xml Qt6::RemoteObjects @@ -228,4 +233,13 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE) endif() target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC}) -qt_finalize_target(${PROJECT}) + +# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this). +if(COMMAND qt_import_qml_plugins) + qt_import_qml_plugins(${PROJECT}) +endif() +if(COMMAND qt_finalize_executable) + qt_finalize_executable(${PROJECT}) +else() + qt_finalize_target(${PROJECT}) +endif() diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index ca61ae798..6a44eaa52 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -26,6 +26,8 @@ import android.os.ParcelFileDescriptor import android.os.SystemClock import android.provider.OpenableColumns import android.provider.Settings +import android.view.InputDevice +import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -274,6 +276,44 @@ class AmneziaActivity : QtActivity() { Log.d(TAG, "Window focus changed: hasFocus=$hasFocus") } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val deviceId = event.deviceId + val keyCode = event.keyCode + val pressed = event.action == KeyEvent.ACTION_DOWN + val source = event.source + + if (deviceId < 0 && pressed) { + when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_DPAD_CENTER -> { + nativeGamepadKeyEvent(0, keyCode, true) + nativeGamepadKeyEvent(0, keyCode, false) + return true + } + } + } + + // Real gamepad events (deviceId >= 0) + if (deviceId >= 0) { + val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD + val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK + val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD + if (isGamepad || isJoystick || isDpad) { + nativeGamepadKeyEvent(deviceId, keyCode, pressed) + return true + } + } + + return super.dispatchKeyEvent(event) + } + + private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean) + override fun onPause() { super.onPause() Log.d(TAG, "Pause Amnezia activity") diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index 4272290d7..179240afa 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -83,6 +83,19 @@ add_compile_definitions(_WINSOCKAPI_) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(BUILD_WITH_QT6 ON) add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain) + +if(ANDROID) + # Use qtgamepad from amnezia-vpn/qtgamepad repository + add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad) + # Link both the C++ module and QML plugin + if(TARGET GamepadLegacy) + target_link_libraries(${PROJECT} PRIVATE GamepadLegacy) + endif() + if(TARGET GamepadLegacyQuickPrivate) + target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate) + endif() +endif() + set(LIBS ${LIBS} qt6keychain) include_directories( diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index a137559ad..6a6c0891d 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -6,6 +6,7 @@ import QtQuick.Dialogs import PageEnum 1.0 import Style 1.0 +import QtGamepadLegacy import "Config" import "Controls2" @@ -83,6 +84,44 @@ Window { } } + + + Loader { + active: Qt.platform.os === "android" + sourceComponent: Component { + Item { + Gamepad { + id: gamepad + deviceId: GamepadManager.connectedGamepads.length > 0 ? GamepadManager.connectedGamepads[0] : -1 + + onButtonStartChanged: { + if (buttonStart) { + ServersModel.setProcessedServerIndex(ServersModel.defaultIndex) + ConnectionController.connectButtonClicked() + } + } + } + + GamepadKeyNavigation { + id: gamepadKeyNav + gamepad: gamepad + active: true + } + + Connections { + target: GamepadManager + function onConnectedGamepadsChanged() { + if (GamepadManager.connectedGamepads.length > 0) { + gamepad.deviceId = GamepadManager.connectedGamepads[0] + } else { + gamepad.deviceId = -1 + } + } + } + } + } + } + Connections { objectName: "pageControllerConnections"