Compare commits

...

2 commits

Author SHA1 Message Date
6159f52e8a Add android support, and other changes. 2024-04-28 12:31:40 -07:00
0d236857b8 Add android support (WIP) 2024-04-25 11:23:38 -07:00
78 changed files with 7347 additions and 259 deletions

4
.gitignore vendored
View file

@ -7,3 +7,7 @@ compile_commands.json
flatpak-repo flatpak-repo
*.flatpak *.flatpak
!build*.sh !build*.sh
!build.gradle
.cxx
.gradle
/sdl-android-project/app/jni

12
.gitmodules vendored
View file

@ -16,3 +16,15 @@
[submodule "subprojects/soundtouch"] [submodule "subprojects/soundtouch"]
path = subprojects/soundtouch path = subprojects/soundtouch
url = https://codeberg.org/soundtouch/soundtouch.git 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

View file

@ -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) project(looper VERSION 1.0.0 LANGUAGES C CXX)
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten") if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten")
@ -86,6 +86,46 @@ if (DEFINED EMSCRIPTEN)
else() else()
set(BUILD_STATIC OFF CACHE BOOL "") set(BUILD_STATIC OFF CACHE BOOL "")
endif() 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) find_package(PkgConfig)
#add_subdirectory(subprojects/jsoncpp) #add_subdirectory(subprojects/jsoncpp)
@ -171,20 +211,78 @@ prefix_all(LIBRARY_SOURCES
util.cpp util.cpp
log.cpp log.cpp
dbus.cpp dbus.cpp
translation.cpp
) )
add_library(liblooper STATIC ${LIBRARY_SOURCES}) add_library(liblooper STATIC ${LIBRARY_SOURCES})
target_include_directories(liblooper PUBLIC ${INC}) 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) if(DEFINED EMSCRIPTEN)
add_subdirectory(subprojects/jsoncpp) add_subdirectory(subprojects/jsoncpp)
add_subdirectory(subprojects/soundtouch) 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") target_compile_options(liblooper PUBLIC "-sUSE_SDL=2")
else() else()
if(NOT BUILD_SOUNDTOUCH)
pkg_check_modules(SoundTouch IMPORTED_TARGET soundtouch) pkg_check_modules(SoundTouch IMPORTED_TARGET soundtouch)
endif()
if (NOT BUILD_JSONCPP)
pkg_check_modules(jsoncpp IMPORTED_TARGET jsoncpp) pkg_check_modules(jsoncpp IMPORTED_TARGET jsoncpp)
endif()
if (NOT BUILD_SDL)
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
find_package(sdbus-c++ REQUIRED) endif()
target_link_libraries(liblooper PUBLIC SDL2::SDL2 ${SDL_MIXER_X_TARGET} PkgConfig::SoundTouch libvgmstream libvgmstream_shared PkgConfig::jsoncpp) 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() endif()
if (${ENABLE_DBUS}) if (${ENABLE_DBUS})
target_link_libraries(liblooper PUBLIC SDBusCpp::sdbus-c++) 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_include_directories(${target} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${INC})
target_link_libraries(${target} PRIVATE liblooper) target_link_libraries(${target} PRIVATE liblooper)
if(${USE_PORTALS}) 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") if (NOT ${libPortal_FOUND} EQUAL "1")
set(USE_PORTALS OFF) set(USE_PORTALS OFF)
else() else()
@ -227,7 +325,7 @@ macro(ui_backend_subdir)
endmacro() endmacro()
set(ENABLED_UIS ) set(ENABLED_UIS )
ui_backend_subdir(NAME "IMGUI" READABLE_NAME "Dear ImGui" SUBDIR backends/ui/imgui) 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) ui_backend_subdir(NAME "GTK" READABLE_NAME "GTK4" SUBDIR backends/ui/gtk)
endif() endif()
execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen_ui_backend_inc.py ${ENABLED_UIS}) 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) 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}") 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() endfunction()
if (DEFINED ANDROID_NDK)
add_library(${TARGET_NAME} SHARED ${SOURCES})
else()
add_executable(${TARGET_NAME} ${SOURCES}) add_executable(${TARGET_NAME} ${SOURCES})
endif()
add_dependencies(${TARGET_NAME} looper_assets ${UI_BACKENDS}) add_dependencies(${TARGET_NAME} looper_assets ${UI_BACKENDS})
if(DEFINED EMSCRIPTEN) if(DEFINED EMSCRIPTEN)
@ -259,6 +361,9 @@ else()
endif() endif()
target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS}) target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS})
install(TARGETS ${TARGET_NAME} ${EXTRA_LIBS}) install(TARGETS ${TARGET_NAME} ${EXTRA_LIBS})
if (${BUILD_SDL2})
install(EXPORT SDL2-static SDL2main)
endif()
if (NOT DEFINED EMSCRIPTEN) if (NOT DEFINED EMSCRIPTEN)
install(FILES assets/icon.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps/) 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) install(FILES assets/com.complecwaft.Looper.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)

View file

@ -13,7 +13,7 @@
<arg type='a{sv}' name='platform_data' direction='in'/> <arg type='a{sv}' name='platform_data' direction='in'/>
</method> </method>
</interface> </interface>
<interface name="com.complecwaft.Looper"> <interface name="com.complecwaft.looper">
<method name="CreateHandle"> <method name="CreateHandle">
<arg type='s' name='new_handle' direction='out' /> <arg type='s' name='new_handle' direction='out' />
</method> </method>
@ -81,7 +81,7 @@
<property name="IsDaemon" type="b" access="read" /> <property name="IsDaemon" type="b" access="read" />
<property name="StreamIdx" type="u" access="read" /> <property name="StreamIdx" type="u" access="read" />
</interface> </interface>
<interface name="com.complecwaft.Looper.Errors" > <interface name="com.complecwaft.looper.Errors" >
<method name="PopFront"> <method name="PopFront">
<arg name="handle" direction="in" type="s" /> <arg name="handle" direction="in" type="s" />
<arg name="error" direction="out" type="s" /> <arg name="error" direction="out" type="s" />

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application"> <component type="desktop-application">
<id>com.complecwaft.Looper</id> <id>com.complecwaft.looper</id>
<developer_name>Catmeow72</developer_name> <developer_name>Catmeow72</developer_name>
<name>Looper</name> <name>Looper</name>

View file

@ -48,13 +48,13 @@ private:
namespace com { namespace com {
namespace complecwaft { namespace complecwaft {
class Looper_adaptor class looper_adaptor
{ {
public: public:
static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper"; static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper";
protected: protected:
Looper_adaptor(sdbus::IObject& object) looper_adaptor(sdbus::IObject& object)
: object_(&object) : object_(&object)
{ {
object_->registerMethod("CreateHandle").onInterface(INTERFACE_NAME).withOutputParamNames("new_handle").implementedAs([this](){ return this->CreateHandle(); }); 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(); }); object_->registerProperty("StreamIdx").onInterface(INTERFACE_NAME).withGetter([this](){ return this->StreamIdx(); });
} }
Looper_adaptor(const Looper_adaptor&) = delete; looper_adaptor(const looper_adaptor&) = delete;
Looper_adaptor& operator=(const Looper_adaptor&) = delete; looper_adaptor& operator=(const looper_adaptor&) = delete;
Looper_adaptor(Looper_adaptor&&) = default; looper_adaptor(looper_adaptor&&) = default;
Looper_adaptor& operator=(Looper_adaptor&&) = default; looper_adaptor& operator=(looper_adaptor&&) = default;
~Looper_adaptor() = default; ~looper_adaptor() = default;
public: public:
void emitPlaybackEngineStarted() void emitPlaybackEngineStarted()
@ -183,12 +183,12 @@ private:
namespace com { namespace com {
namespace complecwaft { namespace complecwaft {
namespace Looper { namespace looper {
class Errors_adaptor class Errors_adaptor
{ {
public: public:
static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper.Errors"; static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper.Errors";
protected: protected:
Errors_adaptor(sdbus::IObject& object) Errors_adaptor(sdbus::IObject& object)

View file

@ -56,13 +56,13 @@ private:
namespace com { namespace com {
namespace complecwaft { namespace complecwaft {
class Looper_proxy class looper_proxy
{ {
public: public:
static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper"; static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper";
protected: protected:
Looper_proxy(sdbus::IProxy& proxy) looper_proxy(sdbus::IProxy& proxy)
: proxy_(&proxy) : proxy_(&proxy)
{ {
proxy_->uponSignal("PlaybackEngineStarted").onInterface(INTERFACE_NAME).call([this](){ this->onPlaybackEngineStarted(); }); 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); }); 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(const looper_proxy&) = delete;
Looper_proxy& operator=(const Looper_proxy&) = delete; looper_proxy& operator=(const looper_proxy&) = delete;
Looper_proxy(Looper_proxy&&) = default; looper_proxy(looper_proxy&&) = default;
Looper_proxy& operator=(Looper_proxy&&) = default; looper_proxy& operator=(looper_proxy&&) = default;
~Looper_proxy() = default; ~looper_proxy() = default;
virtual void onPlaybackEngineStarted() = 0; virtual void onPlaybackEngineStarted() = 0;
virtual void onSpeedChanged(const double& new_speed) = 0; virtual void onSpeedChanged(const double& new_speed) = 0;
@ -247,12 +247,12 @@ private:
namespace com { namespace com {
namespace complecwaft { namespace complecwaft {
namespace Looper { namespace looper {
class Errors_proxy class Errors_proxy
{ {
public: public:
static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper.Errors"; static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper.Errors";
protected: protected:
Errors_proxy(sdbus::IProxy& proxy) Errors_proxy(sdbus::IProxy& proxy)

View file

@ -6,9 +6,10 @@ else()
set(GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE OFF) set(GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE OFF)
endif() endif()
option(USE_GLES "Enable using OpenGL ES" ${GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE}) 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_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(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 translation.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}) foreach(SRC IN ITEMS ${IMGUI_BACKEND_SRC})
list(APPEND IMGUI_SRC backends/${SRC}) list(APPEND IMGUI_SRC backends/${SRC})
endforeach() 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}) foreach(INCDIR IN ITEMS ${BACKEND_IMGUI_INC_BASE})
set(BACKEND_IMGUI_INC ${BACKEND_IMGUI_INC} ${CMAKE_CURRENT_SOURCE_DIR}/${INCDIR}) set(BACKEND_IMGUI_INC ${BACKEND_IMGUI_INC} ${CMAKE_CURRENT_SOURCE_DIR}/${INCDIR})
endforeach() endforeach()
if(${USE_GLES}) if(USE_GLES)
set(GLComponents GLES2) set(GLComponents GLES${GLES_VERSION})
set(GLTarget GLES2) set(GLTarget GLES${GLES_VERSION})
else() else()
set(GLComponents OpenGL) set(GLComponents OpenGL)
set(GLTarget GL) set(GLTarget GL)
endif() 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}) add_ui_backend(imgui_ui ${BACKEND_IMGUI_SRC})
if(${USE_GLES}) if(USE_GLES)
target_compile_definitions(imgui_ui PRIVATE "IMGUI_IMPL_OPENGL_ES2") target_compile_definitions(imgui_ui PRIVATE IMGUI_IMPL_OPENGL_ES${GLES_VERSION})
endif() endif()
if(DEFINED EMSCRIPTEN) if(DEFINED EMSCRIPTEN)
target_compile_options(imgui_ui PRIVATE "-sUSE_SDL_IMAGE=2") 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_options(imgui_ui PUBLIC "-sMAX_WEBGL_VERSION=2" "-sMIN_WEBGL_VERSION=2" "-sFULL_ES3")
target_link_libraries(imgui PRIVATE ${LIBINTL_LIBRARY})
else() else()
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
find_package(SDL2_image 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() 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") target_compile_definitions(imgui_ui PRIVATE IMGUI_USER_CONFIG="imgui_config.h")

View file

@ -5,18 +5,23 @@
#include "config.h" #include "config.h"
#include <SDL_image.h> #include <SDL_image.h>
#include <string> #include <string>
#include <tuple>
#include <initializer_list>
#include "theme.h" #include "theme.h"
#include "imgui_stdlib.h" #include "imgui_stdlib.h"
#include "imgui_impl_sdl2.h" #include "imgui_impl_sdl2.h"
#include "imgui_impl_opengl3.h" #include "imgui_impl_sdlrenderer2.h"
#include "base85.h" #include "base85.h"
#include <thread> #include <thread>
#include "translation.h" #include <translation.hpp>
#include <log.hpp> #include <log.hpp>
#include <options.hpp>
using std::vector; using std::vector;
using namespace Looper::Options;
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
extern "C" { extern "C" {
extern void get_size(int32_t *x, int32_t *y); extern void get_size(int32_t *x, int32_t *y);
extern double get_dpi();
} }
#endif #endif
void RendererBackend::on_resize() { void RendererBackend::on_resize() {
@ -24,6 +29,7 @@ void RendererBackend::on_resize() {
int32_t x, y; int32_t x, y;
get_size(&x, &y); get_size(&x, &y);
SDL_SetWindowSize(window, (int)x, (int)y); SDL_SetWindowSize(window, (int)x, (int)y);
UpdateScale();
#endif #endif
} }
static RendererBackend *renderer_backend; static RendererBackend *renderer_backend;
@ -40,14 +46,41 @@ void main_loop() {
} }
#endif #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() { void RendererBackend::BackendDeinit() {
ImGuiIO& io = ImGui::GetIO(); (void)io; ImGuiIO& io = ImGui::GetIO(); (void)io;
// Cleanup // Cleanup
ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplSDLRenderer2_Shutdown();
ImGui_ImplSDL2_Shutdown(); ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext(); ImGui::DestroyContext();
SDL_GL_DeleteContext(gl_context); SDL_DestroyRenderer(rend);
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
IMG_Quit(); IMG_Quit();
SDL_Quit(); SDL_Quit();
@ -55,7 +88,11 @@ void RendererBackend::BackendDeinit() {
Deinit(); Deinit();
renderer_backend = nullptr; renderer_backend = nullptr;
} }
bool RendererBackend::isTouchScreenMode() {
return touchScreenModeOverride.value_or(SDL_GetNumTouchDevices() > 0);
}
void RendererBackend::LoopFunction() { void RendererBackend::LoopFunction() {
SDL_RenderSetVSync(rend, vsync ? 1 : 0);
ImGuiIO& io = ImGui::GetIO(); (void)io; ImGuiIO& io = ImGui::GetIO(); (void)io;
if (resize_needed) { if (resize_needed) {
on_resize(); on_resize();
@ -92,67 +129,57 @@ void RendererBackend::LoopFunction() {
} }
} }
} }
bool touchScreenMode = isTouchScreenMode();
if (touchScreenMode) {
io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
} else {
io.ConfigFlags &= ~ImGuiConfigFlags_IsTouchScreen;
}
// Start the Dear ImGui frame // Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDLRenderer2_NewFrame();
ImGui_ImplSDL2_NewFrame(); ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame(); ImGui::NewFrame();
// Run the GUI // Run the GUI
GuiFunction(); GuiFunction();
// Rendering // Rendering
ImGui::Render(); ImGui::Render();
// Update the window size. 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));
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); SDL_RenderClear(rend);
// 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);
// Tell ImGui to render. // 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. // 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 not doing VSync, wait until the next frame needs to be rendered.
if (!vsync) { if (!vsync) {
std::this_thread::sleep_until(next_frame); 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) { std::map<std::string, ImFont *> add_font(FontData data_vec, double scale) {
ImFont* font = nullptr;
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
for (auto data : data_vec) { 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(); ImFontConfig font_cfg = ImFontConfig();
font_cfg.SizePixels = size; font_cfg.SizePixels = size;
font_cfg.OversampleH = font_cfg.OversampleV = 1; font_cfg.OversampleH = font_cfg.OversampleV = 1;
font_cfg.PixelSnapH = true; font_cfg.PixelSnapH = true;
if (font_cfg.SizePixels <= 0.0f) if (font_cfg.SizePixels <= 0.0f)
font_cfg.SizePixels = 13.0f * 1.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.EllipsisChar = (ImWchar)0x0085;
//font_cfg.GlyphOffset.y = 1.0f * IM_FLOOR(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units //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 char *ttf_compressed_base85 = data;
const ImWchar* glyph_ranges = data.ranges; const ImWchar *glyph_ranges = data_vec.ranges;
auto new_font = io.Fonts->AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, glyph_ranges); font = io.Fonts->AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85,
if (font == nullptr) font = new_font; font_cfg.SizePixels,
&font_cfg, glyph_ranges);
} }
{ {
ImFontConfig config; ImFontConfig config;
@ -161,9 +188,12 @@ ImFont *add_font(vector<FontData> data_vec, int size = 13) {
config.SizePixels = size; config.SizePixels = size;
config.DstFont = font; config.DstFont = font;
static const ImWchar icon_ranges[] = {ICON_MIN_FK, ICON_MAX_FK, 0}; static const ImWchar icon_ranges[] = {ICON_MIN_FK, ICON_MAX_FK, 0};
io.Fonts->AddFontFromMemoryCompressedBase85TTF(forkawesome_compressed_data_base85, float(size), &config, icon_ranges); io.Fonts->AddFontFromMemoryCompressedBase85TTF(forkawesome_compressed_data_base85,
float(size), &config, icon_ranges);
} }
return font; output[id] = font;
}
return output;
} }
RendererBackend::RendererBackend() { RendererBackend::RendererBackend() {
} }
@ -184,6 +214,9 @@ void RendererBackend::UpdateScale() {
#else #else
96.0; 96.0;
#endif #endif
#ifdef __EMSCRIPTEN__
scale = get_dpi() / defaultDPI;
#else
float dpi = defaultDPI; float dpi = defaultDPI;
if (SDL_GetDisplayDPI(SDL_GetWindowDisplayIndex(window), NULL, &dpi, NULL) == 0){ if (SDL_GetDisplayDPI(SDL_GetWindowDisplayIndex(window), NULL, &dpi, NULL) == 0){
scale = dpi / defaultDPI; scale = dpi / defaultDPI;
@ -191,13 +224,18 @@ void RendererBackend::UpdateScale() {
WARNING.writeln("DPI couldn't be determined!"); WARNING.writeln("DPI couldn't be determined!");
scale = 1.0; scale = 1.0;
} }
#ifndef __ANDROID__
SDL_SetWindowSize(window, window_width * scale, window_height * scale); SDL_SetWindowSize(window, window_width * scale, window_height * scale);
#endif
#endif
AddFonts(); AddFonts();
} }
void RendererBackend::SetWindowSize(int w, int h) { void RendererBackend::SetWindowSize(int w, int h) {
#if !(defined(__ANDROID__) || defined(__EMSCRIPTEN__))
window_width = w; window_width = w;
window_height = h; window_height = h;
SDL_SetWindowSize(window, w * scale, h * scale); SDL_SetWindowSize(window, w * scale, h * scale);
#endif
} }
void RendererBackend::GetWindowsize(int *w, int *h) { void RendererBackend::GetWindowsize(int *w, int *h) {
int ww, wh; int ww, wh;
@ -208,12 +246,25 @@ void RendererBackend::GetWindowsize(int *w, int *h) {
if (h) *h = wh; if (h) *h = wh;
} }
void RendererBackend::AddFonts() { void RendererBackend::AddFonts() {
ImGui_ImplOpenGL3_DestroyFontsTexture(); ImGui_ImplSDLRenderer2_DestroyFontsTexture();
auto& io = ImGui::GetIO(); (void)io; auto& io = ImGui::GetIO(); (void)io;
io.Fonts->Clear(); 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); std::string font_type = get_option<std::string>("font_type", "default");
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); std::string default_font = "default";
ImGui_ImplOpenGL3_CreateFontsTexture(); 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__ #ifdef __EMSCRIPTEN__
static EM_BOOL resize_callback(int event_type, const EmscriptenUiEvent *event, void *userdata) { 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 #endif
int RendererBackend::Run() { int RendererBackend::Run() {
setlocale(LC_ALL, ""); setup_locale("neko_player");
bindtextdomain("neko_player", LOCALE_DIR);
textdomain("neko_player");
DEBUG.writefln("Loaded locale '%s' from '%s'...", CURRENT_LANGUAGE, LOCALE_DIR); DEBUG.writefln("Loaded locale '%s' from '%s'...", CURRENT_LANGUAGE, LOCALE_DIR);
DEBUG.writefln("Locale name: %s", _TR_CTX("Language name", "English (United States)")); DEBUG.writefln("Locale name: %s", _TR_CTX("Language name", "English (United States)"));
bool enable_kms = std::getenv("LAP_KMS") != nullptr; bool enable_kms = std::getenv("LAP_KMS") != nullptr;
@ -239,54 +288,31 @@ int RendererBackend::Run() {
enable_kms = true; enable_kms = true;
} }
IMG_Init(IMG_INIT_PNG|IMG_INIT_WEBP); IMG_Init(IMG_INIT_PNG|IMG_INIT_WEBP);
prefPath = SDL_GetPrefPath("Catmeow72", NAME); #ifdef __ANDROID__
Theme::prefPath = prefPath; prefPath = SDL_AndroidGetInternalStoragePath();
// 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 #else
// GL 3.0 + GLSL 130 prefPath = SDL_GetPrefPath("Catmeow72", NAME);
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 #endif
Theme::prefPath = prefPath;
// From 2.0.18: Enable native IME. // From 2.0.18: Enable native IME.
#ifdef SDL_HINT_IME_SHOW_UI #ifdef SDL_HINT_IME_SHOW_UI
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
#endif #endif
// Create window with graphics context SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_CreateWindowAndRenderer(window_width, window_height, window_flags, &window, &rend);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); #ifndef __ANDROID__
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_SetWindowMinimumSize(window, window_width, window_height); SDL_SetWindowMinimumSize(window, window_width, window_height);
if (enable_kms) { if (enable_kms) {
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
} }
#endif
SDL_EventState(SDL_DROPFILE, SDL_ENABLE); SDL_EventState(SDL_DROPFILE, SDL_ENABLE);
const vector<unsigned char> icon_data = DecodeBase85(icon_compressed_data_base85); 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_Surface* icon = IMG_Load_RW(SDL_RWFromConstMem(icon_data.data(), icon_data.size()), 1);
SDL_SetWindowIcon(window, icon); SDL_SetWindowIcon(window, icon);
gl_context = SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, gl_context);
// Setup Dear ImGui context // Setup Dear ImGui context
IMGUI_CHECKVERSION(); IMGUI_CHECKVERSION();
@ -295,6 +321,9 @@ int RendererBackend::Run() {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
if (SDL_GetNumTouchDevices() > 0) {
io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
}
//io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; //io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
io.IniFilename = strdup((std::string(prefPath) + "imgui.ini").c_str()); io.IniFilename = strdup((std::string(prefPath) + "imgui.ini").c_str());
if (enable_kms) { if (enable_kms) {
@ -306,8 +335,8 @@ int RendererBackend::Run() {
//ImGui::StyleColorsLight(); //ImGui::StyleColorsLight();
// Setup Platform/Renderer backends // Setup Platform/Renderer backends
ImGui_ImplSDL2_InitForOpenGL(window, gl_context); ImGui_ImplSDL2_InitForSDLRenderer(window, rend);
ImGui_ImplOpenGL3_Init(glsl_version); ImGui_ImplSDLRenderer2_Init(rend);
// Load Fonts // 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. // - 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. // - 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); theme = new Theme(false);
#ifdef __ANDROID__
userdir = SDL_AndroidGetExternalStoragePath();
#else
userdir = std::getenv( userdir = std::getenv(
#ifdef _WIN32 #ifdef _WIN32
"UserProfile" "UserProfile"
@ -334,9 +366,8 @@ int RendererBackend::Run() {
"HOME" "HOME"
#endif #endif
); );
#ifndef __EMSCRIPTEN__
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
#endif #endif
SDL_RenderSetVSync(rend, vsync ? 1 : 0);
theme->Apply(accent_color); theme->Apply(accent_color);
Init(); Init();
SDL_ShowWindow(window); SDL_ShowWindow(window);

View file

@ -19,13 +19,16 @@ static const char* NAME = "Looper";
class RendererBackend { class RendererBackend {
void BackendDeinit(); void BackendDeinit();
void LoopFunction(); void LoopFunction();
SDL_GLContext gl_context; //SDL_GLContext gl_context;
bool resize_needed = true; bool resize_needed = true;
void on_resize(); void on_resize();
public: public:
std::optional<bool> touchScreenModeOverride;
bool isTouchScreenMode();
static void resize_static(); static void resize_static();
double scale = 1.0; double scale = 1.0;
SDL_Window *window; SDL_Window *window;
SDL_Renderer *rend;
int window_width = 475; int window_width = 475;
int window_height = 354; int window_height = 354;
bool done = false; bool done = false;

View file

@ -4,6 +4,41 @@
#include <cctype> #include <cctype>
#include <stdio.h> #include <stdio.h>
#include <log.hpp> #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__ #ifdef __EMSCRIPTEN__
extern "C" { extern "C" {
extern void open_filepicker(); extern void open_filepicker();
@ -28,6 +63,7 @@ FileBrowser::FileBrowser(bool save, ImGuiFileBrowserFlags extra_fallback_flags)
this->save = save; this->save = save;
this->flags = (save ? ImGuiFileBrowserFlags_CreateNewDir|ImGuiFileBrowserFlags_EnterNewFilename : 0) | extra_fallback_flags; this->flags = (save ? ImGuiFileBrowserFlags_CreateNewDir|ImGuiFileBrowserFlags_EnterNewFilename : 0) | extra_fallback_flags;
fallback = ImGui::FileBrowser(this->flags); fallback = ImGui::FileBrowser(this->flags);
} }
void FileBrowser::SetTypeFilters(string name, vector<string> filters) { void FileBrowser::SetTypeFilters(string name, vector<string> filters) {
filter_name = name; filter_name = name;
@ -68,13 +104,15 @@ void FileBrowser::SetTypeFilters(string name, vector<string> filters) {
} }
void FileBrowser::SetPwd(path path) { void FileBrowser::SetPwd(path path) {
pwd = path; pwd = path;
#ifndef PORTALS #if !(defined(PORTALS) || defined(__ANDROID__))
fallback.SetPwd(path); fallback.SetPwd(path);
#endif #endif
} }
bool FileBrowser::HasSelected() { bool FileBrowser::HasSelected() {
#ifdef PORTALS #ifdef PORTALS
return selected.has_value(); return selected.has_value();
#elif defined(__ANDROID__)
return strlen(GetPickedFile()) > 0;
#elif defined(__EMSCRIPTEN__) #elif defined(__EMSCRIPTEN__)
return file_picker_confirmed(); return file_picker_confirmed();
#else #else
@ -84,6 +122,13 @@ bool FileBrowser::HasSelected() {
path FileBrowser::GetSelected() { path FileBrowser::GetSelected() {
#ifdef PORTALS #ifdef PORTALS
return selected.value_or(path()); 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__) #elif defined(__EMSCRIPTEN__)
if (HasSelected()) { if (HasSelected()) {
const char *c_file = get_first_file(); const char *c_file = get_first_file();
@ -113,6 +158,10 @@ void FileBrowser::Open() {
} else { } else {
xdp_portal_open_file(portal, NULL, title.c_str(), variant, NULL, NULL, XDP_OPEN_FILE_FLAG_NONE, NULL, &FileBrowser::FileBrowserOpenCallback, this); 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__) #elif defined(__EMSCRIPTEN__)
open_filepicker(); open_filepicker();
#else #else
@ -194,6 +243,27 @@ void FileBrowser::FileBrowserSaveCallback(GObject *src, GAsyncResult *res, gpoin
void FileBrowser::Display() { void FileBrowser::Display() {
#ifdef PORTALS #ifdef PORTALS
g_main_context_iteration(main_context, false); 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__) #elif defined(__EMSCRIPTEN__)
if (file_picker_visible() || file_picker_loading()) { if (file_picker_visible() || file_picker_loading()) {
if((flags & ImGuiFileBrowserFlags_NoModal)) if((flags & ImGuiFileBrowserFlags_NoModal))
@ -245,6 +315,9 @@ void FileBrowser::ClearSelected() {
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
clear_file_selection(); clear_file_selection();
#endif #endif
#ifdef __ANDROID__
::ClearSelected();
#endif
#ifndef PORTALS #ifndef PORTALS
fallback.ClearSelected(); fallback.ClearSelected();
#endif #endif
@ -258,6 +331,8 @@ void FileBrowser::SetTitle(string title) {
bool FileBrowser::IsOpened() { bool FileBrowser::IsOpened() {
#ifdef PORTALS #ifdef PORTALS
return open; return open;
#elif defined(__ANDROID__)
return open;
#elif defined(__EMSCRIPTEN__) #elif defined(__EMSCRIPTEN__)
return !file_picker_closed() || file_picker_confirmed(); return !file_picker_closed() || file_picker_confirmed();
#else #else

View file

@ -196,7 +196,9 @@ inline ImGui::FileBrowser::FileBrowser(ImGuiFileBrowserFlags flags)
inputNameBuf_->front() = '\0'; inputNameBuf_->front() = '\0';
inputNameBuf_->back() = '\0'; inputNameBuf_->back() = '\0';
SetTitle("file browser"); SetTitle("file browser");
#ifndef __ANDROID__
SetPwd(std::filesystem::current_path()); SetPwd(std::filesystem::current_path());
#endif
typeFilters_.clear(); typeFilters_.clear();
typeFilterIndex_ = 0; typeFilterIndex_ = 0;

View file

@ -14,7 +14,6 @@
#pragma once #pragma once
#ifdef IMGUI_IMPL_OPENGL_ES2 #ifdef IMGUI_IMPL_OPENGL_ES2
#undef IMGUI_IMPL_OPENGL_ES2
#include <SDL_opengles2.h> #include <SDL_opengles2.h>
#else #else
#define IMGUI_IMPL_OPENGL_LOADER_CUSTOM #define IMGUI_IMPL_OPENGL_LOADER_CUSTOM

View file

@ -12,7 +12,7 @@ extern "C" {
extern void enable_puter(bool enable); extern void enable_puter(bool enable);
} }
#endif #endif
using namespace Looper::Options;
void MainLoop::Init() { void MainLoop::Init() {
#ifdef PORTALS #ifdef PORTALS
g_set_application_name("Looper"); g_set_application_name("Looper");
@ -35,15 +35,12 @@ void MainLoop::Init() {
{ {
Json::Value config; Json::Value config;
std::ifstream stream; std::ifstream stream;
stream.open(path(prefPath) / "config.json"); path jsonConfigPath = path(prefPath) / "config.json";
stream.open(jsonConfigPath);
if (stream.is_open()) { if (stream.is_open()) {
stream >> config; stream >> config;
if (config.isMember("theme_name")) { if (config.isMember("theme_name")) {
path themePath = theme->themeDir / config["theme_name"].asString(); init_option<std::string>("ui.imgui.theme", config["theme_name"].asString());
if (exists(themePath)) {
delete theme;
theme = new Theme(themePath);
}
} }
if (config.isMember("accent_color")) { if (config.isMember("accent_color")) {
if (config["accent_color"].isNumeric()) { if (config["accent_color"].isNumeric()) {
@ -52,26 +49,51 @@ void MainLoop::Init() {
Json::Value accentColor = config["accent_color"]; Json::Value accentColor = config["accent_color"];
accent_color = ImVec4(accentColor["h"].asFloat(), accentColor["s"].asFloat(), accentColor["v"].asFloat(), accentColor["a"].asFloat()); 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")) { 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")) { if (config.isMember("vsync")) {
vsync = config["vsync"].asBool(); init_option<bool>("ui.imgui.vsync", config["vsync"].asBool());
} }
if (config.isMember("framerate")) { if (config.isMember("framerate")) {
framerate = config["framerate"].asUInt(); init_option<int64_t>("ui.imgui.framerate", (int64_t)config["framerate"].asUInt());
} }
if (config.isMember("lang")) { if (config.isMember("lang")) {
Json::Value langValue; Json::Value langValue;
if (langValue.isNull()) { if (!langValue.isNull()) {
lang = DEFAULT_LANG; init_option<std::string>("ui.imgui.lang", config["lang"].asString());
} else {
lang = config["lang"].asString();
} }
SET_LANG(lang.c_str());
} }
stream.close(); 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)) { if (is_empty(Theme::themeDir)) {
path lightPath = Theme::themeDir / "light.toml"; path lightPath = Theme::themeDir / "light.toml";
@ -114,7 +136,9 @@ void MainLoop::FileLoaded() {
streams = playback->get_streams(); streams = playback->get_streams();
} }
void MainLoop::GuiFunction() { void MainLoop::GuiFunction() {
#if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
playback->LoopHook(); playback->LoopHook();
#endif
position = playback->GetPosition(); position = playback->GetPosition();
length = playback->GetLength(); length = playback->GetLength();
// Set the window title if the file changed, or playback stopped. // Set the window title if the file changed, or playback stopped.
@ -278,9 +302,7 @@ void MainLoop::GuiFunction() {
} }
ImGui::EndCombo(); ImGui::EndCombo();
} }
if (ImGui::Checkbox(_TR_CTX("Preference | VSync checkbox", "Enable VSync"), &vsync)) { ImGui::Checkbox(_TR_CTX("Preference | VSync checkbox", "Enable VSync"), &vsync);
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
}
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - ImGui::GetStyle().WindowPadding.x); 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")); 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()); 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 = ""; static string filter = "";
ImGui::TextUnformatted(_TR_CTX("Preference | Theme selector | Filter label", "Filter:")); ImGui::SameLine(); ImGui::TextUnformatted(_TR_CTX("Preference | Theme selector | Filter label", "Filter:")); ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - ImGui::GetStyle().WindowPadding.x); ImGui::SetNextItemWidth(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - ImGui::GetStyle().WindowPadding.x);
@ -429,32 +466,23 @@ void MainLoop::LoadFile(std::string file) {
void MainLoop::Deinit() { void MainLoop::Deinit() {
{ {
Json::Value config;
std::ofstream stream;
stream.open(path(prefPath) / "config.json");
path themePath(theme->file_path); path themePath(theme->file_path);
themePath = themePath.filename(); themePath = themePath.filename();
if (!themePath.empty()) { if (!themePath.empty()) {
config["theme_name"] = themePath.filename().string(); set_option<std::string>("ui.imgui.theme", themePath.filename().string());
} }
{ set_option<double>("ui.imgui.accent_color.h", accent_color.x);
Json::Value accentColor; set_option<double>("ui.imgui.accent_color.s", accent_color.y);
accentColor["h"] = accent_color.x; set_option<double>("ui.imgui.accent_color.v", accent_color.z);
accentColor["s"] = accent_color.y; set_option<double>("ui.imgui.accent_color.a", accent_color.w);
accentColor["v"] = accent_color.z; set_option<double>("ui.imgui.demo_window", show_demo_window);
accentColor["a"] = accent_color.w; set_option<bool>("ui.imgui.vsync", vsync);
config["accent_color"] = accentColor; set_option<int64_t>("ui.imgui.framerate", framerate);
}
config["demo_window"] = show_demo_window;
config["vsync"] = vsync;
config["framerate"] = framerate;
if (lang == DEFAULT_LANG) { if (lang == DEFAULT_LANG) {
config["lang"] = Json::Value::nullSingleton(); delete_option("ui.imgui.lang");
} else { } else {
config["lang"] = lang; set_option<std::string>("ui.imgui.lang", lang);
} }
stream << config;
stream.close();
} }
} }
MainLoop::MainLoop() : RendererBackend() { MainLoop::MainLoop() : RendererBackend() {

View file

@ -9,7 +9,6 @@
#include <fstream> #include <fstream>
#include <json/json.h> #include <json/json.h>
#include <stdio.h> #include <stdio.h>
#include <numbers>
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <string> #include <string>
@ -28,14 +27,13 @@
#include "IconFontCppHeaders/IconsForkAwesome.h" #include "IconFontCppHeaders/IconsForkAwesome.h"
#include "imgui/imgui.h" #include "imgui/imgui.h"
#include "imgui/misc/cpp/imgui_stdlib.h" #include "imgui/misc/cpp/imgui_stdlib.h"
#include "translation.h" #include <translation.hpp>
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
#include "emscripten_mainloop_stub.h" #include "emscripten_mainloop_stub.h"
#endif #endif
#include "../../../backend.hpp" #include "../../../backend.hpp"
#include "ui_backend.hpp" #include "ui_backend.hpp"
using namespace std::filesystem; using namespace std::filesystem;
using namespace std::numbers;
using std::string; using std::string;
#define IMGUI_FRONTEND #define IMGUI_FRONTEND
class MainLoop : public RendererBackend { class MainLoop : public RendererBackend {

View file

@ -2,10 +2,9 @@
#include "imgui.h" #include "imgui.h"
#include "json/value.h" #include "json/value.h"
#include "thirdparty/toml.hpp" #include "thirdparty/toml.hpp"
#include "translation.h" #include <translation.hpp>
#include <cmath> #include <cmath>
#include <exception> #include <exception>
#include <numbers>
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <filesystem> #include <filesystem>
@ -15,7 +14,6 @@
#include <log.hpp> #include <log.hpp>
using namespace std::filesystem; using namespace std::filesystem;
using namespace std::numbers;
const char* Theme::prefPath = NULL; const char* Theme::prefPath = NULL;
path Theme::themeDir = path(); path Theme::themeDir = path();
std::set<path> Theme::availableThemes = std::set<path>(); std::set<path> Theme::availableThemes = std::set<path>();

View file

@ -1 +0,0 @@
#include "translation.h"

View file

@ -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
View 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
View 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 "$@"

View 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)

View 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()

View 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()

View file

@ -159,8 +159,8 @@ DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string
connection.enterEventLoopAsync(); connection.enterEventLoopAsync();
} }
#endif #endif
const char *DBusAPI::objectPath = "/com/complecwaft/Looper"; const char *DBusAPI::objectPath = "/com/complecwaft/looper";
const char *DBusAPI::busName = "com.complecwaft.Looper"; const char *DBusAPI::busName = "com.complecwaft.looper";
DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) { DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) {
#ifdef DBUS_ENABLED #ifdef DBUS_ENABLED
auto connection = sdbus::createSessionBusConnection(busName); auto connection = sdbus::createSessionBusConnection(busName);

View file

@ -105,7 +105,7 @@ class MprisAPI : public sdbus::AdaptorInterfaces<org::mpris::MediaPlayer2_adapto
#endif #endif
class DBusAPI class DBusAPI
#ifdef DBUS_ENABLED #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 #endif
{ {
std::map<std::string, void*> handles; std::map<std::string, void*> handles;
@ -189,7 +189,7 @@ class DBusAPI
}; };
class DBusAPISender : public Playback class DBusAPISender : public Playback
#ifdef DBUS_ENABLED #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 #endif
{ {
// Cache // Cache

47
log.cpp
View file

@ -1,6 +1,9 @@
#include "log.hpp" #include "log.hpp"
#include <stdarg.h> #include <stdarg.h>
#include <string.h> #include <string.h>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace Looper::Log { namespace Looper::Log {
std::set<FILE*> LogStream::global_outputs; std::set<FILE*> LogStream::global_outputs;
int LogStream::log_level = 0; int LogStream::log_level = 0;
@ -14,6 +17,7 @@ namespace Looper::Log {
} }
return used_outputs; return used_outputs;
} }
std::string line;
void LogStream::writec(const char chr) { void LogStream::writec(const char chr) {
bool is_newline = (chr == '\n' || chr == '\r'); bool is_newline = (chr == '\n' || chr == '\r');
if (my_log_level < log_level) { if (my_log_level < log_level) {
@ -25,6 +29,15 @@ namespace Looper::Log {
stream->writec(chr); stream->writec(chr);
} }
} else { } 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(); std::set<FILE*> used_outputs = get_used_outputs();
for (auto &file : used_outputs) { for (auto &file : used_outputs) {
fwrite(&chr, 1, 1, file); fwrite(&chr, 1, 1, file);
@ -104,7 +117,7 @@ namespace Looper::Log {
vwritefln(fmt, args); vwritefln(fmt, args);
va_end(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), : names(names),
need_prefix(true), need_prefix(true),
my_log_level(log_level), 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::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); this->streams = std::set(streams);
} }
#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) LogStream::LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level)
: LogStream(names, log_level, false) #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); this->outputs = std::set(outputs);
#endif
} }
static LogStream *debug_stream; static LogStream *debug_stream;
static LogStream *info_stream; static LogStream *info_stream;
static LogStream *warning_stream; static LogStream *warning_stream;
static LogStream *error_stream; static LogStream *error_stream;
void init_logging() { 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); debug_stream = new LogStream({"DEBUG"}, {stderr}, -1);
info_stream = new LogStream({"INFO"}, {stdout}, 0); info_stream = new LogStream({"INFO"}, {stdout}, 0);
warning_stream = new LogStream({"WARNING"}, {stderr}, 1); warning_stream = new LogStream({"WARNING"}, {stderr}, 1);
error_stream = new LogStream({"ERROR"}, {stderr}, 2); error_stream = new LogStream({"ERROR"}, {stderr}, 2);
#endif
} }
LogStream &get_log_stream_by_level(int level) { LogStream &get_log_stream_by_level(int level) {
switch (level) { switch (level) {

17
log.hpp
View file

@ -4,6 +4,10 @@
#include <ostream> #include <ostream>
#include <set> #include <set>
#include <vector> #include <vector>
#include <variant>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace Looper::Log { namespace Looper::Log {
struct LogStream { struct LogStream {
std::set<FILE *> outputs; std::set<FILE *> outputs;
@ -14,7 +18,13 @@ namespace Looper::Log {
bool need_prefix; bool need_prefix;
std::vector<std::string> names; std::vector<std::string> names;
std::set<FILE*> get_used_outputs(); 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: public:
static int log_level; static int log_level;
void writeprefix(); void writeprefix();
@ -30,7 +40,12 @@ namespace Looper::Log {
void vwritefln(const char *fmt, va_list args); void vwritefln(const char *fmt, va_list args);
void writefln(const char *fmt, ...); void writefln(const char *fmt, ...);
LogStream(std::initializer_list<std::string> names, std::initializer_list<LogStream*> streams, int log_level = 0); 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); LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level = 0);
#endif
}; };
void init_logging(); void init_logging();
LogStream &get_log_stream_by_level(int level); LogStream &get_log_stream_by_level(int level);

View file

@ -19,7 +19,36 @@ std::unordered_set<LicenseData> license_data;
std::unordered_set<LicenseData> &get_license_data() { std::unordered_set<LicenseData> &get_license_data() {
return 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) { int main(int argc, char **argv) {
#ifdef __ANDROID__
env = (JNIEnv*)SDL_AndroidGetJNIEnv();
initNative();
#endif
std::vector<std::string> args; std::vector<std::string> args;
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
args.push_back(std::string(argv[i])); args.push_back(std::string(argv[i]));

View file

@ -1,6 +1,9 @@
#include "thirdparty/toml.hpp" #include "thirdparty/toml.hpp"
#include "util.hpp" #include "util.hpp"
#include <filesystem> #include <filesystem>
#ifdef __ANDROID__
#include <SDL.h>
#endif
using namespace std::filesystem; using namespace std::filesystem;
namespace Looper::Options { namespace Looper::Options {
toml::table *options; toml::table *options;

View file

@ -5,6 +5,7 @@
#include <vector> #include <vector>
#include <typeinfo> #include <typeinfo>
#include <typeindex> #include <typeindex>
#include <optional>
#define OPTIONS (*Looper::Options::options) #define OPTIONS (*Looper::Options::options)
namespace Looper::Options { namespace Looper::Options {
extern toml::table *options; extern toml::table *options;
@ -46,6 +47,37 @@ namespace Looper::Options {
return false; 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> template<class T>
void set_option(std::string name, T value) { void set_option(std::string name, T value) {
DEBUG.writefln("Setting option '%s'...", name.c_str()); DEBUG.writefln("Setting option '%s'...", name.c_str());

View file

@ -17,7 +17,7 @@ extern "C" {
#include "log.hpp" #include "log.hpp"
#include <filesystem> #include <filesystem>
#include "dbus.hpp" #include "dbus.hpp"
#include <format> #include <string.h>
#include "util.hpp" #include "util.hpp"
using namespace std::chrono; using namespace std::chrono;
@ -102,6 +102,9 @@ void PlaybackInstance::SDLCallbackInner(Uint8 *stream, int len) {
#endif #endif
if (samples > new_samples) { if (samples > new_samples) {
reset_vgmstream(this->stream); reset_vgmstream(this->stream);
position = 0.0;
} else {
position += samples / this->stream->sample_rate;
} }
samples = new_samples; samples = new_samples;
for (int i = 0; i < new_bufsize / sizeof(SAMPLETYPE); i++) { 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); 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) { void PlaybackInstance::SDLCallback(void *userdata, Uint8 *stream, int len) {
((PlaybackInstance*)userdata)->SDLCallbackInner(stream, len); ((PlaybackInstance*)userdata)->SDLCallbackInner(stream, len);
} }
@ -215,7 +227,14 @@ VGMSTREAM *PlaybackInstance::LoadVgm(const char *file, int idx) {
buf = strdup("Unknown"); buf = strdup("Unknown");
} }
if (i == 0) { 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 { } else {
stream.name = buf; stream.name = buf;
} }
@ -333,6 +352,7 @@ void PlaybackInstance::InitLoopFunction() {
desired.userdata = this; desired.userdata = this;
st = new SoundTouch(); 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); 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) { 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()); ERROR.writefln("Error opening audio device: '%s'", SDL_GetError());
set_error("Failed to open audio device!"); set_error("Failed to open audio device!");
@ -340,6 +360,56 @@ void PlaybackInstance::InitLoopFunction() {
loop_started = false; loop_started = false;
return; 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; spec = obtained;
st->setSampleRate(spec.freq); st->setSampleRate(spec.freq);
st->setChannels(spec.channels); st->setChannels(spec.channels);
@ -377,6 +447,7 @@ void PlaybackInstance::InitLoopFunction() {
} else { } else {
playback_ready.store(false); playback_ready.store(false);
} }
load_finished.store(true);
set_signal(PlaybackSignalStarted); set_signal(PlaybackSignalStarted);
} }
void PlaybackInstance::LoopFunction() { void PlaybackInstance::LoopFunction() {
@ -400,8 +471,11 @@ void PlaybackInstance::LoopFunction() {
} }
} }
if (stream_changed.exchange(false)) { if (stream_changed.exchange(false)) {
current_file_mutex.lock();
if (current_file.has_value()) {
std::string file = current_file.value(); std::string file = current_file.value();
if (streams[current_stream].name == "" || streams[current_stream].length <= 0 || current_stream < 0 || current_stream >= streams.size()) { if (current_stream >= streams.size() || current_stream < 0 ||
streams[current_stream].name == "" || streams[current_stream].length <= 0) {
if (stream != nullptr) { if (stream != nullptr) {
current_stream = stream->stream_index; current_stream = stream->stream_index;
} else { } else {
@ -422,12 +496,13 @@ void PlaybackInstance::LoopFunction() {
playback_ready.store(false); playback_ready.store(false);
} }
} }
current_file_mutex.unlock();
}
if (flag_mutex.try_lock()) { if (flag_mutex.try_lock()) {
if (seeking.exchange(false)) { if (seeking.exchange(false)) {
if (stream != nullptr) { if (stream != nullptr) {
SDL_LockAudioDevice(device); SDL_LockAudioDevice(device);
seek_vgmstream(stream, (int32_t)((double)stream->sample_rate * position)); seek_vgmstream(stream, (int32_t)((double)stream->sample_rate * position));
st->flush(); st->flush();
SDL_UnlockAudioDevice(device); SDL_UnlockAudioDevice(device);
} else { } else {
@ -486,13 +561,7 @@ void PlaybackInstance::LoopFunction() {
} }
flag_mutex.unlock(); flag_mutex.unlock();
} }
if (stream != nullptr) { if (music != 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) {
position = Mix_GetMusicPosition(music); position = Mix_GetMusicPosition(music);
} }
@ -506,7 +575,15 @@ void PlaybackInstance::DeinitLoopFunction() {
if (stream != nullptr) { if (stream != nullptr) {
UnloadVgm(stream); UnloadVgm(stream);
} }
#ifndef __ANDROID__
SDL_CloseAudioDevice(device); SDL_CloseAudioDevice(device);
#else
if (ostream && ostream->getState() != oboe::StreamState::Closed) {
ostream->stop();
ostream->close();
}
ostream.reset();
#endif
Mix_CloseAudio(); Mix_CloseAudio();
Mix_Quit(); Mix_Quit();
SDL_QuitSubSystem(SDL_INIT_AUDIO); SDL_QuitSubSystem(SDL_INIT_AUDIO);
@ -566,10 +643,11 @@ void PlaybackInstance::Load(std::string filePath) {
if (running.exchange(true)) { if (running.exchange(true)) {
load_requested.store(true); load_requested.store(true);
} else { } else {
#ifdef __EMSCRIPTEN__ #if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
start_loop(); start_loop();
#else #else
thread = std::thread(&PlaybackInstance::ThreadFunc, this); thread = std::thread(&PlaybackInstance::ThreadFunc, this);
loop_started = true;
#endif #endif
} }
flag_mutex.lock(); flag_mutex.lock();
@ -582,7 +660,7 @@ void PlaybackInstance::Load(std::string filePath) {
void PlaybackInstance::Start(std::string filePath, int streamIdx) { void PlaybackInstance::Start(std::string filePath, int streamIdx) {
Load(filePath); Load(filePath);
while (loop_started && !load_finished.exchange(false)) { while (loop_started && !load_finished.exchange(false)) {
#ifdef __EMSCRIPTEN__ #if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
LoopHook(); LoopHook();
#endif #endif
std::this_thread::sleep_for(20ms); std::this_thread::sleep_for(20ms);
@ -637,7 +715,7 @@ bool PlaybackInstance::IsPaused() {
void PlaybackInstance::Stop() { void PlaybackInstance::Stop() {
if (running.exchange(false)) { if (running.exchange(false)) {
#ifdef __EMSCRIPTEN__ #if defined(__EMSCRIPTEN__)||defined(__ANDROID__)
stop_loop(); stop_loop();
#else #else
thread.join(); thread.join();

View file

@ -3,6 +3,9 @@
extern "C" { extern "C" {
#include <vgmstream.h> #include <vgmstream.h>
} }
#ifdef __ANDROID__
#include <oboe/Oboe.h>
#endif
#include <thread> #include <thread>
#include <SDL.h> #include <SDL.h>
#include <SDL_audio.h> #include <SDL_audio.h>
@ -212,8 +215,21 @@ class Playback {
static Playback *Create(bool *daemon_found, bool daemon = false); static Playback *Create(bool *daemon_found, bool daemon = false);
}; };
class DBusAPISender; class DBusAPISender;
class PlaybackInstance : public Playback { class PlaybackInstance : public Playback
#ifdef __ANDROID__
, public oboe::AudioStreamDataCallback
#endif
{
private: 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::string filePath;
std::atomic_bool running; std::atomic_bool running;
std::atomic_bool file_changed; std::atomic_bool file_changed;

3
sdl-android-project/.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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'
}

View 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 *;
#}

View 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>

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}
}
}
}

View file

@ -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

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

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

View 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>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Looper</string>
</resources>

View file

@ -0,0 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View 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
}

View 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

Binary file not shown.

View 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
View 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
View 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

View 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

View file

@ -0,0 +1 @@
include ':app'

13
setup-android-project.ps1 Normal file
View 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
View 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

@ -0,0 +1 @@
Subproject commit f461d91cd265d7b9a44b4d472b1df0c0ad2855a0

1
subprojects/SDL_image Submodule

@ -0,0 +1 @@
Subproject commit abcf63aa71b4e3ac32120fa9870a6500ddcdcc89

@ -0,0 +1 @@
Subproject commit 44035a0c3fe20ef28f071b35b9f5a653fbfe5e6d

1
subprojects/oboe Submodule

@ -0,0 +1 @@
Subproject commit 86165b8249bc22b9ef70b69e20323244b6f08d88

62
translation.cpp Normal file
View 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
View 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)

View file

@ -1,4 +1,7 @@
#include "util.hpp" #include "util.hpp"
#ifdef __ANDROID__
#include <SDL.h>
#endif
std::string PadZeros(std::string input, size_t required_length) { std::string PadZeros(std::string input, size_t required_length) {
return std::string(required_length - std::min(required_length, input.length()), '0') + input; 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 #endif
std::string get_prefs_path() { std::string get_prefs_path() {
std::string path; std::string path;
#ifdef _WIN32 #ifdef __ANDROID__
path = SDL_AndroidGetInternalStoragePath();
#elif defined(_WIN32)
PWSTR str; PWSTR str;
if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &str) != S_OK) { if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &str) != S_OK) {
CoTaskMemFree(str); CoTaskMemFree(str);