mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
Android Build (#6992)
* android build support * Update es_compatibility.h * Support reporting flags in android * include pref_path in json v2 * add vulkan headers * shaderc includes * remove headers * force use prebuilt vulkan * fix vulkan inverted screen * fix find sdl3 precompiled android * Update CMakeLists.txt * replace sdl2 android function names for sdl3 versions * change sdl includes in osapi * fix sdl3 includes, again * rename function to sdl3 name * remove unused definition * copy libshaderc_shared.so to output in android * do not look for vulkan-loader in android * revert changes to SDL2.cmake * add comment to vulkan pretransform setup * cleanup maxsize function * rename new android functions in osapi * android trap back button * Remove calculate_max_resize_max_size * touch ui opacity control * change opacity control to int * fix opacity slider * Corrected android working folder fallback and set borderless mode as default * Implement shared helper for JNI calls * Add version tag and sha256 to android prebuilt libs * vulkan android check eSuboptimal * An already requested rebuild also avoids redundant Android surface queries * add cache bool to cmakelist * set string ids
This commit is contained in:
1 parent
705d115214
commit
1d0dd687cd
24 files changed
+498
-107
No files matched your search
@@ -185,7 +185,12 @@ int cfile_init(const char *exe_dir, const char *cdrom_dir)
|
||||
|
||||
char buf[CFILE_ROOT_DIRECTORY_LEN];
|
||||
|
||||
strncpy(buf, exe_dir, CFILE_ROOT_DIRECTORY_LEN - 1);
|
||||
#ifndef __ANDROID__
|
||||
strncpy(buf, exe_dir, CFILE_ROOT_DIRECTORY_LEN - 1);
|
||||
#else
|
||||
(void)exe_dir;
|
||||
strncpy(buf, os_android_get_working_folder_path().c_str(), CFILE_ROOT_DIRECTORY_LEN - 1);
|
||||
#endif
|
||||
|
||||
buf[CFILE_ROOT_DIRECTORY_LEN - 1] = '\0';
|
||||
|
||||
|
||||
+85
-50
@@ -75,6 +75,7 @@ const char *cmdline_arg_types[] =
|
||||
enum class flag_output_type {
|
||||
Binary,
|
||||
Json_V1,
|
||||
Json_V2,
|
||||
};
|
||||
|
||||
// variables
|
||||
@@ -555,7 +556,12 @@ int Cmdline_show_pos = 0;
|
||||
int Cmdline_show_stats = 0;
|
||||
int Cmdline_save_render_targets = 0;
|
||||
bool Cmdline_window = false;
|
||||
#ifdef __ANDROID__
|
||||
// Default to borderless in the legacy window-mode path as well.
|
||||
bool Cmdline_fullscreen_window = true;
|
||||
#else
|
||||
bool Cmdline_fullscreen_window = false;
|
||||
#endif
|
||||
std::optional<std::pair<uint16_t, uint16_t>>Cmdline_window_res = std::nullopt;
|
||||
char *Cmdline_res = 0;
|
||||
char *Cmdline_center_res = 0;
|
||||
@@ -1358,51 +1364,58 @@ static void handle_unix_modlist(char **modlist, size_t *len)
|
||||
|
||||
// external entry point into this modules
|
||||
|
||||
static void json_add_version(json_t* root) {
|
||||
auto version_obj = json_object();
|
||||
|
||||
json_object_set_new(version_obj, "full", json_string(FS_VERSION_FULL));
|
||||
json_object_set_new(version_obj, "major", json_integer(FS_VERSION_MAJOR));
|
||||
json_object_set_new(version_obj, "minor", json_integer(FS_VERSION_MINOR));
|
||||
json_object_set_new(version_obj, "build", json_integer(FS_VERSION_BUILD));
|
||||
|
||||
json_object_set_new(version_obj, "has_revision", json_boolean(FS_VERSION_HAS_REVIS));
|
||||
json_object_set_new(version_obj, "revision", json_integer(FS_VERSION_REVIS));
|
||||
json_object_set_new(version_obj, "revision_str", json_string(FS_VERSION_REVIS_STR));
|
||||
|
||||
json_object_set_new(root, "version", version_obj);
|
||||
}
|
||||
|
||||
static void json_add_easy_flags(json_t* root) {
|
||||
auto easy_array = json_array();
|
||||
|
||||
for (auto& easy_flag : easy_flags) {
|
||||
json_array_append_new(easy_array, json_string(easy_flag.name));
|
||||
}
|
||||
|
||||
json_object_set_new(root, "easy_flags", easy_array);
|
||||
}
|
||||
|
||||
static void json_add_flags(json_t* root) {
|
||||
auto flags_array = json_array();
|
||||
|
||||
for (auto& flag : exe_params) {
|
||||
auto flag_obj = json_object();
|
||||
|
||||
json_object_set_new(flag_obj, "name", json_string(flag.name));
|
||||
json_object_set_new(flag_obj, "description", json_string(flag.desc));
|
||||
json_object_set_new(flag_obj, "fso_only", json_boolean(flag.fso_only));
|
||||
json_object_set_new(flag_obj, "on_flags", json_integer(flag.on_flags));
|
||||
json_object_set_new(flag_obj, "off_flags", json_integer(flag.off_flags));
|
||||
json_object_set_new(flag_obj, "type", json_string(flag.type));
|
||||
json_object_set_new(flag_obj, "web_url", json_string(flag.web_url));
|
||||
|
||||
json_array_append_new(flags_array, flag_obj);
|
||||
}
|
||||
|
||||
json_object_set_new(root, "flags", flags_array);
|
||||
}
|
||||
|
||||
static json_t* json_get_v1() {
|
||||
auto root = json_object();
|
||||
|
||||
{
|
||||
auto version_obj = json_object();
|
||||
json_add_version(root);
|
||||
json_add_easy_flags(root);
|
||||
json_add_flags(root);
|
||||
|
||||
json_object_set_new(version_obj, "full", json_string(FS_VERSION_FULL));
|
||||
json_object_set_new(version_obj, "major", json_integer(FS_VERSION_MAJOR));
|
||||
json_object_set_new(version_obj, "minor", json_integer(FS_VERSION_MINOR));
|
||||
json_object_set_new(version_obj, "build", json_integer(FS_VERSION_BUILD));
|
||||
|
||||
json_object_set_new(version_obj, "has_revision", json_boolean(FS_VERSION_HAS_REVIS));
|
||||
json_object_set_new(version_obj, "revision", json_integer(FS_VERSION_REVIS));
|
||||
json_object_set_new(version_obj, "revision_str", json_string(FS_VERSION_REVIS_STR));
|
||||
|
||||
json_object_set_new(root, "version", version_obj);
|
||||
}
|
||||
{
|
||||
auto easy_array = json_array();
|
||||
|
||||
for (auto& easy_flag : easy_flags) {
|
||||
json_array_append_new(easy_array, json_string(easy_flag.name));
|
||||
}
|
||||
|
||||
json_object_set_new(root, "easy_flags", easy_array);
|
||||
}
|
||||
{
|
||||
auto flags_array = json_array();
|
||||
|
||||
for (auto& flag : exe_params) {
|
||||
auto flag_obj = json_object();
|
||||
|
||||
json_object_set_new(flag_obj, "name", json_string(flag.name));
|
||||
json_object_set_new(flag_obj, "description", json_string(flag.desc));
|
||||
json_object_set_new(flag_obj, "fso_only", json_boolean(flag.fso_only));
|
||||
json_object_set_new(flag_obj, "on_flags", json_integer(flag.on_flags));
|
||||
json_object_set_new(flag_obj, "off_flags", json_integer(flag.off_flags));
|
||||
json_object_set_new(flag_obj, "type", json_string(flag.type));
|
||||
json_object_set_new(flag_obj, "web_url", json_string(flag.web_url));
|
||||
|
||||
json_array_append_new(flags_array, flag_obj);
|
||||
}
|
||||
|
||||
json_object_set_new(root, "flags", flags_array);
|
||||
}
|
||||
{
|
||||
auto caps_array = json_array();
|
||||
|
||||
@@ -1513,6 +1526,19 @@ static json_t* json_get_v1() {
|
||||
return root;
|
||||
}
|
||||
|
||||
// json_v2 reports version, supported flags, and the preferences path for launchers,
|
||||
// without querying graphics, audio, or joystick devices.
|
||||
static json_t* json_get_v2() {
|
||||
auto root = json_object();
|
||||
|
||||
json_add_version(root);
|
||||
json_add_easy_flags(root);
|
||||
json_add_flags(root);
|
||||
json_object_set_new(root, "pref_path", json_string(os_get_config_path().c_str()));
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
static void write_flags_file() {
|
||||
FILE *fp = fopen("flags.lch","w");
|
||||
|
||||
@@ -1570,6 +1596,8 @@ static flag_output_type get_flags_output_type() {
|
||||
return flag_output_type::Binary;
|
||||
} else if (type == "json_v1") {
|
||||
return flag_output_type::Json_V1;
|
||||
} else if (type == "json_v2") {
|
||||
return flag_output_type::Json_V2;
|
||||
} else {
|
||||
// This is supposed to make it easy for the launcher to recognize an unsupported type
|
||||
printf("OUTPUT TYPE NOT SUPPORTED!\n");
|
||||
@@ -1580,17 +1608,23 @@ static flag_output_type get_flags_output_type() {
|
||||
|
||||
static void write_flags() {
|
||||
auto type = get_flags_output_type();
|
||||
switch(type) {
|
||||
case flag_output_type::Binary:
|
||||
write_flags_file();
|
||||
break;
|
||||
case flag_output_type::Json_V1:
|
||||
json_t* root = json_get_v1();
|
||||
|
||||
json_dumpf(root, stdout, JSON_INDENT(4));
|
||||
json_decref(root);
|
||||
break;
|
||||
if (type == flag_output_type::Binary) {
|
||||
write_flags_file();
|
||||
return;
|
||||
}
|
||||
|
||||
json_t* root = (type == flag_output_type::Json_V1) ? json_get_v1() : json_get_v2();
|
||||
|
||||
#ifdef __ANDROID__
|
||||
char* dumped = json_dumps(root, JSON_INDENT(4));
|
||||
os_android_set_flags_string(dumped ? dumped : "");
|
||||
free(dumped);
|
||||
#else
|
||||
json_dumpf(root, stdout, JSON_INDENT(4));
|
||||
#endif
|
||||
|
||||
json_decref(root);
|
||||
}
|
||||
|
||||
bool SetCmdlineParams()
|
||||
@@ -1787,6 +1821,7 @@ bool SetCmdlineParams()
|
||||
options::OptionsManager::instance()->setOverride("Graphics.WindowMode",
|
||||
std::unique_ptr<json_t>(json_integer(static_cast<int>(os::ViewportState::Windowed))), "-window");
|
||||
Cmdline_window = true;
|
||||
Cmdline_fullscreen_window = false;
|
||||
}
|
||||
|
||||
if ( fullscreen_window_arg.found( ) )
|
||||
|
||||
@@ -28,6 +28,23 @@ protected:
|
||||
return FALSE;
|
||||
|
||||
m_library = SDL_LoadObject(externlib);
|
||||
|
||||
#ifdef __ANDROID__
|
||||
if (m_library == NULL) {
|
||||
auto android_path = SDL_GetAndroidInternalStoragePath();
|
||||
if (android_path != nullptr) {
|
||||
SCP_string android_full_path = android_path;
|
||||
if (android_full_path.back() != DIR_SEPARATOR_CHAR) {
|
||||
android_full_path += DIR_SEPARATOR_CHAR;
|
||||
}
|
||||
android_full_path += SCP_string("natives") + DIR_SEPARATOR_CHAR + externlib;
|
||||
#ifndef NDEBUG
|
||||
mprintf(("Calling SDL_LoadObject with the following path: '%s'\n", android_full_path.c_str()));
|
||||
#endif
|
||||
m_library = SDL_LoadObject(android_full_path.c_str());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// check full path as a fallback for our own libraries
|
||||
// NOTE: basePath is assumed to have a trailing slash!
|
||||
|
||||
@@ -289,7 +289,11 @@ GraphicsAPI gr_get_configured_render_api()
|
||||
|
||||
#endif
|
||||
|
||||
#ifndef __ANDROID__
|
||||
os::ViewportState Gr_configured_window_state = os::ViewportState::Fullscreen;
|
||||
#else
|
||||
os::ViewportState Gr_configured_window_state = os::ViewportState::Borderless;
|
||||
#endif
|
||||
|
||||
static bool mode_change_func(os::ViewportState state, bool initial)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <direct.h>
|
||||
#endif
|
||||
|
||||
#if !defined __APPLE_CC__ && defined SCP_UNIX
|
||||
#if !defined __APPLE_CC__ && !defined __ANDROID__ && defined SCP_UNIX
|
||||
#include<glad/glad_glx.h>
|
||||
//Required because X defines none and always, which is used later
|
||||
#undef None
|
||||
@@ -1394,7 +1394,7 @@ bool gr_opengl_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps)
|
||||
Error(LOCATION, "Failed to load OpenGL!");
|
||||
}
|
||||
|
||||
#if !defined __APPLE_CC__ && defined SCP_UNIX
|
||||
#if !defined __APPLE_CC__ && !defined __ANDROID__ && defined SCP_UNIX
|
||||
if (!gladLoadGLXLoader(GL_context->getLoaderFunction(), nullptr, 0)) {
|
||||
Error(LOCATION, "Failed to load GLX!");
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@ class VulkanRenderer {
|
||||
void acquireNextSwapChainImage();
|
||||
|
||||
bool recreateSwapChain();
|
||||
bool shouldRecreateSwapChain(SwapChainStatus status) const;
|
||||
|
||||
void createImGuiDescriptorPool();
|
||||
void initImGui();
|
||||
@@ -480,6 +481,7 @@ class VulkanRenderer {
|
||||
bool m_hdrActive = false; // True when an HDR10 (PQ/BT.2020) swap chain was negotiated
|
||||
bool m_hdrMetadataSupported = false; // VK_EXT_hdr_metadata device extension enabled
|
||||
vk::Extent2D m_swapChainExtent;
|
||||
vk::SurfaceTransformFlagBitsKHR m_swapChainPreTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity;
|
||||
// The resolution the engine draws at (gr_screen.max_w/max_h), which is not
|
||||
// always the window size: when Cmdline_window_res is set,
|
||||
// SDLGraphicsOperations::createViewport sizes the window from that instead,
|
||||
|
||||
@@ -81,7 +81,7 @@ void VulkanRenderer::acquireNextSwapChainImage()
|
||||
MAX_ACQUIRE_ATTEMPTS);
|
||||
}
|
||||
|
||||
if (status == SwapChainStatus::eSuboptimal) {
|
||||
if (shouldRecreateSwapChain(status)) {
|
||||
m_swapChainNeedsRecreation = true;
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ void VulkanRenderer::flip()
|
||||
auto presentStatus = m_frames[m_currentFrame]->submitAndPresent(m_currentCommandBuffers,
|
||||
m_renderingFinishedSemaphores[m_currentSwapChainImage].get());
|
||||
|
||||
if (presentStatus == SwapChainStatus::eSuboptimal || presentStatus == SwapChainStatus::eOutOfDate) {
|
||||
if (shouldRecreateSwapChain(presentStatus)) {
|
||||
m_swapChainNeedsRecreation = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -326,17 +326,17 @@ vk::Extent2D windowExtent()
|
||||
return {static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h)};
|
||||
}
|
||||
|
||||
vk::Extent2D chooseSwapChainExtent(const PhysicalDeviceValues& values, uint32_t width, uint32_t height)
|
||||
vk::Extent2D chooseSwapChainExtent(const vk::SurfaceCapabilitiesKHR& capabilities, uint32_t width, uint32_t height)
|
||||
{
|
||||
if (values.surfaceCapabilities.currentExtent.width != UINT32_MAX) {
|
||||
return values.surfaceCapabilities.currentExtent;
|
||||
if (capabilities.currentExtent.width != UINT32_MAX) {
|
||||
return capabilities.currentExtent;
|
||||
} else {
|
||||
VkExtent2D actualExtent = {width, height};
|
||||
|
||||
actualExtent.width = std::max(values.surfaceCapabilities.minImageExtent.width,
|
||||
std::min(values.surfaceCapabilities.maxImageExtent.width, actualExtent.width));
|
||||
actualExtent.height = std::max(values.surfaceCapabilities.minImageExtent.height,
|
||||
std::min(values.surfaceCapabilities.maxImageExtent.height, actualExtent.height));
|
||||
actualExtent.width = std::max(capabilities.minImageExtent.width,
|
||||
std::min(capabilities.maxImageExtent.width, actualExtent.width));
|
||||
actualExtent.height = std::max(capabilities.minImageExtent.height,
|
||||
std::min(capabilities.maxImageExtent.height, actualExtent.height));
|
||||
|
||||
return actualExtent;
|
||||
}
|
||||
@@ -1113,7 +1113,7 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v
|
||||
createInfo.minImageCount = imageCount;
|
||||
createInfo.imageFormat = surfaceFormat.format;
|
||||
createInfo.imageColorSpace = surfaceFormat.colorSpace;
|
||||
createInfo.imageExtent = chooseSwapChainExtent(deviceValues, window.width, window.height);
|
||||
createInfo.imageExtent = chooseSwapChainExtent(deviceValues.surfaceCapabilities, window.width, window.height);
|
||||
createInfo.imageArrayLayers = 1;
|
||||
createInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment
|
||||
| vk::ImageUsageFlagBits::eTransferSrc
|
||||
@@ -1128,7 +1128,19 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v
|
||||
createInfo.imageSharingMode = vk::SharingMode::eExclusive;
|
||||
}
|
||||
|
||||
createInfo.preTransform = deviceValues.surfaceCapabilities.currentTransform;
|
||||
// preTransform describes the transform already applied to the rendered content.
|
||||
// FSO does not pre-rotate it, so request identity whenever supported. If this
|
||||
// differs from currentTransform (e.g. landscape on an Android phone), the
|
||||
// presentation engine compensates during presentation. Passing currentTransform
|
||||
// instead would claim that FSO had already performed that rotation.
|
||||
auto supported = deviceValues.surfaceCapabilities.supportedTransforms;
|
||||
if (supported & vk::SurfaceTransformFlagBitsKHR::eIdentity) {
|
||||
createInfo.preTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity;
|
||||
}
|
||||
else {
|
||||
createInfo.preTransform = deviceValues.surfaceCapabilities.currentTransform;
|
||||
}
|
||||
|
||||
createInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque;
|
||||
createInfo.presentMode = choosePresentMode(deviceValues);
|
||||
createInfo.clipped = true;
|
||||
@@ -1149,6 +1161,7 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v
|
||||
m_hdrActive = (surfaceFormat.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT);
|
||||
Gr_hdr_output_active = m_hdrActive;
|
||||
m_swapChainExtent = createInfo.imageExtent;
|
||||
m_swapChainPreTransform = createInfo.preTransform;
|
||||
|
||||
// Without -window_res the render resolution simply is the window, so follow
|
||||
// gr_screen (which gr_screen_resize() has already updated for this
|
||||
@@ -1210,6 +1223,42 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::shouldRecreateSwapChain(SwapChainStatus status) const
|
||||
{
|
||||
// An already requested rebuild also avoids redundant Android surface queries.
|
||||
if (m_swapChainNeedsRecreation || status == SwapChainStatus::eOutOfDate) {
|
||||
return true;
|
||||
}
|
||||
if (status != SwapChainStatus::eSuboptimal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
if (m_swapChainPreTransform == vk::SurfaceTransformFlagBitsKHR::eIdentity) {
|
||||
vk::SurfaceCapabilitiesKHR capabilities;
|
||||
try {
|
||||
capabilities = m_physicalDevice.getSurfaceCapabilitiesKHR(m_vkSurface.get());
|
||||
} catch (const vk::SystemError&) {
|
||||
// Let the normal recreation path handle a failed surface query.
|
||||
return true;
|
||||
}
|
||||
if (capabilities.currentTransform != m_swapChainPreTransform &&
|
||||
(capabilities.supportedTransforms & m_swapChainPreTransform)) {
|
||||
const auto window = windowExtent();
|
||||
const auto extent = chooseSwapChainExtent(capabilities, window.width, window.height);
|
||||
if (extent == m_swapChainExtent) {
|
||||
// Android can report SUBOPTIMAL on every present while the compositor
|
||||
// compensates for our identity preTransform. Rebuilding with the same
|
||||
// extent and preTransform would just repeat that condition next frame.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanRenderer::recreateSwapChain()
|
||||
{
|
||||
nprintf(("vulkan", "Vulkan: Recreating swap chain...\n"));
|
||||
@@ -1233,7 +1282,7 @@ bool VulkanRenderer::recreateSwapChain()
|
||||
|
||||
// Check for 0x0 extent (minimized window) — caller should retry later
|
||||
const vk::Extent2D window = windowExtent();
|
||||
auto extent = chooseSwapChainExtent(freshValues, window.width, window.height);
|
||||
auto extent = chooseSwapChainExtent(freshValues.surfaceCapabilities, window.width, window.height);
|
||||
if (extent.width == 0 || extent.height == 0) {
|
||||
nprintf(("vulkan", "Vulkan: Surface extent is 0x0 (minimized), deferring swap chain recreation\n"));
|
||||
return false;
|
||||
|
||||
@@ -65,7 +65,7 @@ bool *Lcl_unexpected_tstring_check = nullptr;
|
||||
// NOTE: with map storage of XSTR strings, the indexes no longer need to be contiguous,
|
||||
// but internal strings should still increment XSTR_SIZE to avoid collisions.
|
||||
// retail XSTR_SIZE = 1570
|
||||
// #define XSTR_SIZE 1936 // This is the next available ID
|
||||
// #define XSTR_SIZE 1940 // This is the next available ID
|
||||
|
||||
// struct to allow for strings.tbl-determined x offset
|
||||
// offset is 0 for english, by default
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <jni.h>
|
||||
#include "options/Option.h"
|
||||
#include "utils/finally.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
const char* ORGANIZATION_NAME = "HardLightProductions";
|
||||
@@ -925,3 +933,175 @@ SCP_string os_get_config_path(const SCP_string& subpath)
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
/*
|
||||
Special functions for Android
|
||||
*/
|
||||
|
||||
#ifdef __ANDROID__
|
||||
static bool android_check_exception(JNIEnv* env, const char* operation)
|
||||
{
|
||||
if (!env->ExceptionCheck()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mprintf(("OSAPI: JNI exception in %s.\n", operation));
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
static void android_with_static_method(const char* name, const char* signature, Callback&& callback)
|
||||
{
|
||||
JNIEnv* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
mprintf(("OSAPI: Couldn't get JNI environment for %s.\n", name));
|
||||
return;
|
||||
}
|
||||
if (android_check_exception(env, name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (env->PushLocalFrame(4) < 0) {
|
||||
android_check_exception(env, "PushLocalFrame");
|
||||
mprintf(("OSAPI: Couldn't create JNI local frame for %s.\n", name));
|
||||
return;
|
||||
}
|
||||
// Release the activity, class, and temporary strings on every exit path.
|
||||
auto local_frame = util::finally([env]() { env->PopLocalFrame(nullptr); });
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (android_check_exception(env, "SDL_GetAndroidActivity") || activity == nullptr) {
|
||||
mprintf(("OSAPI: Couldn't get Android activity for %s.\n", name));
|
||||
return;
|
||||
}
|
||||
|
||||
jclass activity_class = env->GetObjectClass(activity);
|
||||
if (android_check_exception(env, "GetObjectClass") || activity_class == nullptr) {
|
||||
mprintf(("OSAPI: Couldn't get Android activity class for %s.\n", name));
|
||||
return;
|
||||
}
|
||||
|
||||
jmethodID method = env->GetStaticMethodID(activity_class, name, signature);
|
||||
if (android_check_exception(env, name) || method == nullptr) {
|
||||
mprintf(("OSAPI: Couldn't find static method %s with signature %s.\n", name, signature));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(env, activity_class, method);
|
||||
android_check_exception(env, name);
|
||||
}
|
||||
|
||||
void os_android_set_flags_string(const char* json)
|
||||
{
|
||||
android_with_static_method("setFlagsJson", "(Ljava/lang/String;)V",
|
||||
[json](JNIEnv* env, jclass activity_class, jmethodID method) {
|
||||
jstring value = env->NewStringUTF(json ? json : "");
|
||||
if (android_check_exception(env, "NewStringUTF") || value == nullptr) {
|
||||
return;
|
||||
}
|
||||
env->CallStaticVoidMethod(activity_class, method, value);
|
||||
});
|
||||
}
|
||||
|
||||
SCP_string os_android_get_working_folder_path()
|
||||
{
|
||||
SCP_string wfp{};
|
||||
android_with_static_method("getWorkingFolder", "()Ljava/lang/String;",
|
||||
[&wfp](JNIEnv* env, jclass activity_class, jmethodID method) {
|
||||
jstring value = static_cast<jstring>(env->CallStaticObjectMethod(activity_class, method));
|
||||
if (android_check_exception(env, "getWorkingFolder") || value == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* working_folder = env->GetStringUTFChars(value, nullptr);
|
||||
if (android_check_exception(env, "GetStringUTFChars") || working_folder == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto release_chars = util::finally([env, value, working_folder]() {
|
||||
env->ReleaseStringUTFChars(value, working_folder);
|
||||
});
|
||||
wfp = working_folder;
|
||||
});
|
||||
|
||||
if (wfp.empty()) {
|
||||
mprintf(("Couldn't get working folder path from Java class, reverting to SDL default.\n"));
|
||||
// SDL's external storage path already includes the app's files directory.
|
||||
const char* fallbackPath = SDL_GetAndroidExternalStoragePath();
|
||||
if (fallbackPath) {
|
||||
wfp = SCP_string(fallbackPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure path ends with a dir separator
|
||||
if (!wfp.empty() && (wfp.back() != DIR_SEPARATOR_CHAR)) {
|
||||
wfp += DIR_SEPARATOR_CHAR;
|
||||
}
|
||||
mprintf(("Using working folder: %s\n", wfp.c_str()));
|
||||
return wfp;
|
||||
}
|
||||
|
||||
void os_android_touch_overlay_toggle(bool status)
|
||||
{
|
||||
android_with_static_method(status ? "enableOverlay" : "disableOverlay", "()V",
|
||||
[](JNIEnv* env, jclass activity_class, jmethodID method) {
|
||||
env->CallStaticVoidMethod(activity_class, method);
|
||||
});
|
||||
}
|
||||
|
||||
void os_android_touch_overlay_set_opacity(int opacity)
|
||||
{
|
||||
android_with_static_method("setOverlayOpacity", "(F)V",
|
||||
[opacity](JNIEnv* env, jclass activity_class, jmethodID method) {
|
||||
env->CallStaticVoidMethod(activity_class, method, static_cast<jfloat>(opacity) / 100.0f);
|
||||
});
|
||||
}
|
||||
|
||||
static bool touch_ui_change(bool new_val, bool initial)
|
||||
{
|
||||
if (initial) {
|
||||
return false;
|
||||
}
|
||||
os_android_touch_overlay_toggle(new_val);
|
||||
return true;
|
||||
}
|
||||
|
||||
static auto TouchOverlayOption = options::OptionBuilder<bool>("Input.TouchOverlay",
|
||||
std::pair<const char*, int>{"Touch Overlay", 1936},
|
||||
std::pair<const char*, int>{"Enable or disable the touch overlay", 1937})
|
||||
.category(std::make_pair("Input", 1827))
|
||||
.level(options::ExpertLevel::Beginner)
|
||||
.change_listener(touch_ui_change)
|
||||
.default_val(true)
|
||||
.importance(2)
|
||||
.finish();
|
||||
|
||||
static bool touch_ui_opacity_change(int new_val, bool initial)
|
||||
{
|
||||
Assertion(new_val >= 0 && new_val <= 100, "Invalid value %d supplied by options system!", new_val);
|
||||
if (initial) {
|
||||
return false;
|
||||
}
|
||||
os_android_touch_overlay_set_opacity(new_val);
|
||||
return true;
|
||||
}
|
||||
|
||||
static auto TouchOverlayOpacityOption = options::OptionBuilder<int>("Input.TouchOverlayOpacity",
|
||||
std::pair<const char*, int>{"Touch Overlay Opacity", 1938},
|
||||
std::pair<const char*, int>{"Set the opacity of the touch overlay", 1939})
|
||||
.category(std::make_pair("Input", 1827))
|
||||
.level(options::ExpertLevel::Beginner)
|
||||
.range(0, 100)
|
||||
.default_val(20)
|
||||
.flags({ options::OptionFlags::RangeTypeInteger })
|
||||
.change_listener(touch_ui_opacity_change)
|
||||
.importance(1)
|
||||
.finish();
|
||||
|
||||
void os_android_touch_overlay_init()
|
||||
{
|
||||
os_android_touch_overlay_toggle(TouchOverlayOption->getValue());
|
||||
os_android_touch_overlay_set_opacity(TouchOverlayOpacityOption->getValue());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -90,6 +90,23 @@ bool os_is_legacy_mode();
|
||||
*/
|
||||
SCP_string os_get_config_path(const SCP_string& subpath = "");
|
||||
|
||||
/*
|
||||
Special functions for Android
|
||||
*/
|
||||
#ifdef __ANDROID__
|
||||
// Get working folder absolute path from Android Java Class
|
||||
SCP_string os_android_get_working_folder_path();
|
||||
|
||||
// Calls to display the touch overlay depending on its last state
|
||||
void os_android_touch_overlay_init();
|
||||
|
||||
// Enable or disable the touch UI overlay
|
||||
void os_android_touch_overlay_toggle(bool status);
|
||||
|
||||
// Send the JSON flags string to the Android Java layer
|
||||
void os_android_set_flags_string(const char* json);
|
||||
#endif
|
||||
|
||||
namespace os
|
||||
{
|
||||
/**
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
#define _AL_H
|
||||
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#if defined(__APPLE__) || defined(__ANDROID__)
|
||||
#include "al.h"
|
||||
#include "alc.h"
|
||||
#else
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include "utils/unicode.h"
|
||||
#include "speech.h"
|
||||
#include <jni.h>
|
||||
#include "SDL.h"
|
||||
#include "SDL_system.h"
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
bool Speech_init = false;
|
||||
static jclass j_game_class = nullptr;
|
||||
@@ -41,8 +41,8 @@ bool speech_init()
|
||||
mprintf(("Speech : Try to init TTSManager on GameActivity...\n"));
|
||||
|
||||
// Get the JNI Environment pointer and current Activity instance via SDL
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
jobject activity = (jobject)SDL_AndroidGetActivity();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
jobject activity = (jobject)SDL_GetAndroidActivity();
|
||||
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
@@ -101,7 +101,7 @@ bool speech_play(const SCP_string& text)
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
return false;
|
||||
@@ -123,7 +123,7 @@ bool speech_stop()
|
||||
if (!Speech_init)
|
||||
return false;
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
return false;
|
||||
@@ -137,7 +137,7 @@ bool speech_pause()
|
||||
if (!Speech_init)
|
||||
return false;
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
return false;
|
||||
@@ -151,7 +151,7 @@ bool speech_resume()
|
||||
if (!Speech_init)
|
||||
return false;
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
return false;
|
||||
@@ -165,7 +165,7 @@ bool speech_is_speaking()
|
||||
if (!Speech_init)
|
||||
return false;
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
return false;
|
||||
@@ -179,7 +179,7 @@ void speech_deinit()
|
||||
if (!Speech_init)
|
||||
return;
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env != nullptr) {
|
||||
env->CallStaticVoidMethod(j_game_class, tts_shutdown);
|
||||
env->DeleteGlobalRef(j_game_class);
|
||||
@@ -217,7 +217,7 @@ bool speech_set_rate(float rate_percent)
|
||||
else if(rate_percent < 50.0)
|
||||
rate_percent = 50.0;
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
return false;
|
||||
@@ -233,7 +233,7 @@ bool speech_set_voice(int voice)
|
||||
if (!Speech_init)
|
||||
return false;
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
return false;
|
||||
@@ -263,7 +263,7 @@ SCP_vector<std::pair<int, SCP_string>> speech_enumerate_voices()
|
||||
if (!Speech_init)
|
||||
return voices;
|
||||
|
||||
JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
||||
if (env == nullptr) {
|
||||
mprintf(("Speech : Unable to get JNI environment!\n"));
|
||||
return voices;
|
||||
|
||||
@@ -49,6 +49,7 @@ int filelength(int fd)
|
||||
|
||||
SCP_string dump_stacktrace()
|
||||
{
|
||||
#ifndef __ANDROID__
|
||||
#ifdef SCP_HAVE_EXECINFO_H
|
||||
// The following is adapted from here: https://panthema.net/2008/0901-stacktrace-demangled/
|
||||
const int ADDR_SIZE = 64;
|
||||
@@ -139,6 +140,9 @@ SCP_string dump_stacktrace()
|
||||
#else
|
||||
return "No stacktrace available";
|
||||
#endif
|
||||
#else
|
||||
return "No stacktrace available";
|
||||
#endif
|
||||
}
|
||||
|
||||
// retrieve the current working directory
|
||||
|
||||
Reference in new issue
Block a user