Compare commits
2 commits
ceaf24268c
...
6159f52e8a
Author | SHA1 | Date | |
---|---|---|---|
6159f52e8a | |||
0d236857b8 |
78 changed files with 7347 additions and 259 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -7,3 +7,7 @@ compile_commands.json
|
|||
flatpak-repo
|
||||
*.flatpak
|
||||
!build*.sh
|
||||
!build.gradle
|
||||
.cxx
|
||||
.gradle
|
||||
/sdl-android-project/app/jni
|
12
.gitmodules
vendored
12
.gitmodules
vendored
|
@ -16,3 +16,15 @@
|
|||
[submodule "subprojects/soundtouch"]
|
||||
path = subprojects/soundtouch
|
||||
url = https://codeberg.org/soundtouch/soundtouch.git
|
||||
[submodule "subprojects/SDL"]
|
||||
path = subprojects/SDL
|
||||
url = https://github.com/libsdl-org/SDL.git
|
||||
[submodule "subprojects/SDL_image"]
|
||||
path = subprojects/SDL_image
|
||||
url = https://github.com/libsdl-org/SDL_image.git
|
||||
[submodule "subprojects/libintl-lite"]
|
||||
path = subprojects/libintl-lite
|
||||
url = https://github.com/hathlife/libintl-lite.git
|
||||
[submodule "subprojects/oboe"]
|
||||
path = subprojects/oboe
|
||||
url = https://github.com/google/oboe.git
|
||||
|
|
125
CMakeLists.txt
125
CMakeLists.txt
|
@ -1,4 +1,4 @@
|
|||
cmake_minimum_required(VERSION 3.28)
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(looper VERSION 1.0.0 LANGUAGES C CXX)
|
||||
|
||||
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten")
|
||||
|
@ -86,6 +86,46 @@ if (DEFINED EMSCRIPTEN)
|
|||
else()
|
||||
set(BUILD_STATIC OFF CACHE BOOL "")
|
||||
endif()
|
||||
option(BUILD_SDL "Enables built-in SDL" OFF)
|
||||
option(BUILD_SDL_IMAGE "Enables built-in SDL_image" ${BUILD_SDL})
|
||||
if(DEFINED ANDROID_NDK)
|
||||
set(USE_SYSTEM_SDL2 ON CACHE BOOL "" FORCE)
|
||||
set(AUDIOCODECS_BUILD_TIMIDITYSDL OFF CACHE BOOL "" FORCE)
|
||||
set(AUDIOCODECS_BUILD_OPUS OFF CACHE BOOL "" FORCE)
|
||||
set(USE_OGG_VORBIS_STB ON CACHE BOOL "" FORCE)
|
||||
set(AUDIOCODECS_BUILD_WAVPACK OFF CACHE BOOL "" FORCE)
|
||||
set(USE_WAVPACK OFF CACHE BOOL "" FORCE)
|
||||
set(USE_MODPLUG OFF CACHE BOOL "" FORCE)
|
||||
set(USE_XMP OFF CACHE BOOL "" FORCE)
|
||||
set(USE_GME OFF CACHE BOOL "" FORCE)
|
||||
set(USE_MIDI_EDMIDI OFF CACHE BOOL "" FORCE)
|
||||
set(USE_OPUS OFF CACHE BOOL "" FORCE)
|
||||
set(USE_MPEG OFF CACHE BOOL "" FORCE)
|
||||
set(USE_CELT OFF CACHE BOOL "" FORCE)
|
||||
set(USE_ATRAC9 OFF CACHE BOOL "" FORCE)
|
||||
set(USE_SPEEX OFF CACHE BOOL "" FORCE)
|
||||
set(USE_G719 OFF CACHE BOOL "" FORCE)
|
||||
set(USE_VORBIS OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
if (BUILD_SDL)
|
||||
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(SDL_TEST OFF CACHE BOOL "" FORCE)
|
||||
set(USE_SYSTEM_SDL2 ON CACHE BOOL "" FORCE)
|
||||
set(SDL_MIXER_X_STATIC ON CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/subprojects/SDL)
|
||||
install(TARGETS SDL2-static SDL2main EXPORT SDL_EXPORTS)
|
||||
install(EXPORT SDL_EXPORTS DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||
export(TARGETS SDL2-static sdl-build-options SDL2main FILE ${CMAKE_CURRENT_BINARY_DIR}/SDL2Config.cmake NAMESPACE SDL2)
|
||||
set(SDL2_DIR ${CMAKE_CURRENT_SOURCE_DIR}/cmake/built_sdl)
|
||||
endif()
|
||||
if (BUILD_SDL_IMAGE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(SDL2IMAGE_SAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(SDL2IMAGE_TESTS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/subprojects/SDL_image)
|
||||
set(SDL2_image_DIR ${CMAKE_CURRENT_SOURCE_DIR}/cmake/built_sdl_image)
|
||||
endif()
|
||||
find_package(PkgConfig)
|
||||
#add_subdirectory(subprojects/jsoncpp)
|
||||
|
||||
|
@ -171,20 +211,78 @@ prefix_all(LIBRARY_SOURCES
|
|||
util.cpp
|
||||
log.cpp
|
||||
dbus.cpp
|
||||
translation.cpp
|
||||
)
|
||||
add_library(liblooper STATIC ${LIBRARY_SOURCES})
|
||||
target_include_directories(liblooper PUBLIC ${INC})
|
||||
set(JSONCPP_TARGET PkgConfig::jsoncpp)
|
||||
set(SOUNDTOUCH_TARGET PkgConfig::SoundTouch)
|
||||
function(set_to_value_of_condition var)
|
||||
if (${ARGN})
|
||||
set(${var} ON PARENT_SCOPE)
|
||||
else()
|
||||
set(${var} OFF PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
set_to_value_of_condition(BUILD_THIRDPARTY DEFINED EMSCRIPTEN OR DEFINED ANDROID_NDK)
|
||||
find_package(Intl)
|
||||
set_to_value_of_condition(_USE_LIBINTL_LITE NOT Intl_FOUND)
|
||||
option(BUILD_JSONCPP "Builds JsonCpp instead of using the system package." ${BUILD_THIRDPARTY})
|
||||
option(BUILD_SOUNDTOUCH "Builds SoundTouch instead of using the system package" ${BUILD_THIRDPARTY})
|
||||
option(USE_LIBINTL_LITE "Set to ON to build and use libintl-lite" ${_USE_LIBINTL_LITE})
|
||||
set(LIBINTL_LIBRARY)
|
||||
set(LIBINTL_INCDIRS)
|
||||
if(BUILD_JSONCPP)
|
||||
add_subdirectory(subprojects/jsoncpp)
|
||||
set(JSONCPP_TARGET jsoncpp_static)
|
||||
endif()
|
||||
if(BUILD_SOUNDTOUCH)
|
||||
add_subdirectory(subprojects/soundtouch)
|
||||
set(SOUNDTOUCH_TARGET SoundTouch)
|
||||
endif()
|
||||
if(USE_LIBINTL_LITE)
|
||||
add_subdirectory(subprojects/libintl-lite)
|
||||
set(LIBINTL_LIBRARY intl CACHE INTERNAL "")
|
||||
set(LIBINTL_INCDIRS "subprojects/libintl-lite" CACHE INTERNAL "")
|
||||
else()
|
||||
find_package(Intl REQUIRED)
|
||||
set(LIBINTL_LIBRARY Intl::Intl CACHE INTERNAL "")
|
||||
set(LIBINTL_INCDIRS ${Intl_INCLUDE_DIRS} CACHE INTERNAL "")
|
||||
endif()
|
||||
|
||||
if (DEFINED ANDROID_NDK)
|
||||
add_subdirectory(subprojects/oboe)
|
||||
target_link_libraries(liblooper PUBLIC oboe)
|
||||
endif()
|
||||
target_include_directories(liblooper PUBLIC ${LIBINTL_INCDIRS})
|
||||
target_link_libraries(liblooper PUBLIC ${LIBINTL_LIBRARY})
|
||||
if(DEFINED EMSCRIPTEN)
|
||||
add_subdirectory(subprojects/jsoncpp)
|
||||
add_subdirectory(subprojects/soundtouch)
|
||||
target_link_libraries(liblooper PUBLIC ${SDL_MIXER_X_TARGET} SoundTouch libvgmstream jsoncpp_static)
|
||||
target_link_libraries(liblooper PUBLIC ${SDL_MIXER_X_TARGET} ${SOUNDTOUCH_TARGET} libvgmstream ${JSONCPP_TARGET})
|
||||
target_compile_options(liblooper PUBLIC "-sUSE_SDL=2")
|
||||
else()
|
||||
pkg_check_modules(SoundTouch IMPORTED_TARGET soundtouch)
|
||||
pkg_check_modules(jsoncpp IMPORTED_TARGET jsoncpp)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(sdbus-c++ REQUIRED)
|
||||
target_link_libraries(liblooper PUBLIC SDL2::SDL2 ${SDL_MIXER_X_TARGET} PkgConfig::SoundTouch libvgmstream libvgmstream_shared PkgConfig::jsoncpp)
|
||||
if(NOT BUILD_SOUNDTOUCH)
|
||||
pkg_check_modules(SoundTouch IMPORTED_TARGET soundtouch)
|
||||
endif()
|
||||
if (NOT BUILD_JSONCPP)
|
||||
pkg_check_modules(jsoncpp IMPORTED_TARGET jsoncpp)
|
||||
endif()
|
||||
if (NOT BUILD_SDL)
|
||||
find_package(SDL2 REQUIRED)
|
||||
endif()
|
||||
if (ENABLE_DBUS)
|
||||
find_package(sdbus-c++)
|
||||
if(NOT ${sdbus-c++_FOUND})
|
||||
set(ENABLE_DBUS OFF)
|
||||
message("Warning: Dbus support not found - Not enabling DBus")
|
||||
endif()
|
||||
endif()
|
||||
set(SDL2_TARGET SDL2::SDL2)
|
||||
if (TARGET SDL2-static)
|
||||
set(SDL2_TARGET SDL2-static)
|
||||
endif()
|
||||
target_link_libraries(liblooper PUBLIC ${SDL2_TARGET} SDL2main ${SDL_MIXER_X_TARGET} ${SOUNDTOUCH_TARGET} libvgmstream libvgmstream_shared ${JSONCPP_TARGET})
|
||||
endif()
|
||||
if (${ENABLE_DBUS})
|
||||
target_link_libraries(liblooper PUBLIC SDBusCpp::sdbus-c++)
|
||||
|
@ -201,7 +299,7 @@ macro(add_ui_backend)
|
|||
target_include_directories(${target} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${INC})
|
||||
target_link_libraries(${target} PRIVATE liblooper)
|
||||
if(${USE_PORTALS})
|
||||
target_pkgconfig(TARGETS imgui_ui PRIVATE PREFIX libPortal LIBRARIES libportal)
|
||||
target_pkgconfig(TARGETS ${target} PRIVATE PREFIX libPortal LIBRARIES libportal)
|
||||
if (NOT ${libPortal_FOUND} EQUAL "1")
|
||||
set(USE_PORTALS OFF)
|
||||
else()
|
||||
|
@ -227,7 +325,7 @@ macro(ui_backend_subdir)
|
|||
endmacro()
|
||||
set(ENABLED_UIS )
|
||||
ui_backend_subdir(NAME "IMGUI" READABLE_NAME "Dear ImGui" SUBDIR backends/ui/imgui)
|
||||
if (NOT DEFINED EMSCRIPTEN)
|
||||
if (NOT (DEFINED EMSCRIPTEN OR DEFINED ANDROID_NDK))
|
||||
ui_backend_subdir(NAME "GTK" READABLE_NAME "GTK4" SUBDIR backends/ui/gtk)
|
||||
endif()
|
||||
execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen_ui_backend_inc.py ${ENABLED_UIS})
|
||||
|
@ -243,7 +341,11 @@ endif()
|
|||
function(copy_to_bindir src dst)
|
||||
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/${src}" "$<TARGET_FILE_DIR:${TARGET_NAME}>/${dst}")
|
||||
endfunction()
|
||||
add_executable(${TARGET_NAME} ${SOURCES})
|
||||
if (DEFINED ANDROID_NDK)
|
||||
add_library(${TARGET_NAME} SHARED ${SOURCES})
|
||||
else()
|
||||
add_executable(${TARGET_NAME} ${SOURCES})
|
||||
endif()
|
||||
add_dependencies(${TARGET_NAME} looper_assets ${UI_BACKENDS})
|
||||
|
||||
if(DEFINED EMSCRIPTEN)
|
||||
|
@ -259,6 +361,9 @@ else()
|
|||
endif()
|
||||
target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS})
|
||||
install(TARGETS ${TARGET_NAME} ${EXTRA_LIBS})
|
||||
if (${BUILD_SDL2})
|
||||
install(EXPORT SDL2-static SDL2main)
|
||||
endif()
|
||||
if (NOT DEFINED EMSCRIPTEN)
|
||||
install(FILES assets/icon.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps/)
|
||||
install(FILES assets/com.complecwaft.Looper.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<arg type='a{sv}' name='platform_data' direction='in'/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="com.complecwaft.Looper">
|
||||
<interface name="com.complecwaft.looper">
|
||||
<method name="CreateHandle">
|
||||
<arg type='s' name='new_handle' direction='out' />
|
||||
</method>
|
||||
|
@ -81,7 +81,7 @@
|
|||
<property name="IsDaemon" type="b" access="read" />
|
||||
<property name="StreamIdx" type="u" access="read" />
|
||||
</interface>
|
||||
<interface name="com.complecwaft.Looper.Errors" >
|
||||
<interface name="com.complecwaft.looper.Errors" >
|
||||
<method name="PopFront">
|
||||
<arg name="handle" direction="in" type="s" />
|
||||
<arg name="error" direction="out" type="s" />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>com.complecwaft.Looper</id>
|
||||
<id>com.complecwaft.looper</id>
|
||||
<developer_name>Catmeow72</developer_name>
|
||||
|
||||
<name>Looper</name>
|
||||
|
|
|
@ -48,13 +48,13 @@ private:
|
|||
namespace com {
|
||||
namespace complecwaft {
|
||||
|
||||
class Looper_adaptor
|
||||
class looper_adaptor
|
||||
{
|
||||
public:
|
||||
static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper";
|
||||
static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper";
|
||||
|
||||
protected:
|
||||
Looper_adaptor(sdbus::IObject& object)
|
||||
looper_adaptor(sdbus::IObject& object)
|
||||
: object_(&object)
|
||||
{
|
||||
object_->registerMethod("CreateHandle").onInterface(INTERFACE_NAME).withOutputParamNames("new_handle").implementedAs([this](){ return this->CreateHandle(); });
|
||||
|
@ -90,12 +90,12 @@ protected:
|
|||
object_->registerProperty("StreamIdx").onInterface(INTERFACE_NAME).withGetter([this](){ return this->StreamIdx(); });
|
||||
}
|
||||
|
||||
Looper_adaptor(const Looper_adaptor&) = delete;
|
||||
Looper_adaptor& operator=(const Looper_adaptor&) = delete;
|
||||
Looper_adaptor(Looper_adaptor&&) = default;
|
||||
Looper_adaptor& operator=(Looper_adaptor&&) = default;
|
||||
looper_adaptor(const looper_adaptor&) = delete;
|
||||
looper_adaptor& operator=(const looper_adaptor&) = delete;
|
||||
looper_adaptor(looper_adaptor&&) = default;
|
||||
looper_adaptor& operator=(looper_adaptor&&) = default;
|
||||
|
||||
~Looper_adaptor() = default;
|
||||
~looper_adaptor() = default;
|
||||
|
||||
public:
|
||||
void emitPlaybackEngineStarted()
|
||||
|
@ -183,12 +183,12 @@ private:
|
|||
|
||||
namespace com {
|
||||
namespace complecwaft {
|
||||
namespace Looper {
|
||||
namespace looper {
|
||||
|
||||
class Errors_adaptor
|
||||
{
|
||||
public:
|
||||
static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper.Errors";
|
||||
static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper.Errors";
|
||||
|
||||
protected:
|
||||
Errors_adaptor(sdbus::IObject& object)
|
||||
|
|
|
@ -56,13 +56,13 @@ private:
|
|||
namespace com {
|
||||
namespace complecwaft {
|
||||
|
||||
class Looper_proxy
|
||||
class looper_proxy
|
||||
{
|
||||
public:
|
||||
static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper";
|
||||
static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper";
|
||||
|
||||
protected:
|
||||
Looper_proxy(sdbus::IProxy& proxy)
|
||||
looper_proxy(sdbus::IProxy& proxy)
|
||||
: proxy_(&proxy)
|
||||
{
|
||||
proxy_->uponSignal("PlaybackEngineStarted").onInterface(INTERFACE_NAME).call([this](){ this->onPlaybackEngineStarted(); });
|
||||
|
@ -76,12 +76,12 @@ protected:
|
|||
proxy_->uponSignal("FileChanged").onInterface(INTERFACE_NAME).call([this](const std::string& path, const std::string& title){ this->onFileChanged(path, title); });
|
||||
}
|
||||
|
||||
Looper_proxy(const Looper_proxy&) = delete;
|
||||
Looper_proxy& operator=(const Looper_proxy&) = delete;
|
||||
Looper_proxy(Looper_proxy&&) = default;
|
||||
Looper_proxy& operator=(Looper_proxy&&) = default;
|
||||
looper_proxy(const looper_proxy&) = delete;
|
||||
looper_proxy& operator=(const looper_proxy&) = delete;
|
||||
looper_proxy(looper_proxy&&) = default;
|
||||
looper_proxy& operator=(looper_proxy&&) = default;
|
||||
|
||||
~Looper_proxy() = default;
|
||||
~looper_proxy() = default;
|
||||
|
||||
virtual void onPlaybackEngineStarted() = 0;
|
||||
virtual void onSpeedChanged(const double& new_speed) = 0;
|
||||
|
@ -247,12 +247,12 @@ private:
|
|||
|
||||
namespace com {
|
||||
namespace complecwaft {
|
||||
namespace Looper {
|
||||
namespace looper {
|
||||
|
||||
class Errors_proxy
|
||||
{
|
||||
public:
|
||||
static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper.Errors";
|
||||
static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper.Errors";
|
||||
|
||||
protected:
|
||||
Errors_proxy(sdbus::IProxy& proxy)
|
||||
|
|
|
@ -6,9 +6,10 @@ else()
|
|||
set(GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE OFF)
|
||||
endif()
|
||||
option(USE_GLES "Enable using OpenGL ES" ${GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE})
|
||||
option(GLES_VERSION "Version of OpenGL ES" 3)
|
||||
set(IMGUI_SRC imgui_demo.cpp imgui_draw.cpp imgui_tables.cpp imgui_widgets.cpp imgui.cpp misc/cpp/imgui_stdlib.cpp)
|
||||
set(IMGUI_BACKEND_SRC imgui_impl_opengl3.cpp imgui_impl_sdl2.cpp)
|
||||
set(BACKEND_IMGUI_SRC_BASE main.cpp base85.cpp file_browser.cpp main.cpp RendererBackend.cpp theme.cpp translation.cpp)
|
||||
set(IMGUI_BACKEND_SRC imgui_impl_sdlrenderer2.cpp imgui_impl_sdl2.cpp)
|
||||
set(BACKEND_IMGUI_SRC_BASE main.cpp base85.cpp file_browser.cpp main.cpp RendererBackend.cpp theme.cpp)
|
||||
foreach(SRC IN ITEMS ${IMGUI_BACKEND_SRC})
|
||||
list(APPEND IMGUI_SRC backends/${SRC})
|
||||
endforeach()
|
||||
|
@ -25,25 +26,38 @@ set(BACKEND_IMGUI_INC ${INC} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_
|
|||
foreach(INCDIR IN ITEMS ${BACKEND_IMGUI_INC_BASE})
|
||||
set(BACKEND_IMGUI_INC ${BACKEND_IMGUI_INC} ${CMAKE_CURRENT_SOURCE_DIR}/${INCDIR})
|
||||
endforeach()
|
||||
if(${USE_GLES})
|
||||
set(GLComponents GLES2)
|
||||
set(GLTarget GLES2)
|
||||
if(USE_GLES)
|
||||
set(GLComponents GLES${GLES_VERSION})
|
||||
set(GLTarget GLES${GLES_VERSION})
|
||||
else()
|
||||
set(GLComponents OpenGL)
|
||||
set(GLTarget GL)
|
||||
endif()
|
||||
find_package(OpenGL REQUIRED COMPONENTS ${GLComponents})
|
||||
find_package(OpenGL COMPONENTS ${GLComponents})
|
||||
if (NOT ${OpenGL_FOUND})
|
||||
if (DEFINED CMAKE_ANDROID_NDK)
|
||||
if (USE_GLES)
|
||||
find_path(GLES_INCLUDE_DIR GLES${GLES_VERSION}/gl${GLES_VERSION}.h HINTS ${CMAKE_ANDROID_NDK})
|
||||
find_library(GLES2_LIBRARY libGLESv2.so HINTS ${GLES_INCLUDE_DIR}/../lib)
|
||||
find_library(GLES3_LIBRARY libGLESv3.so HINTS ${GLES_INCLUDE_DIR}/../lib)
|
||||
add_library(OpenGL::${GLTarget} INTERFACE IMPORTED)
|
||||
target_include_directories(OpenGL::${GLTarget} INTERFACE ${GLES2_INCLUDE_DIR})
|
||||
target_link_libraries(OpenGL::${GLTarget} INTERFACE ${GLES2_LIBRARY} ${GLES3_LIBRARY})
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
add_ui_backend(imgui_ui ${BACKEND_IMGUI_SRC})
|
||||
if(${USE_GLES})
|
||||
target_compile_definitions(imgui_ui PRIVATE "IMGUI_IMPL_OPENGL_ES2")
|
||||
if(USE_GLES)
|
||||
target_compile_definitions(imgui_ui PRIVATE IMGUI_IMPL_OPENGL_ES${GLES_VERSION})
|
||||
endif()
|
||||
if(DEFINED EMSCRIPTEN)
|
||||
target_compile_options(imgui_ui PRIVATE "-sUSE_SDL_IMAGE=2")
|
||||
target_link_options(imgui_ui PUBLIC "-sMAX_WEBGL_VERSION=2" "-sMIN_WEBGL_VERSION=2" "-sFULL_ES3")
|
||||
target_link_libraries(imgui PRIVATE ${LIBINTL_LIBRARY})
|
||||
else()
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(SDL2_image REQUIRED)
|
||||
target_link_libraries(imgui_ui PRIVATE OpenGL::${GLTarget} SDL2::SDL2 SDL2_image::SDL2_image)
|
||||
target_link_libraries(imgui_ui PRIVATE OpenGL::${GLTarget} SDL2::SDL2 SDL2_image::SDL2_image ${LIBINTL_LIBRARY})
|
||||
endif()
|
||||
target_include_directories(imgui_ui PRIVATE ${BACKEND_IMGUI_INC})
|
||||
target_include_directories(imgui_ui PRIVATE ${BACKEND_IMGUI_INC} ${LIBINTL_INCDIRS})
|
||||
target_compile_definitions(imgui_ui PRIVATE IMGUI_USER_CONFIG="imgui_config.h")
|
||||
|
|
|
@ -5,18 +5,23 @@
|
|||
#include "config.h"
|
||||
#include <SDL_image.h>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <initializer_list>
|
||||
#include "theme.h"
|
||||
#include "imgui_stdlib.h"
|
||||
#include "imgui_impl_sdl2.h"
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include "imgui_impl_sdlrenderer2.h"
|
||||
#include "base85.h"
|
||||
#include <thread>
|
||||
#include "translation.h"
|
||||
#include <translation.hpp>
|
||||
#include <log.hpp>
|
||||
#include <options.hpp>
|
||||
using std::vector;
|
||||
using namespace Looper::Options;
|
||||
#ifdef __EMSCRIPTEN__
|
||||
extern "C" {
|
||||
extern void get_size(int32_t *x, int32_t *y);
|
||||
extern double get_dpi();
|
||||
}
|
||||
#endif
|
||||
void RendererBackend::on_resize() {
|
||||
|
@ -24,6 +29,7 @@ void RendererBackend::on_resize() {
|
|||
int32_t x, y;
|
||||
get_size(&x, &y);
|
||||
SDL_SetWindowSize(window, (int)x, (int)y);
|
||||
UpdateScale();
|
||||
#endif
|
||||
}
|
||||
static RendererBackend *renderer_backend;
|
||||
|
@ -40,14 +46,41 @@ void main_loop() {
|
|||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
struct FontData {
|
||||
const ImWchar *ranges;
|
||||
std::map<std::string, std::pair<const char *, double>> data;
|
||||
void Init(const ImWchar *ranges_in, std::map<std::string, std::pair<const char *, double>> data_in) {
|
||||
ranges = ranges_in;
|
||||
data = data_in;
|
||||
}
|
||||
FontData(const ImWchar *ranges, std::initializer_list<std::pair<std::string, std::pair<const char*, double>>> data) {
|
||||
std::map<std::string, std::pair<const char*, double>> out_data;
|
||||
for (auto pair : data) {
|
||||
out_data[pair.first] = pair.second;
|
||||
}
|
||||
Init(ranges, out_data);
|
||||
}
|
||||
FontData(const ImWchar *ranges, std::initializer_list<std::tuple<std::string, const char*, double>> data) {
|
||||
std::map<std::string, std::pair<const char*, double>> out_data;
|
||||
for (auto tuple : data) {
|
||||
std::pair<const char*, double> outPair = {std::get<1>(tuple), std::get<2>(tuple)};
|
||||
out_data[std::get<0>(tuple)] = outPair;
|
||||
}
|
||||
Init(ranges, out_data);
|
||||
}
|
||||
FontData(const ImWchar *ranges, std::map<std::string, std::pair<const char*, double>> data) {
|
||||
Init(ranges, data);
|
||||
}
|
||||
};
|
||||
void RendererBackend::BackendDeinit() {
|
||||
ImGuiIO& io = ImGui::GetIO(); (void)io;
|
||||
// Cleanup
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDLRenderer2_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
SDL_GL_DeleteContext(gl_context);
|
||||
SDL_DestroyRenderer(rend);
|
||||
SDL_DestroyWindow(window);
|
||||
IMG_Quit();
|
||||
SDL_Quit();
|
||||
|
@ -55,7 +88,11 @@ void RendererBackend::BackendDeinit() {
|
|||
Deinit();
|
||||
renderer_backend = nullptr;
|
||||
}
|
||||
bool RendererBackend::isTouchScreenMode() {
|
||||
return touchScreenModeOverride.value_or(SDL_GetNumTouchDevices() > 0);
|
||||
}
|
||||
void RendererBackend::LoopFunction() {
|
||||
SDL_RenderSetVSync(rend, vsync ? 1 : 0);
|
||||
ImGuiIO& io = ImGui::GetIO(); (void)io;
|
||||
if (resize_needed) {
|
||||
on_resize();
|
||||
|
@ -92,78 +129,71 @@ void RendererBackend::LoopFunction() {
|
|||
}
|
||||
}
|
||||
}
|
||||
bool touchScreenMode = isTouchScreenMode();
|
||||
if (touchScreenMode) {
|
||||
io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
|
||||
} else {
|
||||
io.ConfigFlags &= ~ImGuiConfigFlags_IsTouchScreen;
|
||||
}
|
||||
// Start the Dear ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDLRenderer2_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
// Run the GUI
|
||||
GuiFunction();
|
||||
// Rendering
|
||||
ImGui::Render();
|
||||
// Update the window size.
|
||||
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
|
||||
// Clear the screen.
|
||||
glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
SDL_SetRenderDrawColor(rend, (Uint8)(clear_color.x * clear_color.w * 255), (Uint8)(clear_color.y * clear_color.w * 255), (Uint8)(clear_color.z * clear_color.w * 255), (Uint8)(clear_color.w * 255));
|
||||
SDL_RenderClear(rend);
|
||||
// Tell ImGui to render.
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData());
|
||||
|
||||
// Update and Render additional Platform Windows
|
||||
// (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere.
|
||||
// For this specific demo app we could also call SDL_GL_MakeCurrent(window, gl_context) directly)
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
||||
{
|
||||
SDL_Window* backup_current_window = SDL_GL_GetCurrentWindow();
|
||||
SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext();
|
||||
ImGui::UpdatePlatformWindows();
|
||||
ImGui::RenderPlatformWindowsDefault();
|
||||
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
|
||||
}
|
||||
|
||||
// Swap the buffers, and do VSync if enabled.
|
||||
SDL_GL_SwapWindow(window);
|
||||
SDL_RenderPresent(rend);
|
||||
// If not doing VSync, wait until the next frame needs to be rendered.
|
||||
if (!vsync) {
|
||||
std::this_thread::sleep_until(next_frame);
|
||||
}
|
||||
}
|
||||
struct FontData {
|
||||
const char* data;
|
||||
const ImWchar *ranges;
|
||||
};
|
||||
|
||||
ImFont *add_font(vector<FontData> data_vec, int size = 13) {
|
||||
ImFont* font = nullptr;
|
||||
std::map<std::string, ImFont *> add_font(FontData data_vec, double scale) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
for (auto data : data_vec) {
|
||||
ImFontConfig font_cfg = ImFontConfig();
|
||||
font_cfg.SizePixels = size;
|
||||
font_cfg.OversampleH = font_cfg.OversampleV = 1;
|
||||
font_cfg.PixelSnapH = true;
|
||||
if (font_cfg.SizePixels <= 0.0f)
|
||||
font_cfg.SizePixels = 13.0f * 1.0f;
|
||||
if (font != nullptr) {
|
||||
font_cfg.DstFont = font;
|
||||
font_cfg.MergeMode = true;
|
||||
}
|
||||
//font_cfg.EllipsisChar = (ImWchar)0x0085;
|
||||
//font_cfg.GlyphOffset.y = 1.0f * IM_FLOOR(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units
|
||||
std::map<std::string, ImFont*> output;
|
||||
for (auto value : data_vec.data) {
|
||||
ImFont* font = nullptr;
|
||||
std::string id = value.first;
|
||||
const char *data = value.second.first;
|
||||
double size = value.second.second * scale;
|
||||
{
|
||||
ImFontConfig font_cfg = ImFontConfig();
|
||||
font_cfg.SizePixels = size;
|
||||
font_cfg.OversampleH = font_cfg.OversampleV = 1;
|
||||
font_cfg.PixelSnapH = true;
|
||||
if (font_cfg.SizePixels <= 0.0f)
|
||||
font_cfg.SizePixels = 13.0f * 1.0f;
|
||||
//font_cfg.EllipsisChar = (ImWchar)0x0085;
|
||||
//font_cfg.GlyphOffset.y = 1.0f * IM_FLOOR(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units
|
||||
|
||||
const char* ttf_compressed_base85 = data.data;
|
||||
const ImWchar* glyph_ranges = data.ranges;
|
||||
auto new_font = io.Fonts->AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, glyph_ranges);
|
||||
if (font == nullptr) font = new_font;
|
||||
const char *ttf_compressed_base85 = data;
|
||||
const ImWchar *glyph_ranges = data_vec.ranges;
|
||||
font = io.Fonts->AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85,
|
||||
font_cfg.SizePixels,
|
||||
&font_cfg, glyph_ranges);
|
||||
}
|
||||
{
|
||||
ImFontConfig config;
|
||||
config.MergeMode = true;
|
||||
config.GlyphMinAdvanceX = size;
|
||||
config.SizePixels = size;
|
||||
config.DstFont = font;
|
||||
static const ImWchar icon_ranges[] = {ICON_MIN_FK, ICON_MAX_FK, 0};
|
||||
io.Fonts->AddFontFromMemoryCompressedBase85TTF(forkawesome_compressed_data_base85,
|
||||
float(size), &config, icon_ranges);
|
||||
}
|
||||
output[id] = font;
|
||||
}
|
||||
{
|
||||
ImFontConfig config;
|
||||
config.MergeMode = true;
|
||||
config.GlyphMinAdvanceX = size;
|
||||
config.SizePixels = size;
|
||||
config.DstFont = font;
|
||||
static const ImWchar icon_ranges[] = { ICON_MIN_FK, ICON_MAX_FK, 0 };
|
||||
io.Fonts->AddFontFromMemoryCompressedBase85TTF(forkawesome_compressed_data_base85, float(size), &config, icon_ranges);
|
||||
}
|
||||
return font;
|
||||
return output;
|
||||
}
|
||||
RendererBackend::RendererBackend() {
|
||||
}
|
||||
|
@ -184,6 +214,9 @@ void RendererBackend::UpdateScale() {
|
|||
#else
|
||||
96.0;
|
||||
#endif
|
||||
#ifdef __EMSCRIPTEN__
|
||||
scale = get_dpi() / defaultDPI;
|
||||
#else
|
||||
float dpi = defaultDPI;
|
||||
if (SDL_GetDisplayDPI(SDL_GetWindowDisplayIndex(window), NULL, &dpi, NULL) == 0){
|
||||
scale = dpi / defaultDPI;
|
||||
|
@ -191,13 +224,18 @@ void RendererBackend::UpdateScale() {
|
|||
WARNING.writeln("DPI couldn't be determined!");
|
||||
scale = 1.0;
|
||||
}
|
||||
#ifndef __ANDROID__
|
||||
SDL_SetWindowSize(window, window_width * scale, window_height * scale);
|
||||
#endif
|
||||
#endif
|
||||
AddFonts();
|
||||
}
|
||||
void RendererBackend::SetWindowSize(int w, int h) {
|
||||
#if !(defined(__ANDROID__) || defined(__EMSCRIPTEN__))
|
||||
window_width = w;
|
||||
window_height = h;
|
||||
SDL_SetWindowSize(window, w * scale, h * scale);
|
||||
#endif
|
||||
}
|
||||
void RendererBackend::GetWindowsize(int *w, int *h) {
|
||||
int ww, wh;
|
||||
|
@ -208,12 +246,25 @@ void RendererBackend::GetWindowsize(int *w, int *h) {
|
|||
if (h) *h = wh;
|
||||
}
|
||||
void RendererBackend::AddFonts() {
|
||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||
ImGui_ImplSDLRenderer2_DestroyFontsTexture();
|
||||
auto& io = ImGui::GetIO(); (void)io;
|
||||
io.Fonts->Clear();
|
||||
add_font(vector<FontData>({FontData {notosans_regular_compressed_data_base85, io.Fonts->GetGlyphRangesDefault()}, FontData {notosansjp_regular_compressed_data_base85, io.Fonts->GetGlyphRangesJapanese()}}), 13 * scale);
|
||||
title = add_font(vector<FontData>({FontData {notosans_thin_compressed_data_base85, io.Fonts->GetGlyphRangesDefault()}, FontData {notosansjp_thin_compressed_data_base85, io.Fonts->GetGlyphRangesJapanese()}}), 48 * scale);
|
||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||
std::string font_type = get_option<std::string>("font_type", "default");
|
||||
std::string default_font = "default";
|
||||
std::string jp_font = "japanese";
|
||||
auto glyph_ranges_default = io.Fonts->GetGlyphRangesDefault();
|
||||
auto glyph_ranges_jp = io.Fonts->GetGlyphRangesJapanese();
|
||||
FontData latin(glyph_ranges_default, {{"normal", notosans_regular_compressed_data_base85, 13}, {"title", notosans_thin_compressed_data_base85, 32}});
|
||||
FontData jp(glyph_ranges_jp, {{"normal", notosansjp_regular_compressed_data_base85, 13}, {"title", notosansjp_thin_compressed_data_base85, 32}});
|
||||
std::map<std::string, ImFont*> font_map;
|
||||
if (font_type == jp_font) {
|
||||
font_map = add_font(jp, scale);
|
||||
} else {
|
||||
font_map = add_font(latin, scale);
|
||||
}
|
||||
title = font_map["title"];
|
||||
io.FontDefault = font_map["normal"];
|
||||
ImGui_ImplSDLRenderer2_CreateFontsTexture();
|
||||
}
|
||||
#ifdef __EMSCRIPTEN__
|
||||
static EM_BOOL resize_callback(int event_type, const EmscriptenUiEvent *event, void *userdata) {
|
||||
|
@ -221,9 +272,7 @@ static EM_BOOL resize_callback(int event_type, const EmscriptenUiEvent *event, v
|
|||
}
|
||||
#endif
|
||||
int RendererBackend::Run() {
|
||||
setlocale(LC_ALL, "");
|
||||
bindtextdomain("neko_player", LOCALE_DIR);
|
||||
textdomain("neko_player");
|
||||
setup_locale("neko_player");
|
||||
DEBUG.writefln("Loaded locale '%s' from '%s'...", CURRENT_LANGUAGE, LOCALE_DIR);
|
||||
DEBUG.writefln("Locale name: %s", _TR_CTX("Language name", "English (United States)"));
|
||||
bool enable_kms = std::getenv("LAP_KMS") != nullptr;
|
||||
|
@ -239,54 +288,31 @@ int RendererBackend::Run() {
|
|||
enable_kms = true;
|
||||
}
|
||||
IMG_Init(IMG_INIT_PNG|IMG_INIT_WEBP);
|
||||
#ifdef __ANDROID__
|
||||
prefPath = SDL_AndroidGetInternalStoragePath();
|
||||
#else
|
||||
prefPath = SDL_GetPrefPath("Catmeow72", NAME);
|
||||
#endif
|
||||
Theme::prefPath = prefPath;
|
||||
|
||||
// Decide GL+GLSL versions
|
||||
#if defined(IMGUI_IMPL_OPENGL_ES2)
|
||||
// GL ES 2.0 + GLSL 100
|
||||
const char* glsl_version = "#version 100";
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
#elif defined(__APPLE__)
|
||||
// GL 3.2 Core + GLSL 150
|
||||
const char* glsl_version = "#version 150";
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
|
||||
#else
|
||||
// GL 3.0 + GLSL 130
|
||||
const char* glsl_version = "#version 130";
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
#endif
|
||||
|
||||
// From 2.0.18: Enable native IME.
|
||||
#ifdef SDL_HINT_IME_SHOW_UI
|
||||
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
|
||||
#endif
|
||||
|
||||
// Create window with graphics context
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
|
||||
window = SDL_CreateWindow(NAME, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, window_width, window_height, window_flags);
|
||||
SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
|
||||
SDL_CreateWindowAndRenderer(window_width, window_height, window_flags, &window, &rend);
|
||||
|
||||
#ifndef __ANDROID__
|
||||
SDL_SetWindowMinimumSize(window, window_width, window_height);
|
||||
if (enable_kms) {
|
||||
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
|
||||
}
|
||||
#endif
|
||||
SDL_EventState(SDL_DROPFILE, SDL_ENABLE);
|
||||
const vector<unsigned char> icon_data = DecodeBase85(icon_compressed_data_base85);
|
||||
SDL_Surface* icon = IMG_Load_RW(SDL_RWFromConstMem(icon_data.data(), icon_data.size()), 1);
|
||||
SDL_SetWindowIcon(window, icon);
|
||||
gl_context = SDL_GL_CreateContext(window);
|
||||
SDL_GL_MakeCurrent(window, gl_context);
|
||||
|
||||
// Setup Dear ImGui context
|
||||
IMGUI_CHECKVERSION();
|
||||
|
@ -295,6 +321,9 @@ int RendererBackend::Run() {
|
|||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
if (SDL_GetNumTouchDevices() > 0) {
|
||||
io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
|
||||
}
|
||||
//io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
||||
io.IniFilename = strdup((std::string(prefPath) + "imgui.ini").c_str());
|
||||
if (enable_kms) {
|
||||
|
@ -306,8 +335,8 @@ int RendererBackend::Run() {
|
|||
//ImGui::StyleColorsLight();
|
||||
|
||||
// Setup Platform/Renderer backends
|
||||
ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
|
||||
ImGui_ImplOpenGL3_Init(glsl_version);
|
||||
ImGui_ImplSDL2_InitForSDLRenderer(window, rend);
|
||||
ImGui_ImplSDLRenderer2_Init(rend);
|
||||
// Load Fonts
|
||||
// - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them.
|
||||
// - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple.
|
||||
|
@ -327,6 +356,9 @@ int RendererBackend::Run() {
|
|||
|
||||
|
||||
theme = new Theme(false);
|
||||
#ifdef __ANDROID__
|
||||
userdir = SDL_AndroidGetExternalStoragePath();
|
||||
#else
|
||||
userdir = std::getenv(
|
||||
#ifdef _WIN32
|
||||
"UserProfile"
|
||||
|
@ -334,9 +366,8 @@ int RendererBackend::Run() {
|
|||
"HOME"
|
||||
#endif
|
||||
);
|
||||
#ifndef __EMSCRIPTEN__
|
||||
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
|
||||
#endif
|
||||
#endif
|
||||
SDL_RenderSetVSync(rend, vsync ? 1 : 0);
|
||||
theme->Apply(accent_color);
|
||||
Init();
|
||||
SDL_ShowWindow(window);
|
||||
|
|
|
@ -19,13 +19,16 @@ static const char* NAME = "Looper";
|
|||
class RendererBackend {
|
||||
void BackendDeinit();
|
||||
void LoopFunction();
|
||||
SDL_GLContext gl_context;
|
||||
//SDL_GLContext gl_context;
|
||||
bool resize_needed = true;
|
||||
void on_resize();
|
||||
public:
|
||||
std::optional<bool> touchScreenModeOverride;
|
||||
bool isTouchScreenMode();
|
||||
static void resize_static();
|
||||
double scale = 1.0;
|
||||
SDL_Window *window;
|
||||
SDL_Renderer *rend;
|
||||
int window_width = 475;
|
||||
int window_height = 354;
|
||||
bool done = false;
|
||||
|
|
|
@ -4,6 +4,41 @@
|
|||
#include <cctype>
|
||||
#include <stdio.h>
|
||||
#include <log.hpp>
|
||||
#ifdef __ANDROID__
|
||||
#include <jni.h>
|
||||
jclass MainActivity;
|
||||
jobject mainActivity;
|
||||
jmethodID GetUserDir_Method;
|
||||
jmethodID OpenFilePicker_Method;
|
||||
jmethodID GetPickedFile_Method;
|
||||
jmethodID ClearSelected_Method;
|
||||
jmethodID IsLoading_Method;
|
||||
JNIEnv *env;
|
||||
bool IsLoading() {
|
||||
return env->CallStaticBooleanMethod(MainActivity, IsLoading_Method, 0);
|
||||
}
|
||||
const char *GetUserDir() {
|
||||
jstring str = (jstring)env->CallStaticObjectMethod(MainActivity, GetUserDir_Method, 0);;
|
||||
jboolean tmp = true;
|
||||
return env->GetStringUTFChars(str, &tmp);
|
||||
}
|
||||
void OpenFilePicker(const char *pwd) {
|
||||
if (pwd == nullptr) {
|
||||
jstring string = env->NewStringUTF(pwd);
|
||||
env->CallStaticVoidMethod(MainActivity, OpenFilePicker_Method, string);
|
||||
} else {
|
||||
env->CallStaticVoidMethod(MainActivity, OpenFilePicker_Method, nullptr);
|
||||
}
|
||||
}
|
||||
const char *GetPickedFile() {
|
||||
jstring str = (jstring)env->CallStaticObjectMethod(MainActivity, GetPickedFile_Method, 0);
|
||||
jboolean tmp = true;
|
||||
return env->GetStringUTFChars(str, &tmp);
|
||||
}
|
||||
void ClearSelected() {
|
||||
env->CallStaticVoidMethod(MainActivity, ClearSelected_Method, 0);
|
||||
}
|
||||
#endif
|
||||
#ifdef __EMSCRIPTEN__
|
||||
extern "C" {
|
||||
extern void open_filepicker();
|
||||
|
@ -28,6 +63,7 @@ FileBrowser::FileBrowser(bool save, ImGuiFileBrowserFlags extra_fallback_flags)
|
|||
this->save = save;
|
||||
this->flags = (save ? ImGuiFileBrowserFlags_CreateNewDir|ImGuiFileBrowserFlags_EnterNewFilename : 0) | extra_fallback_flags;
|
||||
fallback = ImGui::FileBrowser(this->flags);
|
||||
|
||||
}
|
||||
void FileBrowser::SetTypeFilters(string name, vector<string> filters) {
|
||||
filter_name = name;
|
||||
|
@ -68,13 +104,15 @@ void FileBrowser::SetTypeFilters(string name, vector<string> filters) {
|
|||
}
|
||||
void FileBrowser::SetPwd(path path) {
|
||||
pwd = path;
|
||||
#ifndef PORTALS
|
||||
#if !(defined(PORTALS) || defined(__ANDROID__))
|
||||
fallback.SetPwd(path);
|
||||
#endif
|
||||
}
|
||||
bool FileBrowser::HasSelected() {
|
||||
#ifdef PORTALS
|
||||
return selected.has_value();
|
||||
#elif defined(__ANDROID__)
|
||||
return strlen(GetPickedFile()) > 0;
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
return file_picker_confirmed();
|
||||
#else
|
||||
|
@ -84,6 +122,13 @@ bool FileBrowser::HasSelected() {
|
|||
path FileBrowser::GetSelected() {
|
||||
#ifdef PORTALS
|
||||
return selected.value_or(path());
|
||||
#elif defined(__ANDROID__)
|
||||
const char *file = GetPickedFile();
|
||||
if (strlen(file) > 0) {
|
||||
return std::string(file);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
if (HasSelected()) {
|
||||
const char *c_file = get_first_file();
|
||||
|
@ -113,6 +158,10 @@ void FileBrowser::Open() {
|
|||
} else {
|
||||
xdp_portal_open_file(portal, NULL, title.c_str(), variant, NULL, NULL, XDP_OPEN_FILE_FLAG_NONE, NULL, &FileBrowser::FileBrowserOpenCallback, this);
|
||||
}
|
||||
#elif defined(__ANDROID__)
|
||||
ClearSelected();
|
||||
open = true;
|
||||
::OpenFilePicker(nullptr);
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
open_filepicker();
|
||||
#else
|
||||
|
@ -194,6 +243,27 @@ void FileBrowser::FileBrowserSaveCallback(GObject *src, GAsyncResult *res, gpoin
|
|||
void FileBrowser::Display() {
|
||||
#ifdef PORTALS
|
||||
g_main_context_iteration(main_context, false);
|
||||
#elif defined(__ANDROID__)
|
||||
if (HasSelected()) {
|
||||
open = false;
|
||||
}
|
||||
if (IsLoading()) {
|
||||
ImVec2 pos(0, 0);
|
||||
ImVec2 size = ImGui::GetMainViewport()->Size;
|
||||
ImGui::SetNextWindowPos(pos);
|
||||
ImGui::SetNextWindowSize(size);
|
||||
ImGui::Begin("Loading...", NULL, ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_Modal|ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoTitleBar);
|
||||
ImVec2 textSize = ImGui::CalcTextSize("Loading...");
|
||||
ImVec2 textPos = size;
|
||||
textPos.x -= textSize.x;
|
||||
textPos.y -= textSize.y;
|
||||
textPos.x /= 2;
|
||||
textPos.y /= 2;
|
||||
ImGui::SetCursorPos(textPos);
|
||||
ImGui::TextUnformatted("Loading...");
|
||||
ImGui::End();
|
||||
|
||||
}
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
if (file_picker_visible() || file_picker_loading()) {
|
||||
if((flags & ImGuiFileBrowserFlags_NoModal))
|
||||
|
@ -245,6 +315,9 @@ void FileBrowser::ClearSelected() {
|
|||
#ifdef __EMSCRIPTEN__
|
||||
clear_file_selection();
|
||||
#endif
|
||||
#ifdef __ANDROID__
|
||||
::ClearSelected();
|
||||
#endif
|
||||
#ifndef PORTALS
|
||||
fallback.ClearSelected();
|
||||
#endif
|
||||
|
@ -258,6 +331,8 @@ void FileBrowser::SetTitle(string title) {
|
|||
bool FileBrowser::IsOpened() {
|
||||
#ifdef PORTALS
|
||||
return open;
|
||||
#elif defined(__ANDROID__)
|
||||
return open;
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
return !file_picker_closed() || file_picker_confirmed();
|
||||
#else
|
||||
|
@ -277,4 +352,4 @@ FileBrowser::~FileBrowser() {
|
|||
}
|
||||
g_main_loop_quit(main_loop);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -196,7 +196,9 @@ inline ImGui::FileBrowser::FileBrowser(ImGuiFileBrowserFlags flags)
|
|||
inputNameBuf_->front() = '\0';
|
||||
inputNameBuf_->back() = '\0';
|
||||
SetTitle("file browser");
|
||||
#ifndef __ANDROID__
|
||||
SetPwd(std::filesystem::current_path());
|
||||
#endif
|
||||
|
||||
typeFilters_.clear();
|
||||
typeFilterIndex_ = 0;
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
#pragma once
|
||||
#ifdef IMGUI_IMPL_OPENGL_ES2
|
||||
#undef IMGUI_IMPL_OPENGL_ES2
|
||||
#include <SDL_opengles2.h>
|
||||
#else
|
||||
#define IMGUI_IMPL_OPENGL_LOADER_CUSTOM
|
||||
|
|
|
@ -12,7 +12,7 @@ extern "C" {
|
|||
extern void enable_puter(bool enable);
|
||||
}
|
||||
#endif
|
||||
|
||||
using namespace Looper::Options;
|
||||
void MainLoop::Init() {
|
||||
#ifdef PORTALS
|
||||
g_set_application_name("Looper");
|
||||
|
@ -35,15 +35,12 @@ void MainLoop::Init() {
|
|||
{
|
||||
Json::Value config;
|
||||
std::ifstream stream;
|
||||
stream.open(path(prefPath) / "config.json");
|
||||
path jsonConfigPath = path(prefPath) / "config.json";
|
||||
stream.open(jsonConfigPath);
|
||||
if (stream.is_open()) {
|
||||
stream >> config;
|
||||
if (config.isMember("theme_name")) {
|
||||
path themePath = theme->themeDir / config["theme_name"].asString();
|
||||
if (exists(themePath)) {
|
||||
delete theme;
|
||||
theme = new Theme(themePath);
|
||||
}
|
||||
init_option<std::string>("ui.imgui.theme", config["theme_name"].asString());
|
||||
}
|
||||
if (config.isMember("accent_color")) {
|
||||
if (config["accent_color"].isNumeric()) {
|
||||
|
@ -52,27 +49,52 @@ void MainLoop::Init() {
|
|||
Json::Value accentColor = config["accent_color"];
|
||||
accent_color = ImVec4(accentColor["h"].asFloat(), accentColor["s"].asFloat(), accentColor["v"].asFloat(), accentColor["a"].asFloat());
|
||||
}
|
||||
toml::table accent_color_table;
|
||||
accent_color_table.insert("h", accent_color.x);
|
||||
accent_color_table.insert("s", accent_color.y);
|
||||
accent_color_table.insert("v", accent_color.z);
|
||||
accent_color_table.insert("a", accent_color.w);
|
||||
init_option<toml::table>("ui.imgui.accent_color", accent_color_table);
|
||||
}
|
||||
if (config.isMember("demo_window")) {
|
||||
show_demo_window = config["demo_window"].asBool();
|
||||
init_option<bool>("ui.imgui.demo_window", config["demo_window"].asBool());
|
||||
}
|
||||
if (config.isMember("vsync")) {
|
||||
vsync = config["vsync"].asBool();
|
||||
init_option<bool>("ui.imgui.vsync", config["vsync"].asBool());
|
||||
}
|
||||
if (config.isMember("framerate")) {
|
||||
framerate = config["framerate"].asUInt();
|
||||
init_option<int64_t>("ui.imgui.framerate", (int64_t)config["framerate"].asUInt());
|
||||
}
|
||||
if (config.isMember("lang")) {
|
||||
Json::Value langValue;
|
||||
if (langValue.isNull()) {
|
||||
lang = DEFAULT_LANG;
|
||||
} else {
|
||||
lang = config["lang"].asString();
|
||||
if (!langValue.isNull()) {
|
||||
init_option<std::string>("ui.imgui.lang", config["lang"].asString());
|
||||
}
|
||||
SET_LANG(lang.c_str());
|
||||
}
|
||||
stream.close();
|
||||
}
|
||||
std::remove(jsonConfigPath.c_str());
|
||||
}
|
||||
{
|
||||
std::string themeName = get_option<std::string>("ui.imgui.theme", "light");
|
||||
path themePath = Theme::themeDir / path(themeName + ".toml");
|
||||
if (exists(themePath)) {
|
||||
delete theme;
|
||||
theme = new Theme(themePath);
|
||||
}
|
||||
if (option_set("ui.imgui.lang")) {
|
||||
lang = get_option<std::string>("ui.imgui.lang");
|
||||
} else {
|
||||
lang = DEFAULT_LANG;
|
||||
}
|
||||
SET_LANG(lang.c_str());
|
||||
show_demo_window = get_option<bool>("ui.imgui.demo_window", false);
|
||||
vsync = get_option<bool>("ui.imgui.vsync", true);
|
||||
framerate = (unsigned)get_option<int64_t>("ui.imgui.framerate", 60);
|
||||
accent_color.x = (float)get_option<double>("ui.imgui.accent_color.h", accent_color.x);
|
||||
accent_color.y = (float)get_option<double>("ui.imgui.accent_color.s", accent_color.y);
|
||||
accent_color.z = (float)get_option<double>("ui.imgui.accent_color.v", accent_color.z);
|
||||
accent_color.w = (float)get_option<double>("ui.imgui.accent_color.a", accent_color.w);
|
||||
}
|
||||
if (is_empty(Theme::themeDir)) {
|
||||
path lightPath = Theme::themeDir / "light.toml";
|
||||
path darkPath = Theme::themeDir / "dark.toml";
|
||||
|
@ -114,7 +136,9 @@ void MainLoop::FileLoaded() {
|
|||
streams = playback->get_streams();
|
||||
}
|
||||
void MainLoop::GuiFunction() {
|
||||
#if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
|
||||
playback->LoopHook();
|
||||
#endif
|
||||
position = playback->GetPosition();
|
||||
length = playback->GetLength();
|
||||
// Set the window title if the file changed, or playback stopped.
|
||||
|
@ -278,9 +302,7 @@ void MainLoop::GuiFunction() {
|
|||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::Checkbox(_TR_CTX("Preference | VSync checkbox", "Enable VSync"), &vsync)) {
|
||||
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
|
||||
}
|
||||
ImGui::Checkbox(_TR_CTX("Preference | VSync checkbox", "Enable VSync"), &vsync);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - ImGui::GetStyle().WindowPadding.x);
|
||||
ImGui::SliderInt("##Framerate", &framerate, 10, 480, _TR_CTX("Preferences | Framerate slider", "Max framerate without VSync: %d"));
|
||||
|
@ -309,6 +331,21 @@ void MainLoop::GuiFunction() {
|
|||
SET_LANG(lang.c_str());
|
||||
}
|
||||
}
|
||||
bool overrideTouchscreenMode = touchScreenModeOverride.has_value();
|
||||
if (ImGui::Checkbox(_TR_CTX("Preference | override enable checkbox", "Override touchscreen mode"), &overrideTouchscreenMode)) {
|
||||
if (overrideTouchscreenMode) {
|
||||
touchScreenModeOverride = isTouchScreenMode();
|
||||
} else {
|
||||
touchScreenModeOverride = {};
|
||||
}
|
||||
}
|
||||
if (overrideTouchscreenMode) {
|
||||
bool touchScreenMode = isTouchScreenMode();
|
||||
if (ImGui::Checkbox(_TRI_CTX(ICON_FK_HAND_O_UP, "Preference | Checkbox (Only shown when override enabled)", "Enable touch screen mode."), &touchScreenMode)) {
|
||||
touchScreenModeOverride = touchScreenMode;
|
||||
}
|
||||
}
|
||||
|
||||
static string filter = "";
|
||||
ImGui::TextUnformatted(_TR_CTX("Preference | Theme selector | Filter label", "Filter:")); ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - ImGui::GetStyle().WindowPadding.x);
|
||||
|
@ -429,32 +466,23 @@ void MainLoop::LoadFile(std::string file) {
|
|||
void MainLoop::Deinit() {
|
||||
|
||||
{
|
||||
Json::Value config;
|
||||
std::ofstream stream;
|
||||
stream.open(path(prefPath) / "config.json");
|
||||
path themePath(theme->file_path);
|
||||
themePath = themePath.filename();
|
||||
if (!themePath.empty()) {
|
||||
config["theme_name"] = themePath.filename().string();
|
||||
set_option<std::string>("ui.imgui.theme", themePath.filename().string());
|
||||
}
|
||||
{
|
||||
Json::Value accentColor;
|
||||
accentColor["h"] = accent_color.x;
|
||||
accentColor["s"] = accent_color.y;
|
||||
accentColor["v"] = accent_color.z;
|
||||
accentColor["a"] = accent_color.w;
|
||||
config["accent_color"] = accentColor;
|
||||
}
|
||||
config["demo_window"] = show_demo_window;
|
||||
config["vsync"] = vsync;
|
||||
config["framerate"] = framerate;
|
||||
set_option<double>("ui.imgui.accent_color.h", accent_color.x);
|
||||
set_option<double>("ui.imgui.accent_color.s", accent_color.y);
|
||||
set_option<double>("ui.imgui.accent_color.v", accent_color.z);
|
||||
set_option<double>("ui.imgui.accent_color.a", accent_color.w);
|
||||
set_option<double>("ui.imgui.demo_window", show_demo_window);
|
||||
set_option<bool>("ui.imgui.vsync", vsync);
|
||||
set_option<int64_t>("ui.imgui.framerate", framerate);
|
||||
if (lang == DEFAULT_LANG) {
|
||||
config["lang"] = Json::Value::nullSingleton();
|
||||
delete_option("ui.imgui.lang");
|
||||
} else {
|
||||
config["lang"] = lang;
|
||||
set_option<std::string>("ui.imgui.lang", lang);
|
||||
}
|
||||
stream << config;
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
MainLoop::MainLoop() : RendererBackend() {
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
#include <fstream>
|
||||
#include <json/json.h>
|
||||
#include <stdio.h>
|
||||
#include <numbers>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
@ -28,14 +27,13 @@
|
|||
#include "IconFontCppHeaders/IconsForkAwesome.h"
|
||||
#include "imgui/imgui.h"
|
||||
#include "imgui/misc/cpp/imgui_stdlib.h"
|
||||
#include "translation.h"
|
||||
#include <translation.hpp>
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include "emscripten_mainloop_stub.h"
|
||||
#endif
|
||||
#include "../../../backend.hpp"
|
||||
#include "ui_backend.hpp"
|
||||
using namespace std::filesystem;
|
||||
using namespace std::numbers;
|
||||
using std::string;
|
||||
#define IMGUI_FRONTEND
|
||||
class MainLoop : public RendererBackend {
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
#include "imgui.h"
|
||||
#include "json/value.h"
|
||||
#include "thirdparty/toml.hpp"
|
||||
#include "translation.h"
|
||||
#include <translation.hpp>
|
||||
#include <cmath>
|
||||
#include <exception>
|
||||
#include <numbers>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
@ -15,7 +14,6 @@
|
|||
#include <log.hpp>
|
||||
|
||||
using namespace std::filesystem;
|
||||
using namespace std::numbers;
|
||||
const char* Theme::prefPath = NULL;
|
||||
path Theme::themeDir = path();
|
||||
std::set<path> Theme::availableThemes = std::set<path>();
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
#include "translation.h"
|
|
@ -1,26 +0,0 @@
|
|||
#pragma once
|
||||
#include <libintl.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
// Based on the code found in gettext.h, but without any additional things we don't need.
|
||||
inline static const char *gettext_ctx(const char *ctx, const char *msgid) {
|
||||
std::string msg_ctxt_id = (std::string(ctx) + std::string("\004") + std::string(msgid));
|
||||
const char *translation = gettext(msg_ctxt_id.c_str());
|
||||
if (std::string(translation) == msg_ctxt_id) {
|
||||
return msgid;
|
||||
} else {
|
||||
return translation;
|
||||
}
|
||||
}
|
||||
#define _TR(str) gettext(str)
|
||||
#define _TR_CTX(ctx, str) gettext_ctx(ctx, str)
|
||||
#define _TRS(str) std::string(_TR(str))
|
||||
#define _TRS_CTX(ctx, str) std::string(_TR_CTX(ctx, str))
|
||||
#define _TRIS(icon, str) (std::string(icon) + _TRS(str))
|
||||
#define _TRI(icon, str) _TRIS(icon, str).c_str()
|
||||
#define _TRIS_CTX(icon, ctx, str) (std::string(icon) + _TRS_CTX(ctx, str))
|
||||
#define _TRI_CTX(icon, ctx, str) _TRIS_CTX(icon, ctx, str).c_str()
|
||||
#define CURRENT_LANGUAGE setlocale(LC_MESSAGES, NULL)
|
||||
// The value required to set the operating system's default language.
|
||||
#define DEFAULT_LANG ""
|
||||
#define SET_LANG(lang) setlocale(LC_MESSAGES, lang)
|
11
build-android.sh
Executable file
11
build-android.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/bin/env -S NOT_SOURCED=1 /bin/sh
|
||||
if ! [ "$NOT_SOURCED" = "1" ]; then
|
||||
echo "Error: This script must not be sourced!" >&2
|
||||
return 1
|
||||
fi
|
||||
pushd "$(dirname "$0")"
|
||||
./setup-android-project.sh
|
||||
cd sdl-android-project
|
||||
ln -fs "$(dirname "$(pwd)")" ./app/jni
|
||||
./gradlew build
|
||||
popd
|
4
cmake-android.sh
Executable file
4
cmake-android.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
export CMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake"
|
||||
#export CXX=$ANDROID_ABI-linux-$ANDROID_PLATFORM-clang++ CC=$ANDROID_ABI-linux-$ANDROID_PLATFORM-clang
|
||||
cmake -DUSE_GLES=ON -DUSE_PORTALS=OFF -DANDROID_ABI="$ANDROID_ABI" -DANDROID_NDK="$ANDROID_NDK_HOME" -DANDROID_PLATFORM="$ANDROID_PLATFORM" -DDOWNLOAD_AUDIO_CODECS_DEPENDENCY=ON -DCMAKE_TOOLCHAIN_FILE="$CMAKE_TOOLCHAIN_FILE" -DENABLE_DBUS=OFF -DBUILD_SDL=ON -DBUILD_SDL_IMAGE=ON "$@"
|
73
cmake/CheckCXXCompilerFlag.cmake
Normal file
73
cmake/CheckCXXCompilerFlag.cmake
Normal file
|
@ -0,0 +1,73 @@
|
|||
# - Check whether the CXX compiler supports a given flag.
|
||||
# CHECK_CXX_COMPILER_FLAG(<flag> <var>)
|
||||
# <flag> - the compiler flag
|
||||
# <var> - variable to store the result
|
||||
# This internally calls the check_cxx_source_compiles macro. See help
|
||||
# for CheckCXXSourceCompiles for a listing of variables that can
|
||||
# modify the build.
|
||||
|
||||
#=============================================================================
|
||||
# Copyright 2006-2009 Kitware, Inc.
|
||||
# Copyright 2006 Alexander Neundorf <neundorf@kde.org>
|
||||
# Copyright 2011-2013 Matthias Kretz <kretz@kde.org>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# * The names of Kitware, Inc., the Insight Consortium, or the names of
|
||||
# any consortium members, or of any contributors, may not be used to
|
||||
# endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS IS''
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
|
||||
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#=============================================================================
|
||||
|
||||
INCLUDE(CheckCXXSourceCompiles)
|
||||
|
||||
MACRO (CHECK_CXX_COMPILER_FLAG _FLAG _RESULT)
|
||||
SET(SAFE_CMAKE_REQUIRED_DEFINITIONS "${CMAKE_REQUIRED_DEFINITIONS}")
|
||||
SET(CMAKE_REQUIRED_DEFINITIONS "${_FLAG}")
|
||||
if(${ARGC} GREATER 2)
|
||||
SET(TEST_SOURCE "${ARGV2}")
|
||||
else()
|
||||
SET(TEST_SOURCE "int main() { return 0;}")
|
||||
endif()
|
||||
CHECK_CXX_SOURCE_COMPILES("${TEST_SOURCE}" ${_RESULT}
|
||||
# Some compilers do not fail with a bad flag
|
||||
FAIL_REGEX "error: bad value (.*) for .* switch" # GNU
|
||||
FAIL_REGEX "argument unused during compilation" # clang
|
||||
FAIL_REGEX "is valid for .* but not for C\\\\+\\\\+" # GNU
|
||||
FAIL_REGEX "unrecognized .*option" # GNU
|
||||
FAIL_REGEX "ignored for target" # GNU
|
||||
FAIL_REGEX "ignoring unknown option" # MSVC
|
||||
FAIL_REGEX "warning D9002" # MSVC
|
||||
FAIL_REGEX "[Uu]nknown option" # HP
|
||||
FAIL_REGEX "[Ww]arning: [Oo]ption" # SunPro
|
||||
FAIL_REGEX "command option .* is not recognized" # XL
|
||||
FAIL_REGEX "WARNING: unknown flag:" # Open64
|
||||
FAIL_REGEX "command line error" # ICC
|
||||
FAIL_REGEX "command line warning" # ICC
|
||||
FAIL_REGEX "#10236:" # ICC: File not found
|
||||
FAIL_REGEX " #10159: " # ICC
|
||||
FAIL_REGEX " #10353: " # ICC: option '-mfma' ignored, suggest using '-march=core-avx2'
|
||||
)
|
||||
SET (CMAKE_REQUIRED_DEFINITIONS "${SAFE_CMAKE_REQUIRED_DEFINITIONS}")
|
||||
ENDMACRO (CHECK_CXX_COMPILER_FLAG)
|
||||
|
38
cmake/built_sdl/SDL2Config.cmake
Normal file
38
cmake/built_sdl/SDL2Config.cmake
Normal file
|
@ -0,0 +1,38 @@
|
|||
# sdl2 cmake project-config input for CMakeLists.txt script
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(SDL2 PROPERTIES
|
||||
URL "https://www.libsdl.org/"
|
||||
DESCRIPTION "low level access to audio, keyboard, mouse, joystick, and graphics hardware"
|
||||
)
|
||||
|
||||
|
||||
########################################################################
|
||||
|
||||
set(SDL2_FOUND TRUE CACHE INTERNAL "")
|
||||
|
||||
set(SDL2_SDL2_FOUND TRUE CACHE INTERNAL "")
|
||||
set(SDL2_SDL2-static_FOUND TRUE CACHE INTERNAL "")
|
||||
set(SDL2_SDL2test_FOUND OFF CACHE INTERNAL "")
|
||||
if (NOT DEFINED SDL2_FOUND)
|
||||
if (SDL2::SDL2main)
|
||||
set(SDL2_SDL2main_FOUND ON)
|
||||
else()
|
||||
set(SDL2_SDL2main_FOUND OFF)
|
||||
endif()
|
||||
set(SDL2_LIBRARY SDL2::SDL2 CACHE INTERNAL "")
|
||||
set(SDL2_INCLUDE_DIR CACHE INTERNAL "")
|
||||
set(SDL2_LIBRARIES SDL2::SDL2 CACHE INTERNAL "")
|
||||
set(SDL2_STATIC_LIBRARIES SDL2::SDL2 CACHE INTERNAL "")
|
||||
set(SDL2_STATIC_PRIVATE_LIBS "" CACHE INTERNAL "")
|
||||
set(SDL2_INCLUDE_DIRS "" CACHE INTERNAL "")
|
||||
#get_target_property(SDL2_STATIC_PRIVATE_LIBS SDL2-static LINK_LIBRARIES)
|
||||
get_target_property(_SDL2_INCLUDE_DIRS SDL2::SDL2-static INCLUDE_DIRECTORIES)
|
||||
set(SDL2_INCLUDE_DIRS ${_SDL2_INCLUDE_DIRS} CACHE INTERNAL "")
|
||||
unset(_SDL2_INCLUDE_DIRS)
|
||||
if(SDL2_SDL2main_FOUND)
|
||||
set(SDL2MAIN_LIBRARY SDL2::SDL2main CACHE INTERNAL "")
|
||||
else()
|
||||
set(SDL2MAIN_LIBRARY CACHE INTERNAL "")
|
||||
endif()
|
||||
endif()
|
18
cmake/built_sdl_image/sdl2_image-config.cmake
Normal file
18
cmake/built_sdl_image/sdl2_image-config.cmake
Normal file
|
@ -0,0 +1,18 @@
|
|||
# sdl2_image cmake project-config input for ./configure scripts
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(SDL2_image PROPERTIES
|
||||
URL "https://www.libsdl.org/projects/SDL_image/"
|
||||
DESCRIPTION "SDL_image is an image file loading library"
|
||||
)
|
||||
if (NOT DEFINED SDL2_image_FOUND)
|
||||
|
||||
|
||||
set(SDL2_image_FOUND TRUE)
|
||||
|
||||
set(SDL2IMAGE_VENDORED FALSE)
|
||||
if (NOT TARGET SDL2_image::SDL2_image AND TARGET SDL2_image::SDL2_image-static)
|
||||
add_library(SDL2_image::SDL2_image INTERFACE IMPORTED)
|
||||
target_link_libraries(SDL2_image::SDL2_image INTERFACE SDL2_image::SDL2_image-static)
|
||||
endif()
|
||||
endif()
|
4
dbus.cpp
4
dbus.cpp
|
@ -159,8 +159,8 @@ DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string
|
|||
connection.enterEventLoopAsync();
|
||||
}
|
||||
#endif
|
||||
const char *DBusAPI::objectPath = "/com/complecwaft/Looper";
|
||||
const char *DBusAPI::busName = "com.complecwaft.Looper";
|
||||
const char *DBusAPI::objectPath = "/com/complecwaft/looper";
|
||||
const char *DBusAPI::busName = "com.complecwaft.looper";
|
||||
DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) {
|
||||
#ifdef DBUS_ENABLED
|
||||
auto connection = sdbus::createSessionBusConnection(busName);
|
||||
|
|
4
dbus.hpp
4
dbus.hpp
|
@ -105,7 +105,7 @@ class MprisAPI : public sdbus::AdaptorInterfaces<org::mpris::MediaPlayer2_adapto
|
|||
#endif
|
||||
class DBusAPI
|
||||
#ifdef DBUS_ENABLED
|
||||
: public sdbus::AdaptorInterfaces<com::complecwaft::Looper_adaptor, com::complecwaft::Looper::Errors_adaptor, org::freedesktop::Application_adaptor>
|
||||
: public sdbus::AdaptorInterfaces<com::complecwaft::looper_adaptor, com::complecwaft::looper::Errors_adaptor, org::freedesktop::Application_adaptor>
|
||||
#endif
|
||||
{
|
||||
std::map<std::string, void*> handles;
|
||||
|
@ -189,7 +189,7 @@ class DBusAPI
|
|||
};
|
||||
class DBusAPISender : public Playback
|
||||
#ifdef DBUS_ENABLED
|
||||
, public sdbus::ProxyInterfaces<com::complecwaft::Looper_proxy, com::complecwaft::Looper::Errors_proxy, org::freedesktop::Application_proxy, sdbus::Peer_proxy>
|
||||
, public sdbus::ProxyInterfaces<com::complecwaft::looper_proxy, com::complecwaft::looper::Errors_proxy, org::freedesktop::Application_proxy, sdbus::Peer_proxy>
|
||||
#endif
|
||||
{
|
||||
// Cache
|
||||
|
|
49
log.cpp
49
log.cpp
|
@ -1,6 +1,9 @@
|
|||
#include "log.hpp"
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
namespace Looper::Log {
|
||||
std::set<FILE*> LogStream::global_outputs;
|
||||
int LogStream::log_level = 0;
|
||||
|
@ -14,6 +17,7 @@ namespace Looper::Log {
|
|||
}
|
||||
return used_outputs;
|
||||
}
|
||||
std::string line;
|
||||
void LogStream::writec(const char chr) {
|
||||
bool is_newline = (chr == '\n' || chr == '\r');
|
||||
if (my_log_level < log_level) {
|
||||
|
@ -25,6 +29,15 @@ namespace Looper::Log {
|
|||
stream->writec(chr);
|
||||
}
|
||||
} else {
|
||||
#ifdef __ANDROID__
|
||||
if (!is_newline) {
|
||||
line += chr;
|
||||
} else {
|
||||
for (auto logPriority : android_outputs) {
|
||||
__android_log_print(logPriority, "Looper", "%s", line.c_str());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
std::set<FILE*> used_outputs = get_used_outputs();
|
||||
for (auto &file : used_outputs) {
|
||||
fwrite(&chr, 1, 1, file);
|
||||
|
@ -104,7 +117,7 @@ namespace Looper::Log {
|
|||
vwritefln(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
LogStream::LogStream(std::initializer_list<std::string> names, int log_level, bool nested)
|
||||
LogStream::LogStream(std::initializer_list<std::string> names, int log_level, bool nested, void *discriminator)
|
||||
: names(names),
|
||||
need_prefix(true),
|
||||
my_log_level(log_level),
|
||||
|
@ -113,24 +126,52 @@ namespace Looper::Log {
|
|||
|
||||
}
|
||||
LogStream::LogStream(std::initializer_list<std::string> names, std::initializer_list<LogStream*> streams, int log_level)
|
||||
: LogStream(names, log_level, true)
|
||||
: LogStream(names, log_level, true, nullptr)
|
||||
{
|
||||
this->streams = std::set(streams);
|
||||
}
|
||||
LogStream::LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level)
|
||||
: LogStream(names, log_level, false)
|
||||
#ifdef __ANDROID__
|
||||
LogStream::LogStream(std::initializer_list<std::string> names, std::initializer_list<std::variant<FILE*, android_LogPriority>> outputs, int log_level)
|
||||
#else
|
||||
LogStream::LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level)
|
||||
#endif
|
||||
: LogStream(names, log_level, false, nullptr)
|
||||
{
|
||||
#ifdef __ANDROID__
|
||||
std::set<FILE*> file_outputs;
|
||||
std::set<android_LogPriority> android_outputs;
|
||||
for (auto output : outputs) {
|
||||
android_LogPriority *logPriority = std::get_if<android_LogPriority>(&output);
|
||||
FILE **file = std::get_if<FILE*>(&output);
|
||||
if (logPriority != nullptr) {
|
||||
android_outputs.insert(*logPriority);
|
||||
} else if (file != nullptr){
|
||||
file_outputs.insert(*file);
|
||||
}
|
||||
}
|
||||
this->android_outputs = android_outputs;
|
||||
this->outputs = file_outputs;
|
||||
#else
|
||||
|
||||
this->outputs = std::set(outputs);
|
||||
#endif
|
||||
}
|
||||
static LogStream *debug_stream;
|
||||
static LogStream *info_stream;
|
||||
static LogStream *warning_stream;
|
||||
static LogStream *error_stream;
|
||||
void init_logging() {
|
||||
#ifdef __ANDROID__
|
||||
debug_stream = new LogStream({"DEBUG"}, {ANDROID_LOG_DEBUG}, -1);
|
||||
info_stream = new LogStream({"INFO"}, {ANDROID_LOG_INFO}, 0);
|
||||
warning_stream = new LogStream({"WARNING"}, {ANDROID_LOG_WARN}, 1);
|
||||
error_stream = new LogStream({"ERROR"}, {ANDROID_LOG_ERROR}, 2);
|
||||
#else
|
||||
debug_stream = new LogStream({"DEBUG"}, {stderr}, -1);
|
||||
info_stream = new LogStream({"INFO"}, {stdout}, 0);
|
||||
warning_stream = new LogStream({"WARNING"}, {stderr}, 1);
|
||||
error_stream = new LogStream({"ERROR"}, {stderr}, 2);
|
||||
#endif
|
||||
}
|
||||
LogStream &get_log_stream_by_level(int level) {
|
||||
switch (level) {
|
||||
|
|
17
log.hpp
17
log.hpp
|
@ -4,6 +4,10 @@
|
|||
#include <ostream>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <variant>
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
namespace Looper::Log {
|
||||
struct LogStream {
|
||||
std::set<FILE *> outputs;
|
||||
|
@ -14,7 +18,13 @@ namespace Looper::Log {
|
|||
bool need_prefix;
|
||||
std::vector<std::string> names;
|
||||
std::set<FILE*> get_used_outputs();
|
||||
LogStream(std::initializer_list<std::string> names, int log_level, bool nested);
|
||||
|
||||
#ifdef __ANDROID__
|
||||
std::string line;
|
||||
std::set<android_LogPriority> android_outputs;
|
||||
#endif
|
||||
LogStream(std::initializer_list<std::string> names, int log_level, bool nested, void* discriminator);
|
||||
|
||||
public:
|
||||
static int log_level;
|
||||
void writeprefix();
|
||||
|
@ -30,7 +40,12 @@ namespace Looper::Log {
|
|||
void vwritefln(const char *fmt, va_list args);
|
||||
void writefln(const char *fmt, ...);
|
||||
LogStream(std::initializer_list<std::string> names, std::initializer_list<LogStream*> streams, int log_level = 0);
|
||||
|
||||
#ifdef __ANDROID__
|
||||
LogStream(std::initializer_list<std::string> names, std::initializer_list<std::variant<FILE*, android_LogPriority>> outputs, int log_level = 0);
|
||||
#else
|
||||
LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level = 0);
|
||||
#endif
|
||||
};
|
||||
void init_logging();
|
||||
LogStream &get_log_stream_by_level(int level);
|
||||
|
|
29
main.cpp
29
main.cpp
|
@ -19,7 +19,36 @@ std::unordered_set<LicenseData> license_data;
|
|||
std::unordered_set<LicenseData> &get_license_data() {
|
||||
return license_data;
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
#include <SDL.h>
|
||||
#include <jni.h>
|
||||
extern jclass MainActivity;
|
||||
extern jobject mainActivity;
|
||||
extern jmethodID GetUserDir_Method;
|
||||
extern jmethodID OpenFilePicker_Method;
|
||||
extern jmethodID GetPickedFile_Method;
|
||||
extern jmethodID ClearSelected_Method;
|
||||
extern jmethodID IsLoading_Method;
|
||||
extern JNIEnv *env;
|
||||
void initNative() {
|
||||
MainActivity = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass("com/complecwaft/looper/MainActivity")));
|
||||
GetUserDir_Method = env->GetStaticMethodID(MainActivity, "GetUserDir",
|
||||
"()Ljava/lang/String;");
|
||||
OpenFilePicker_Method = env->GetStaticMethodID(MainActivity, "OpenFilePicker",
|
||||
"(Ljava/lang/Object;)V");
|
||||
GetPickedFile_Method = env->GetStaticMethodID(MainActivity, "GetPickedFile",
|
||||
"()Ljava/lang/String;");
|
||||
ClearSelected_Method = env->GetStaticMethodID(MainActivity, "ClearSelected", "()V");
|
||||
IsLoading_Method = env->GetStaticMethodID(MainActivity, "IsLoading", "()Z");
|
||||
jfieldID singleton = env->GetStaticFieldID(MainActivity, "mSingleton", "Lcom/complecwaft/looper/MainActivity;");
|
||||
mainActivity = reinterpret_cast<jobject>(env->NewGlobalRef(env->GetStaticObjectField(MainActivity, singleton)));
|
||||
}
|
||||
#endif
|
||||
int main(int argc, char **argv) {
|
||||
#ifdef __ANDROID__
|
||||
env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
initNative();
|
||||
#endif
|
||||
std::vector<std::string> args;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
args.push_back(std::string(argv[i]));
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
#include "thirdparty/toml.hpp"
|
||||
#include "util.hpp"
|
||||
#include <filesystem>
|
||||
#ifdef __ANDROID__
|
||||
#include <SDL.h>
|
||||
#endif
|
||||
using namespace std::filesystem;
|
||||
namespace Looper::Options {
|
||||
toml::table *options;
|
||||
|
|
32
options.hpp
32
options.hpp
|
@ -5,6 +5,7 @@
|
|||
#include <vector>
|
||||
#include <typeinfo>
|
||||
#include <typeindex>
|
||||
#include <optional>
|
||||
#define OPTIONS (*Looper::Options::options)
|
||||
namespace Looper::Options {
|
||||
extern toml::table *options;
|
||||
|
@ -46,6 +47,37 @@ namespace Looper::Options {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
inline void delete_option(std::string name) {
|
||||
DEBUG.writefln("Deleting option '%s'...", name.c_str());
|
||||
toml::path path(name);
|
||||
auto *tmp = &OPTIONS;
|
||||
std::vector<std::string> components;
|
||||
for (auto component : path) {
|
||||
std::string component_str = (std::string)component;
|
||||
components.push_back(component_str);
|
||||
}
|
||||
while (!path.empty()) {
|
||||
if (option_set(path.str())) {
|
||||
toml::path parent_path = path.parent();
|
||||
auto &parent = OPTIONS.at(parent_path.str());
|
||||
auto last_component = path[path.size() - 1];
|
||||
if (parent.is_table()) {
|
||||
auto &table = *parent.as_table();
|
||||
table.erase((std::string)last_component);
|
||||
if (!table.empty()) {
|
||||
return;
|
||||
}
|
||||
} else if (parent.is_array()) {
|
||||
auto &array = *parent.as_array();
|
||||
array.erase(array.begin() + last_component.index());
|
||||
if (!array.empty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
path = parent_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
template<class T>
|
||||
void set_option(std::string name, T value) {
|
||||
DEBUG.writefln("Setting option '%s'...", name.c_str());
|
||||
|
|
138
playback.cpp
138
playback.cpp
|
@ -17,7 +17,7 @@ extern "C" {
|
|||
#include "log.hpp"
|
||||
#include <filesystem>
|
||||
#include "dbus.hpp"
|
||||
#include <format>
|
||||
#include <string.h>
|
||||
#include "util.hpp"
|
||||
using namespace std::chrono;
|
||||
|
||||
|
@ -102,6 +102,9 @@ void PlaybackInstance::SDLCallbackInner(Uint8 *stream, int len) {
|
|||
#endif
|
||||
if (samples > new_samples) {
|
||||
reset_vgmstream(this->stream);
|
||||
position = 0.0;
|
||||
} else {
|
||||
position += samples / this->stream->sample_rate;
|
||||
}
|
||||
samples = new_samples;
|
||||
for (int i = 0; i < new_bufsize / sizeof(SAMPLETYPE); i++) {
|
||||
|
@ -115,6 +118,15 @@ void PlaybackInstance::SDLCallbackInner(Uint8 *stream, int len) {
|
|||
}
|
||||
st->receiveSamples((SAMPLETYPE*)stream, len / unit);
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
oboe::DataCallbackResult PlaybackInstance::onAudioReady(
|
||||
oboe::AudioStream *audioStream,
|
||||
void *audioData,
|
||||
int32_t numFrames) {
|
||||
SDLCallbackInner((Uint8*)audioData, numFrames * audioStream->getBytesPerFrame());
|
||||
return oboe::DataCallbackResult::Continue;
|
||||
}
|
||||
#endif
|
||||
void PlaybackInstance::SDLCallback(void *userdata, Uint8 *stream, int len) {
|
||||
((PlaybackInstance*)userdata)->SDLCallbackInner(stream, len);
|
||||
}
|
||||
|
@ -215,7 +227,14 @@ VGMSTREAM *PlaybackInstance::LoadVgm(const char *file, int idx) {
|
|||
buf = strdup("Unknown");
|
||||
}
|
||||
if (i == 0) {
|
||||
stream.name = std::format("Default ({})", buf);
|
||||
char *buf2 = NULL;
|
||||
#define DEFAULT_FORMAT_STR "Default (%s)"
|
||||
size_t buflen = snprintf(NULL, 0, DEFAULT_FORMAT_STR, buf) + 1;
|
||||
buf2= (char*)malloc(buflen);
|
||||
memset(buf2, 0, buflen);
|
||||
snprintf(buf2, buflen, DEFAULT_FORMAT_STR, buf);
|
||||
stream.name = buf2;
|
||||
free(buf);
|
||||
} else {
|
||||
stream.name = buf;
|
||||
}
|
||||
|
@ -333,6 +352,7 @@ void PlaybackInstance::InitLoopFunction() {
|
|||
desired.userdata = this;
|
||||
st = new SoundTouch();
|
||||
Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK);
|
||||
#ifndef __ANDROID__
|
||||
if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) {
|
||||
ERROR.writefln("Error opening audio device: '%s'", SDL_GetError());
|
||||
set_error("Failed to open audio device!");
|
||||
|
@ -340,6 +360,56 @@ void PlaybackInstance::InitLoopFunction() {
|
|||
loop_started = false;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
oboe::AudioStreamBuilder builder;
|
||||
builder.setDirection(oboe::Direction::Output);
|
||||
builder.setSharingMode(oboe::SharingMode::Shared);
|
||||
builder.setPerformanceMode(oboe::PerformanceMode::None);
|
||||
builder.setFormat(
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
|
||||
oboe::AudioFormat::I16
|
||||
#else
|
||||
oboe::AudioFormat::Float
|
||||
#endif
|
||||
);
|
||||
builder.setDataCallback(this);
|
||||
auto res = builder.openStream(ostream);
|
||||
if (res != oboe::Result::OK) {
|
||||
ERROR.writefln("Error opening audio device.");
|
||||
set_error("Failed to open audio device!");
|
||||
running = false;
|
||||
loop_started = false;
|
||||
return;
|
||||
}
|
||||
obtained = desired;
|
||||
obtained.channels = ostream->getChannelCount();
|
||||
obtained.freq = ostream->getSampleRate();
|
||||
auto bufferFrames = ostream->getBufferSizeInFrames();
|
||||
auto bytesPerFrame = ostream->getBytesPerFrame();
|
||||
auto bytesPerSample = ostream->getBytesPerSample();
|
||||
obtained.size = bufferFrames * bytesPerFrame;
|
||||
obtained.samples = obtained.size / bytesPerSample;
|
||||
oboe::AudioFormat format = ostream->getFormat();
|
||||
DEBUG.writefln("About to start audio stream.\n\
|
||||
Format: %s\n\
|
||||
Channel count: %u\n\
|
||||
Sample rate: %u\n\
|
||||
Buffer size (frames): %u\n\
|
||||
Bytes per frame: %u\n\
|
||||
Total bytes: %u\n\
|
||||
Bytes per sample: %u\n\
|
||||
Total samples: %u"
|
||||
, oboe::convertToText(format)
|
||||
, obtained.channels
|
||||
, obtained.freq
|
||||
, bufferFrames
|
||||
, bytesPerFrame
|
||||
, obtained.size
|
||||
, bytesPerSample
|
||||
, obtained.samples
|
||||
);
|
||||
ostream->requestStart();
|
||||
#endif
|
||||
spec = obtained;
|
||||
st->setSampleRate(spec.freq);
|
||||
st->setChannels(spec.channels);
|
||||
|
@ -377,6 +447,7 @@ void PlaybackInstance::InitLoopFunction() {
|
|||
} else {
|
||||
playback_ready.store(false);
|
||||
}
|
||||
load_finished.store(true);
|
||||
set_signal(PlaybackSignalStarted);
|
||||
}
|
||||
void PlaybackInstance::LoopFunction() {
|
||||
|
@ -400,34 +471,38 @@ void PlaybackInstance::LoopFunction() {
|
|||
}
|
||||
}
|
||||
if (stream_changed.exchange(false)) {
|
||||
std::string file = current_file.value();
|
||||
if (streams[current_stream].name == "" || streams[current_stream].length <= 0 || current_stream < 0 || current_stream >= streams.size()) {
|
||||
if (stream != nullptr) {
|
||||
current_stream = stream->stream_index;
|
||||
current_file_mutex.lock();
|
||||
if (current_file.has_value()) {
|
||||
std::string file = current_file.value();
|
||||
if (current_stream >= streams.size() || current_stream < 0 ||
|
||||
streams[current_stream].name == "" || streams[current_stream].length <= 0) {
|
||||
if (stream != nullptr) {
|
||||
current_stream = stream->stream_index;
|
||||
} else {
|
||||
current_stream = 0;
|
||||
}
|
||||
} else {
|
||||
current_stream = 0;
|
||||
if (stream != nullptr) {
|
||||
UnloadVgm(stream);
|
||||
stream = LoadVgm(file.c_str(), current_stream);
|
||||
} else if (music != nullptr) {
|
||||
UnloadMix(music);
|
||||
music = LoadMix(file.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (stream != nullptr) {
|
||||
UnloadVgm(stream);
|
||||
stream = LoadVgm(file.c_str(), current_stream);
|
||||
} else if (music != nullptr) {
|
||||
UnloadMix(music);
|
||||
music = LoadMix(file.c_str());
|
||||
if (music || stream) {
|
||||
playback_ready.store(true);
|
||||
} else {
|
||||
playback_ready.store(false);
|
||||
}
|
||||
}
|
||||
if (music || stream) {
|
||||
playback_ready.store(true);
|
||||
} else {
|
||||
playback_ready.store(false);
|
||||
}
|
||||
current_file_mutex.unlock();
|
||||
}
|
||||
if (flag_mutex.try_lock()) {
|
||||
if (seeking.exchange(false)) {
|
||||
if (stream != nullptr) {
|
||||
SDL_LockAudioDevice(device);
|
||||
seek_vgmstream(stream, (int32_t)((double)stream->sample_rate * position));
|
||||
|
||||
st->flush();
|
||||
SDL_UnlockAudioDevice(device);
|
||||
} else {
|
||||
|
@ -486,13 +561,7 @@ void PlaybackInstance::LoopFunction() {
|
|||
}
|
||||
flag_mutex.unlock();
|
||||
}
|
||||
if (stream != nullptr) {
|
||||
double maybe_new_position = (double)stream->current_sample / stream->sample_rate;
|
||||
if (position > maybe_new_position) {
|
||||
position = maybe_new_position;
|
||||
}
|
||||
position += 0.02 * (speed * tempo);
|
||||
} else if (music != nullptr) {
|
||||
if (music != nullptr) {
|
||||
position = Mix_GetMusicPosition(music);
|
||||
}
|
||||
|
||||
|
@ -506,7 +575,15 @@ void PlaybackInstance::DeinitLoopFunction() {
|
|||
if (stream != nullptr) {
|
||||
UnloadVgm(stream);
|
||||
}
|
||||
#ifndef __ANDROID__
|
||||
SDL_CloseAudioDevice(device);
|
||||
#else
|
||||
if (ostream && ostream->getState() != oboe::StreamState::Closed) {
|
||||
ostream->stop();
|
||||
ostream->close();
|
||||
}
|
||||
ostream.reset();
|
||||
#endif
|
||||
Mix_CloseAudio();
|
||||
Mix_Quit();
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
|
@ -566,10 +643,11 @@ void PlaybackInstance::Load(std::string filePath) {
|
|||
if (running.exchange(true)) {
|
||||
load_requested.store(true);
|
||||
} else {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
|
||||
start_loop();
|
||||
#else
|
||||
thread = std::thread(&PlaybackInstance::ThreadFunc, this);
|
||||
loop_started = true;
|
||||
#endif
|
||||
}
|
||||
flag_mutex.lock();
|
||||
|
@ -582,7 +660,7 @@ void PlaybackInstance::Load(std::string filePath) {
|
|||
void PlaybackInstance::Start(std::string filePath, int streamIdx) {
|
||||
Load(filePath);
|
||||
while (loop_started && !load_finished.exchange(false)) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
|
||||
LoopHook();
|
||||
#endif
|
||||
std::this_thread::sleep_for(20ms);
|
||||
|
@ -637,7 +715,7 @@ bool PlaybackInstance::IsPaused() {
|
|||
|
||||
void PlaybackInstance::Stop() {
|
||||
if (running.exchange(false)) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
|
||||
stop_loop();
|
||||
#else
|
||||
thread.join();
|
||||
|
|
18
playback.h
18
playback.h
|
@ -3,6 +3,9 @@
|
|||
extern "C" {
|
||||
#include <vgmstream.h>
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
#include <oboe/Oboe.h>
|
||||
#endif
|
||||
#include <thread>
|
||||
#include <SDL.h>
|
||||
#include <SDL_audio.h>
|
||||
|
@ -212,8 +215,21 @@ class Playback {
|
|||
static Playback *Create(bool *daemon_found, bool daemon = false);
|
||||
};
|
||||
class DBusAPISender;
|
||||
class PlaybackInstance : public Playback {
|
||||
class PlaybackInstance : public Playback
|
||||
#ifdef __ANDROID__
|
||||
, public oboe::AudioStreamDataCallback
|
||||
#endif
|
||||
{
|
||||
private:
|
||||
#ifdef __ANDROID__
|
||||
std::shared_ptr<oboe::AudioStream> ostream;
|
||||
public:
|
||||
oboe::DataCallbackResult onAudioReady(
|
||||
oboe::AudioStream *audioStream,
|
||||
void *audioData,
|
||||
int32_t numFrames) override;
|
||||
private:
|
||||
#endif
|
||||
std::string filePath;
|
||||
std::atomic_bool running;
|
||||
std::atomic_bool file_changed;
|
||||
|
|
3
sdl-android-project/.idea/.gitignore
vendored
Normal file
3
sdl-android-project/.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
6
sdl-android-project/.idea/compiler.xml
Normal file
6
sdl-android-project/.idea/compiler.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
23
sdl-android-project/.idea/deploymentTargetDropDown.xml
Normal file
23
sdl-android-project/.idea/deploymentTargetDropDown.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State>
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Copy_of_Pixel_3a_API_34_extension_level_7_x86_64.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2024-04-28T16:05:58.159965481Z" />
|
||||
</State>
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
19
sdl-android-project/.idea/gradle.xml
Normal file
19
sdl-android-project/.idea/gradle.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="JniFindClass" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
10
sdl-android-project/.idea/migrations.xml
Normal file
10
sdl-android-project/.idea/migrations.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
10
sdl-android-project/.idea/misc.xml
Normal file
10
sdl-android-project/.idea/misc.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
7
sdl-android-project/.idea/vcs.xml
Normal file
7
sdl-android-project/.idea/vcs.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/app/jni" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
70
sdl-android-project/app/build.gradle
Normal file
70
sdl-android-project/app/build.gradle
Normal file
|
@ -0,0 +1,70 @@
|
|||
def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY');
|
||||
def buildAsApplication = !buildAsLibrary
|
||||
if (buildAsApplication) {
|
||||
apply plugin: 'com.android.application'
|
||||
}
|
||||
else {
|
||||
apply plugin: 'com.android.library'
|
||||
}
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
prefab true
|
||||
}
|
||||
if (buildAsApplication) {
|
||||
namespace "com.complecwaft.looper"
|
||||
}
|
||||
compileSdkVersion 34
|
||||
defaultConfig {
|
||||
minSdkVersion 30
|
||||
targetSdkVersion 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DUSE_GLES=ON", "-DUSE_PORTALS=OFF", "-DDOWNLOAD_AUDIO_CODECS_DEPENDENCY=ON", "-DENABLE_DBUS=OFF", "-DBUILD_SDL=ON", "-DBUILD_SDL_IMAGE=ON", "-DDISABLE_GTK_UI=ON", "-DDISABLE_IMGUI_UI=OFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'jni/CMakeLists.txt';
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
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'
|
||||
}
|
||||
|
||||
}
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
if (buildAsLibrary) {
|
||||
libraryVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def outputFile = output.outputFile
|
||||
if (outputFile != null && outputFile.name.endsWith(".aar")) {
|
||||
def fileName = "com.complecwaft.looper.app.aar";
|
||||
output.outputFile = new File(outputFile.parent, fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation group: 'com.getkeepsafe.relinker', name: 'relinker', version: '1.4.5'
|
||||
}
|
17
sdl-android-project/app/proguard-rules.pro
vendored
Normal file
17
sdl-android-project/app/proguard-rules.pro
vendored
Normal file
|
@ -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 *;
|
||||
#}
|
105
sdl-android-project/app/src/main/AndroidManifest.xml
Normal file
105
sdl-android-project/app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Replace com.test.game with the identifier of your game below, e.g.
|
||||
com.gamemaker.game
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:installLocation="auto"
|
||||
package="com.complecwaft.looper">
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.OPEN_DOCUMENT" />
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
</intent>
|
||||
</queries>
|
||||
<!-- OpenGL ES 2.0 -->
|
||||
<uses-feature android:glEsVersion="0x00030000" />
|
||||
|
||||
<!-- Touchscreen support -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Game controller support -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
|
||||
<!-- External mouse input events -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.type.pc"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Audio recording support -->
|
||||
<!-- if you want to capture audio, uncomment this. -->
|
||||
<!-- <uses-feature
|
||||
android:name="android.hardware.microphone"
|
||||
android:required="false" /> -->
|
||||
|
||||
<!-- Allow downloading to the external storage on Android 5.1 and older -->
|
||||
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="22" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<!-- Allow access to Bluetooth devices -->
|
||||
<!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM -->
|
||||
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> -->
|
||||
<!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> -->
|
||||
|
||||
<!-- Allow access to the vibrator -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- if you want to capture audio, uncomment this. -->
|
||||
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
|
||||
|
||||
<!-- Create a Java class extending SDLActivity and place it in a
|
||||
directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java
|
||||
|
||||
then replace "SDLActivity" with the name of your class (e.g. "MyGame")
|
||||
in the XML below.
|
||||
|
||||
An example Java class can be found in README-android.md
|
||||
-->
|
||||
<application android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
|
||||
android:hardwareAccelerated="true" >
|
||||
|
||||
<!-- Example of setting SDL hints from AndroidManifest.xml:
|
||||
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/>
|
||||
-->
|
||||
|
||||
<activity android:name="MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
|
||||
android:preferMinimalPostProcessing="true"
|
||||
android:exported="true"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- Let Android know that we can handle some USB devices and should receive this event -->
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
<!-- Drop file event -->
|
||||
<!--
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
-->
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,114 @@
|
|||
package com.complecwaft.looper;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Objects;
|
||||
import android.media.AudioManager;
|
||||
public class MainActivity extends SDLActivity
|
||||
{
|
||||
public static MainActivity mSingleton;
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mSingleton = this;
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
private static Intent openDocumentIntent = null;
|
||||
public static String GetUserDir() {
|
||||
return System.getProperty("user.home");
|
||||
}
|
||||
private static final int PICK_FILE = 0x33;
|
||||
public static void OpenFilePicker(Object extraInitialUri) {
|
||||
openDocumentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
openDocumentIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
openDocumentIntent.setType("*/*");
|
||||
if (extraInitialUri instanceof String) {
|
||||
openDocumentIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, (String)extraInitialUri);
|
||||
}
|
||||
if (openDocumentIntent.resolveActivity(mSingleton.getPackageManager()) != null) {
|
||||
mSingleton.startActivityForResult(Intent.createChooser(openDocumentIntent, "Open a file..."), PICK_FILE);
|
||||
} else {
|
||||
Log.d("Looper", "Unable to resolve Intent.ACTION_OPEN_DOCUMENT {}");
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
|
||||
if (requestCode == PICK_FILE && resultCode == RESULT_OK) {
|
||||
Uri uri = null;
|
||||
if (resultData != null) {
|
||||
uri = resultData.getData();
|
||||
InputStream input = null;
|
||||
loading = true;
|
||||
try {
|
||||
input = getApplication().getContentResolver().openInputStream(uri);
|
||||
} catch (FileNotFoundException ex) {
|
||||
ex.printStackTrace();
|
||||
PickedFile = null;
|
||||
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
File file = new File(getCacheDir(), uri.getLastPathSegment());
|
||||
try {
|
||||
file.getParentFile().mkdirs();
|
||||
file.createNewFile();
|
||||
FileOutputStream os = null;
|
||||
os = new FileOutputStream(file);
|
||||
int len = 0;
|
||||
int pos = 0;
|
||||
byte[] buf = new byte[1024*4];
|
||||
|
||||
while ((len = input.read(buf)) >= 0) {
|
||||
os.write(buf, 0, len);
|
||||
pos += len;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
PickedFile = null;
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
loading = false;
|
||||
PickedFile = file.getAbsolutePath();
|
||||
} else {
|
||||
PickedFile = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static boolean loading;
|
||||
public static boolean IsLoading() {
|
||||
return loading;
|
||||
}
|
||||
private static String PickedFile = null;
|
||||
public static String GetPickedFile() {
|
||||
if (openDocumentIntent == null) {
|
||||
return "";
|
||||
} else if (PickedFile == null) {
|
||||
return "";
|
||||
} else {
|
||||
return PickedFile;
|
||||
}
|
||||
}
|
||||
public static void ClearSelected() {
|
||||
PickedFile = null;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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<GattOperation> 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<GattOperation>();
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,691 @@
|
|||
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<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
|
||||
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
|
||||
private int mNextDeviceId = 0;
|
||||
private SharedPreferences mSharedPreferences = null;
|
||||
private boolean mIsChromebook = false;
|
||||
private UsbManager mUsbManager;
|
||||
private Handler mHandler;
|
||||
private BluetoothManager mBluetoothManager;
|
||||
private List<BluetoothDevice> 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 = {
|
||||
0x03f0, // HP
|
||||
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
|
||||
0x3537, // GameSir
|
||||
};
|
||||
|
||||
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<Integer> devices = new ArrayList<Integer>();
|
||||
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 >= 31 /* Android 12 */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
|
||||
return;
|
||||
}
|
||||
|
||||
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<BluetoothDevice>();
|
||||
|
||||
// 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<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
|
||||
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
|
||||
|
||||
List<BluetoothDevice> 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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,514 @@
|
|||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioDeviceCallback;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.AudioTrack;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class SDLAudioManager {
|
||||
protected static final String TAG = "SDLAudio";
|
||||
|
||||
protected static AudioTrack mAudioTrack;
|
||||
protected static AudioRecord mAudioRecord;
|
||||
protected static Context mContext;
|
||||
|
||||
private static final int[] NO_DEVICES = {};
|
||||
|
||||
private static AudioDeviceCallback mAudioDeviceCallback;
|
||||
|
||||
public static void initialize() {
|
||||
mAudioTrack = null;
|
||||
mAudioRecord = null;
|
||||
mAudioDeviceCallback = null;
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
|
||||
{
|
||||
mAudioDeviceCallback = new AudioDeviceCallback() {
|
||||
@Override
|
||||
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
||||
Arrays.stream(addedDevices).forEach(deviceInfo -> addAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
||||
Arrays.stream(removedDevices).forEach(deviceInfo -> removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static void setContext(Context context) {
|
||||
mContext = context;
|
||||
if (context != null) {
|
||||
registerAudioDeviceCallback();
|
||||
}
|
||||
}
|
||||
|
||||
public static void release(Context context) {
|
||||
unregisterAudioDeviceCallback(context);
|
||||
}
|
||||
|
||||
// Audio
|
||||
|
||||
protected static String getAudioFormatString(int audioFormat) {
|
||||
switch (audioFormat) {
|
||||
case AudioFormat.ENCODING_PCM_8BIT:
|
||||
return "8-bit";
|
||||
case AudioFormat.ENCODING_PCM_16BIT:
|
||||
return "16-bit";
|
||||
case AudioFormat.ENCODING_PCM_FLOAT:
|
||||
return "float";
|
||||
default:
|
||||
return Integer.toString(audioFormat);
|
||||
}
|
||||
}
|
||||
|
||||
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||
int channelConfig;
|
||||
int sampleSize;
|
||||
int frameSize;
|
||||
|
||||
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
|
||||
|
||||
/* On older devices let's use known good settings */
|
||||
if (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||
if (desiredChannels > 2) {
|
||||
desiredChannels = 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
|
||||
if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) {
|
||||
if (sampleRate < 8000) {
|
||||
sampleRate = 8000;
|
||||
} else if (sampleRate > 48000) {
|
||||
sampleRate = 48000;
|
||||
}
|
||||
}
|
||||
|
||||
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
|
||||
int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */);
|
||||
if (Build.VERSION.SDK_INT < minSDKVersion) {
|
||||
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||
}
|
||||
}
|
||||
switch (audioFormat)
|
||||
{
|
||||
case AudioFormat.ENCODING_PCM_8BIT:
|
||||
sampleSize = 1;
|
||||
break;
|
||||
case AudioFormat.ENCODING_PCM_16BIT:
|
||||
sampleSize = 2;
|
||||
break;
|
||||
case AudioFormat.ENCODING_PCM_FLOAT:
|
||||
sampleSize = 4;
|
||||
break;
|
||||
default:
|
||||
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
|
||||
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||
sampleSize = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isCapture) {
|
||||
switch (desiredChannels) {
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_IN_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
|
||||
break;
|
||||
default:
|
||||
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
|
||||
desiredChannels = 2;
|
||||
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (desiredChannels) {
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
case 3:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
|
||||
break;
|
||||
case 4:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||
break;
|
||||
case 5:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
|
||||
break;
|
||||
case 6:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||
break;
|
||||
case 7:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
|
||||
break;
|
||||
case 8:
|
||||
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
|
||||
} else {
|
||||
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
|
||||
desiredChannels = 6;
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
|
||||
desiredChannels = 2;
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
Log.v(TAG, "Speaker configuration (and order of channels):");
|
||||
|
||||
if ((channelConfig & 0x00000004) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT");
|
||||
}
|
||||
if ((channelConfig & 0x00000008) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT");
|
||||
}
|
||||
if ((channelConfig & 0x00000010) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER");
|
||||
}
|
||||
if ((channelConfig & 0x00000020) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY");
|
||||
}
|
||||
if ((channelConfig & 0x00000040) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT");
|
||||
}
|
||||
if ((channelConfig & 0x00000080) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT");
|
||||
}
|
||||
if ((channelConfig & 0x00000100) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
|
||||
}
|
||||
if ((channelConfig & 0x00000200) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
|
||||
}
|
||||
if ((channelConfig & 0x00000400) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER");
|
||||
}
|
||||
if ((channelConfig & 0x00000800) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT");
|
||||
}
|
||||
if ((channelConfig & 0x00001000) != 0) {
|
||||
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT");
|
||||
}
|
||||
*/
|
||||
}
|
||||
frameSize = (sampleSize * desiredChannels);
|
||||
|
||||
// Let the user pick a larger buffer if they really want -- but ye
|
||||
// gods they probably shouldn't, the minimums are horrifyingly high
|
||||
// latency already
|
||||
int minBufferSize;
|
||||
if (isCapture) {
|
||||
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
|
||||
} else {
|
||||
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
|
||||
}
|
||||
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
|
||||
|
||||
int[] results = new int[4];
|
||||
|
||||
if (isCapture) {
|
||||
if (mAudioRecord == null) {
|
||||
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
|
||||
channelConfig, audioFormat, desiredFrames * frameSize);
|
||||
|
||||
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
|
||||
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
|
||||
Log.e(TAG, "Failed during initialization of AudioRecord");
|
||||
mAudioRecord.release();
|
||||
mAudioRecord = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
|
||||
mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId));
|
||||
}
|
||||
|
||||
mAudioRecord.startRecording();
|
||||
}
|
||||
|
||||
results[0] = mAudioRecord.getSampleRate();
|
||||
results[1] = mAudioRecord.getAudioFormat();
|
||||
results[2] = mAudioRecord.getChannelCount();
|
||||
|
||||
} else {
|
||||
if (mAudioTrack == null) {
|
||||
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
|
||||
|
||||
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
|
||||
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
|
||||
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
|
||||
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
|
||||
/* Try again, with safer values */
|
||||
|
||||
Log.e(TAG, "Failed during initialization of Audio Track");
|
||||
mAudioTrack.release();
|
||||
mAudioTrack = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
|
||||
mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId));
|
||||
}
|
||||
|
||||
mAudioTrack.play();
|
||||
}
|
||||
|
||||
results[0] = mAudioTrack.getSampleRate();
|
||||
results[1] = mAudioTrack.getAudioFormat();
|
||||
results[2] = mAudioTrack.getChannelCount();
|
||||
}
|
||||
results[3] = desiredFrames;
|
||||
|
||||
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
|
||||
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
|
||||
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void unregisterAudioDeviceCallback(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int[] getAudioOutputDevices() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
|
||||
} else {
|
||||
return NO_DEVICES;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int[] getAudioInputDevices() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
|
||||
} else {
|
||||
return NO_DEVICES;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void audioWriteFloatBuffer(float[] buffer) {
|
||||
if (mAudioTrack == null) {
|
||||
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||
Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < buffer.length;) {
|
||||
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
|
||||
if (result > 0) {
|
||||
i += result;
|
||||
} else if (result == 0) {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch(InterruptedException e) {
|
||||
// Nom nom
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "SDL audio: error return from write(float)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void audioWriteShortBuffer(short[] buffer) {
|
||||
if (mAudioTrack == null) {
|
||||
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < buffer.length;) {
|
||||
int result = mAudioTrack.write(buffer, i, buffer.length - i);
|
||||
if (result > 0) {
|
||||
i += result;
|
||||
} else if (result == 0) {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch(InterruptedException e) {
|
||||
// Nom nom
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "SDL audio: error return from write(short)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void audioWriteByteBuffer(byte[] buffer) {
|
||||
if (mAudioTrack == null) {
|
||||
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < buffer.length; ) {
|
||||
int result = mAudioTrack.write(buffer, i, buffer.length - i);
|
||||
if (result > 0) {
|
||||
i += result;
|
||||
} else if (result == 0) {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch(InterruptedException e) {
|
||||
// Nom nom
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "SDL audio: error return from write(byte)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
|
||||
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||
return 0;
|
||||
} else {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
|
||||
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length);
|
||||
} else {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
|
||||
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length);
|
||||
} else {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static void audioClose() {
|
||||
if (mAudioTrack != null) {
|
||||
mAudioTrack.stop();
|
||||
mAudioTrack.release();
|
||||
mAudioTrack = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static void captureClose() {
|
||||
if (mAudioRecord != null) {
|
||||
mAudioRecord.stop();
|
||||
mAudioRecord.release();
|
||||
mAudioRecord = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static void audioSetThreadPriority(boolean iscapture, int device_id) {
|
||||
try {
|
||||
|
||||
/* Set thread name */
|
||||
if (iscapture) {
|
||||
Thread.currentThread().setName("SDLAudioC" + device_id);
|
||||
} else {
|
||||
Thread.currentThread().setName("SDLAudioP" + device_id);
|
||||
}
|
||||
|
||||
/* Set thread priority */
|
||||
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "modify thread properties failed " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static native int nativeSetupJNI();
|
||||
|
||||
public static native void removeAudioDevice(boolean isCapture, int deviceId);
|
||||
|
||||
public static native void addAudioDevice(boolean isCapture, int deviceId);
|
||||
|
||||
}
|
|
@ -0,0 +1,856 @@
|
|||
package org.libsdl.app;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
|
||||
public class SDLControllerManager
|
||||
{
|
||||
|
||||
public static native int nativeSetupJNI();
|
||||
|
||||
public static native int nativeAddJoystick(int device_id, String name, String desc,
|
||||
int vendor_id, int product_id,
|
||||
boolean is_accelerometer, int button_mask,
|
||||
int naxes, int axis_mask, int nhats, int nballs);
|
||||
public static native int nativeRemoveJoystick(int device_id);
|
||||
public static native int nativeAddHaptic(int device_id, String name);
|
||||
public static native int nativeRemoveHaptic(int device_id);
|
||||
public static native int onNativePadDown(int device_id, int keycode);
|
||||
public static native int onNativePadUp(int device_id, int keycode);
|
||||
public static native void onNativeJoy(int device_id, int axis,
|
||||
float value);
|
||||
public static native void onNativeHat(int device_id, int hat_id,
|
||||
int x, int y);
|
||||
|
||||
protected static SDLJoystickHandler mJoystickHandler;
|
||||
protected static SDLHapticHandler mHapticHandler;
|
||||
|
||||
private static final String TAG = "SDLControllerManager";
|
||||
|
||||
public static void initialize() {
|
||||
if (mJoystickHandler == null) {
|
||||
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
|
||||
mJoystickHandler = new SDLJoystickHandler_API19();
|
||||
} else {
|
||||
mJoystickHandler = new SDLJoystickHandler_API16();
|
||||
}
|
||||
}
|
||||
|
||||
if (mHapticHandler == null) {
|
||||
if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
|
||||
mHapticHandler = new SDLHapticHandler_API26();
|
||||
} else {
|
||||
mHapticHandler = new SDLHapticHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
|
||||
public static boolean handleJoystickMotionEvent(MotionEvent event) {
|
||||
return mJoystickHandler.handleMotionEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void pollInputDevices() {
|
||||
mJoystickHandler.pollInputDevices();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void pollHapticDevices() {
|
||||
mHapticHandler.pollHapticDevices();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void hapticRun(int device_id, float intensity, int length) {
|
||||
mHapticHandler.run(device_id, intensity, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void hapticStop(int device_id)
|
||||
{
|
||||
mHapticHandler.stop(device_id);
|
||||
}
|
||||
|
||||
// Check if a given device is considered a possible SDL joystick
|
||||
public static boolean isDeviceSDLJoystick(int deviceId) {
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
// We cannot use InputDevice.isVirtual before API 16, so let's accept
|
||||
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
|
||||
if ((device == null) || (deviceId < 0)) {
|
||||
return false;
|
||||
}
|
||||
int sources = device.getSources();
|
||||
|
||||
/* This is called for every button press, so let's not spam the logs */
|
||||
/*
|
||||
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
|
||||
}
|
||||
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
|
||||
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
|
||||
}
|
||||
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
|
||||
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
|
||||
}
|
||||
*/
|
||||
|
||||
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
|
||||
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
|
||||
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SDLJoystickHandler {
|
||||
|
||||
/**
|
||||
* Handles given MotionEvent.
|
||||
* @param event the event to be handled.
|
||||
* @return if given event was processed.
|
||||
*/
|
||||
public boolean handleMotionEvent(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding and removing of input devices.
|
||||
*/
|
||||
public void pollInputDevices() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Actual joystick functionality available for API >= 12 devices */
|
||||
class SDLJoystickHandler_API16 extends SDLJoystickHandler {
|
||||
|
||||
static class SDLJoystick {
|
||||
public int device_id;
|
||||
public String name;
|
||||
public String desc;
|
||||
public ArrayList<InputDevice.MotionRange> axes;
|
||||
public ArrayList<InputDevice.MotionRange> hats;
|
||||
}
|
||||
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
|
||||
@Override
|
||||
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
|
||||
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
|
||||
int arg0Axis = arg0.getAxis();
|
||||
int arg1Axis = arg1.getAxis();
|
||||
if (arg0Axis == MotionEvent.AXIS_GAS) {
|
||||
arg0Axis = MotionEvent.AXIS_BRAKE;
|
||||
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
|
||||
arg0Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
if (arg1Axis == MotionEvent.AXIS_GAS) {
|
||||
arg1Axis = MotionEvent.AXIS_BRAKE;
|
||||
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
|
||||
arg1Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
|
||||
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
|
||||
// This is because the usual pairing are:
|
||||
// - AXIS_X + AXIS_Y (left stick).
|
||||
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
|
||||
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
|
||||
// This sorts the axes in the above order, which tends to be correct
|
||||
// for Xbox-ish game pads that have the right stick on RX/RY and the
|
||||
// triggers on Z/RZ.
|
||||
//
|
||||
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
|
||||
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
|
||||
//
|
||||
// References:
|
||||
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
|
||||
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
|
||||
if (arg0Axis == MotionEvent.AXIS_Z) {
|
||||
arg0Axis = MotionEvent.AXIS_RZ - 1;
|
||||
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
|
||||
--arg0Axis;
|
||||
}
|
||||
if (arg1Axis == MotionEvent.AXIS_Z) {
|
||||
arg1Axis = MotionEvent.AXIS_RZ - 1;
|
||||
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
|
||||
--arg1Axis;
|
||||
}
|
||||
|
||||
return arg0Axis - arg1Axis;
|
||||
}
|
||||
}
|
||||
|
||||
private final ArrayList<SDLJoystick> mJoysticks;
|
||||
|
||||
public SDLJoystickHandler_API16() {
|
||||
|
||||
mJoysticks = new ArrayList<SDLJoystick>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pollInputDevices() {
|
||||
int[] deviceIds = InputDevice.getDeviceIds();
|
||||
|
||||
for (int device_id : deviceIds) {
|
||||
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
|
||||
SDLJoystick joystick = getJoystick(device_id);
|
||||
if (joystick == null) {
|
||||
InputDevice joystickDevice = InputDevice.getDevice(device_id);
|
||||
joystick = new SDLJoystick();
|
||||
joystick.device_id = device_id;
|
||||
joystick.name = joystickDevice.getName();
|
||||
joystick.desc = getJoystickDescriptor(joystickDevice);
|
||||
joystick.axes = new ArrayList<InputDevice.MotionRange>();
|
||||
joystick.hats = new ArrayList<InputDevice.MotionRange>();
|
||||
|
||||
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
|
||||
Collections.sort(ranges, new RangeComparator());
|
||||
for (InputDevice.MotionRange range : ranges) {
|
||||
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
|
||||
joystick.hats.add(range);
|
||||
} else {
|
||||
joystick.axes.add(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mJoysticks.add(joystick);
|
||||
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
|
||||
getVendorId(joystickDevice), getProductId(joystickDevice), false,
|
||||
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check removed devices */
|
||||
ArrayList<Integer> removedDevices = null;
|
||||
for (SDLJoystick joystick : mJoysticks) {
|
||||
int device_id = joystick.device_id;
|
||||
int i;
|
||||
for (i = 0; i < deviceIds.length; i++) {
|
||||
if (device_id == deviceIds[i]) break;
|
||||
}
|
||||
if (i == deviceIds.length) {
|
||||
if (removedDevices == null) {
|
||||
removedDevices = new ArrayList<Integer>();
|
||||
}
|
||||
removedDevices.add(device_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (removedDevices != null) {
|
||||
for (int device_id : removedDevices) {
|
||||
SDLControllerManager.nativeRemoveJoystick(device_id);
|
||||
for (int i = 0; i < mJoysticks.size(); i++) {
|
||||
if (mJoysticks.get(i).device_id == device_id) {
|
||||
mJoysticks.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected SDLJoystick getJoystick(int device_id) {
|
||||
for (SDLJoystick joystick : mJoysticks) {
|
||||
if (joystick.device_id == device_id) {
|
||||
return joystick;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMotionEvent(MotionEvent event) {
|
||||
int actionPointerIndex = event.getActionIndex();
|
||||
int action = event.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_MOVE) {
|
||||
SDLJoystick joystick = getJoystick(event.getDeviceId());
|
||||
if (joystick != null) {
|
||||
for (int i = 0; i < joystick.axes.size(); i++) {
|
||||
InputDevice.MotionRange range = joystick.axes.get(i);
|
||||
/* Normalize the value to -1...1 */
|
||||
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
|
||||
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
|
||||
}
|
||||
for (int i = 0; i < joystick.hats.size() / 2; i++) {
|
||||
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
|
||||
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
|
||||
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getJoystickDescriptor(InputDevice joystickDevice) {
|
||||
String desc = joystickDevice.getDescriptor();
|
||||
|
||||
if (desc != null && !desc.isEmpty()) {
|
||||
return desc;
|
||||
}
|
||||
|
||||
return joystickDevice.getName();
|
||||
}
|
||||
public int getProductId(InputDevice joystickDevice) {
|
||||
return 0;
|
||||
}
|
||||
public int getVendorId(InputDevice joystickDevice) {
|
||||
return 0;
|
||||
}
|
||||
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
|
||||
return -1;
|
||||
}
|
||||
public int getButtonMask(InputDevice joystickDevice) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
|
||||
|
||||
@Override
|
||||
public int getProductId(InputDevice joystickDevice) {
|
||||
return joystickDevice.getProductId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVendorId(InputDevice joystickDevice) {
|
||||
return joystickDevice.getVendorId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
|
||||
// For compatibility, keep computing the axis mask like before,
|
||||
// only really distinguishing 2, 4 and 6 axes.
|
||||
int axis_mask = 0;
|
||||
if (ranges.size() >= 2) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY))
|
||||
axis_mask |= 0x0003;
|
||||
}
|
||||
if (ranges.size() >= 4) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY))
|
||||
axis_mask |= 0x000c;
|
||||
}
|
||||
if (ranges.size() >= 6) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
|
||||
axis_mask |= 0x0030;
|
||||
}
|
||||
// Also add an indicator bit for whether the sorting order has changed.
|
||||
// This serves to disable outdated gamecontrollerdb.txt mappings.
|
||||
boolean have_z = false;
|
||||
boolean have_past_z_before_rz = false;
|
||||
for (InputDevice.MotionRange range : ranges) {
|
||||
int axis = range.getAxis();
|
||||
if (axis == MotionEvent.AXIS_Z) {
|
||||
have_z = true;
|
||||
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
|
||||
have_past_z_before_rz = true;
|
||||
}
|
||||
}
|
||||
if (have_z && have_past_z_before_rz) {
|
||||
// If both these exist, the compare() function changed sorting order.
|
||||
// Set a bit to indicate this fact.
|
||||
axis_mask |= 0x8000;
|
||||
}
|
||||
return axis_mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getButtonMask(InputDevice joystickDevice) {
|
||||
int button_mask = 0;
|
||||
int[] keys = new int[] {
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_BUTTON_B,
|
||||
KeyEvent.KEYCODE_BUTTON_X,
|
||||
KeyEvent.KEYCODE_BUTTON_Y,
|
||||
KeyEvent.KEYCODE_BACK,
|
||||
KeyEvent.KEYCODE_MENU,
|
||||
KeyEvent.KEYCODE_BUTTON_MODE,
|
||||
KeyEvent.KEYCODE_BUTTON_START,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR,
|
||||
KeyEvent.KEYCODE_BUTTON_L1,
|
||||
KeyEvent.KEYCODE_BUTTON_R1,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT,
|
||||
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||
|
||||
// These don't map into any SDL controller buttons directly
|
||||
KeyEvent.KEYCODE_BUTTON_L2,
|
||||
KeyEvent.KEYCODE_BUTTON_R2,
|
||||
KeyEvent.KEYCODE_BUTTON_C,
|
||||
KeyEvent.KEYCODE_BUTTON_Z,
|
||||
KeyEvent.KEYCODE_BUTTON_1,
|
||||
KeyEvent.KEYCODE_BUTTON_2,
|
||||
KeyEvent.KEYCODE_BUTTON_3,
|
||||
KeyEvent.KEYCODE_BUTTON_4,
|
||||
KeyEvent.KEYCODE_BUTTON_5,
|
||||
KeyEvent.KEYCODE_BUTTON_6,
|
||||
KeyEvent.KEYCODE_BUTTON_7,
|
||||
KeyEvent.KEYCODE_BUTTON_8,
|
||||
KeyEvent.KEYCODE_BUTTON_9,
|
||||
KeyEvent.KEYCODE_BUTTON_10,
|
||||
KeyEvent.KEYCODE_BUTTON_11,
|
||||
KeyEvent.KEYCODE_BUTTON_12,
|
||||
KeyEvent.KEYCODE_BUTTON_13,
|
||||
KeyEvent.KEYCODE_BUTTON_14,
|
||||
KeyEvent.KEYCODE_BUTTON_15,
|
||||
KeyEvent.KEYCODE_BUTTON_16,
|
||||
};
|
||||
int[] masks = new int[] {
|
||||
(1 << 0), // A -> A
|
||||
(1 << 1), // B -> B
|
||||
(1 << 2), // X -> X
|
||||
(1 << 3), // Y -> Y
|
||||
(1 << 4), // BACK -> BACK
|
||||
(1 << 6), // MENU -> START
|
||||
(1 << 5), // MODE -> GUIDE
|
||||
(1 << 6), // START -> START
|
||||
(1 << 7), // THUMBL -> LEFTSTICK
|
||||
(1 << 8), // THUMBR -> RIGHTSTICK
|
||||
(1 << 9), // L1 -> LEFTSHOULDER
|
||||
(1 << 10), // R1 -> RIGHTSHOULDER
|
||||
(1 << 11), // DPAD_UP -> DPAD_UP
|
||||
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
|
||||
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
|
||||
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
|
||||
(1 << 4), // SELECT -> BACK
|
||||
(1 << 0), // DPAD_CENTER -> A
|
||||
(1 << 15), // L2 -> ??
|
||||
(1 << 16), // R2 -> ??
|
||||
(1 << 17), // C -> ??
|
||||
(1 << 18), // Z -> ??
|
||||
(1 << 20), // 1 -> ??
|
||||
(1 << 21), // 2 -> ??
|
||||
(1 << 22), // 3 -> ??
|
||||
(1 << 23), // 4 -> ??
|
||||
(1 << 24), // 5 -> ??
|
||||
(1 << 25), // 6 -> ??
|
||||
(1 << 26), // 7 -> ??
|
||||
(1 << 27), // 8 -> ??
|
||||
(1 << 28), // 9 -> ??
|
||||
(1 << 29), // 10 -> ??
|
||||
(1 << 30), // 11 -> ??
|
||||
(1 << 31), // 12 -> ??
|
||||
// We're out of room...
|
||||
0xFFFFFFFF, // 13 -> ??
|
||||
0xFFFFFFFF, // 14 -> ??
|
||||
0xFFFFFFFF, // 15 -> ??
|
||||
0xFFFFFFFF, // 16 -> ??
|
||||
};
|
||||
boolean[] has_keys = joystickDevice.hasKeys(keys);
|
||||
for (int i = 0; i < keys.length; ++i) {
|
||||
if (has_keys[i]) {
|
||||
button_mask |= masks[i];
|
||||
}
|
||||
}
|
||||
return button_mask;
|
||||
}
|
||||
}
|
||||
|
||||
class SDLHapticHandler_API26 extends SDLHapticHandler {
|
||||
@Override
|
||||
public void run(int device_id, float intensity, int length) {
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
|
||||
if (intensity == 0.0f) {
|
||||
stop(device_id);
|
||||
return;
|
||||
}
|
||||
|
||||
int vibeValue = Math.round(intensity * 255);
|
||||
|
||||
if (vibeValue > 255) {
|
||||
vibeValue = 255;
|
||||
}
|
||||
if (vibeValue < 1) {
|
||||
stop(device_id);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
|
||||
// something went horribly wrong with the Android 8.0 APIs.
|
||||
haptic.vib.vibrate(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SDLHapticHandler {
|
||||
|
||||
static class SDLHaptic {
|
||||
public int device_id;
|
||||
public String name;
|
||||
public Vibrator vib;
|
||||
}
|
||||
|
||||
private final ArrayList<SDLHaptic> mHaptics;
|
||||
|
||||
public SDLHapticHandler() {
|
||||
mHaptics = new ArrayList<SDLHaptic>();
|
||||
}
|
||||
|
||||
public void run(int device_id, float intensity, int length) {
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
haptic.vib.vibrate(length);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop(int device_id) {
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
haptic.vib.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void pollHapticDevices() {
|
||||
|
||||
final int deviceId_VIBRATOR_SERVICE = 999999;
|
||||
boolean hasVibratorService = false;
|
||||
|
||||
int[] deviceIds = InputDevice.getDeviceIds();
|
||||
// It helps processing the device ids in reverse order
|
||||
// For example, in the case of the XBox 360 wireless dongle,
|
||||
// so the first controller seen by SDL matches what the receiver
|
||||
// considers to be the first controller
|
||||
|
||||
for (int i = deviceIds.length - 1; i > -1; i--) {
|
||||
SDLHaptic haptic = getHaptic(deviceIds[i]);
|
||||
if (haptic == null) {
|
||||
InputDevice device = InputDevice.getDevice(deviceIds[i]);
|
||||
Vibrator vib = device.getVibrator();
|
||||
if (vib != null) {
|
||||
if (vib.hasVibrator()) {
|
||||
haptic = new SDLHaptic();
|
||||
haptic.device_id = deviceIds[i];
|
||||
haptic.name = device.getName();
|
||||
haptic.vib = vib;
|
||||
mHaptics.add(haptic);
|
||||
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check VIBRATOR_SERVICE */
|
||||
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (vib != null) {
|
||||
hasVibratorService = vib.hasVibrator();
|
||||
|
||||
if (hasVibratorService) {
|
||||
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
|
||||
if (haptic == null) {
|
||||
haptic = new SDLHaptic();
|
||||
haptic.device_id = deviceId_VIBRATOR_SERVICE;
|
||||
haptic.name = "VIBRATOR_SERVICE";
|
||||
haptic.vib = vib;
|
||||
mHaptics.add(haptic);
|
||||
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check removed devices */
|
||||
ArrayList<Integer> removedDevices = null;
|
||||
for (SDLHaptic haptic : mHaptics) {
|
||||
int device_id = haptic.device_id;
|
||||
int i;
|
||||
for (i = 0; i < deviceIds.length; i++) {
|
||||
if (device_id == deviceIds[i]) break;
|
||||
}
|
||||
|
||||
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
|
||||
if (i == deviceIds.length) {
|
||||
if (removedDevices == null) {
|
||||
removedDevices = new ArrayList<Integer>();
|
||||
}
|
||||
removedDevices.add(device_id);
|
||||
}
|
||||
} // else: don't remove the vibrator if it is still present
|
||||
}
|
||||
|
||||
if (removedDevices != null) {
|
||||
for (int device_id : removedDevices) {
|
||||
SDLControllerManager.nativeRemoveHaptic(device_id);
|
||||
for (int i = 0; i < mHaptics.size(); i++) {
|
||||
if (mHaptics.get(i).device_id == device_id) {
|
||||
mHaptics.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected SDLHaptic getHaptic(int device_id) {
|
||||
for (SDLHaptic haptic : mHaptics) {
|
||||
if (haptic.device_id == device_id) {
|
||||
return haptic;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
@Override
|
||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||
float x, y;
|
||||
int action;
|
||||
|
||||
switch ( event.getSource() ) {
|
||||
case InputDevice.SOURCE_JOYSTICK:
|
||||
return SDLControllerManager.handleJoystickMotionEvent(event);
|
||||
|
||||
case InputDevice.SOURCE_MOUSE:
|
||||
action = event.getActionMasked();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Event was not managed
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsRelativeMouse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean inRelativeMode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void reclaimRelativeMouseModeIfNeeded()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public float getEventX(MotionEvent event) {
|
||||
return event.getX(0);
|
||||
}
|
||||
|
||||
public float getEventY(MotionEvent event) {
|
||||
return event.getY(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
|
||||
private boolean mRelativeModeEnabled;
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||
|
||||
// Handle relative mouse mode
|
||||
if (mRelativeModeEnabled) {
|
||||
if (event.getSource() == InputDevice.SOURCE_MOUSE) {
|
||||
int action = event.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_HOVER_MOVE) {
|
||||
float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||
float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event was not managed, call SDLGenericMotionListener_API12 method
|
||||
return super.onGenericMotion(v, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRelativeMouse() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean inRelativeMode() {
|
||||
return mRelativeModeEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||
mRelativeModeEnabled = enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getEventX(MotionEvent event) {
|
||||
if (mRelativeModeEnabled) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||
} else {
|
||||
return event.getX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getEventY(MotionEvent event) {
|
||||
if (mRelativeModeEnabled) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||
} else {
|
||||
return event.getY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
private boolean mRelativeModeEnabled;
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||
float x, y;
|
||||
int action;
|
||||
|
||||
switch ( event.getSource() ) {
|
||||
case InputDevice.SOURCE_JOYSTICK:
|
||||
return SDLControllerManager.handleJoystickMotionEvent(event);
|
||||
|
||||
case InputDevice.SOURCE_MOUSE:
|
||||
// DeX desktop mouse cursor is a separate non-standard input type.
|
||||
case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
|
||||
action = event.getActionMasked();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case InputDevice.SOURCE_MOUSE_RELATIVE:
|
||||
action = event.getActionMasked();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Event was not managed
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRelativeMouse() {
|
||||
return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean inRelativeMode() {
|
||||
return mRelativeModeEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||
if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) {
|
||||
if (enabled) {
|
||||
SDLActivity.getContentView().requestPointerCapture();
|
||||
} else {
|
||||
SDLActivity.getContentView().releasePointerCapture();
|
||||
}
|
||||
mRelativeModeEnabled = enabled;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reclaimRelativeMouseModeIfNeeded()
|
||||
{
|
||||
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
|
||||
SDLActivity.getContentView().requestPointerCapture();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getEventX(MotionEvent event) {
|
||||
// Relative mouse in capture mode will only have relative for X/Y
|
||||
return event.getX(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getEventY(MotionEvent event) {
|
||||
// Relative mouse in capture mode will only have relative for X/Y
|
||||
return event.getY(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,405 @@
|
|||
package org.libsdl.app;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
|
||||
/**
|
||||
SDLSurface. This is what we draw on, so we need to know when it's created
|
||||
in order to do anything useful.
|
||||
|
||||
Because of this, that's where we set up the SDL thread
|
||||
*/
|
||||
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
||||
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
|
||||
|
||||
// Sensors
|
||||
protected SensorManager mSensorManager;
|
||||
protected Display mDisplay;
|
||||
|
||||
// Keep track of the surface size to normalize touch events
|
||||
protected float mWidth, mHeight;
|
||||
|
||||
// Is SurfaceView ready for rendering
|
||||
public boolean mIsSurfaceReady;
|
||||
|
||||
// Startup
|
||||
public SDLSurface(Context context) {
|
||||
super(context);
|
||||
getHolder().addCallback(this);
|
||||
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
|
||||
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
|
||||
setOnGenericMotionListener(SDLActivity.getMotionListener());
|
||||
|
||||
// Some arbitrary defaults to avoid a potential division by zero
|
||||
mWidth = 1.0f;
|
||||
mHeight = 1.0f;
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
}
|
||||
|
||||
public void handlePause() {
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
||||
}
|
||||
|
||||
public void handleResume() {
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
||||
}
|
||||
|
||||
public Surface getNativeSurface() {
|
||||
return getHolder().getSurface();
|
||||
}
|
||||
|
||||
// Called when we have a valid drawing surface
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceCreated()");
|
||||
SDLActivity.onNativeSurfaceCreated();
|
||||
}
|
||||
|
||||
// Called when we lose the surface
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceDestroyed()");
|
||||
|
||||
// Transition to pause, if needed
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
||||
SDLActivity.handleNativeState();
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
SDLActivity.onNativeSurfaceDestroyed();
|
||||
}
|
||||
|
||||
// Called when the surface is resized
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder,
|
||||
int format, int width, int height) {
|
||||
Log.v("SDL", "surfaceChanged()");
|
||||
|
||||
if (SDLActivity.mSingleton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
int nDeviceWidth = width;
|
||||
int nDeviceHeight = height;
|
||||
try
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) {
|
||||
DisplayMetrics realMetrics = new DisplayMetrics();
|
||||
mDisplay.getRealMetrics( realMetrics );
|
||||
nDeviceWidth = realMetrics.widthPixels;
|
||||
nDeviceHeight = realMetrics.heightPixels;
|
||||
}
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
synchronized(SDLActivity.getContext()) {
|
||||
// In case we're waiting on a size change after going fullscreen, send a notification.
|
||||
SDLActivity.getContext().notifyAll();
|
||||
}
|
||||
|
||||
Log.v("SDL", "Window size: " + width + "x" + height);
|
||||
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
||||
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
|
||||
SDLActivity.onNativeResize();
|
||||
|
||||
// Prevent a screen distortion glitch,
|
||||
// for instance when the device is in Landscape and a Portrait App is resumed.
|
||||
boolean skip = false;
|
||||
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
||||
|
||||
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
||||
if (mWidth > mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
||||
if (mWidth < mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special Patch for Square Resolution: Black Berry Passport
|
||||
if (skip) {
|
||||
double min = Math.min(mWidth, mHeight);
|
||||
double max = Math.max(mWidth, mHeight);
|
||||
|
||||
if (max / min < 1.20) {
|
||||
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't skip in MultiWindow.
|
||||
if (skip) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
|
||||
Log.v("SDL", "Don't skip in Multi-Window");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
Log.v("SDL", "Skip .. Surface is not ready.");
|
||||
mIsSurfaceReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
||||
SDLActivity.onNativeSurfaceChanged();
|
||||
|
||||
/* Surface is ready */
|
||||
mIsSurfaceReady = true;
|
||||
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
||||
SDLActivity.handleNativeState();
|
||||
}
|
||||
|
||||
// Key events
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
|
||||
}
|
||||
|
||||
// Touch events
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
/* Ref: http://developer.android.com/training/gestures/multi.html */
|
||||
int touchDevId = event.getDeviceId();
|
||||
final int pointerCount = event.getPointerCount();
|
||||
int action = event.getActionMasked();
|
||||
int pointerFingerId;
|
||||
int i = -1;
|
||||
float x,y,p;
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
// 12290 = Samsung DeX mode desktop mouse
|
||||
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
|
||||
// 0x2 = SOURCE_CLASS_POINTER
|
||||
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
|
||||
int mouseButton = 1;
|
||||
try {
|
||||
Object object = event.getClass().getMethod("getButtonState").invoke(event);
|
||||
if (object != null) {
|
||||
mouseButton = (Integer) object;
|
||||
}
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
||||
// if we are. We'll leverage our existing mouse motion listener
|
||||
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
|
||||
x = motionListener.getEventX(event);
|
||||
y = motionListener.getEventY(event);
|
||||
|
||||
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
|
||||
} else {
|
||||
switch(action) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
for (i = 0; i < pointerCount; i++) {
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// Primary pointer up/down, the index is always zero
|
||||
i = 0;
|
||||
/* fallthrough */
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
// Non primary pointer up/down
|
||||
if (i == -1) {
|
||||
i = event.getActionIndex();
|
||||
}
|
||||
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
for (i = 0; i < pointerCount; i++) {
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sensor events
|
||||
public void enableSensor(int sensortype, boolean enabled) {
|
||||
// TODO: This uses getDefaultSensor - what if we have >1 accels?
|
||||
if (enabled) {
|
||||
mSensorManager.registerListener(this,
|
||||
mSensorManager.getDefaultSensor(sensortype),
|
||||
SensorManager.SENSOR_DELAY_GAME, null);
|
||||
} else {
|
||||
mSensorManager.unregisterListener(this,
|
||||
mSensorManager.getDefaultSensor(sensortype));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||
|
||||
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
||||
// We thus should check here.
|
||||
int newOrientation;
|
||||
|
||||
float x, y;
|
||||
switch (mDisplay.getRotation()) {
|
||||
case Surface.ROTATION_90:
|
||||
x = -event.values[1];
|
||||
y = event.values[0];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
x = event.values[1];
|
||||
y = -event.values[0];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
x = -event.values[0];
|
||||
y = -event.values[1];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
|
||||
break;
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
x = event.values[0];
|
||||
y = event.values[1];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newOrientation != SDLActivity.mCurrentOrientation) {
|
||||
SDLActivity.mCurrentOrientation = newOrientation;
|
||||
SDLActivity.onNativeOrientationChanged(newOrientation);
|
||||
}
|
||||
|
||||
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
||||
y / SensorManager.GRAVITY_EARTH,
|
||||
event.values[2] / SensorManager.GRAVITY_EARTH);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Captured pointer events for API 26.
|
||||
public boolean onCapturedPointerEvent(MotionEvent event)
|
||||
{
|
||||
int action = event.getActionMasked();
|
||||
|
||||
float x, y;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||
case MotionEvent.ACTION_BUTTON_RELEASE:
|
||||
|
||||
// Change our action value to what SDL's code expects.
|
||||
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
||||
action = MotionEvent.ACTION_DOWN;
|
||||
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
||||
action = MotionEvent.ACTION_UP;
|
||||
}
|
||||
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
int button = event.getButtonState();
|
||||
|
||||
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
BIN
sdl-android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
sdl-android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
sdl-android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
sdl-android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
6
sdl-android-project/app/src/main/res/values/colors.xml
Normal file
6
sdl-android-project/app/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#3F51B5</color>
|
||||
<color name="colorPrimaryDark">#303F9F</color>
|
||||
<color name="colorAccent">#FF4081</color>
|
||||
</resources>
|
3
sdl-android-project/app/src/main/res/values/strings.xml
Normal file
3
sdl-android-project/app/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Looper</string>
|
||||
</resources>
|
8
sdl-android-project/app/src/main/res/values/styles.xml
Normal file
8
sdl-android-project/app/src/main/res/values/styles.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
|
||||
</resources>
|
25
sdl-android-project/build.gradle
Normal file
25
sdl-android-project/build.gradle
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.3.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
17
sdl-android-project/gradle.properties
Normal file
17
sdl-android-project/gradle.properties
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
BIN
sdl-android-project/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
sdl-android-project/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
sdl-android-project/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
sdl-android-project/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
#Thu Nov 11 18:20:34 PST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
160
sdl-android-project/gradlew
vendored
Executable file
160
sdl-android-project/gradlew
vendored
Executable file
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|grep -E -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|grep -E -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
90
sdl-android-project/gradlew.bat
vendored
Normal file
90
sdl-android-project/gradlew.bat
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
8
sdl-android-project/local.properties
Normal file
8
sdl-android-project/local.properties
Normal file
|
@ -0,0 +1,8 @@
|
|||
## This file must *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
#
|
||||
# Location of the SDK. This is only used by Gradle.
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
#Thu Apr 25 11:32:33 PDT 2024
|
||||
sdk.dir=/home/catmeow/Android/Sdk
|
1
sdl-android-project/settings.gradle
Normal file
1
sdl-android-project/settings.gradle
Normal file
|
@ -0,0 +1 @@
|
|||
include ':app'
|
13
setup-android-project.ps1
Normal file
13
setup-android-project.ps1
Normal file
|
@ -0,0 +1,13 @@
|
|||
function Get-ScriptDirectory {
|
||||
$scriptpath = $MyInvocation.MyCommand.Path
|
||||
return Split-Path $scriptpath
|
||||
}
|
||||
$scriptdir = Get-ScriptDirectory
|
||||
Push-Location $scriptdir
|
||||
$android_project_dir = Join-Path($scriptdir, "sdl-android-project")
|
||||
$android_app_dir = Join-Path($android_project_dir, "app")
|
||||
$android_jni_dir = Join-Path($android_app_dir, "jni")
|
||||
if (test-path $android_jni_dir) {
|
||||
rm $android_jni_dir
|
||||
}
|
||||
New-Item -ItemType Junction -Path $android_jni_dir -Value $scriptdir
|
20
setup-android-project.sh
Executable file
20
setup-android-project.sh
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/bin/env -S NOT_SOURCED=1 /bin/sh
|
||||
if ! [ "$NOT_SOURCED" = "1" ]; then
|
||||
echo "Error: This script must not be sourced!" >&2
|
||||
return 1
|
||||
fi
|
||||
get_abs_filename() {
|
||||
echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" | sed 's@/\./@/@g' | sed 's@/\.$@@g'
|
||||
}
|
||||
|
||||
export PROJECT_DIR="$(get_abs_filename $(dirname "$0"))"
|
||||
export ANDROID_PROJECT_DIR="${PROJECT_DIR}/sdl-android-project"
|
||||
export ANDROID_APP_DIR="${ANDROID_PROJECT_DIR}/app"
|
||||
export ANDROID_JNI_DIR="${ANDROID_APP_DIR}/jni"
|
||||
echo "Project directory: $PROJECT_DIR"
|
||||
echo "Android project directory: $ANDROID_PROJECT_DIR"
|
||||
echo "Android JNI symlink: $ANDROID_JNI_DIR -> $PROJECT_DIR"
|
||||
pushd "${PROJECT_DIR}"
|
||||
[ -d "$ANDROID_JNI_DIR" ] && rm -rf "$ANDROID_JNI_DIR"
|
||||
ln -sf "$PROJECT_DIR" "$ANDROID_JNI_DIR"
|
||||
popd
|
1
subprojects/SDL
Submodule
1
subprojects/SDL
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit f461d91cd265d7b9a44b4d472b1df0c0ad2855a0
|
1
subprojects/SDL_image
Submodule
1
subprojects/SDL_image
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit abcf63aa71b4e3ac32120fa9870a6500ddcdcc89
|
1
subprojects/libintl-lite
Submodule
1
subprojects/libintl-lite
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 44035a0c3fe20ef28f071b35b9f5a653fbfe5e6d
|
1
subprojects/oboe
Submodule
1
subprojects/oboe
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 86165b8249bc22b9ef70b69e20323244b6f08d88
|
62
translation.cpp
Normal file
62
translation.cpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#include "translation.hpp"
|
||||
#include <libintl.h>
|
||||
#include "config.h"
|
||||
static const char *orig_language;
|
||||
static const char *cur_locale_dir;
|
||||
static const char *cur_locale;
|
||||
const char *tr_ctx(const char *ctx, const char *msgid) {
|
||||
std::string msg_ctxt_id = (std::string(ctx) + std::string("\004") + std::string(msgid));
|
||||
const char *translation = gettext(msg_ctxt_id.c_str());
|
||||
if (std::string(translation) == msg_ctxt_id) {
|
||||
return msgid;
|
||||
} else {
|
||||
return translation;
|
||||
}
|
||||
}
|
||||
const char *tr(const char *msgid) {
|
||||
return gettext(msgid);
|
||||
}
|
||||
void set_language(const char *language) {
|
||||
if (language == nullptr) {
|
||||
language = "";
|
||||
}
|
||||
#ifdef LIBINTL_LITE_API
|
||||
if (language[0] == '\0') {
|
||||
setenv("LANGUAGE", orig_language, 1);
|
||||
} else {
|
||||
setenv("LANGUAGE", language, 1);
|
||||
}
|
||||
#else
|
||||
setlocale(LC_ALL, language);
|
||||
#endif
|
||||
|
||||
bindtextdomain(cur_locale, cur_locale_dir);
|
||||
textdomain(cur_locale);
|
||||
}
|
||||
char *get_language() {
|
||||
#ifdef LIBINTL_LITE_API
|
||||
return getenv("LANGUAGE");
|
||||
#else
|
||||
return setlocale(LC_MESSAGES, NULL);
|
||||
#endif
|
||||
}
|
||||
void setup_locale(const char *locale, const char *locale_dir) {
|
||||
if (orig_language == nullptr) {
|
||||
orig_language = getenv("LANGUAGE");
|
||||
if (orig_language == nullptr) {
|
||||
orig_language = "C.UTF-8";
|
||||
}
|
||||
}
|
||||
if (locale_dir == nullptr) {
|
||||
if (cur_locale_dir == nullptr) {
|
||||
locale_dir = LOCALE_DIR;
|
||||
} else {
|
||||
locale_dir = cur_locale_dir;
|
||||
}
|
||||
}
|
||||
if (cur_locale_dir == nullptr) {
|
||||
cur_locale_dir = locale_dir;
|
||||
}
|
||||
cur_locale = locale;
|
||||
set_language();
|
||||
}
|
20
translation.hpp
Normal file
20
translation.hpp
Normal file
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
// Based on the code found in gettext.h, but without any additional things we don't need.
|
||||
const char *tr_ctx(const char *ctx, const char *msgid);
|
||||
const char *tr(const char *msgid);
|
||||
void setup_locale(const char *domain, const char *locale_dir = nullptr);
|
||||
char *get_language();
|
||||
void set_language(const char *language = nullptr);
|
||||
#define _TR(str) tr(str)
|
||||
#define _TR_CTX(ctx, str) tr_ctx(ctx, str)
|
||||
#define _TRS(str) std::string(_TR(str))
|
||||
#define _TRS_CTX(ctx, str) std::string(_TR_CTX(ctx, str))
|
||||
#define _TRIS(icon, str) (std::string(icon) + _TRS(str))
|
||||
#define _TRI(icon, str) _TRIS(icon, str).c_str()
|
||||
#define _TRIS_CTX(icon, ctx, str) (std::string(icon) + _TRS_CTX(ctx, str))
|
||||
#define _TRI_CTX(icon, ctx, str) _TRIS_CTX(icon, ctx, str).c_str()
|
||||
#define CURRENT_LANGUAGE get_language()
|
||||
// The value required to set the operating system's default language.
|
||||
#define DEFAULT_LANG ""
|
||||
#define SET_LANG(lang) set_language(lang)
|
7
util.cpp
7
util.cpp
|
@ -1,4 +1,7 @@
|
|||
#include "util.hpp"
|
||||
#ifdef __ANDROID__
|
||||
#include <SDL.h>
|
||||
#endif
|
||||
std::string PadZeros(std::string input, size_t required_length) {
|
||||
return std::string(required_length - std::min(required_length, input.length()), '0') + input;
|
||||
}
|
||||
|
@ -40,7 +43,9 @@ std::string TimeToString(double time_code, uint8_t min_components) {
|
|||
#endif
|
||||
std::string get_prefs_path() {
|
||||
std::string path;
|
||||
#ifdef _WIN32
|
||||
#ifdef __ANDROID__
|
||||
path = SDL_AndroidGetInternalStoragePath();
|
||||
#elif defined(_WIN32)
|
||||
PWSTR str;
|
||||
if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &str) != S_OK) {
|
||||
CoTaskMemFree(str);
|
||||
|
|
Loading…
Reference in a new issue