From a7975fe55d735a02baba4691465e875e44e48d18 Mon Sep 17 00:00:00 2001 From: exelix11 <13405476+exelix11@users.noreply.github.com> Date: Sun, 27 Aug 2023 01:14:45 +0200 Subject: [PATCH 001/125] Experimental new client GUI --- Client/Client.csproj | 15 +- Client/Core/DebugHelpers.cs | 37 + Client/{ => Core}/DebugOptions.cs | 18 +- Client/Core/ErrorCodeExtensions.cs | 48 + Client/Core/Log.cs | 52 + Client/Core/Native.cs | 27 + Client/Core/StreamInfo.cs | 26 + Client/Core/StreamManager.cs | 180 + Client/Core/StreamThread.cs | 262 + Client/DebugHelpers.cs | 37 - Client/FileOutput/LoggingTarget.cs | 61 - Client/FileOutput/Mp4Output.cs | 173 - Client/GUI/Image.cs | 84 + Client/GUI/Interfaces.cs | 68 + Client/GUI/MainView.cs | 162 + Client/Help.txt | 25 - Client/Platform/Android/.gitignore | 40 + Client/Platform/Android/app/build.gradle | 76 + Client/Platform/Android/app/jni/Android.mk | 1 + .../Platform/Android/app/jni/Application.mk | 10 + .../Platform/Android/app/jni/CMakeLists.txt | 20 + .../Android/app/jni/Client/Android.mk | 11 + .../Platform/Android/app/jni/SDL/Android.mk | 130 + .../Android/app/jni/cimgui/Android.mk | 32 + .../Platform/Android/app/jni/src/Android.mk | 17 + Client/Platform/Android/app/jni/src/main.c | 30 + .../Platform/Android/app/proguard-rules.pro | 17 + .../Android/app/src/main/AndroidManifest.xml | 100 + .../java/exelix11/sysdvr/sysdvrActivity.java | 7 + .../main/java/org/libsdl/app/HIDDevice.java | 22 + .../app/HIDDeviceBLESteamController.java | 650 + .../java/org/libsdl/app/HIDDeviceManager.java | 683 + .../java/org/libsdl/app/HIDDeviceUSB.java | 309 + .../app/src/main/java/org/libsdl/app/SDL.java | 86 + .../main/java/org/libsdl/app/SDLActivity.java | 2119 +++ .../java/org/libsdl/app/SDLAudioManager.java | 514 + .../org/libsdl/app/SDLControllerManager.java | 854 + .../main/java/org/libsdl/app/SDLSurface.java | 405 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2683 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1698 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3872 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6874 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 14526 bytes .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 8 + Client/Platform/Android/build.gradle | 25 + Client/Platform/Android/buildbinaries.sh | 5 + Client/Platform/Android/gradle.properties | 17 + .../Android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54213 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + Client/Platform/Android/gradlew | 160 + Client/Platform/Android/gradlew.bat | 90 + Client/Platform/Android/readme.md | 2 + Client/Platform/Android/settings.gradle | 1 + Client/Platform/DynamicLibraryLoader.cs | 131 + .../Linux}/build-flatpak.sh | 0 .../Linux}/com.github.exelix11.sysdvr.json | 0 .../Linux}/flatpak-content/launcher.sh | 0 Client/{linux => Platform/Linux}/sysdvr.rules | 0 Client/Platform/Resources.cs | 56 + .../Platform/Resources/resources/OpenSans.ttf | Bin 0 -> 16668 bytes .../Platform/Resources/resources/ico_usb.png | Bin 0 -> 8407 bytes .../Platform/Resources/resources/ico_wifi.png | Bin 0 -> 11231 bytes .../Resources/resources}/loading.yuv | 0 Client/Platform/Resources/resources/logo.png | Bin 0 -> 19955 bytes .../Specific/WinAntiInject.cs} | 1 + Client/Player/LibavUtils.cs | 83 - Client/Player/Player.cs | 686 - Client/Player/Streams.cs | 222 - Client/Program.cs | 1356 +- Client/RTSP/Exten.cs | 17 - Client/RTSP/H264Packetizer.cs | 132 - Client/RTSP/LE16Packetizer.cs | 61 - Client/RTSP/RTPPacketUtil.cs | 53 - Client/RTSP/RtspServer.cs | 596 - Client/RTSP/Streams.cs | 72 - Client/Sources/RecordedSource.cs | 5 +- Client/Sources/StubSource.cs | 1 + Client/Sources/TCPBridge.cs | 3 +- Client/Sources/UsbStreaming.cs | 7 +- Client/StandardStreamOutputs.cs | 106 - Client/StreamInfo.cs | 26 - Client/StreamManager.cs | 190 - Client/StreamThread.cs | 262 - Client/Targets/FileOutput/LoggingTarget.cs | 62 + Client/Targets/FileOutput/Mp4Output.cs | 174 + Client/{ => Targets}/FileOutput/Mp4Streams.cs | 11 +- Client/Targets/Player/LibavUtils.cs | 84 + Client/Targets/Player/Player.cs | 569 + Client/Targets/Player/Streams.cs | 224 + Client/{ => Targets}/Player/SyncHelper.cs | 9 +- Client/Targets/RTSP/Exten.cs | 17 + Client/Targets/RTSP/H264Packetizer.cs | 132 + Client/Targets/RTSP/LE16Packetizer.cs | 61 + Client/Targets/RTSP/RTPPacketUtil.cs | 53 + Client/Targets/RTSP/RtspServer.cs | 598 + Client/Targets/RTSP/Streams.cs | 74 + Client/Targets/StandardStreamOutputs.cs | 106 + Client/ThirdParty/ImGui.NET/Custom/Custom.cs | 51 + Client/ThirdParty/ImGui.NET/Delegates.cs | 17 + .../ImGui.NET/Generated/ImColor.gen.cs | 48 + .../ImGui.NET/Generated/ImDrawChannel.gen.cs | 24 + .../ImGui.NET/Generated/ImDrawCmd.gen.cs | 43 + .../Generated/ImDrawCmdHeader.gen.cs | 26 + .../ImGui.NET/Generated/ImDrawData.gen.cs | 54 + .../ImGui.NET/Generated/ImDrawFlags.gen.cs | 21 + .../ImGui.NET/Generated/ImDrawList.gen.cs | 483 + .../Generated/ImDrawListFlags.gen.cs | 12 + .../Generated/ImDrawListSplitter.gen.cs | 53 + .../ImGui.NET/Generated/ImDrawVert.gen.cs | 26 + .../ImGui.NET/Generated/ImFont.gen.cs | 126 + .../ImGui.NET/Generated/ImFontAtlas.gen.cs | 654 + .../Generated/ImFontAtlasCustomRect.gen.cs | 45 + .../Generated/ImFontAtlasFlags.gen.cs | 11 + .../ImGui.NET/Generated/ImFontConfig.gen.cs | 62 + .../ImGui.NET/Generated/ImFontGlyph.gen.cs | 44 + .../Generated/ImFontGlyphRangesBuilder.gen.cs | 86 + .../ImGui.NET/Generated/ImGui.gen.cs | 15882 ++++++++++++++++ .../Generated/ImGuiBackendFlags.gen.cs | 15 + .../Generated/ImGuiButtonFlags.gen.cs | 13 + .../ImGui.NET/Generated/ImGuiCol.gen.cs | 62 + .../Generated/ImGuiColorEditFlags.gen.cs | 36 + .../Generated/ImGuiComboFlags.gen.cs | 16 + .../ImGui.NET/Generated/ImGuiCond.gen.cs | 11 + .../Generated/ImGuiConfigFlags.gen.cs | 20 + .../ImGui.NET/Generated/ImGuiDataType.gen.cs | 17 + .../ImGui.NET/Generated/ImGuiDir.gen.cs | 12 + .../Generated/ImGuiDockNodeFlags.gen.cs | 14 + .../Generated/ImGuiDragDropFlags.gen.cs | 18 + .../Generated/ImGuiFocusedFlags.gen.cs | 14 + .../Generated/ImGuiHoveredFlags.gen.cs | 23 + .../ImGui.NET/Generated/ImGuiIO.gen.cs | 983 + .../ImGuiInputTextCallbackData.gen.cs | 98 + .../Generated/ImGuiInputTextFlags.gen.cs | 29 + .../ImGui.NET/Generated/ImGuiKey.gen.cs | 160 + .../ImGui.NET/Generated/ImGuiKeyData.gen.cs | 28 + .../Generated/ImGuiListClipper.gen.cs | 60 + .../ImGui.NET/Generated/ImGuiModFlags.gen.cs | 12 + .../Generated/ImGuiMouseButton.gen.cs | 10 + .../Generated/ImGuiMouseCursor.gen.cs | 17 + .../Generated/ImGuiMouseSource.gen.cs | 10 + .../ImGui.NET/Generated/ImGuiNative.gen.cs | 1282 ++ .../ImGui.NET/Generated/ImGuiNavInput.gen.cs | 23 + .../Generated/ImGuiOnceUponAFrame.gen.cs | 26 + .../ImGui.NET/Generated/ImGuiPayload.gen.cs | 85 + .../Generated/ImGuiPlatformIO.gen.cs | 74 + .../Generated/ImGuiPlatformImeData.gen.cs | 30 + .../Generated/ImGuiPlatformMonitor.gen.cs | 34 + .../Generated/ImGuiPopupFlags.gen.cs | 18 + .../Generated/ImGuiSelectableFlags.gen.cs | 13 + .../Generated/ImGuiSizeCallbackData.gen.cs | 28 + .../Generated/ImGuiSliderFlags.gen.cs | 13 + .../Generated/ImGuiSortDirection.gen.cs | 9 + .../ImGui.NET/Generated/ImGuiStorage.gen.cs | 137 + .../ImGui.NET/Generated/ImGuiStyle.gen.cs | 170 + .../ImGui.NET/Generated/ImGuiStyleVar.gen.cs | 35 + .../Generated/ImGuiTabBarFlags.gen.cs | 18 + .../Generated/ImGuiTabItemFlags.gen.cs | 16 + .../Generated/ImGuiTableBgTarget.gen.cs | 10 + .../Generated/ImGuiTableColumnFlags.gen.cs | 34 + .../ImGuiTableColumnSortSpecs.gen.cs | 32 + .../Generated/ImGuiTableFlags.gen.cs | 43 + .../Generated/ImGuiTableRowFlags.gen.cs | 9 + .../Generated/ImGuiTableSortSpecs.gen.cs | 30 + .../Generated/ImGuiTextBuffer.gen.cs | 120 + .../Generated/ImGuiTextFilter.gen.cs | 162 + .../ImGui.NET/Generated/ImGuiTextRange.gen.cs | 40 + .../Generated/ImGuiTreeNodeFlags.gen.cs | 23 + .../ImGui.NET/Generated/ImGuiViewport.gen.cs | 70 + .../Generated/ImGuiViewportFlags.gen.cs | 22 + .../Generated/ImGuiWindowClass.gen.cs | 40 + .../Generated/ImGuiWindowFlags.gen.cs | 39 + .../Generated/STB_TexteditState.gen.cs | 48 + .../ImGui.NET/Generated/StbTexteditRow.gen.cs | 32 + .../ImGui.NET/Generated/StbUndoRecord.gen.cs | 28 + .../ImGui.NET/Generated/StbUndoState.gen.cs | 130 + .../ThirdParty/ImGui.NET/ImDrawData.Manual.cs | 7 + .../ThirdParty/ImGui.NET/ImDrawList.Manual.cs | 46 + Client/ThirdParty/ImGui.NET/ImGui.Manual.cs | 651 + Client/ThirdParty/ImGui.NET/ImGui.NET.csproj | 34 + .../ImGui.NET/ImGuiNative.Manual.cs | 13 + .../ThirdParty/ImGui.NET/ImGuiSizeCallback.cs | 4 + .../ImGui.NET/ImGuiTextEditCallback.cs | 7 + Client/ThirdParty/ImGui.NET/ImVector.cs | 82 + .../ImGui.NET/NullTerminatedString.cs | 29 + Client/ThirdParty/ImGui.NET/Pair.cs | 32 + Client/ThirdParty/ImGui.NET/RangeAccessor.cs | 68 + Client/ThirdParty/ImGui.NET/Util.cs | 95 + .../ThirdParty/SDL2-CS-master/SDL2-CS.csproj | 19 + Client/ThirdParty/SDL2-CS-master/src/SDL2.cs | 8805 +++++++++ .../ThirdParty/SDL2-CS-master/src/SDL2_gfx.cs | 390 + .../SDL2-CS-master/src/SDL2_image.cs | 317 + .../SDL2-CS-master/src/SDL2_mixer.cs | 666 + .../ThirdParty/SDL2-CS-master/src/SDL2_ttf.cs | 769 + 195 files changed, 45027 insertions(+), 3347 deletions(-) create mode 100644 Client/Core/DebugHelpers.cs rename Client/{ => Core}/DebugOptions.cs (93%) create mode 100644 Client/Core/ErrorCodeExtensions.cs create mode 100644 Client/Core/Log.cs create mode 100644 Client/Core/Native.cs create mode 100644 Client/Core/StreamInfo.cs create mode 100644 Client/Core/StreamManager.cs create mode 100644 Client/Core/StreamThread.cs delete mode 100644 Client/DebugHelpers.cs delete mode 100644 Client/FileOutput/LoggingTarget.cs delete mode 100644 Client/FileOutput/Mp4Output.cs create mode 100644 Client/GUI/Image.cs create mode 100644 Client/GUI/Interfaces.cs create mode 100644 Client/GUI/MainView.cs delete mode 100644 Client/Help.txt create mode 100644 Client/Platform/Android/.gitignore create mode 100644 Client/Platform/Android/app/build.gradle create mode 100644 Client/Platform/Android/app/jni/Android.mk create mode 100644 Client/Platform/Android/app/jni/Application.mk create mode 100644 Client/Platform/Android/app/jni/CMakeLists.txt create mode 100644 Client/Platform/Android/app/jni/Client/Android.mk create mode 100644 Client/Platform/Android/app/jni/SDL/Android.mk create mode 100644 Client/Platform/Android/app/jni/cimgui/Android.mk create mode 100644 Client/Platform/Android/app/jni/src/Android.mk create mode 100644 Client/Platform/Android/app/jni/src/main.c create mode 100644 Client/Platform/Android/app/proguard-rules.pro create mode 100644 Client/Platform/Android/app/src/main/AndroidManifest.xml create mode 100644 Client/Platform/Android/app/src/main/java/exelix11/sysdvr/sysdvrActivity.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDevice.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/SDL.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/SDLActivity.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/SDLAudioManager.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/SDLControllerManager.java create mode 100644 Client/Platform/Android/app/src/main/java/org/libsdl/app/SDLSurface.java create mode 100644 Client/Platform/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 Client/Platform/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 Client/Platform/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 Client/Platform/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 Client/Platform/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 Client/Platform/Android/app/src/main/res/values/colors.xml create mode 100644 Client/Platform/Android/app/src/main/res/values/strings.xml create mode 100644 Client/Platform/Android/app/src/main/res/values/styles.xml create mode 100644 Client/Platform/Android/build.gradle create mode 100644 Client/Platform/Android/buildbinaries.sh create mode 100644 Client/Platform/Android/gradle.properties create mode 100644 Client/Platform/Android/gradle/wrapper/gradle-wrapper.jar create mode 100644 Client/Platform/Android/gradle/wrapper/gradle-wrapper.properties create mode 100644 Client/Platform/Android/gradlew create mode 100644 Client/Platform/Android/gradlew.bat create mode 100644 Client/Platform/Android/readme.md create mode 100644 Client/Platform/Android/settings.gradle create mode 100644 Client/Platform/DynamicLibraryLoader.cs rename Client/{linux => Platform/Linux}/build-flatpak.sh (100%) rename Client/{linux => Platform/Linux}/com.github.exelix11.sysdvr.json (100%) rename Client/{linux => Platform/Linux}/flatpak-content/launcher.sh (100%) rename Client/{linux => Platform/Linux}/sysdvr.rules (100%) create mode 100644 Client/Platform/Resources.cs create mode 100644 Client/Platform/Resources/resources/OpenSans.ttf create mode 100644 Client/Platform/Resources/resources/ico_usb.png create mode 100644 Client/Platform/Resources/resources/ico_wifi.png rename Client/{runtimes => Platform/Resources/resources}/loading.yuv (100%) create mode 100644 Client/Platform/Resources/resources/logo.png rename Client/{Windows/AntiInject.cs => Platform/Specific/WinAntiInject.cs} (99%) delete mode 100644 Client/Player/LibavUtils.cs delete mode 100644 Client/Player/Player.cs delete mode 100644 Client/Player/Streams.cs delete mode 100644 Client/RTSP/Exten.cs delete mode 100644 Client/RTSP/H264Packetizer.cs delete mode 100644 Client/RTSP/LE16Packetizer.cs delete mode 100644 Client/RTSP/RTPPacketUtil.cs delete mode 100644 Client/RTSP/RtspServer.cs delete mode 100644 Client/RTSP/Streams.cs delete mode 100644 Client/StandardStreamOutputs.cs delete mode 100644 Client/StreamInfo.cs delete mode 100644 Client/StreamManager.cs delete mode 100644 Client/StreamThread.cs create mode 100644 Client/Targets/FileOutput/LoggingTarget.cs create mode 100644 Client/Targets/FileOutput/Mp4Output.cs rename Client/{ => Targets}/FileOutput/Mp4Streams.cs (97%) create mode 100644 Client/Targets/Player/LibavUtils.cs create mode 100644 Client/Targets/Player/Player.cs create mode 100644 Client/Targets/Player/Streams.cs rename Client/{ => Targets}/Player/SyncHelper.cs (94%) create mode 100644 Client/Targets/RTSP/Exten.cs create mode 100644 Client/Targets/RTSP/H264Packetizer.cs create mode 100644 Client/Targets/RTSP/LE16Packetizer.cs create mode 100644 Client/Targets/RTSP/RTPPacketUtil.cs create mode 100644 Client/Targets/RTSP/RtspServer.cs create mode 100644 Client/Targets/RTSP/Streams.cs create mode 100644 Client/Targets/StandardStreamOutputs.cs create mode 100644 Client/ThirdParty/ImGui.NET/Custom/Custom.cs create mode 100644 Client/ThirdParty/ImGui.NET/Delegates.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImColor.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawChannel.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawCmd.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawCmdHeader.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawData.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawList.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawListFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawListSplitter.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImDrawVert.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImFont.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImFontAtlas.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImFontAtlasCustomRect.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImFontAtlasFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImFontConfig.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImFontGlyph.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImFontGlyphRangesBuilder.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGui.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiBackendFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiButtonFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiCol.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiColorEditFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiComboFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiCond.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiConfigFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiDataType.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiDir.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiDockNodeFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiDragDropFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiFocusedFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiHoveredFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiIO.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiInputTextCallbackData.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiInputTextFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiKey.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiKeyData.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiListClipper.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiModFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiMouseButton.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiMouseCursor.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiMouseSource.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiNative.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiNavInput.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiOnceUponAFrame.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiPayload.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiPlatformIO.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiPlatformImeData.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiPlatformMonitor.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiPopupFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiSelectableFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiSizeCallbackData.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiSliderFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiSortDirection.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiStorage.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiStyle.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiStyleVar.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTabBarFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTabItemFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTableBgTarget.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTableColumnFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTableColumnSortSpecs.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTableFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTableRowFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTableSortSpecs.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTextBuffer.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTextFilter.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTextRange.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiTreeNodeFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiViewport.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiViewportFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiWindowClass.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/ImGuiWindowFlags.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/STB_TexteditState.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/StbTexteditRow.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/StbUndoRecord.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/Generated/StbUndoState.gen.cs create mode 100644 Client/ThirdParty/ImGui.NET/ImDrawData.Manual.cs create mode 100644 Client/ThirdParty/ImGui.NET/ImDrawList.Manual.cs create mode 100644 Client/ThirdParty/ImGui.NET/ImGui.Manual.cs create mode 100644 Client/ThirdParty/ImGui.NET/ImGui.NET.csproj create mode 100644 Client/ThirdParty/ImGui.NET/ImGuiNative.Manual.cs create mode 100644 Client/ThirdParty/ImGui.NET/ImGuiSizeCallback.cs create mode 100644 Client/ThirdParty/ImGui.NET/ImGuiTextEditCallback.cs create mode 100644 Client/ThirdParty/ImGui.NET/ImVector.cs create mode 100644 Client/ThirdParty/ImGui.NET/NullTerminatedString.cs create mode 100644 Client/ThirdParty/ImGui.NET/Pair.cs create mode 100644 Client/ThirdParty/ImGui.NET/RangeAccessor.cs create mode 100644 Client/ThirdParty/ImGui.NET/Util.cs create mode 100644 Client/ThirdParty/SDL2-CS-master/SDL2-CS.csproj create mode 100644 Client/ThirdParty/SDL2-CS-master/src/SDL2.cs create mode 100644 Client/ThirdParty/SDL2-CS-master/src/SDL2_gfx.cs create mode 100644 Client/ThirdParty/SDL2-CS-master/src/SDL2_image.cs create mode 100644 Client/ThirdParty/SDL2-CS-master/src/SDL2_mixer.cs create mode 100644 Client/ThirdParty/SDL2-CS-master/src/SDL2_ttf.cs diff --git a/Client/Client.csproj b/Client/Client.csproj index ee698b71..b93f1d0c 100644 --- a/Client/Client.csproj +++ b/Client/Client.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 AnyCPU SysDVR-Client https://github.com/exelix11/SysDVR @@ -34,17 +34,22 @@ - - + PreserveNewest runtimes\%(RecursiveDir)\%(Filename)%(Extension) - - + + + + + + + + PreserveNewest diff --git a/Client/Core/DebugHelpers.cs b/Client/Core/DebugHelpers.cs new file mode 100644 index 00000000..4d6ec434 --- /dev/null +++ b/Client/Core/DebugHelpers.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SysDVR.Client.Core +{ + public class TimeTrace : IDisposable + { + Stopwatch sw = new Stopwatch(); + StringBuilder sb = new(); + + public TimeTrace Begin(string kind, string extra, string funcname) + { + sb.Clear(); + sb.Append($"[{kind}] [{funcname}] {extra} "); + sw.Restart(); + return this; + } + + public void Mark(string name) + { + sb.Append($"{sw.ElapsedMilliseconds} ms | {name} "); + sw.Restart(); + } + + // Abuses the using statement for RAII-like behavior, is not actually disposed + public void Dispose() + { + sw.Stop(); + sb.Append($"{sw.ElapsedMilliseconds} ms"); + Console.WriteLine(sb.ToString()); + } + } +} diff --git a/Client/DebugOptions.cs b/Client/Core/DebugOptions.cs similarity index 93% rename from Client/DebugOptions.cs rename to Client/Core/DebugOptions.cs index f01f35e3..6acb73c8 100644 --- a/Client/DebugOptions.cs +++ b/Client/Core/DebugOptions.cs @@ -1,4 +1,4 @@ -using SysDVR.Client.RTSP; +using SysDVR.Client.Targets.RTSP; using System; using System.Collections.Generic; using System.Diagnostics; @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; -namespace SysDVR.Client +namespace SysDVR.Client.Core { public record DebugOptions(bool Stats, bool Log, bool Keyframe, bool Nal, bool Fps, bool NoSync, bool NoProt) { @@ -65,12 +65,12 @@ public static DebugOptions Parse(string? options) } } - class FramerateCounter + class FramerateCounter { Stopwatch sw = new(); uint frames = 0; - public void Start() + public void Start() { sw.Restart(); } @@ -80,7 +80,7 @@ public void OnFrame() unchecked { frames++; } } - public bool GetFps(out int fps) + public bool GetFps(out int fps) { if (sw.ElapsedMilliseconds > 1000) { @@ -126,13 +126,13 @@ void IOutStream.SendData(PoolBuffer block, ulong ts) if (firstInSeq) { var now = DateTime.Now; - var diff = (int)((now - lastNal).TotalMilliseconds); + var diff = (int)(now - lastNal).TotalMilliseconds; sb.AppendFormat("{0}ms ", diff); lastNal = now; firstInSeq = false; } - sb.AppendFormat("{0:x} ", (nal[0] & 0x1F)); + sb.AppendFormat("{0:x} ", nal[0] & 0x1F); } if (DebugOptions.Current.Keyframe) @@ -148,8 +148,8 @@ void IOutStream.SendData(PoolBuffer block, ulong ts) } } - if (sb.Length != 1) - { + if (sb.Length != 1) + { sb.Append("] "); Console.Write(sb.ToString()); } diff --git a/Client/Core/ErrorCodeExtensions.cs b/Client/Core/ErrorCodeExtensions.cs new file mode 100644 index 00000000..738e995f --- /dev/null +++ b/Client/Core/ErrorCodeExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SysDVR.Client.Core +{ + static class Exten + { + public static void AssertEqual(this int code, int expectedValue, Func MessageFun = null) + { + if (code != expectedValue) + throw new Exception($"Assertion failed {code} != {expectedValue} : {MessageFun?.Invoke() ?? "Unknown error:"}"); + } + + public static void AssertNotZero(this uint code, Func MessageFun = null) + { + if (code == 0) + throw new Exception($"Assertion failed: {code} {MessageFun?.Invoke() ?? "Unknown error"}"); + } + + public static void AssertZero(this int code, Func MessageFun = null) + { + if (code != 0) + throw new Exception($"Assertion failed: {code} {MessageFun?.Invoke() ?? "Unknown error"}"); + } + + public static void AssertNotNeg(this int code, Func MessageFun = null) + { + if (code < 0) + throw new Exception($"Assertion failed: {code} {MessageFun?.Invoke() ?? "Unknown error"}"); + } + + public static void AssertZero(this int code, string Message) + { + if (code != 0) + throw new Exception($"Assertion failed: {code} {Message}"); + } + + public static IntPtr AssertNotNull(this IntPtr val, Func MessageFun = null) + { + if (val == IntPtr.Zero) + throw new Exception($"Assertion failed: pointer is null {MessageFun?.Invoke() ?? "Unknown error"}"); + return val; + } + } +} diff --git a/Client/Core/Log.cs b/Client/Core/Log.cs new file mode 100644 index 00000000..7e5aa3c1 --- /dev/null +++ b/Client/Core/Log.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Text; +using System.Threading.Tasks; + +namespace SysDVR.Client.Core +{ + public static class Log + { + public static void Setup(in NativeInitBlock init) + { + if (init.PrintFunction != IntPtr.Zero) + { + Console.SetOut(new AndroidTextOut(init.PrintFunction)); + } + } + } + + public class AndroidTextOut : TextWriter + { + delegate void PrintImpl([MarshalAs(UnmanagedType.LPStr)] string func); + readonly PrintImpl Print; + + public AndroidTextOut(IntPtr print) + { + Print = Marshal.GetDelegateForFunctionPointer(print); + Print("Native console out is set"); + } + + public override Encoding Encoding => Encoding.ASCII; + + public override void Write(string? value) + { + Print(value); + } + + public override void Write(char[] buffer, int index, int count) + { + Write(new string(buffer, index, count)); + } + + public override void Write(char value) + { + base.Write(value.ToString()); + } + } +} diff --git a/Client/Core/Native.cs b/Client/Core/Native.cs new file mode 100644 index 00000000..530d12e5 --- /dev/null +++ b/Client/Core/Native.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace SysDVR.Client.Core +{ + [StructLayout(LayoutKind.Sequential)] + public struct NativeInitBlock + { + public const IntPtr BlockVersion = (IntPtr)1; + + public IntPtr Version; + public IntPtr PrintFunction; + + public unsafe static NativeInitBlock Read(IntPtr ptr) => + *(NativeInitBlock*)ptr; + + public static NativeInitBlock Empty = new NativeInitBlock() + { + Version = BlockVersion, + PrintFunction = IntPtr.Zero + }; + } +} diff --git a/Client/Core/StreamInfo.cs b/Client/Core/StreamInfo.cs new file mode 100644 index 00000000..098cfcd7 --- /dev/null +++ b/Client/Core/StreamInfo.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; + +namespace SysDVR.Client.Core +{ + public static class StreamInfo + { + public static readonly byte[] SPS = { 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x0C, 0x20, 0xAC, 0x2B, 0x40, 0x28, 0x02, 0xDD, 0x35, 0x01, 0x0D, 0x01, 0xE0, 0x80 }; + public static readonly byte[] PPS = { 0x00, 0x00, 0x00, 0x01, 0x68, 0xEE, 0x3C, 0xB0 }; + + // This is VideoPayloadSize, which is also the max payload possible over the SysDVR protocol + public const int MaxPayloadSize = 0x50000; + + public const int VideoWidth = 1280; + public const int VideoHeight = 720; + + public const int AudioPayloadSize = 0x1000; + public const int MaxAudioBatching = 3; + + public const int AudioChannels = 2; + public const int AudioSampleRate = 48000; + public const int AudioSampleSize = 2; + + // Doesn't accoutn for batching, there may be more samples than this + public const int MinAudioSamplesPerPayload = AudioPayloadSize / (AudioChannels * AudioSampleSize); + } +} diff --git a/Client/Core/StreamManager.cs b/Client/Core/StreamManager.cs new file mode 100644 index 00000000..5adf7700 --- /dev/null +++ b/Client/Core/StreamManager.cs @@ -0,0 +1,180 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace SysDVR.Client.Core +{ + enum StreamKind + { + Both, + Video, + Audio + }; + + struct PoolBuffer + { + private readonly static ArrayPool pool = ArrayPool.Shared; + + public int Length { get; private set; } + private byte[] _buffer; + + public byte[] RawBuffer => _buffer ?? throw new Exception("The buffer has been freed"); + + public void Free() + { + pool.Return(RawBuffer); + _buffer = null; + } + + public static PoolBuffer Rent(int len) => + new PoolBuffer(pool.Rent(len), len); + + private PoolBuffer(byte[] buf, int len) + { + Length = len; + _buffer = buf; + } + + public Span Span => + new Span(RawBuffer, 0, Length); + + public ArraySegment ArraySegment => + new ArraySegment(RawBuffer, 0, Length); + + public static implicit operator Span(PoolBuffer o) => o.Span; + } + + interface IOutStream + { + // This must be called before sending any data + void UseCancellationToken(CancellationToken tok); + + // The implementation must call Free() on the buffer + void SendData(PoolBuffer block, ulong ts); + } + + abstract class BaseStreamManager : IDisposable + { + private bool disposedValue; + + // Usb streaming may require a single thread + private StreamThread Thread1; + private StreamThread? Thread2; + + protected IOutStream VideoTarget { get; set; } + protected IOutStream AudioTarget { get; set; } + + public StreamKind? Streams { get; private set; } + + public bool HasVideo => Streams is StreamKind.Both or StreamKind.Video; + public bool HasAudio => Streams is StreamKind.Both or StreamKind.Audio; + + public BaseStreamManager(IOutStream videoTarget, IOutStream audioTarget) + { + VideoTarget = videoTarget; + AudioTarget = audioTarget; + } + + IOutStream? WrapVideoTarget() + { + if (DebugOptions.Current.RequiresH264Analysis && VideoTarget is not null) + return new H264LoggingWrapperTarget(VideoTarget); + + return VideoTarget; + } + + public void AddSource(IStreamingSource source) + { + if (source.SourceKind == StreamKind.Video) + { + if (HasVideo) throw new Exception("Already has a video source"); + Thread1 = new SingleStreamThread(source, WrapVideoTarget()); + + Streams = HasAudio ? StreamKind.Both : StreamKind.Video; + } + else if (source.SourceKind == StreamKind.Audio) + { + if (HasAudio) throw new Exception("Already has an audio source"); + Thread2 = new SingleStreamThread(source, AudioTarget); + + Streams = HasVideo ? StreamKind.Both : StreamKind.Audio; + } + else if (source.SourceKind == StreamKind.Both) + { + if (HasAudio || HasVideo) throw new Exception("Already has a multi source"); + Thread1 = new MultiStreamThread(source, WrapVideoTarget(), AudioTarget); + + Streams = StreamKind.Both; + } + } + + public virtual void Begin() + { + // Sanity checks + if (Streams is null) + throw new Exception("No streams have been set"); + + if (Streams == StreamKind.Video && AudioTarget != null) + throw new Exception("There should be no audio target for a video only stream"); + if (Streams == StreamKind.Audio && VideoTarget != null) + throw new Exception("There should be no video target for an audio only stream"); + if (Streams == StreamKind.Both && (VideoTarget == null || AudioTarget == null)) + throw new Exception("One or more targets are not set"); + + Thread1?.Start(); + Thread2?.Start(); + } + + public virtual void Stop() + { + Thread1?.Stop(); + Thread2?.Stop(); + +#if MEASURE_STATS + var vDiff = DateTime.Now - VideoThread.FirstByteTs; + var aDiff = DateTime.Now - AudioThread.FirstByteTs; + var total = VideoThread.ReceivedBytes + AudioThread.ReceivedBytes; + + var max = vDiff > aDiff ? vDiff : aDiff; + + Console.WriteLine($"MEASURE_STATS: received {total} bytes in {max.TotalSeconds} s of streaming, avg of {total / max.TotalSeconds} B/s."); + Console.WriteLine($"Per thread stats:"); + Console.WriteLine($"\tVideo: {VideoThread.ReceivedBytes} bytes in {vDiff.TotalSeconds} s, avg of {VideoThread.ReceivedBytes / vDiff.TotalSeconds} B/s"); + Console.WriteLine($"\tAudio: {AudioThread.ReceivedBytes} bytes in {aDiff.TotalSeconds} s, avg of {AudioThread.ReceivedBytes / aDiff.TotalSeconds} B/s"); +#endif + + Thread1?.Join(); + Thread2?.Join(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + Thread1?.Dispose(); + Thread2?.Dispose(); + + if (VideoTarget is IDisposable iv) + iv.Dispose(); + if (AudioTarget is IDisposable ia) + ia.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Client/Core/StreamThread.cs b/Client/Core/StreamThread.cs new file mode 100644 index 00000000..1038b089 --- /dev/null +++ b/Client/Core/StreamThread.cs @@ -0,0 +1,262 @@ +//#define EXCEPTION_DEBUG + +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Reflection.PortableExecutable; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace SysDVR.Client.Core +{ + [StructLayout(LayoutKind.Sequential)] + struct PacketHeader + { + public uint Magic; + public int DataSize; + public ulong Timestamp; + + public override string ToString() => + $"Magic: {Magic:X8} Len: {DataSize + StructLength} Bytes - ts: {Timestamp}"; + + public const int StructLength = 16; + + // Note: to make the TCP implementation easier these should be composed of 4 identical bytes + public const uint MagicResponseVideo = 0xDDDDDDDD; + public const uint MagicResponseAudio = 0xEEEEEEEE; + + public const int MaxTransferSize = StreamInfo.MaxPayloadSize + StructLength; + + static PacketHeader() + { + if (Marshal.SizeOf() != StructLength) + throw new Exception("PacketHeader struct binary size is wrong"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsVideo() => Magic == MagicResponseVideo; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsAudio() => Magic == MagicResponseAudio; + } + + interface IStreamingSource + { + // Note that the source should respect the target output type, + // this means that by the time it's added to a StreamManager + // this field should match the NoAudio/NoVideo state of the target + StreamKind SourceKind { get; } + + DebugOptions Logging { get; set; } + void UseCancellationToken(CancellationToken tok); + + void WaitForConnection(); + void StopStreaming(); + + void Flush(); + + bool ReadHeader(byte[] buffer); + bool ReadPayload(byte[] buffer, int length); + } + + abstract class StreamThread : IDisposable + { + Thread DeviceThread; + CancellationTokenSource Cancel; + + readonly public IStreamingSource Source; + readonly public StreamKind Kind; + + protected abstract void SetCancellationToken(CancellationToken token); + protected abstract bool DataReceived(in PacketHeader header, PoolBuffer body); + +#if MEASURE_STATS + public ulong ReceivedBytes { get; private set; } + public DateTime FirstByteTs { get; private set; } + + public void StatsReceivedData(int length) + { + if (ReceivedBytes == 0) + FirstByteTs = DateTime.Now; + ReceivedBytes += (uint)length + PacketHeader.StructLength; + } +#endif + + protected StreamThread(IStreamingSource source) + { + Source = source; + Kind = Source.SourceKind; + } + + public void Start() + { + Cancel = new CancellationTokenSource(); + + DeviceThread = new Thread(() => DeviceThreadMain(Cancel.Token)); + DeviceThread.Name = "DeviceThread for " + Kind; + + DeviceThread.Start(); + } + + public void Stop() + { + Cancel.Cancel(); + Source.StopStreaming(); + } + + public void Join() + { + DeviceThread.Join(); + } + + TimeTrace trace = new TimeTrace(); + void DeviceThreadMain(CancellationToken token) + { + var logStats = DebugOptions.Current.Stats; + var logDbg = DebugOptions.Current.Log; + + Source.Logging = DebugOptions.Current; + Source.UseCancellationToken(token); + + SetCancellationToken(token); + + var HeaderData = new byte[PacketHeader.StructLength]; + ref var Header = ref MemoryMarshal.Cast(HeaderData)[0]; + try + { + Source.WaitForConnection(); + loop_again: + while (!token.IsCancellationRequested) + { + while (!Source.ReadHeader(HeaderData)) + { + Source.Flush(); + goto loop_again; + } + + if (logStats) + Console.WriteLine($"[{Kind}] {Header}"); + + if (Header.Magic is not PacketHeader.MagicResponseAudio and not PacketHeader.MagicResponseVideo) + { + if (logDbg) + Console.WriteLine($"[{Kind}] Wrong header magic: {Header.Magic:X}"); + + Source.Flush(); + continue; + } + + if (Header.DataSize > StreamInfo.MaxPayloadSize) + { + if (logDbg) + Console.WriteLine($"[{Kind}] Data size exceeds max size: {Header.DataSize:X}"); + + Source.Flush(); + continue; + } + + var Data = PoolBuffer.Rent(Header.DataSize); + if (!Source.ReadPayload(Data.RawBuffer, Header.DataSize)) + { + if (logDbg) + Console.WriteLine($"[{Kind}] Read payload failed."); + + Source.Flush(); + Data.Free(); + continue; + } + + if (!DataReceived(Header, Data)) + { + if (logDbg) + Console.WriteLine($"[{Kind}] DataReceived rejected the packet, header magic was {Header.Magic:X}"); + + Data.Free(); + continue; + } + } + + } +#if !EXCEPTION_DEBUG || RELEASE + catch (Exception ex) + { + if (!token.IsCancellationRequested) + Console.WriteLine($"Terminating ReceiveFromDeviceThread for {Kind} due to {ex}"); + } +#endif + } + + public void Dispose() + { + if (DeviceThread.IsAlive) + throw new Exception($"{Kind} Thread is still running"); + + Cancel.Dispose(); + } + } + + class SingleStreamThread : StreamThread + { + readonly IOutStream Target; + + public SingleStreamThread(IStreamingSource source, IOutStream target) : base(source) + { + Target = target; + } + + protected override void SetCancellationToken(CancellationToken token) + { + Target.UseCancellationToken(token); + } + + protected override bool DataReceived(in PacketHeader header, PoolBuffer body) + { + var valid = + Kind == StreamKind.Video && header.IsVideo() || + Kind == StreamKind.Audio && header.IsAudio(); + + if (!valid) + return false; + + Target.SendData(body, header.Timestamp); + + return true; + } + } + + class MultiStreamThread : StreamThread + { + readonly IOutStream VideoTarget; + readonly IOutStream AudioTarget; + + public MultiStreamThread(IStreamingSource source, IOutStream videoTarget, IOutStream audioTarget) : base(source) + { + if (source.SourceKind != StreamKind.Both) + throw new Exception("Source must be able to provide both streams"); + + VideoTarget = videoTarget; + AudioTarget = audioTarget; + } + + protected override void SetCancellationToken(CancellationToken token) + { + VideoTarget.UseCancellationToken(token); + AudioTarget.UseCancellationToken(token); + } + + protected override bool DataReceived(in PacketHeader header, PoolBuffer body) + { + if (header.IsVideo()) + VideoTarget.SendData(body, header.Timestamp); + else if (header.IsAudio()) + AudioTarget.SendData(body, header.Timestamp); + else + return false; + + return true; + } + } +} diff --git a/Client/DebugHelpers.cs b/Client/DebugHelpers.cs deleted file mode 100644 index e8814584..00000000 --- a/Client/DebugHelpers.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SysDVR.Client -{ - public class TimeTrace : IDisposable - { - Stopwatch sw = new Stopwatch(); - StringBuilder sb = new(); - - public TimeTrace Begin(string kind, string extra, string funcname) - { - sb.Clear(); - sb.Append($"[{kind}] [{funcname}] {extra} "); - sw.Restart(); - return this; - } - - public void Mark(string name) - { - sb.Append($"{sw.ElapsedMilliseconds} ms | {name} "); - sw.Restart(); - } - - // Abuses the using statement for RAII-like behavior, is not actually disposed - public void Dispose() - { - sw.Stop(); - sb.Append($"{sw.ElapsedMilliseconds} ms"); - Console.WriteLine(sb.ToString()); - } - } -} diff --git a/Client/FileOutput/LoggingTarget.cs b/Client/FileOutput/LoggingTarget.cs deleted file mode 100644 index cabc5be4..00000000 --- a/Client/FileOutput/LoggingTarget.cs +++ /dev/null @@ -1,61 +0,0 @@ -#if DEBUG -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; - -namespace SysDVR.Client.FileOutput -{ - class LoggingTarget : IOutStream - { - readonly BinaryWriter bin; - readonly MemoryStream mem = new MemoryStream(); - readonly string filename; - - public LoggingTarget(string filename) - { - this.filename = filename; - bin = new BinaryWriter(mem); - } - - public void WriteToDisk() - { - File.WriteAllBytes(filename, mem.ToArray()); - } - - Stopwatch sw = new Stopwatch(); - - public void SendData(PoolBuffer data, UInt64 ts) - { - Console.WriteLine($"{filename} - ts: {ts}"); - bin.Write(0xAAAAAAAA); - bin.Write(sw.ElapsedMilliseconds); - bin.Write(ts); - bin.Write(data.Length); - bin.Write(data.Span); - sw.Restart(); - - data.Free(); - } - - public void UseCancellationToken(CancellationToken tok) { } - } - - class LoggingManager : BaseStreamManager - { - public LoggingManager(string VPath, string APath) : base( - VPath != null ? new LoggingTarget(VPath) : null, - APath != null ? new LoggingTarget(APath) : null) - { - - } - - public override void Stop() - { - base.Stop(); - (VideoTarget as LoggingTarget)?.WriteToDisk(); - (AudioTarget as LoggingTarget)?.WriteToDisk(); - } - } -} -#endif diff --git a/Client/FileOutput/Mp4Output.cs b/Client/FileOutput/Mp4Output.cs deleted file mode 100644 index 6542335a..00000000 --- a/Client/FileOutput/Mp4Output.cs +++ /dev/null @@ -1,173 +0,0 @@ -using FFmpeg.AutoGen; -using SysDVR.Client.Player; -using System; -using static FFmpeg.AutoGen.ffmpeg; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Threading; -using System.Runtime.InteropServices; -using System.IO; - -namespace SysDVR.Client.FileOutput -{ - class Mp4OutputManager : BaseStreamManager, IDisposable - { - private bool disposedValue; - readonly Mp4Output output; - - public Mp4OutputManager(string filename, bool HasVideo, bool HasAudio) : base( - HasVideo ? new Mp4VideoTarget() : null, - HasAudio ? new Mp4AudioTarget() : null) - { - output = new Mp4Output(filename, VideoTarget as Mp4VideoTarget, AudioTarget as Mp4AudioTarget); - } - - public override void Begin() - { - // Open output handles before launching threads - output.Start(); - base.Begin(); - } - - public override void Stop() - { - // Close the output first because sometimes the other threads can get stuck (especially with USB) and prevent the recorder from finalizing the file. - output.Stop(); - base.Stop(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (!disposedValue) - { - if (disposing) - { - output.Dispose(); - } - - disposedValue = true; - } - } - } - - unsafe class Mp4Output : IDisposable - { - bool Running = false; - string Filename; - - AVFormatContext* OutCtx; - AVStream* VStream, AStream; - - Mp4VideoTarget? Vid; - Mp4AudioTarget? Aud; - - public Mp4Output(string filename, Mp4VideoTarget? vTarget, Mp4AudioTarget? aTarget) - { - Vid = vTarget; - Aud = aTarget; - Filename = filename; - } - - public void Start() - { - var OutFmt = av_guess_format(null, Filename, null); - if (OutFmt == null) throw new Exception("Couldn't find output format"); - - AVFormatContext* ctx = null; - avformat_alloc_output_context2(&ctx, OutFmt, null, null).AssertNotNeg(); - OutCtx = ctx != null ? ctx : throw new Exception("Couldn't allocate output context"); - - if (Vid is not null) - { - VStream = avformat_new_stream(OutCtx, avcodec_find_encoder(AVCodecID.AV_CODEC_ID_H264)); - if (VStream == null) throw new Exception("Couldn't allocate video stream"); - - VStream->codecpar->codec_id = AVCodecID.AV_CODEC_ID_H264; - VStream->codecpar->codec_type = AVMediaType.AVMEDIA_TYPE_VIDEO; - VStream->codecpar->width = StreamInfo.VideoWidth; - VStream->codecpar->height = StreamInfo.VideoHeight; - VStream->codecpar->format = (int)AVPixelFormat.AV_PIX_FMT_YUV420P; - - /* - * TODO: This is needed for MKV files but doesn't seem to be quite right: - * ffmpeg shows several errors and seeking in mpv doesn't work. Adding this to mp4 files breaks video in the windows 10 video player. - */ - //var (ptr, sz) = LibavUtils.AllocateH264Extradata();; - //VStream->codecpar->extradata = (byte*)ptr.ToPointer(); - //VStream->codecpar->extradata_size = sz; - } - - if (Aud is not null) - { - AStream = avformat_new_stream(OutCtx, avcodec_find_encoder(AVCodecID.AV_CODEC_ID_MP2)); - if (AStream == null) throw new Exception("Couldn't allocate audio stream"); - - AStream->id = Vid == null ? 0 : 1; - AStream->codecpar->codec_id = AVCodecID.AV_CODEC_ID_MP2; - AStream->codecpar->codec_type = AVMediaType.AVMEDIA_TYPE_AUDIO; - AStream->codecpar->sample_rate = StreamInfo.AudioSampleRate; - av_channel_layout_default(&AStream->codecpar->ch_layout, StreamInfo.AudioChannels); - AStream->codecpar->format = (int)AVSampleFormat.AV_SAMPLE_FMT_S16; - AStream->codecpar->frame_size = StreamInfo.MinAudioSamplesPerPayload; - AStream->codecpar->bit_rate = 128000; - } - - avio_open(&OutCtx->pb, Filename, AVIO_FLAG_WRITE).AssertZero(); - avformat_write_header(OutCtx, null).AssertZero(); - - object sync = new object(); - Vid?.StartWithContext(OutCtx, sync); - Aud?.StartWithContext(OutCtx, sync, AStream->id); - - var defColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("if you close SysDVR-Client via the X button the output video may become corrupted."); - Console.ForegroundColor = defColor; - - Running = true; - } - - public unsafe void Stop() - { - Aud?.Stop(); - Vid?.Stop(); - - Console.WriteLine("Finalizing file..."); - - av_write_trailer(OutCtx); - avio_close(OutCtx->pb); - avformat_free_context(OutCtx); - OutCtx = null; - - Running = false; - - Aud?.Dispose(); - } - - private bool disposedValue; - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (Running) - Stop(); - - disposedValue = true; - } - } - - ~Mp4Output() - { - Dispose(disposing: false); - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } -} diff --git a/Client/GUI/Image.cs b/Client/GUI/Image.cs new file mode 100644 index 00000000..5a85b679 --- /dev/null +++ b/Client/GUI/Image.cs @@ -0,0 +1,84 @@ +using ImGuiNET; +using SDL2; +using SysDVR.Client.Platform; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SysDVR.Client.GUI +{ + public class Image : IDisposable + { + public readonly IntPtr Texture; + public readonly int Width; + public readonly int Height; + + internal bool Disposed = false; + + private Image(IntPtr texture) + { + this.Texture = texture; + SDL.SDL_QueryTexture(Texture, out _, out _, out Width, out Height); + } + + public static Image FromFile(string filename) + { + var tex = SDL_image.IMG_LoadTexture(Program.Instance.SdlRenderer, filename); + if (tex == IntPtr.Zero) + throw new Exception($"Loading image {filename} failed: {SDL_image.IMG_GetError()}"); + + return new Image(tex); + } + + // W / w = H / h h = w * H / W + public int ScaleHeight(int width) => Height * width / Width; + + public int ScaleWidth(int height) => Width * height / Height; + + public void Dispose() + { + if (Disposed) return; + SDL.SDL_DestroyTexture(Texture); + Disposed = true; + } + } + + internal class LazyImage + { + public readonly string Filename; + private Image image = null; + + public LazyImage(string filename) + { + this.Filename = filename; + } + + public IntPtr Texture => Get().Texture; + public int Width { get; private set; } + public int Height {get; private set; } + + public Image Get() + { + if (image == null) + { + image = Image.FromFile(Filename); + Width = image.Width; + Height = image.Height; + } + + return image; + } + + static public implicit operator Image(LazyImage i) => i.Get(); + + public void Free() + { + image?.Dispose(); + image = null; + } + } +} diff --git a/Client/GUI/Interfaces.cs b/Client/GUI/Interfaces.cs new file mode 100644 index 00000000..bfd0fea4 --- /dev/null +++ b/Client/GUI/Interfaces.cs @@ -0,0 +1,68 @@ +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Diagnostics.SymbolStore; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace SysDVR.Client.GUI +{ + public abstract class View + { + public bool UsesImgui = true; + + public abstract void Draw(); + + public virtual void RawDraw() + { + Program.Instance.ClearScrren(); + } + + public virtual void BackPressed() + { + Program.Instance.PopView(); + } + + public virtual void EnterForeground() { } + public virtual void LeveForeground() { } + public virtual void Destroy() { } + } + + public static class Gui + { + public static void BeginWindow(string name) + { + ImGui.SetNextWindowSize(ImGui.GetIO().DisplaySize); + ImGui.SetNextWindowPos(Vector2.Zero); + ImGui.Begin(name, ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize); + } + + public static void CenterImage(Image image, int height) + { + var width = (int)(image.Width * ((float)height / image.Height)); + var pos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(new Vector2((ImGui.GetWindowSize().X - width) / 2, pos.Y)); + ImGui.Image(image.Texture, new Vector2(width, height)); + } + + public static void CenterText(string text) + { + var size = ImGui.CalcTextSize(text); + var pos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(new Vector2((ImGui.GetWindowSize().X - size.X) / 2, pos.Y)); + ImGui.Text(text); + } + + public static void H1() + { + ImGui.PushFont(Program.Instance.FontH1); + } + + public static void H2() + { + ImGui.PushFont(Program.Instance.FontH2); + } + } +} diff --git a/Client/GUI/MainView.cs b/Client/GUI/MainView.cs new file mode 100644 index 00000000..1fd14f0b --- /dev/null +++ b/Client/GUI/MainView.cs @@ -0,0 +1,162 @@ +using ImGuiNET; +using SysDVR.Client.Core; +using SysDVR.Client.Platform; +using SysDVR.Client.Sources; +using SysDVR.Client.Targets.Player; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace SysDVR.Client.GUI +{ + internal class MainView : View + { + byte[] text = new byte[1024]; + + readonly string UsbModeWarn; + readonly bool IsWindows; + + public MainView() + { + IsWindows = OperatingSystem.IsWindows(); + if (OperatingSystem.IsWindows()) + UsbModeWarn = "Requires USB drivers to be installed."; + else if (OperatingSystem.IsLinux()) + UsbModeWarn = "Requires udev rules to be configured correctly."; + } + + //void LaunchTcp() + //{ + // var StreamManager = new PlayerManager(true, true, false, null, null) + // { + // WindowTitle = "", + // StartFullScreen = false + // }; + + // var len = Array.IndexOf(text, 0); + // var ip = Encoding.ASCII.GetString(text, 0, len).Trim(); + // StreamManager.AddSource(new TCPBridgeSource(ip, StreamKind.Video)); + // StreamManager.AddSource(new TCPBridgeSource(ip, StreamKind.Audio)); + + // StreamManager.Begin(); + // Program.PlayerInstance = StreamManager.player; + //} + + //void LaunchStub() + //{ + // var StreamManager = new PlayerManager(true, true, false, null, null) + // { + // WindowTitle = "", + // StartFullScreen = false + // }; + + // StreamManager.AddSource(new StubSource(true, true)); + + // StreamManager.Begin(); + // Program.PlayerInstance = StreamManager.player; + //} + + bool ModeButton2(Image image, string title, int width, int height) + { + const int InnerPadding = 15; + + // This is what we're looking for: + // TITLE TITLE + // + + Gui.H2(); + var titleLen = ImGui.CalcTextSize(title); + ImGui.PopFont(); + + Vector2 imageFrame = new( + width - InnerPadding * 2, + image.ScaleHeight(width - InnerPadding * 2)); + + var imageSpaceY = height - InnerPadding * 3 - titleLen.Y; + + if (imageFrame.Y > imageSpaceY) + imageFrame = new(image.ScaleWidth((int)imageSpaceY), imageSpaceY); + + var x = ImGui.GetCursorPosX(); + var y = ImGui.GetCursorPosY(); + + ImRect bodySize = new ImRect(); + bodySize.Min = new(x, y); + bodySize.Max = new(x + width, y + height); + + var id = ImGui.GetID(title); + + if (!ImGui.ItemAdd(bodySize, id, IntPtr.Zero, ImGuiItemFlags.None)) + return false; + + var btn = ImGui.ButtonBehavior(bodySize, id, out var hovered, out var held, ImGuiButtonFlags.MouseButtonDefault); + var col = ImGui.GetColorU32((held && hovered) ? ImGuiCol.ButtonActive : hovered ? ImGuiCol.ButtonHovered : ImGuiCol.Button); + + ImGui.RenderNavHighlight(bodySize, id, 0); + ImGui.RenderFrame(bodySize.Min, bodySize.Max, col, true, 0); + + y += InnerPadding; + + // Title + ImGui.SetCursorPos(new(x + width / 2 - titleLen.X / 2, y)); + Gui.H2(); + ImGui.Text(title); + ImGui.PopFont(); + + y += InnerPadding + titleLen.Y; + + ImGui.SetCursorPos(new Vector2(x + width / 2 - imageFrame.X / 2, y)); + ImGui.Image(image.Texture, imageFrame); + + return btn; + } + + public override void Draw() + { + Gui.BeginWindow("Main"); + Gui.CenterImage(Resources.Logo, 120); + Gui.H1(); + Gui.CenterText("SysDVR-Client"); + ImGui.PopFont(); + + bool wifi = false, usb = false; + + int ModeButtonWidth = (int)(Program.Instance.UiScale * 350); + int ModeButtonHeight = (int)(Program.Instance.UiScale * 200); + var w = ImGui.GetContentRegionAvail().X; + var y = ImGui.GetCursorPosY() + 20; + + if (w < ModeButtonWidth * 2.2) + { + var center = w / 2 - ModeButtonWidth / 2; + + ImGui.SetCursorPos(new(center, y)); + wifi = ModeButton2(Resources.WifiIcon, "Network mode", ModeButtonWidth, ModeButtonHeight); + + ImGui.SetCursorPos(new(center, y + 20 + ModeButtonHeight)); + usb = ModeButton2(Resources.UsbIcon, "USB mode", ModeButtonWidth, ModeButtonHeight); + } + else + { + var center = w / 2 - (ModeButtonWidth + ModeButtonWidth + 20) / 2; + ImGui.SetCursorPos(new(center, y)); + wifi = ModeButton2(Resources.WifiIcon, "Network mode", ModeButtonWidth, ModeButtonHeight); + + ImGui.SetCursorPos(new(center + ModeButtonWidth + 20, y)); + usb = ModeButton2(Resources.UsbIcon, "USB mode", ModeButtonWidth, ModeButtonHeight); + } + + ImGui.Button("Open the guide"); + if (IsWindows) + { + ImGui.SameLine(); + ImGui.Button("Install USB driver"); + } + + ImGui.End(); + } + } +} diff --git a/Client/Help.txt b/Client/Help.txt deleted file mode 100644 index a8af866b..00000000 --- a/Client/Help.txt +++ /dev/null @@ -1,25 +0,0 @@ -To use SysDVR-Client you need .NET 6 https://dotnet.microsoft.com/download -Download .NET for your OS, don't use mono. - -You can launch SysDVR-Client with this command: `dotnet SysDVR-Client.dll` -Check out the guide on Github: https://github.com/exelix11/SysDVR/wiki - -In case of errors you may need to install the following dependencies: - -On windows: -Install https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads -On a 32 bit windows install you'll have to find an ffmpeg dlls build (called shared build) for 32 bit and copy them in this folder, 32 bit builds are not provided offcially anymore so i'm not including them. - -On linux: -Install sdl2 and ffmpeg with your package manager. -On ubuntu with apt you can use `sudo apt install ffmpeg libsdl2-dev` - -On mac: -Install sdl2 and ffmpeg with brew -`brew install SDL2` -`brew install ffmpeg` - -To stream via USB see the USB driver setup part of the guide: https://github.com/exelix11/SysDVR/wiki/USB-Driver - -In case of problems see the common issues page: https://github.com/exelix11/SysDVR/wiki/Troubleshooting - diff --git a/Client/Platform/Android/.gitignore b/Client/Platform/Android/.gitignore new file mode 100644 index 00000000..90801747 --- /dev/null +++ b/Client/Platform/Android/.gitignore @@ -0,0 +1,40 @@ +app/jni/SDL/include/ +app/jni/SDL/src/ +app/jni/SDL_Image/ +app/jni/cimgui/cimgui/ +app/src/main/assets/ +*.so + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/Client/Platform/Android/app/build.gradle b/Client/Platform/Android/app/build.gradle new file mode 100644 index 00000000..05f20d9e --- /dev/null +++ b/Client/Platform/Android/app/build.gradle @@ -0,0 +1,76 @@ +def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY'); +def buildAsApplication = !buildAsLibrary +if (buildAsApplication) { + apply plugin: 'com.android.application' +} +else { + apply plugin: 'com.android.library' +} + +android { + compileSdkVersion 31 + defaultConfig { + if (buildAsApplication) { + applicationId "exelix11.sysdvr" + } + minSdkVersion 16 + targetSdkVersion 31 + versionCode 1 + versionName "1.0" + externalNativeBuild { + ndkBuild { + arguments "APP_PLATFORM=android-16" + abiFilters 'arm64-v8a' + } + // cmake { + // arguments "-DANDROID_APP_PLATFORM=android-16", "-DANDROID_STL=c++_static" + // // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + // abiFilters 'arm64-v8a' + // } + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + } + applicationVariants.all { variant -> + tasks["merge${variant.name.capitalize()}Assets"] + .dependsOn("externalNativeBuild${variant.name.capitalize()}") + } + if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) { + sourceSets.main { + jniLibs.srcDir 'libs' + } + externalNativeBuild { + ndkBuild { + path 'jni/Android.mk' + } + // cmake { + // path 'jni/CMakeLists.txt' + // } + } + + } + lintOptions { + abortOnError false + } + + if (buildAsLibrary) { + libraryVariants.all { variant -> + variant.outputs.each { output -> + def outputFile = output.outputFile + if (outputFile != null && outputFile.name.endsWith(".aar")) { + def fileName = "exelix11.sysdvr.aar"; + output.outputFile = new File(outputFile.parent, fileName); + } + } + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') +} diff --git a/Client/Platform/Android/app/jni/Android.mk b/Client/Platform/Android/app/jni/Android.mk new file mode 100644 index 00000000..5053e7d6 --- /dev/null +++ b/Client/Platform/Android/app/jni/Android.mk @@ -0,0 +1 @@ +include $(call all-subdir-makefiles) diff --git a/Client/Platform/Android/app/jni/Application.mk b/Client/Platform/Android/app/jni/Application.mk new file mode 100644 index 00000000..023bc20d --- /dev/null +++ b/Client/Platform/Android/app/jni/Application.mk @@ -0,0 +1,10 @@ + +# Uncomment this if you're using STL in your project +# You can find more information here: +# https://developer.android.com/ndk/guides/cpp-support +# APP_STL := c++_shared + +APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 + +# Min runtime API level +APP_PLATFORM=android-16 diff --git a/Client/Platform/Android/app/jni/CMakeLists.txt b/Client/Platform/Android/app/jni/CMakeLists.txt new file mode 100644 index 00000000..3d49cf34 --- /dev/null +++ b/Client/Platform/Android/app/jni/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.6) + +project(GAME) + +# armeabi-v7a requires cpufeatures library +# include(AndroidNdkModules) +# android_ndk_import_module_cpufeatures() + + +# SDL sources are in a subfolder named "SDL" +add_subdirectory(SDL) + +# Compilation of companion libraries +#add_subdirectory(SDL_image) +#add_subdirectory(SDL_mixer) +#add_subdirectory(SDL_ttf) + +# Your game and its CMakeLists.txt are in a subfolder named "src" +add_subdirectory(src) + diff --git a/Client/Platform/Android/app/jni/Client/Android.mk b/Client/Platform/Android/app/jni/Client/Android.mk new file mode 100644 index 00000000..7c38e73d --- /dev/null +++ b/Client/Platform/Android/app/jni/Client/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := Client-prebuilt + +LOCAL_SRC_FILES := libClient.so + +LOCAL_SHARED_LIBRARIES := SDL2 SDL2_image cimgui log + +include $(PREBUILT_SHARED_LIBRARY) diff --git a/Client/Platform/Android/app/jni/SDL/Android.mk b/Client/Platform/Android/app/jni/SDL/Android.mk new file mode 100644 index 00000000..9c9a1600 --- /dev/null +++ b/Client/Platform/Android/app/jni/SDL/Android.mk @@ -0,0 +1,130 @@ +LOCAL_PATH := $(call my-dir) + +########################### +# +# SDL shared library +# +########################### + +include $(CLEAR_VARS) + +LOCAL_MODULE := SDL2 + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/include + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES) + +LOCAL_SRC_FILES := \ + $(subst $(LOCAL_PATH)/,, \ + $(wildcard $(LOCAL_PATH)/src/*.c) \ + $(wildcard $(LOCAL_PATH)/src/audio/*.c) \ + $(wildcard $(LOCAL_PATH)/src/audio/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/audio/dummy/*.c) \ + $(wildcard $(LOCAL_PATH)/src/audio/aaudio/*.c) \ + $(wildcard $(LOCAL_PATH)/src/audio/openslES/*.c) \ + $(LOCAL_PATH)/src/atomic/SDL_atomic.c.arm \ + $(LOCAL_PATH)/src/atomic/SDL_spinlock.c.arm \ + $(wildcard $(LOCAL_PATH)/src/core/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/cpuinfo/*.c) \ + $(wildcard $(LOCAL_PATH)/src/dynapi/*.c) \ + $(wildcard $(LOCAL_PATH)/src/events/*.c) \ + $(wildcard $(LOCAL_PATH)/src/file/*.c) \ + $(wildcard $(LOCAL_PATH)/src/haptic/*.c) \ + $(wildcard $(LOCAL_PATH)/src/haptic/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/hidapi/*.c) \ + $(wildcard $(LOCAL_PATH)/src/hidapi/android/*.cpp) \ + $(wildcard $(LOCAL_PATH)/src/joystick/*.c) \ + $(wildcard $(LOCAL_PATH)/src/joystick/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/joystick/hidapi/*.c) \ + $(wildcard $(LOCAL_PATH)/src/joystick/virtual/*.c) \ + $(wildcard $(LOCAL_PATH)/src/loadso/dlopen/*.c) \ + $(wildcard $(LOCAL_PATH)/src/locale/*.c) \ + $(wildcard $(LOCAL_PATH)/src/locale/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/misc/*.c) \ + $(wildcard $(LOCAL_PATH)/src/misc/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/power/*.c) \ + $(wildcard $(LOCAL_PATH)/src/power/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/filesystem/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/sensor/*.c) \ + $(wildcard $(LOCAL_PATH)/src/sensor/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/render/*.c) \ + $(wildcard $(LOCAL_PATH)/src/render/*/*.c) \ + $(wildcard $(LOCAL_PATH)/src/stdlib/*.c) \ + $(wildcard $(LOCAL_PATH)/src/thread/*.c) \ + $(wildcard $(LOCAL_PATH)/src/thread/pthread/*.c) \ + $(wildcard $(LOCAL_PATH)/src/timer/*.c) \ + $(wildcard $(LOCAL_PATH)/src/timer/unix/*.c) \ + $(wildcard $(LOCAL_PATH)/src/video/*.c) \ + $(wildcard $(LOCAL_PATH)/src/video/android/*.c) \ + $(wildcard $(LOCAL_PATH)/src/video/yuv2rgb/*.c) \ + $(wildcard $(LOCAL_PATH)/src/test/*.c)) + +LOCAL_CFLAGS += -DGL_GLEXT_PROTOTYPES +LOCAL_CFLAGS += \ + -Wall -Wextra \ + -Wdocumentation \ + -Wmissing-prototypes \ + -Wunreachable-code-break \ + -Wunneeded-internal-declaration \ + -Wmissing-variable-declarations \ + -Wfloat-conversion \ + -Wshorten-64-to-32 \ + -Wunreachable-code-return \ + -Wshift-sign-overflow \ + -Wstrict-prototypes \ + -Wkeyword-macro \ + +# Warnings we haven't fixed (yet) +LOCAL_CFLAGS += -Wno-unused-parameter -Wno-sign-compare + +LOCAL_CXXFLAGS += -std=gnu++11 + +LOCAL_LDLIBS := -ldl -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid + +LOCAL_LDFLAGS := -Wl,--no-undefined + +ifeq ($(NDK_DEBUG),1) + cmd-strip := +endif + +LOCAL_STATIC_LIBRARIES := cpufeatures + +include $(BUILD_SHARED_LIBRARY) + + +########################### +# +# SDL static library +# +########################### + +LOCAL_MODULE := SDL2_static + +LOCAL_MODULE_FILENAME := libSDL2 + +LOCAL_LDLIBS := + +LOCAL_LDFLAGS := + +LOCAL_EXPORT_LDLIBS := -ldl -lGLESv1_CM -lGLESv2 -llog -landroid + +include $(BUILD_STATIC_LIBRARY) + + +########################### +# +# SDL main static library +# +########################### + +include $(CLEAR_VARS) + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/include + +LOCAL_MODULE := SDL2_main + +LOCAL_MODULE_FILENAME := libSDL2main + +include $(BUILD_STATIC_LIBRARY) + +$(call import-module,android/cpufeatures) diff --git a/Client/Platform/Android/app/jni/cimgui/Android.mk b/Client/Platform/Android/app/jni/cimgui/Android.mk new file mode 100644 index 00000000..119d6562 --- /dev/null +++ b/Client/Platform/Android/app/jni/cimgui/Android.mk @@ -0,0 +1,32 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := cimgui + +SDL_PATH := ../SDL + +LOCAL_CFLAGS := \ + -DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1 \ + -DIMGUI_IMPL_API='extern "C" ' + +LOCAL_C_INCLUDES := \ + $(LOCAL_PATH)/$(SDL_PATH)/include \ + $(LOCAL_PATH)/cimgui/imgui + +# Add your application source files here... +LOCAL_SRC_FILES := \ + cimgui/imgui/imgui.cpp \ + cimgui/imgui/imgui_tables.cpp \ + cimgui/imgui/imgui_draw.cpp \ + cimgui/imgui/imgui_demo.cpp \ + cimgui/imgui/imgui_widgets.cpp \ + cimgui/imgui/backends/imgui_impl_sdl2.cpp \ + cimgui/imgui/backends/imgui_impl_sdlrenderer2.cpp \ + cimgui/cimgui.cpp + +LOCAL_SHARED_LIBRARIES := SDL2 SDL2_image + +LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid + +include $(BUILD_SHARED_LIBRARY) diff --git a/Client/Platform/Android/app/jni/src/Android.mk b/Client/Platform/Android/app/jni/src/Android.mk new file mode 100644 index 00000000..affe86f2 --- /dev/null +++ b/Client/Platform/Android/app/jni/src/Android.mk @@ -0,0 +1,17 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := main + +SDL_PATH := ../SDL + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include + +LOCAL_SRC_FILES := main.c + +LOCAL_SHARED_LIBRARIES := SDL2 SDL2_image cimgui Client-prebuilt + +LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid + +include $(BUILD_SHARED_LIBRARY) \ No newline at end of file diff --git a/Client/Platform/Android/app/jni/src/main.c b/Client/Platform/Android/app/jni/src/main.c new file mode 100644 index 00000000..dfed4371 --- /dev/null +++ b/Client/Platform/Android/app/jni/src/main.c @@ -0,0 +1,30 @@ +// SDL.h provides the SDL_Main() symbol and must be kept +#include "SDL.h" +#include + +struct NativeInitBlock +{ + void* Version; + void* PrintFunction; +}; + +extern void sysdvr_entrypoint(struct NativeInitBlock* init); + +void LOG(const char* string) +{ + __android_log_print(ANDROID_LOG_ERROR, "SysDVRLogger", "%s", string); +} + +struct NativeInitBlock g_native = +{ + .Version = (void*)1, + .PrintFunction = (void*)LOG +}; + +int main(int argc, char *argv[]) +{ + LOG("sdl main called()"); + sysdvr_entrypoint(&g_native); + LOG("sysdvr_entrypoint returned"); + return 0; +} diff --git a/Client/Platform/Android/app/proguard-rules.pro b/Client/Platform/Android/app/proguard-rules.pro new file mode 100644 index 00000000..eaf0e916 --- /dev/null +++ b/Client/Platform/Android/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in [sdk]/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Client/Platform/Android/app/src/main/AndroidManifest.xml b/Client/Platform/Android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a1a2f237 --- /dev/null +++ b/Client/Platform/Android/app/src/main/AndroidManifest.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client/Platform/Android/app/src/main/java/exelix11/sysdvr/sysdvrActivity.java b/Client/Platform/Android/app/src/main/java/exelix11/sysdvr/sysdvrActivity.java new file mode 100644 index 00000000..dd4f1999 --- /dev/null +++ b/Client/Platform/Android/app/src/main/java/exelix11/sysdvr/sysdvrActivity.java @@ -0,0 +1,7 @@ +package exelix11.sysdvr; + +import org.libsdl.app.SDLActivity; + +public class sysdvrActivity extends SDLActivity +{ +} diff --git a/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDevice.java b/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDevice.java new file mode 100644 index 00000000..955df5d1 --- /dev/null +++ b/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDevice.java @@ -0,0 +1,22 @@ +package org.libsdl.app; + +import android.hardware.usb.UsbDevice; + +interface HIDDevice +{ + public int getId(); + public int getVendorId(); + public int getProductId(); + public String getSerialNumber(); + public int getVersion(); + public String getManufacturerName(); + public String getProductName(); + public UsbDevice getDevice(); + public boolean open(); + public int sendFeatureReport(byte[] report); + public int sendOutputReport(byte[] report); + public boolean getFeatureReport(byte[] report); + public void setFrozen(boolean frozen); + public void close(); + public void shutdown(); +} diff --git a/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java new file mode 100644 index 00000000..ee5521fd --- /dev/null +++ b/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -0,0 +1,650 @@ +package org.libsdl.app; + +import android.content.Context; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothGattService; +import android.hardware.usb.UsbDevice; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.os.*; + +//import com.android.internal.util.HexDump; + +import java.lang.Runnable; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.UUID; + +class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { + + private static final String TAG = "hidapi"; + private HIDDeviceManager mManager; + private BluetoothDevice mDevice; + private int mDeviceId; + private BluetoothGatt mGatt; + private boolean mIsRegistered = false; + private boolean mIsConnected = false; + private boolean mIsChromebook = false; + private boolean mIsReconnecting = false; + private boolean mFrozen = false; + private LinkedList mOperations; + GattOperation mCurrentOperation = null; + private Handler mHandler; + + private static final int TRANSPORT_AUTO = 0; + private static final int TRANSPORT_BREDR = 1; + private static final int TRANSPORT_LE = 2; + + private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; + + static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); + static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); + static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; + + static class GattOperation { + private enum Operation { + CHR_READ, + CHR_WRITE, + ENABLE_NOTIFICATION + } + + Operation mOp; + UUID mUuid; + byte[] mValue; + BluetoothGatt mGatt; + boolean mResult = true; + + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + } + + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mValue = value; + } + + public void run() { + // This is executed in main thread + BluetoothGattCharacteristic chr; + + switch (mOp) { + case CHR_READ: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Reading characteristic " + chr.getUuid()); + if (!mGatt.readCharacteristic(chr)) { + Log.e(TAG, "Unable to read characteristic " + mUuid.toString()); + mResult = false; + break; + } + mResult = true; + break; + case CHR_WRITE: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value)); + chr.setValue(mValue); + if (!mGatt.writeCharacteristic(chr)) { + Log.e(TAG, "Unable to write characteristic " + mUuid.toString()); + mResult = false; + break; + } + mResult = true; + break; + case ENABLE_NOTIFICATION: + chr = getCharacteristic(mUuid); + //Log.v(TAG, "Writing descriptor of " + chr.getUuid()); + if (chr != null) { + BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); + if (cccd != null) { + int properties = chr.getProperties(); + byte[] value; + if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) { + value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; + } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) { + value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; + } else { + Log.e(TAG, "Unable to start notifications on input characteristic"); + mResult = false; + return; + } + + mGatt.setCharacteristicNotification(chr, true); + cccd.setValue(value); + if (!mGatt.writeDescriptor(cccd)) { + Log.e(TAG, "Unable to write descriptor " + mUuid.toString()); + mResult = false; + return; + } + mResult = true; + } + } + } + } + + public boolean finish() { + return mResult; + } + + private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { + BluetoothGattService valveService = mGatt.getService(steamControllerService); + if (valveService == null) + return null; + return valveService.getCharacteristic(uuid); + } + + static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) { + return new GattOperation(gatt, Operation.CHR_READ, uuid); + } + + static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) { + return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value); + } + + static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { + return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); + } + } + + public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { + mManager = manager; + mDevice = device; + mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); + mIsRegistered = false; + mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + mOperations = new LinkedList(); + mHandler = new Handler(Looper.getMainLooper()); + + mGatt = connectGatt(); + // final HIDDeviceBLESteamController finalThis = this; + // mHandler.postDelayed(new Runnable() { + // @Override + // public void run() { + // finalThis.checkConnectionForChromebookIssue(); + // } + // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); + } + + public String getIdentifier() { + return String.format("SteamController.%s", mDevice.getAddress()); + } + + public BluetoothGatt getGatt() { + return mGatt; + } + + // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead + // of TRANSPORT_LE. Let's force ourselves to connect low energy. + private BluetoothGatt connectGatt(boolean managed) { + if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) { + try { + return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE); + } catch (Exception e) { + return mDevice.connectGatt(mManager.getContext(), managed, this); + } + } else { + return mDevice.connectGatt(mManager.getContext(), managed, this); + } + } + + private BluetoothGatt connectGatt() { + return connectGatt(false); + } + + protected int getConnectionState() { + + Context context = mManager.getContext(); + if (context == null) { + // We are lacking any context to get our Bluetooth information. We'll just assume disconnected. + return BluetoothProfile.STATE_DISCONNECTED; + } + + BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE); + if (btManager == null) { + // This device doesn't support Bluetooth. We should never be here, because how did + // we instantiate a device to start with? + return BluetoothProfile.STATE_DISCONNECTED; + } + + return btManager.getConnectionState(mDevice, BluetoothProfile.GATT); + } + + public void reconnect() { + + if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) { + mGatt.disconnect(); + mGatt = connectGatt(); + } + + } + + protected void checkConnectionForChromebookIssue() { + if (!mIsChromebook) { + // We only do this on Chromebooks, because otherwise it's really annoying to just attempt + // over and over. + return; + } + + int connectionState = getConnectionState(); + + switch (connectionState) { + case BluetoothProfile.STATE_CONNECTED: + if (!mIsConnected) { + // We are in the Bad Chromebook Place. We can force a disconnect + // to try to recover. + Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect."); + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + } + else if (!isRegistered()) { + if (mGatt.getServices().size() > 0) { + Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover."); + probeService(this); + } + else { + Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover."); + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + } + } + else { + Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!"); + return; + } + break; + + case BluetoothProfile.STATE_DISCONNECTED: + Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover."); + + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + break; + + case BluetoothProfile.STATE_CONNECTING: + Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer."); + break; + } + + final HIDDeviceBLESteamController finalThis = this; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + finalThis.checkConnectionForChromebookIssue(); + } + }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); + } + + private boolean isRegistered() { + return mIsRegistered; + } + + private void setRegistered() { + mIsRegistered = true; + } + + private boolean probeService(HIDDeviceBLESteamController controller) { + + if (isRegistered()) { + return true; + } + + if (!mIsConnected) { + return false; + } + + Log.v(TAG, "probeService controller=" + controller); + + for (BluetoothGattService service : mGatt.getServices()) { + if (service.getUuid().equals(steamControllerService)) { + Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); + + for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { + if (chr.getUuid().equals(inputCharacteristic)) { + Log.v(TAG, "Found input characteristic"); + // Start notifications + BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); + if (cccd != null) { + enableNotification(chr.getUuid()); + } + } + } + return true; + } + } + + if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) { + Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us."); + mIsConnected = false; + mIsReconnecting = true; + mGatt.disconnect(); + mGatt = connectGatt(false); + } + + return false; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private void finishCurrentGattOperation() { + GattOperation op = null; + synchronized (mOperations) { + if (mCurrentOperation != null) { + op = mCurrentOperation; + mCurrentOperation = null; + } + } + if (op != null) { + boolean result = op.finish(); // TODO: Maybe in main thread as well? + + // Our operation failed, let's add it back to the beginning of our queue. + if (!result) { + mOperations.addFirst(op); + } + } + executeNextGattOperation(); + } + + private void executeNextGattOperation() { + synchronized (mOperations) { + if (mCurrentOperation != null) + return; + + if (mOperations.isEmpty()) + return; + + mCurrentOperation = mOperations.removeFirst(); + } + + // Run in main thread + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mOperations) { + if (mCurrentOperation == null) { + Log.e(TAG, "Current operation null in executor?"); + return; + } + + mCurrentOperation.run(); + // now wait for the GATT callback and when it comes, finish this operation + } + } + }); + } + + private void queueGattOperation(GattOperation op) { + synchronized (mOperations) { + mOperations.add(op); + } + executeNextGattOperation(); + } + + private void enableNotification(UUID chrUuid) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); + queueGattOperation(op); + } + + public void writeCharacteristic(UUID uuid, byte[] value) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); + queueGattOperation(op); + } + + public void readCharacteristic(UUID uuid) { + GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); + queueGattOperation(op); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////// BluetoothGattCallback overridden methods + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + public void onConnectionStateChange(BluetoothGatt g, int status, int newState) { + //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState); + mIsReconnecting = false; + if (newState == 2) { + mIsConnected = true; + // Run directly, without GattOperation + if (!isRegistered()) { + mHandler.post(new Runnable() { + @Override + public void run() { + mGatt.discoverServices(); + } + }); + } + } + else if (newState == 0) { + mIsConnected = false; + } + + // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent. + } + + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + //Log.v(TAG, "onServicesDiscovered status=" + status); + if (status == 0) { + if (gatt.getServices().size() == 0) { + Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack."); + mIsReconnecting = true; + mIsConnected = false; + gatt.disconnect(); + mGatt = connectGatt(false); + } + else { + probeService(this); + } + } + } + + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid()); + + if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { + mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue()); + } + + finishCurrentGattOperation(); + } + + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid()); + + if (characteristic.getUuid().equals(reportCharacteristic)) { + // Only register controller with the native side once it has been fully configured + if (!isRegistered()) { + Log.v(TAG, "Registering Steam Controller with ID: " + getId()); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0); + setRegistered(); + } + } + + finishCurrentGattOperation(); + } + + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + // Enable this for verbose logging of controller input reports + //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); + + if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { + mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); + } + } + + public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + //Log.v(TAG, "onDescriptorRead status=" + status); + } + + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); + //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); + + if (chr.getUuid().equals(inputCharacteristic)) { + boolean hasWrittenInputDescriptor = true; + BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); + if (reportChr != null) { + Log.v(TAG, "Writing report characteristic to enter valve mode"); + reportChr.setValue(enterValveMode); + gatt.writeCharacteristic(reportChr); + } + } + + finishCurrentGattOperation(); + } + + public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { + //Log.v(TAG, "onReliableWriteCompleted status=" + status); + } + + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + //Log.v(TAG, "onReadRemoteRssi status=" + status); + } + + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + //Log.v(TAG, "onMtuChanged status=" + status); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + //////// Public API + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public int getId() { + return mDeviceId; + } + + @Override + public int getVendorId() { + // Valve Corporation + final int VALVE_USB_VID = 0x28DE; + return VALVE_USB_VID; + } + + @Override + public int getProductId() { + // We don't have an easy way to query from the Bluetooth device, but we know what it is + final int D0G_BLE2_PID = 0x1106; + return D0G_BLE2_PID; + } + + @Override + public String getSerialNumber() { + // This will be read later via feature report by Steam + return "12345"; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public String getManufacturerName() { + return "Valve Corporation"; + } + + @Override + public String getProductName() { + return "Steam Controller"; + } + + @Override + public UsbDevice getDevice() { + return null; + } + + @Override + public boolean open() { + return true; + } + + @Override + public int sendFeatureReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return -1; + } + + // We need to skip the first byte, as that doesn't go over the air + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report)); + writeCharacteristic(reportCharacteristic, actual_report); + return report.length; + } + + @Override + public int sendOutputReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return -1; + } + + //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report)); + writeCharacteristic(reportCharacteristic, report); + return report.length; + } + + @Override + public boolean getFeatureReport(byte[] report) { + if (!isRegistered()) { + Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); + if (mIsConnected) { + probeService(this); + } + return false; + } + + //Log.v(TAG, "getFeatureReport"); + readCharacteristic(reportCharacteristic); + return true; + } + + @Override + public void close() { + } + + @Override + public void setFrozen(boolean frozen) { + mFrozen = frozen; + } + + @Override + public void shutdown() { + close(); + + BluetoothGatt g = mGatt; + if (g != null) { + g.disconnect(); + g.close(); + mGatt = null; + } + mManager = null; + mIsRegistered = false; + mIsConnected = false; + mOperations.clear(); + } + +} + diff --git a/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java new file mode 100644 index 00000000..6f7013b2 --- /dev/null +++ b/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -0,0 +1,683 @@ +package org.libsdl.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.os.Build; +import android.util.Log; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.hardware.usb.*; +import android.os.Handler; +import android.os.Looper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +public class HIDDeviceManager { + private static final String TAG = "hidapi"; + private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION"; + + private static HIDDeviceManager sManager; + private static int sManagerRefCount = 0; + + public static HIDDeviceManager acquire(Context context) { + if (sManagerRefCount == 0) { + sManager = new HIDDeviceManager(context); + } + ++sManagerRefCount; + return sManager; + } + + public static void release(HIDDeviceManager manager) { + if (manager == sManager) { + --sManagerRefCount; + if (sManagerRefCount == 0) { + sManager.close(); + sManager = null; + } + } + } + + private Context mContext; + private HashMap mDevicesById = new HashMap(); + private HashMap mBluetoothDevices = new HashMap(); + private int mNextDeviceId = 0; + private SharedPreferences mSharedPreferences = null; + private boolean mIsChromebook = false; + private UsbManager mUsbManager; + private Handler mHandler; + private BluetoothManager mBluetoothManager; + private List mLastBluetoothDevices; + + private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDeviceAttached(usbDevice); + } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDeviceDetached(usbDevice); + } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); + } + } + }; + + private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + // Bluetooth device was connected. If it was a Steam Controller, handle it + if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.d(TAG, "Bluetooth device connected: " + device); + + if (isSteamController(device)) { + connectBluetoothDevice(device); + } + } + + // Bluetooth device was disconnected, remove from controller manager (if any) + if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.d(TAG, "Bluetooth device disconnected: " + device); + + disconnectBluetoothDevice(device); + } + } + }; + + private HIDDeviceManager(final Context context) { + mContext = context; + + HIDDeviceRegisterCallback(); + + mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); + mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + +// if (shouldClear) { +// SharedPreferences.Editor spedit = mSharedPreferences.edit(); +// spedit.clear(); +// spedit.commit(); +// } +// else + { + mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); + } + } + + public Context getContext() { + return mContext; + } + + public int getDeviceIDForIdentifier(String identifier) { + SharedPreferences.Editor spedit = mSharedPreferences.edit(); + + int result = mSharedPreferences.getInt(identifier, 0); + if (result == 0) { + result = mNextDeviceId++; + spedit.putInt("next_device_id", mNextDeviceId); + } + + spedit.putInt(identifier, result); + spedit.commit(); + return result; + } + + private void initializeUSB() { + mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); + if (mUsbManager == null) { + return; + } + + /* + // Logging + for (UsbDevice device : mUsbManager.getDeviceList().values()) { + Log.i(TAG,"Path: " + device.getDeviceName()); + Log.i(TAG,"Manufacturer: " + device.getManufacturerName()); + Log.i(TAG,"Product: " + device.getProductName()); + Log.i(TAG,"ID: " + device.getDeviceId()); + Log.i(TAG,"Class: " + device.getDeviceClass()); + Log.i(TAG,"Protocol: " + device.getDeviceProtocol()); + Log.i(TAG,"Vendor ID " + device.getVendorId()); + Log.i(TAG,"Product ID: " + device.getProductId()); + Log.i(TAG,"Interface count: " + device.getInterfaceCount()); + Log.i(TAG,"---------------------------------------"); + + // Get interface details + for (int index = 0; index < device.getInterfaceCount(); index++) { + UsbInterface mUsbInterface = device.getInterface(index); + Log.i(TAG," ***** *****"); + Log.i(TAG," Interface index: " + index); + Log.i(TAG," Interface ID: " + mUsbInterface.getId()); + Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass()); + Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass()); + Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); + Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); + + // Get endpoint details + for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) + { + UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); + Log.i(TAG," ++++ ++++ ++++"); + Log.i(TAG," Endpoint index: " + epi); + Log.i(TAG," Attributes: " + mEndpoint.getAttributes()); + Log.i(TAG," Direction: " + mEndpoint.getDirection()); + Log.i(TAG," Number: " + mEndpoint.getEndpointNumber()); + Log.i(TAG," Interval: " + mEndpoint.getInterval()); + Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize()); + Log.i(TAG," Type: " + mEndpoint.getType()); + } + } + } + Log.i(TAG," No more devices connected."); + */ + + // Register for USB broadcasts and permission completions + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); + mContext.registerReceiver(mUsbBroadcast, filter); + + for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { + handleUsbDeviceAttached(usbDevice); + } + } + + UsbManager getUSBManager() { + return mUsbManager; + } + + private void shutdownUSB() { + try { + mContext.unregisterReceiver(mUsbBroadcast); + } catch (Exception e) { + // We may not have registered, that's okay + } + } + + private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) { + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { + return true; + } + if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) { + return true; + } + return false; + } + + private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) { + final int XB360_IFACE_SUBCLASS = 93; + final int XB360_IFACE_PROTOCOL = 1; // Wired + final int XB360W_IFACE_PROTOCOL = 129; // Wireless + final int[] SUPPORTED_VENDORS = { + 0x0079, // GPD Win 2 + 0x044f, // Thrustmaster + 0x045e, // Microsoft + 0x046d, // Logitech + 0x056e, // Elecom + 0x06a3, // Saitek + 0x0738, // Mad Catz + 0x07ff, // Mad Catz + 0x0e6f, // PDP + 0x0f0d, // Hori + 0x1038, // SteelSeries + 0x11c9, // Nacon + 0x12ab, // Unknown + 0x1430, // RedOctane + 0x146b, // BigBen + 0x1532, // Razer Sabertooth + 0x15e4, // Numark + 0x162e, // Joytech + 0x1689, // Razer Onza + 0x1949, // Lab126, Inc. + 0x1bad, // Harmonix + 0x20d6, // PowerA + 0x24c6, // PowerA + 0x2c22, // Qanba + 0x2dc8, // 8BitDo + 0x9886, // ASTRO Gaming + }; + + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && + usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS && + (usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL || + usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) { + int vendor_id = usbDevice.getVendorId(); + for (int supportedVid : SUPPORTED_VENDORS) { + if (vendor_id == supportedVid) { + return true; + } + } + } + return false; + } + + private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) { + final int XB1_IFACE_SUBCLASS = 71; + final int XB1_IFACE_PROTOCOL = 208; + final int[] SUPPORTED_VENDORS = { + 0x044f, // Thrustmaster + 0x045e, // Microsoft + 0x0738, // Mad Catz + 0x0e6f, // PDP + 0x0f0d, // Hori + 0x10f5, // Turtle Beach + 0x1532, // Razer Wildcat + 0x20d6, // PowerA + 0x24c6, // PowerA + 0x2dc8, // 8BitDo + 0x2e24, // Hyperkin + }; + + if (usbInterface.getId() == 0 && + usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && + usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS && + usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) { + int vendor_id = usbDevice.getVendorId(); + for (int supportedVid : SUPPORTED_VENDORS) { + if (vendor_id == supportedVid) { + return true; + } + } + } + return false; + } + + private void handleUsbDeviceAttached(UsbDevice usbDevice) { + connectHIDDeviceUSB(usbDevice); + } + + private void handleUsbDeviceDetached(UsbDevice usbDevice) { + List devices = new ArrayList(); + for (HIDDevice device : mDevicesById.values()) { + if (usbDevice.equals(device.getDevice())) { + devices.add(device.getId()); + } + } + for (int id : devices) { + HIDDevice device = mDevicesById.get(id); + mDevicesById.remove(id); + device.shutdown(); + HIDDeviceDisconnected(id); + } + } + + private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) { + for (HIDDevice device : mDevicesById.values()) { + if (usbDevice.equals(device.getDevice())) { + boolean opened = false; + if (permission_granted) { + opened = device.open(); + } + HIDDeviceOpenResult(device.getId(), opened); + } + } + } + + private void connectHIDDeviceUSB(UsbDevice usbDevice) { + synchronized (this) { + int interface_mask = 0; + for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) { + UsbInterface usbInterface = usbDevice.getInterface(interface_index); + if (isHIDDeviceInterface(usbDevice, usbInterface)) { + // Check to see if we've already added this interface + // This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive + int interface_id = usbInterface.getId(); + if ((interface_mask & (1 << interface_id)) != 0) { + continue; + } + interface_mask |= (1 << interface_id); + + HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); + int id = device.getId(); + mDevicesById.put(id, device); + HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol()); + } + } + } + } + + private void initializeBluetooth() { + Log.d(TAG, "Initializing Bluetooth"); + + if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ && + mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); + return; + } + + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) { + Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE"); + return; + } + + // Find bonded bluetooth controllers and create SteamControllers for them + mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE); + if (mBluetoothManager == null) { + // This device doesn't support Bluetooth. + return; + } + + BluetoothAdapter btAdapter = mBluetoothManager.getAdapter(); + if (btAdapter == null) { + // This device has Bluetooth support in the codebase, but has no available adapters. + return; + } + + // Get our bonded devices. + for (BluetoothDevice device : btAdapter.getBondedDevices()) { + + Log.d(TAG, "Bluetooth device available: " + device); + if (isSteamController(device)) { + connectBluetoothDevice(device); + } + + } + + // NOTE: These don't work on Chromebooks, to my undying dismay. + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + mContext.registerReceiver(mBluetoothBroadcast, filter); + + if (mIsChromebook) { + mHandler = new Handler(Looper.getMainLooper()); + mLastBluetoothDevices = new ArrayList(); + + // final HIDDeviceManager finalThis = this; + // mHandler.postDelayed(new Runnable() { + // @Override + // public void run() { + // finalThis.chromebookConnectionHandler(); + // } + // }, 5000); + } + } + + private void shutdownBluetooth() { + try { + mContext.unregisterReceiver(mBluetoothBroadcast); + } catch (Exception e) { + // We may not have registered, that's okay + } + } + + // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly. + // This function provides a sort of dummy version of that, watching for changes in the + // connected devices and attempting to add controllers as things change. + public void chromebookConnectionHandler() { + if (!mIsChromebook) { + return; + } + + ArrayList disconnected = new ArrayList(); + ArrayList connected = new ArrayList(); + + List currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); + + for (BluetoothDevice bluetoothDevice : currentConnected) { + if (!mLastBluetoothDevices.contains(bluetoothDevice)) { + connected.add(bluetoothDevice); + } + } + for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) { + if (!currentConnected.contains(bluetoothDevice)) { + disconnected.add(bluetoothDevice); + } + } + + mLastBluetoothDevices = currentConnected; + + for (BluetoothDevice bluetoothDevice : disconnected) { + disconnectBluetoothDevice(bluetoothDevice); + } + for (BluetoothDevice bluetoothDevice : connected) { + connectBluetoothDevice(bluetoothDevice); + } + + final HIDDeviceManager finalThis = this; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + finalThis.chromebookConnectionHandler(); + } + }, 10000); + } + + public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { + Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice); + synchronized (this) { + if (mBluetoothDevices.containsKey(bluetoothDevice)) { + Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect"); + + HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); + device.reconnect(); + + return false; + } + HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice); + int id = device.getId(); + mBluetoothDevices.put(bluetoothDevice, device); + mDevicesById.put(id, device); + + // The Steam Controller will mark itself connected once initialization is complete + } + return true; + } + + public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { + synchronized (this) { + HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); + if (device == null) + return; + + int id = device.getId(); + mBluetoothDevices.remove(bluetoothDevice); + mDevicesById.remove(id); + device.shutdown(); + HIDDeviceDisconnected(id); + } + } + + public boolean isSteamController(BluetoothDevice bluetoothDevice) { + // Sanity check. If you pass in a null device, by definition it is never a Steam Controller. + if (bluetoothDevice == null) { + return false; + } + + // If the device has no local name, we really don't want to try an equality check against it. + if (bluetoothDevice.getName() == null) { + return false; + } + + return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); + } + + private void close() { + shutdownUSB(); + shutdownBluetooth(); + synchronized (this) { + for (HIDDevice device : mDevicesById.values()) { + device.shutdown(); + } + mDevicesById.clear(); + mBluetoothDevices.clear(); + HIDDeviceReleaseCallback(); + } + } + + public void setFrozen(boolean frozen) { + synchronized (this) { + for (HIDDevice device : mDevicesById.values()) { + device.setFrozen(frozen); + } + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private HIDDevice getDevice(int id) { + synchronized (this) { + HIDDevice result = mDevicesById.get(id); + if (result == null) { + Log.v(TAG, "No device for id: " + id); + Log.v(TAG, "Available devices: " + mDevicesById.keySet()); + } + return result; + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////// JNI interface functions + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + public boolean initialize(boolean usb, boolean bluetooth) { + Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); + + if (usb) { + initializeUSB(); + } + if (bluetooth) { + initializeBluetooth(); + } + return true; + } + + public boolean openDevice(int deviceID) { + Log.v(TAG, "openDevice deviceID=" + deviceID); + HIDDevice device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return false; + } + + // Look to see if this is a USB device and we have permission to access it + UsbDevice usbDevice = device.getDevice(); + if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) { + HIDDeviceOpenPending(deviceID); + try { + final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31 + int flags; + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { + flags = FLAG_MUTABLE; + } else { + flags = 0; + } + mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags)); + } catch (Exception e) { + Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); + HIDDeviceOpenResult(deviceID, false); + } + return false; + } + + try { + return device.open(); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return false; + } + + public int sendOutputReport(int deviceID, byte[] report) { + try { + //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return -1; + } + + return device.sendOutputReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return -1; + } + + public int sendFeatureReport(int deviceID, byte[] report) { + try { + //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return -1; + } + + return device.sendFeatureReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return -1; + } + + public boolean getFeatureReport(int deviceID, byte[] report) { + try { + //Log.v(TAG, "getFeatureReport deviceID=" + deviceID); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return false; + } + + return device.getFeatureReport(report); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + return false; + } + + public void closeDevice(int deviceID) { + try { + Log.v(TAG, "closeDevice deviceID=" + deviceID); + HIDDevice device; + device = getDevice(deviceID); + if (device == null) { + HIDDeviceDisconnected(deviceID); + return; + } + + device.close(); + } catch (Exception e) { + Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); + } + } + + + ////////////////////////////////////////////////////////////////////////////////////////////////////// + /////////////// Native methods + ////////////////////////////////////////////////////////////////////////////////////////////////////// + + private native void HIDDeviceRegisterCallback(); + private native void HIDDeviceReleaseCallback(); + + native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol); + native void HIDDeviceOpenPending(int deviceID); + native void HIDDeviceOpenResult(int deviceID, boolean opened); + native void HIDDeviceDisconnected(int deviceID); + + native void HIDDeviceInputReport(int deviceID, byte[] report); + native void HIDDeviceFeatureReport(int deviceID, byte[] report); +} diff --git a/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java new file mode 100644 index 00000000..bfe0cf95 --- /dev/null +++ b/Client/Platform/Android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java @@ -0,0 +1,309 @@ +package org.libsdl.app; + +import android.hardware.usb.*; +import android.os.Build; +import android.util.Log; +import java.util.Arrays; + +class HIDDeviceUSB implements HIDDevice { + + private static final String TAG = "hidapi"; + + protected HIDDeviceManager mManager; + protected UsbDevice mDevice; + protected int mInterfaceIndex; + protected int mInterface; + protected int mDeviceId; + protected UsbDeviceConnection mConnection; + protected UsbEndpoint mInputEndpoint; + protected UsbEndpoint mOutputEndpoint; + protected InputThread mInputThread; + protected boolean mRunning; + protected boolean mFrozen; + + public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { + mManager = manager; + mDevice = usbDevice; + mInterfaceIndex = interface_index; + mInterface = mDevice.getInterface(mInterfaceIndex).getId(); + mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); + mRunning = false; + } + + public String getIdentifier() { + return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex); + } + + @Override + public int getId() { + return mDeviceId; + } + + @Override + public int getVendorId() { + return mDevice.getVendorId(); + } + + @Override + public int getProductId() { + return mDevice.getProductId(); + } + + @Override + public String getSerialNumber() { + String result = null; + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { + try { + result = mDevice.getSerialNumber(); + } + catch (SecurityException exception) { + //Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage()); + } + } + if (result == null) { + result = ""; + } + return result; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public String getManufacturerName() { + String result = null; + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { + result = mDevice.getManufacturerName(); + } + if (result == null) { + result = String.format("%x", getVendorId()); + } + return result; + } + + @Override + public String getProductName() { + String result = null; + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { + result = mDevice.getProductName(); + } + if (result == null) { + result = String.format("%x", getProductId()); + } + return result; + } + + @Override + public UsbDevice getDevice() { + return mDevice; + } + + public String getDeviceName() { + return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; + } + + @Override + public boolean open() { + mConnection = mManager.getUSBManager().openDevice(mDevice); + if (mConnection == null) { + Log.w(TAG, "Unable to open USB device " + getDeviceName()); + return false; + } + + // Force claim our interface + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + if (!mConnection.claimInterface(iface, true)) { + Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName()); + close(); + return false; + } + + // Find the endpoints + for (int j = 0; j < iface.getEndpointCount(); j++) { + UsbEndpoint endpt = iface.getEndpoint(j); + switch (endpt.getDirection()) { + case UsbConstants.USB_DIR_IN: + if (mInputEndpoint == null) { + mInputEndpoint = endpt; + } + break; + case UsbConstants.USB_DIR_OUT: + if (mOutputEndpoint == null) { + mOutputEndpoint = endpt; + } + break; + } + } + + // Make sure the required endpoints were present + if (mInputEndpoint == null || mOutputEndpoint == null) { + Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); + close(); + return false; + } + + // Start listening for input + mRunning = true; + mInputThread = new InputThread(); + mInputThread.start(); + + return true; + } + + @Override + public int sendFeatureReport(byte[] report) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + ++offset; + --length; + skipped_report_id = true; + } + + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, + 0x09/*HID set_report*/, + (3/*HID feature*/ << 8) | report_number, + mInterface, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); + return -1; + } + + if (skipped_report_id) { + ++length; + } + return length; + } + + @Override + public int sendOutputReport(byte[] report) { + int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); + if (r != report.length) { + Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); + } + return r; + } + + @Override + public boolean getFeatureReport(byte[] report) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + /* Offset the return buffer by 1, so that the report ID + will remain in byte 0. */ + ++offset; + --length; + skipped_report_id = true; + } + + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, + 0x01/*HID get_report*/, + (3/*HID feature*/ << 8) | report_number, + mInterface, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName()); + return false; + } + + if (skipped_report_id) { + ++res; + ++length; + } + + byte[] data; + if (res == length) { + data = report; + } else { + data = Arrays.copyOfRange(report, 0, res); + } + mManager.HIDDeviceFeatureReport(mDeviceId, data); + + return true; + } + + @Override + public void close() { + mRunning = false; + if (mInputThread != null) { + while (mInputThread.isAlive()) { + mInputThread.interrupt(); + try { + mInputThread.join(); + } catch (InterruptedException e) { + // Keep trying until we're done + } + } + mInputThread = null; + } + if (mConnection != null) { + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + mConnection.releaseInterface(iface); + mConnection.close(); + mConnection = null; + } + } + + @Override + public void shutdown() { + close(); + mManager = null; + } + + @Override + public void setFrozen(boolean frozen) { + mFrozen = frozen; + } + + protected class InputThread extends Thread { + @Override + public void run() { + int packetSize = mInputEndpoint.getMaxPacketSize(); + byte[] packet = new byte[packetSize]; + while (mRunning) { + int r; + try + { + r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000); + } + catch (Exception e) + { + Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e); + break; + } + if (r < 0) { + // Could be a timeout or an I/O error + } + if (r > 0) { + byte[] data; + if (r == packetSize) { + data = packet; + } else { + data = Arrays.copyOfRange(packet, 0, r); + } + + if (!mFrozen) { + mManager.HIDDeviceInputReport(mDeviceId, data); + } + } + } + } + } +} diff --git a/Client/Platform/Android/app/src/main/java/org/libsdl/app/SDL.java b/Client/Platform/Android/app/src/main/java/org/libsdl/app/SDL.java new file mode 100644 index 00000000..44c21c1c --- /dev/null +++ b/Client/Platform/Android/app/src/main/java/org/libsdl/app/SDL.java @@ -0,0 +1,86 @@ +package org.libsdl.app; + +import android.content.Context; + +import java.lang.Class; +import java.lang.reflect.Method; + +/** + SDL library initialization +*/ +public class SDL { + + // This function should be called first and sets up the native code + // so it can call into the Java classes + public static void setupJNI() { + SDLActivity.nativeSetupJNI(); + SDLAudioManager.nativeSetupJNI(); + SDLControllerManager.nativeSetupJNI(); + } + + // This function should be called each time the activity is started + public static void initialize() { + setContext(null); + + SDLActivity.initialize(); + SDLAudioManager.initialize(); + SDLControllerManager.initialize(); + } + + // This function stores the current activity (SDL or not) + public static void setContext(Context context) { + SDLAudioManager.setContext(context); + mContext = context; + } + + public static Context getContext() { + return mContext; + } + + public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { + + if (libraryName == null) { + throw new NullPointerException("No library name provided."); + } + + try { + // Let's see if we have ReLinker available in the project. This is necessary for + // some projects that have huge numbers of local libraries bundled, and thus may + // trip a bug in Android's native library loader which ReLinker works around. (If + // loadLibrary works properly, ReLinker will simply use the normal Android method + // internally.) + // + // To use ReLinker, just add it as a dependency. For more information, see + // https://github.com/KeepSafe/ReLinker for ReLinker's repository. + // + Class relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker"); + Class relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener"); + Class contextClass = mContext.getClassLoader().loadClass("android.content.Context"); + Class stringClass = mContext.getClassLoader().loadClass("java.lang.String"); + + // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if + // they've changed during updates. + Method forceMethod = relinkClass.getDeclaredMethod("force"); + Object relinkInstance = forceMethod.invoke(null); + Class relinkInstanceClass = relinkInstance.getClass(); + + // Actually load the library! + Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass); + loadMethod.invoke(relinkInstance, mContext, libraryName, null, null); + } + catch (final Throwable e) { + // Fall back + try { + System.loadLibrary(libraryName); + } + catch (final UnsatisfiedLinkError ule) { + throw ule; + } + catch (final SecurityException se) { + throw se; + } + } + } + + protected static Context mContext; +} diff --git a/Client/Platform/Android/app/src/main/java/org/libsdl/app/SDLActivity.java b/Client/Platform/Android/app/src/main/java/org/libsdl/app/SDLActivity.java new file mode 100644 index 00000000..79ddc4c7 --- /dev/null +++ b/Client/Platform/Android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -0,0 +1,2119 @@ +package org.libsdl.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.UiModeManager; +import android.content.ClipboardManager; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.hardware.Sensor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.Editable; +import android.text.InputType; +import android.text.Selection; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseArray; +import android.view.Display; +import android.view.Gravity; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.PointerIcon; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.Hashtable; +import java.util.Locale; + + +/** + SDL Activity +*/ +public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { + private static final String TAG = "SDL"; + private static final int SDL_MAJOR_VERSION = 2; + private static final int SDL_MINOR_VERSION = 28; + private static final int SDL_MICRO_VERSION = 1; +/* + // Display InputType.SOURCE/CLASS of events and devices + // + // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]"); + // SDLActivity.debugSource(event.getSource(), "event"); + public static void debugSource(int sources, String prefix) { + int s = sources; + int s_copy = sources; + String cls = ""; + String src = ""; + int tst = 0; + int FLAG_TAINTED = 0x80000000; + + if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0) cls += " BUTTON"; + if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) cls += " JOYSTICK"; + if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0) cls += " POINTER"; + if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0) cls += " POSITION"; + if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) cls += " TRACKBALL"; + + + int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits + s2 &= ~( InputDevice.SOURCE_CLASS_BUTTON + | InputDevice.SOURCE_CLASS_JOYSTICK + | InputDevice.SOURCE_CLASS_POINTER + | InputDevice.SOURCE_CLASS_POSITION + | InputDevice.SOURCE_CLASS_TRACKBALL); + + if (s2 != 0) cls += "Some_Unkown"; + + s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; + + if (Build.VERSION.SDK_INT >= 23) { + tst = InputDevice.SOURCE_BLUETOOTH_STYLUS; + if ((s & tst) == tst) src += " BLUETOOTH_STYLUS"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_DPAD; + if ((s & tst) == tst) src += " DPAD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_GAMEPAD; + if ((s & tst) == tst) src += " GAMEPAD"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 21) { + tst = InputDevice.SOURCE_HDMI; + if ((s & tst) == tst) src += " HDMI"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_JOYSTICK; + if ((s & tst) == tst) src += " JOYSTICK"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_KEYBOARD; + if ((s & tst) == tst) src += " KEYBOARD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_MOUSE; + if ((s & tst) == tst) src += " MOUSE"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 26) { + tst = InputDevice.SOURCE_MOUSE_RELATIVE; + if ((s & tst) == tst) src += " MOUSE_RELATIVE"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_ROTARY_ENCODER; + if ((s & tst) == tst) src += " ROTARY_ENCODER"; + s2 &= ~tst; + } + tst = InputDevice.SOURCE_STYLUS; + if ((s & tst) == tst) src += " STYLUS"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_TOUCHPAD; + if ((s & tst) == tst) src += " TOUCHPAD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_TOUCHSCREEN; + if ((s & tst) == tst) src += " TOUCHSCREEN"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 18) { + tst = InputDevice.SOURCE_TOUCH_NAVIGATION; + if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_TRACKBALL; + if ((s & tst) == tst) src += " TRACKBALL"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_ANY; + if ((s & tst) == tst) src += " ANY"; + s2 &= ~tst; + + if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; + s2 &= ~FLAG_TAINTED; + + if (s2 != 0) src += " Some_Unkown"; + + Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); + } +*/ + + public static boolean mIsResumedCalled, mHasFocus; + public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */); + + // Cursor types + // private static final int SDL_SYSTEM_CURSOR_NONE = -1; + private static final int SDL_SYSTEM_CURSOR_ARROW = 0; + private static final int SDL_SYSTEM_CURSOR_IBEAM = 1; + private static final int SDL_SYSTEM_CURSOR_WAIT = 2; + private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3; + private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4; + private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5; + private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6; + private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7; + private static final int SDL_SYSTEM_CURSOR_SIZENS = 8; + private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; + private static final int SDL_SYSTEM_CURSOR_NO = 10; + private static final int SDL_SYSTEM_CURSOR_HAND = 11; + + protected static final int SDL_ORIENTATION_UNKNOWN = 0; + protected static final int SDL_ORIENTATION_LANDSCAPE = 1; + protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2; + protected static final int SDL_ORIENTATION_PORTRAIT = 3; + protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; + + protected static int mCurrentOrientation; + protected static Locale mCurrentLocale; + + // Handle the state of the native layer + public enum NativeState { + INIT, RESUMED, PAUSED + } + + public static NativeState mNextNativeState; + public static NativeState mCurrentNativeState; + + /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ + public static boolean mBrokenLibraries = true; + + // Main components + protected static SDLActivity mSingleton; + protected static SDLSurface mSurface; + protected static DummyEdit mTextEdit; + protected static boolean mScreenKeyboardShown; + protected static ViewGroup mLayout; + protected static SDLClipboardHandler mClipboardHandler; + protected static Hashtable mCursors; + protected static int mLastCursorID; + protected static SDLGenericMotionListener_API12 mMotionListener; + protected static HIDDeviceManager mHIDDeviceManager; + + // This is what SDL runs in. It invokes SDL_main(), eventually + protected static Thread mSDLThread; + + protected static SDLGenericMotionListener_API12 getMotionListener() { + if (mMotionListener == null) { + if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { + mMotionListener = new SDLGenericMotionListener_API26(); + } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + mMotionListener = new SDLGenericMotionListener_API24(); + } else { + mMotionListener = new SDLGenericMotionListener_API12(); + } + } + + return mMotionListener; + } + + /** + * This method returns the name of the shared object with the application entry point + * It can be overridden by derived classes. + */ + protected String getMainSharedObject() { + String library; + String[] libraries = SDLActivity.mSingleton.getLibraries(); + if (libraries.length > 0) { + library = "lib" + libraries[libraries.length - 1] + ".so"; + } else { + library = "libmain.so"; + } + return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; + } + + /** + * This method returns the name of the application entry point + * It can be overridden by derived classes. + */ + protected String getMainFunction() { + return "SDL_main"; + } + + /** + * This method is called by SDL before loading the native shared libraries. + * It can be overridden to provide names of shared libraries to be loaded. + * The default implementation returns the defaults. It never returns null. + * An array returned by a new implementation must at least contain "SDL2". + * Also keep in mind that the order the libraries are loaded may matter. + * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). + */ + protected String[] getLibraries() { + return new String[] { + "SDL2", + // "SDL2_image", + // "SDL2_mixer", + // "SDL2_net", + // "SDL2_ttf", + "main" + }; + } + + // Load the .so + public void loadLibraries() { + for (String lib : getLibraries()) { + SDL.loadLibrary(lib); + } + } + + /** + * This method is called by SDL before starting the native application thread. + * It can be overridden to provide the arguments after the application name. + * The default implementation returns an empty array. It never returns null. + * @return arguments for the native application. + */ + protected String[] getArguments() { + return new String[0]; + } + + public static void initialize() { + // The static nature of the singleton and Android quirkyness force us to initialize everything here + // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values + mSingleton = null; + mSurface = null; + mTextEdit = null; + mLayout = null; + mClipboardHandler = null; + mCursors = new Hashtable(); + mLastCursorID = 0; + mSDLThread = null; + mIsResumedCalled = false; + mHasFocus = true; + mNextNativeState = NativeState.INIT; + mCurrentNativeState = NativeState.INIT; + } + + protected SDLSurface createSDLSurface(Context context) { + return new SDLSurface(context); + } + + // Setup + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "Device: " + Build.DEVICE); + Log.v(TAG, "Model: " + Build.MODEL); + Log.v(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + + try { + Thread.currentThread().setName("SDLActivity"); + } catch (Exception e) { + Log.v(TAG, "modify thread properties failed " + e.toString()); + } + + // Load shared libraries + String errorMsgBrokenLib = ""; + try { + loadLibraries(); + mBrokenLibraries = false; /* success */ + } catch(UnsatisfiedLinkError e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } catch(Exception e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } + + if (!mBrokenLibraries) { + String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." + + String.valueOf(SDL_MINOR_VERSION) + "." + + String.valueOf(SDL_MICRO_VERSION); + String version = nativeGetVersion(); + if (!version.equals(expected_version)) { + mBrokenLibraries = true; + errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")"; + } + } + + if (mBrokenLibraries) { + mSingleton = this; + AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); + dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." + + System.getProperty("line.separator") + + System.getProperty("line.separator") + + "Error: " + errorMsgBrokenLib); + dlgAlert.setTitle("SDL Error"); + dlgAlert.setPositiveButton("Exit", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog,int id) { + // if this button is clicked, close current activity + SDLActivity.mSingleton.finish(); + } + }); + dlgAlert.setCancelable(false); + dlgAlert.create().show(); + + return; + } + + // Set up JNI + SDL.setupJNI(); + + // Initialize state + SDL.initialize(); + + // So we can call stuff from static callbacks + mSingleton = this; + SDL.setContext(this); + + mClipboardHandler = new SDLClipboardHandler(); + + mHIDDeviceManager = HIDDeviceManager.acquire(this); + + // Set up the surface + mSurface = createSDLSurface(this); + + mLayout = new RelativeLayout(this); + mLayout.addView(mSurface); + + // Get our current screen orientation and pass it down. + mCurrentOrientation = SDLActivity.getCurrentOrientation(); + // Only record current orientation + SDLActivity.onNativeOrientationChanged(mCurrentOrientation); + + try { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { + mCurrentLocale = getContext().getResources().getConfiguration().locale; + } else { + mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); + } + } catch(Exception ignored) { + } + + setContentView(mLayout); + + setWindowStyle(false); + + getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); + + // Get filename from "Open with" of another application + Intent intent = getIntent(); + if (intent != null && intent.getData() != null) { + String filename = intent.getData().getPath(); + if (filename != null) { + Log.v(TAG, "Got filename: " + filename); + SDLActivity.onNativeDropFile(filename); + } + } + } + + protected void pauseNativeThread() { + mNextNativeState = NativeState.PAUSED; + mIsResumedCalled = false; + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleNativeState(); + } + + protected void resumeNativeThread() { + mNextNativeState = NativeState.RESUMED; + mIsResumedCalled = true; + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleNativeState(); + } + + // Events + @Override + protected void onPause() { + Log.v(TAG, "onPause()"); + super.onPause(); + + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(true); + } + if (!mHasMultiWindow) { + pauseNativeThread(); + } + } + + @Override + protected void onResume() { + Log.v(TAG, "onResume()"); + super.onResume(); + + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(false); + } + if (!mHasMultiWindow) { + resumeNativeThread(); + } + } + + @Override + protected void onStop() { + Log.v(TAG, "onStop()"); + super.onStop(); + if (mHasMultiWindow) { + pauseNativeThread(); + } + } + + @Override + protected void onStart() { + Log.v(TAG, "onStart()"); + super.onStart(); + if (mHasMultiWindow) { + resumeNativeThread(); + } + } + + public static int getCurrentOrientation() { + int result = SDL_ORIENTATION_UNKNOWN; + + Activity activity = (Activity)getContext(); + if (activity == null) { + return result; + } + Display display = activity.getWindowManager().getDefaultDisplay(); + + switch (display.getRotation()) { + case Surface.ROTATION_0: + result = SDL_ORIENTATION_PORTRAIT; + break; + + case Surface.ROTATION_90: + result = SDL_ORIENTATION_LANDSCAPE; + break; + + case Surface.ROTATION_180: + result = SDL_ORIENTATION_PORTRAIT_FLIPPED; + break; + + case Surface.ROTATION_270: + result = SDL_ORIENTATION_LANDSCAPE_FLIPPED; + break; + } + + return result; + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + mHasFocus = hasFocus; + if (hasFocus) { + mNextNativeState = NativeState.RESUMED; + SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded(); + + SDLActivity.handleNativeState(); + nativeFocusChanged(true); + + } else { + nativeFocusChanged(false); + if (!mHasMultiWindow) { + mNextNativeState = NativeState.PAUSED; + SDLActivity.handleNativeState(); + } + } + } + + @Override + public void onLowMemory() { + Log.v(TAG, "onLowMemory()"); + super.onLowMemory(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.nativeLowMemory(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.v(TAG, "onConfigurationChanged()"); + super.onConfigurationChanged(newConfig); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) { + mCurrentLocale = newConfig.locale; + SDLActivity.onNativeLocaleChanged(); + } + } + + @Override + protected void onDestroy() { + Log.v(TAG, "onDestroy()"); + + if (mHIDDeviceManager != null) { + HIDDeviceManager.release(mHIDDeviceManager); + mHIDDeviceManager = null; + } + + SDLAudioManager.release(this); + + if (SDLActivity.mBrokenLibraries) { + super.onDestroy(); + return; + } + + if (SDLActivity.mSDLThread != null) { + + // Send Quit event to "SDLThread" thread + SDLActivity.nativeSendQuit(); + + // Wait for "SDLThread" thread to end + try { + SDLActivity.mSDLThread.join(); + } catch(Exception e) { + Log.v(TAG, "Problem stopping SDLThread: " + e); + } + } + + SDLActivity.nativeQuit(); + + super.onDestroy(); + } + + @Override + public void onBackPressed() { + // Check if we want to block the back button in case of mouse right click. + // + // If we do, the normal hardware back button will no longer work and people have to use home, + // but the mouse right click will work. + // + boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false); + if (trapBack) { + // Exit and let the mouse handler handle this button (if appropriate) + return; + } + + // Default system back button behavior. + if (!isFinishing()) { + super.onBackPressed(); + } + } + + // Called by JNI from SDL. + public static void manualBackButton() { + mSingleton.pressBackButton(); + } + + // Used to get us onto the activity's main thread + public void pressBackButton() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (!SDLActivity.this.isFinishing()) { + SDLActivity.this.superOnBackPressed(); + } + } + }); + } + + // Used to access the system back behavior. + public void superOnBackPressed() { + super.onBackPressed(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + + if (SDLActivity.mBrokenLibraries) { + return false; + } + + int keyCode = event.getKeyCode(); + // Ignore certain special keys so they're handled by Android + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_UP || + keyCode == KeyEvent.KEYCODE_CAMERA || + keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ + keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ + ) { + return false; + } + return super.dispatchKeyEvent(event); + } + + /* Transition to next state */ + public static void handleNativeState() { + + if (mNextNativeState == mCurrentNativeState) { + // Already in same state, discard. + return; + } + + // Try a transition to init state + if (mNextNativeState == NativeState.INIT) { + + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to paused state + if (mNextNativeState == NativeState.PAUSED) { + if (mSDLThread != null) { + nativePause(); + } + if (mSurface != null) { + mSurface.handlePause(); + } + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to resumed state + if (mNextNativeState == NativeState.RESUMED) { + if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) { + if (mSDLThread == null) { + // This is the entry point to the C app. + // Start up the C app thread and enable sensor input for the first time + // FIXME: Why aren't we enabling sensor input at start? + + mSDLThread = new Thread(new SDLMain(), "SDLThread"); + mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); + mSDLThread.start(); + + // No nativeResume(), don't signal Android_ResumeSem + } else { + nativeResume(); + } + mSurface.handleResume(); + + mCurrentNativeState = mNextNativeState; + } + } + } + + // Messages from the SDLMain thread + static final int COMMAND_CHANGE_TITLE = 1; + static final int COMMAND_CHANGE_WINDOW_STYLE = 2; + static final int COMMAND_TEXTEDIT_HIDE = 3; + static final int COMMAND_SET_KEEP_SCREEN_ON = 5; + + protected static final int COMMAND_USER = 0x8000; + + protected static boolean mFullscreenModeActive; + + /** + * This method is called by SDL if SDL did not handle a message itself. + * This happens if a received message contains an unsupported command. + * Method can be overwritten to handle Messages in a different class. + * @param command the command of the message. + * @param param the parameter of the message. May be null. + * @return if the message was handled in overridden method. + */ + protected boolean onUnhandledMessage(int command, Object param) { + return false; + } + + /** + * A Handler class for Messages from native SDL applications. + * It uses current Activities as target (e.g. for the title). + * static to prevent implicit references to enclosing object. + */ + protected static class SDLCommandHandler extends Handler { + @Override + public void handleMessage(Message msg) { + Context context = SDL.getContext(); + if (context == null) { + Log.e(TAG, "error handling message, getContext() returned null"); + return; + } + switch (msg.arg1) { + case COMMAND_CHANGE_TITLE: + if (context instanceof Activity) { + ((Activity) context).setTitle((String)msg.obj); + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); + } + break; + case COMMAND_CHANGE_WINDOW_STYLE: + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { + int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; + window.getDecorView().setSystemUiVisibility(flags); + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + SDLActivity.mFullscreenModeActive = true; + } else { + int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; + window.getDecorView().setSystemUiVisibility(flags); + window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + SDLActivity.mFullscreenModeActive = false; + } + } + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); + } + } + break; + case COMMAND_TEXTEDIT_HIDE: + if (mTextEdit != null) { + // Note: On some devices setting view to GONE creates a flicker in landscape. + // Setting the View's sizes to 0 is similar to GONE but without the flicker. + // The sizes will be set to useful values when the keyboard is shown again. + mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); + + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); + + mScreenKeyboardShown = false; + + mSurface.requestFocus(); + } + break; + case COMMAND_SET_KEEP_SCREEN_ON: + { + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + } + break; + } + default: + if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { + Log.e(TAG, "error handling message, command is " + msg.arg1); + } + } + } + } + + // Handler for the messages + Handler commandHandler = new SDLCommandHandler(); + + // Send a message from the SDLMain thread + boolean sendCommand(int command, Object data) { + Message msg = commandHandler.obtainMessage(); + msg.arg1 = command; + msg.obj = data; + boolean result = commandHandler.sendMessage(msg); + + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { + if (command == COMMAND_CHANGE_WINDOW_STYLE) { + // Ensure we don't return until the resize has actually happened, + // or 500ms have passed. + + boolean bShouldWait = false; + + if (data instanceof Integer) { + // Let's figure out if we're already laid out fullscreen or not. + Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + DisplayMetrics realMetrics = new DisplayMetrics(); + display.getRealMetrics(realMetrics); + + boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && + (realMetrics.heightPixels == mSurface.getHeight())); + + if ((Integer) data == 1) { + // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going + // to change size and should wait for surfaceChanged() before we return, so the size + // is right back in native code. If we're already laid out fullscreen, though, we're + // not going to change size even if we change decor modes, so we shouldn't wait for + // surfaceChanged() -- which may not even happen -- and should return immediately. + bShouldWait = !bFullscreenLayout; + } else { + // If we're laid out fullscreen (even if the status bar and nav bar are present), + // or are actively in fullscreen, we're going to change size and should wait for + // surfaceChanged before we return, so the size is right back in native code. + bShouldWait = bFullscreenLayout; + } + } + + if (bShouldWait && (SDLActivity.getContext() != null)) { + // We'll wait for the surfaceChanged() method, which will notify us + // when called. That way, we know our current size is really the + // size we need, instead of grabbing a size that's still got + // the navigation and/or status bars before they're hidden. + // + // We'll wait for up to half a second, because some devices + // take a surprisingly long time for the surface resize, but + // then we'll just give up and return. + // + synchronized (SDLActivity.getContext()) { + try { + SDLActivity.getContext().wait(500); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + } + } + + return result; + } + + // C functions we call + public static native String nativeGetVersion(); + public static native int nativeSetupJNI(); + public static native int nativeRunMain(String library, String function, Object arguments); + public static native void nativeLowMemory(); + public static native void nativeSendQuit(); + public static native void nativeQuit(); + public static native void nativePause(); + public static native void nativeResume(); + public static native void nativeFocusChanged(boolean hasFocus); + public static native void onNativeDropFile(String filename); + public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate); + public static native void onNativeResize(); + public static native void onNativeKeyDown(int keycode); + public static native void onNativeKeyUp(int keycode); + public static native boolean onNativeSoftReturnKey(); + public static native void onNativeKeyboardFocusLost(); + public static native void onNativeMouse(int button, int action, float x, float y, boolean relative); + public static native void onNativeTouch(int touchDevId, int pointerFingerId, + int action, float x, + float y, float p); + public static native void onNativeAccel(float x, float y, float z); + public static native void onNativeClipboardChanged(); + public static native void onNativeSurfaceCreated(); + public static native void onNativeSurfaceChanged(); + public static native void onNativeSurfaceDestroyed(); + public static native String nativeGetHint(String name); + public static native boolean nativeGetHintBoolean(String name, boolean default_value); + public static native void nativeSetenv(String name, String value); + public static native void onNativeOrientationChanged(int orientation); + public static native void nativeAddTouch(int touchId, String name); + public static native void nativePermissionResult(int requestCode, boolean result); + public static native void onNativeLocaleChanged(); + + /** + * This method is called by SDL using JNI. + */ + public static boolean setActivityTitle(String title) { + // Called from SDLMain() thread and can't directly affect the view + return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); + } + + /** + * This method is called by SDL using JNI. + */ + public static void setWindowStyle(boolean fullscreen) { + // Called from SDLMain() thread and can't directly affect the view + mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0); + } + + /** + * This method is called by SDL using JNI. + * This is a static method for JNI convenience, it calls a non-static method + * so that is can be overridden + */ + public static void setOrientation(int w, int h, boolean resizable, String hint) + { + if (mSingleton != null) { + mSingleton.setOrientationBis(w, h, resizable, hint); + } + } + + /** + * This can be overridden + */ + public void setOrientationBis(int w, int h, boolean resizable, String hint) + { + int orientation_landscape = -1; + int orientation_portrait = -1; + + /* If set, hint "explicitly controls which UI orientations are allowed". */ + if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else if (hint.contains("LandscapeLeft")) { + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + } else if (hint.contains("LandscapeRight")) { + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } + + /* exact match to 'Portrait' to distinguish with PortraitUpsideDown */ + boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); + + if (contains_Portrait && hint.contains("PortraitUpsideDown")) { + orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } else if (contains_Portrait) { + orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } else if (hint.contains("PortraitUpsideDown")) { + orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } + + boolean is_landscape_allowed = (orientation_landscape != -1); + boolean is_portrait_allowed = (orientation_portrait != -1); + int req; /* Requested orientation */ + + /* No valid hint, nothing is explicitly allowed */ + if (!is_portrait_allowed && !is_landscape_allowed) { + if (resizable) { + /* All orientations are allowed */ + req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; + } else { + /* Fixed window and nothing specified. Get orientation from w/h of created window */ + req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + } else { + /* At least one orientation is allowed */ + if (resizable) { + if (is_portrait_allowed && is_landscape_allowed) { + /* hint allows both landscape and portrait, promote to full sensor */ + req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; + } else { + /* Use the only one allowed "orientation" */ + req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); + } + } else { + /* Fixed window and both orientations are allowed. Choose one. */ + if (is_portrait_allowed && is_landscape_allowed) { + req = (w > h ? orientation_landscape : orientation_portrait); + } else { + /* Use the only one allowed "orientation" */ + req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); + } + } + } + + Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); + mSingleton.setRequestedOrientation(req); + } + + /** + * This method is called by SDL using JNI. + */ + public static void minimizeWindow() { + + if (mSingleton == null) { + return; + } + + Intent startMain = new Intent(Intent.ACTION_MAIN); + startMain.addCategory(Intent.CATEGORY_HOME); + startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mSingleton.startActivity(startMain); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean shouldMinimizeOnFocusLoss() { +/* + if (Build.VERSION.SDK_INT >= 24) { + if (mSingleton == null) { + return true; + } + + if (mSingleton.isInMultiWindowMode()) { + return false; + } + + if (mSingleton.isInPictureInPictureMode()) { + return false; + } + } + + return true; +*/ + return false; + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isScreenKeyboardShown() + { + if (mTextEdit == null) { + return false; + } + + if (!mScreenKeyboardShown) { + return false; + } + + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.isAcceptingText(); + + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean supportsRelativeMouse() + { + // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under + // Android 7 APIs, and simply returns no data under Android 8 APIs. + // + // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and + // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result, + // we should stick to relative mode. + // + if (Build.VERSION.SDK_INT < 27 /* Android 8.1 (O_MR1) */ && isDeXMode()) { + return false; + } + + return SDLActivity.getMotionListener().supportsRelativeMouse(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean setRelativeMouseEnabled(boolean enabled) + { + if (enabled && !supportsRelativeMouse()) { + return false; + } + + return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean sendMessage(int command, int param) { + if (mSingleton == null) { + return false; + } + return mSingleton.sendCommand(command, param); + } + + /** + * This method is called by SDL using JNI. + */ + public static Context getContext() { + return SDL.getContext(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isAndroidTV() { + UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { + return true; + } + if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { + return true; + } + if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { + return true; + } + return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV"); + } + + public static double getDiagonal() + { + DisplayMetrics metrics = new DisplayMetrics(); + Activity activity = (Activity)getContext(); + if (activity == null) { + return 0.0; + } + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + double dWidthInches = metrics.widthPixels / (double)metrics.xdpi; + double dHeightInches = metrics.heightPixels / (double)metrics.ydpi; + + return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isTablet() { + // If our diagonal size is seven inches or greater, we consider ourselves a tablet. + return (getDiagonal() >= 7.0); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isChromebook() { + if (getContext() == null) { + return false; + } + return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean isDeXMode() { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { + return false; + } + try { + final Configuration config = getContext().getResources().getConfiguration(); + final Class configClass = config.getClass(); + return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) + == configClass.getField("semDesktopModeEnabled").getInt(config); + } catch(Exception ignored) { + return false; + } + } + + /** + * This method is called by SDL using JNI. + */ + public static DisplayMetrics getDisplayDPI() { + return getContext().getResources().getDisplayMetrics(); + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean getManifestEnvironmentVariables() { + try { + if (getContext() == null) { + return false; + } + + ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); + Bundle bundle = applicationInfo.metaData; + if (bundle == null) { + return false; + } + String prefix = "SDL_ENV."; + final int trimLength = prefix.length(); + for (String key : bundle.keySet()) { + if (key.startsWith(prefix)) { + String name = key.substring(trimLength); + String value = bundle.get(key).toString(); + nativeSetenv(name, value); + } + } + /* environment variables set! */ + return true; + } catch (Exception e) { + Log.v(TAG, "exception " + e.toString()); + } + return false; + } + + // This method is called by SDLControllerManager's API 26 Generic Motion Handler. + public static View getContentView() { + return mLayout; + } + + static class ShowTextInputTask implements Runnable { + /* + * This is used to regulate the pan&scan method to have some offset from + * the bottom edge of the input region and the top edge of an input + * method (soft keyboard) + */ + static final int HEIGHT_PADDING = 15; + + public int x, y, w, h; + + public ShowTextInputTask(int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + + /* Minimum size of 1 pixel, so it takes focus. */ + if (this.w <= 0) { + this.w = 1; + } + if (this.h + HEIGHT_PADDING <= 0) { + this.h = 1 - HEIGHT_PADDING; + } + } + + @Override + public void run() { + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); + params.leftMargin = x; + params.topMargin = y; + + if (mTextEdit == null) { + mTextEdit = new DummyEdit(SDL.getContext()); + + mLayout.addView(mTextEdit, params); + } else { + mTextEdit.setLayoutParams(params); + } + + mTextEdit.setVisibility(View.VISIBLE); + mTextEdit.requestFocus(); + + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mTextEdit, 0); + + mScreenKeyboardShown = true; + } + } + + /** + * This method is called by SDL using JNI. + */ + public static boolean showTextInput(int x, int y, int w, int h) { + // Transfer the task to the main thread as a Runnable + return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); + } + + public static boolean isTextInputEvent(KeyEvent event) { + + // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT + if (event.isCtrlPressed()) { + return false; + } + + return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; + } + + public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) { + int deviceId = event.getDeviceId(); + int source = event.getSource(); + + if (source == InputDevice.SOURCE_UNKNOWN) { + InputDevice device = InputDevice.getDevice(deviceId); + if (device != null) { + source = device.getSources(); + } + } + +// if (event.getAction() == KeyEvent.ACTION_DOWN) { +// Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); +// } else if (event.getAction() == KeyEvent.ACTION_UP) { +// Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); +// } + + // Dispatch the different events depending on where they come from + // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD + // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD + // + // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and + // SOURCE_JOYSTICK, while its key events arrive from the keyboard source + // So, retrieve the device itself and check all of its sources + if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { + // Note that we process events with specific key codes here + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) { + return true; + } + } else if (event.getAction() == KeyEvent.ACTION_UP) { + if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) { + return true; + } + } + } + + if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (isTextInputEvent(event)) { + if (ic != null) { + ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); + } else { + SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); + } + } + onNativeKeyDown(keyCode); + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + onNativeKeyUp(keyCode); + return true; + } + } + + if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { + // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses + // they are ignored here because sending them as mouse input to SDL is messy + if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + case KeyEvent.ACTION_UP: + // mark the event as handled or it will be handled by system + // handling KEYCODE_BACK by system will call onBackPressed() + return true; + } + } + } + + return false; + } + + /** + * This method is called by SDL using JNI. + */ + public static Surface getNativeSurface() { + if (SDLActivity.mSurface == null) { + return null; + } + return SDLActivity.mSurface.getNativeSurface(); + } + + // Input + + /** + * This method is called by SDL using JNI. + */ + public static void initTouch() { + int[] ids = InputDevice.getDeviceIds(); + + for (int id : ids) { + InputDevice device = InputDevice.getDevice(id); + /* Allow SOURCE_TOUCHSCREEN and also Virtual InputDevices because they can send TOUCHSCREEN events */ + if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN + || device.isVirtual())) { + + int touchDevId = device.getId(); + /* + * Prevent id to be -1, since it's used in SDL internal for synthetic events + * Appears when using Android emulator, eg: + * adb shell input mouse tap 100 100 + * adb shell input touchscreen tap 100 100 + */ + if (touchDevId < 0) { + touchDevId -= 1; + } + nativeAddTouch(touchDevId, device.getName()); + } + } + } + + // Messagebox + + /** Result of current messagebox. Also used for blocking the calling thread. */ + protected final int[] messageboxSelection = new int[1]; + + /** + * This method is called by SDL using JNI. + * Shows the messagebox from UI thread and block calling thread. + * buttonFlags, buttonIds and buttonTexts must have same length. + * @param buttonFlags array containing flags for every button. + * @param buttonIds array containing id for every button. + * @param buttonTexts array containing text for every button. + * @param colors null for default or array of length 5 containing colors. + * @return button id or -1. + */ + public int messageboxShowMessageBox( + final int flags, + final String title, + final String message, + final int[] buttonFlags, + final int[] buttonIds, + final String[] buttonTexts, + final int[] colors) { + + messageboxSelection[0] = -1; + + // sanity checks + + if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { + return -1; // implementation broken + } + + // collect arguments for Dialog + + final Bundle args = new Bundle(); + args.putInt("flags", flags); + args.putString("title", title); + args.putString("message", message); + args.putIntArray("buttonFlags", buttonFlags); + args.putIntArray("buttonIds", buttonIds); + args.putStringArray("buttonTexts", buttonTexts); + args.putIntArray("colors", colors); + + // trigger Dialog creation on UI thread + + runOnUiThread(new Runnable() { + @Override + public void run() { + messageboxCreateAndShow(args); + } + }); + + // block the calling thread + + synchronized (messageboxSelection) { + try { + messageboxSelection.wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + return -1; + } + } + + // return selected value + + return messageboxSelection[0]; + } + + protected void messageboxCreateAndShow(Bundle args) { + + // TODO set values from "flags" to messagebox dialog + + // get colors + + int[] colors = args.getIntArray("colors"); + int backgroundColor; + int textColor; + int buttonBorderColor; + int buttonBackgroundColor; + int buttonSelectedColor; + if (colors != null) { + int i = -1; + backgroundColor = colors[++i]; + textColor = colors[++i]; + buttonBorderColor = colors[++i]; + buttonBackgroundColor = colors[++i]; + buttonSelectedColor = colors[++i]; + } else { + backgroundColor = Color.TRANSPARENT; + textColor = Color.TRANSPARENT; + buttonBorderColor = Color.TRANSPARENT; + buttonBackgroundColor = Color.TRANSPARENT; + buttonSelectedColor = Color.TRANSPARENT; + } + + // create dialog with title and a listener to wake up calling thread + + final AlertDialog dialog = new AlertDialog.Builder(this).create(); + dialog.setTitle(args.getString("title")); + dialog.setCancelable(false); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface unused) { + synchronized (messageboxSelection) { + messageboxSelection.notify(); + } + } + }); + + // create text + + TextView message = new TextView(this); + message.setGravity(Gravity.CENTER); + message.setText(args.getString("message")); + if (textColor != Color.TRANSPARENT) { + message.setTextColor(textColor); + } + + // create buttons + + int[] buttonFlags = args.getIntArray("buttonFlags"); + int[] buttonIds = args.getIntArray("buttonIds"); + String[] buttonTexts = args.getStringArray("buttonTexts"); + + final SparseArray