/***************************************************************************** * Copyright (c) 2014-2025 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #ifdef __ANDROID__ #include "Platform.h" #include "../Diagnostic.h" #include "../core/File.h" #include "../core/Guard.hpp" #include "../localisation/Language.h" #include #include #include AndroidClassLoader::~AndroidClassLoader() { JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); env->DeleteGlobalRef(_classLoader); } jobject AndroidClassLoader::_classLoader; jmethodID AndroidClassLoader::_findClassMethod; // Initialized in JNI_OnLoad. Cannot be initialized here as JVM is not // available until after JNI_OnLoad is called. static std::shared_ptr acl; namespace OpenRCT2::Platform { std::string GetFolderPath(SpecialFolder folder) { // Android builds currently only read from /sdcard/openrct2* switch (folder) { case SpecialFolder::userCache: case SpecialFolder::userConfig: case SpecialFolder::userData: case SpecialFolder::userHome: return "/sdcard"; default: return std::string(); } } std::string GetDocsPath() { return std::string(); } std::string GetInstallPath() { return "/sdcard/openrct2"; } std::string GetCurrentExecutablePath() { Guard::Assert(false, "GetCurrentExecutablePath() not implemented for Android."); return std::string(); } u8string StrDecompToPrecomp(u8string_view input) { return u8string(input); } bool HandleSpecialCommandLineArgument(const char* argument) { return false; } uint16_t GetLocaleLanguage() { JNIEnv* env = static_cast(SDL_AndroidGetJNIEnv()); jobject activity = static_cast(SDL_AndroidGetActivity()); jclass activityClass = env->GetObjectClass(activity); jmethodID getDefaultLocale = env->GetMethodID( activityClass, "getDefaultLocale", "([Ljava/lang/String;)Ljava/lang/String;"); jobjectArray jLanguageTags = env->NewObjectArray( LANGUAGE_COUNT, env->FindClass("java/lang/String"), env->NewStringUTF("")); for (int32_t i = 1; i < LANGUAGE_COUNT; ++i) { jstring jTag = env->NewStringUTF(LanguagesDescriptors[i].locale); env->SetObjectArrayElement(jLanguageTags, i, jTag); } jstring jniString = static_cast(env->CallObjectMethod(activity, getDefaultLocale, jLanguageTags)); const char* jniChars = env->GetStringUTFChars(jniString, nullptr); std::string defaultLocale = jniChars; env->ReleaseStringUTFChars(jniString, jniChars); for (int32_t i = 0; i < LANGUAGE_COUNT; ++i) { jobject strToFree = env->GetObjectArrayElement(jLanguageTags, i); env->DeleteLocalRef(strToFree); } env->DeleteLocalRef(jLanguageTags); env->DeleteLocalRef(activity); env->DeleteLocalRef(activityClass); return LanguageGetIDFromLocale(defaultLocale.c_str()); } CurrencyType GetLocaleCurrency() { JNIEnv* env = static_cast(SDL_AndroidGetJNIEnv()); jobject activity = static_cast(SDL_AndroidGetActivity()); jclass activityClass = env->GetObjectClass(activity); jmethodID getDefaultLocale = env->GetMethodID(activityClass, "getLocaleCurrency", "()Ljava/lang/String;"); jstring jniString = static_cast(env->CallObjectMethod(activity, getDefaultLocale)); const char* jniChars = env->GetStringUTFChars(jniString, nullptr); std::string localeCurrencyCode = jniChars; env->ReleaseStringUTFChars(jniString, jniChars); env->DeleteLocalRef(activity); env->DeleteLocalRef(activityClass); return Platform::GetCurrencyValue(localeCurrencyCode.c_str()); } MeasurementFormat GetLocaleMeasurementFormat() { JNIEnv* env = static_cast(SDL_AndroidGetJNIEnv()); jobject activity = static_cast(SDL_AndroidGetActivity()); jclass activityClass = env->GetObjectClass(activity); jmethodID getIsImperialLocaleMeasurementFormat = env->GetMethodID( activityClass, "isImperialLocaleMeasurementFormat", "()Z"); jboolean isImperial = env->CallBooleanMethod(activity, getIsImperialLocaleMeasurementFormat); env->DeleteLocalRef(activity); env->DeleteLocalRef(activityClass); return isImperial == JNI_TRUE ? MeasurementFormat::Imperial : MeasurementFormat::Metric; } std::string GetSteamPath() { return {}; } u8string GetRCT1SteamDir() { return {}; } u8string GetRCT2SteamDir() { return {}; } #ifndef DISABLE_TTF std::string GetFontPath(const TTFFontDescriptor& font) { auto expectedPath = std::string("/system/fonts/") + std::string(font.filename); if (File::Exists(expectedPath)) { return expectedPath; } return ""; } #endif float GetDefaultScale() { JNIEnv* env = static_cast(SDL_AndroidGetJNIEnv()); jobject activity = static_cast(SDL_AndroidGetActivity()); jclass activityClass = env->GetObjectClass(activity); jmethodID getDefaultScale = env->GetMethodID(activityClass, "getDefaultScale", "()F"); jfloat displayScale = env->CallFloatMethod(activity, getDefaultScale); env->DeleteLocalRef(activity); env->DeleteLocalRef(activityClass); return displayScale; } jclass AndroidFindClass(JNIEnv* env, std::string_view name) { return static_cast(env->CallObjectMethod( AndroidClassLoader::_classLoader, AndroidClassLoader::_findClassMethod, env->NewStringUTF(std::string(name).c_str()))); } std::vector GetSearchablePathsRCT1() { return { "/sdcard/rct1" }; } std::vector GetSearchablePathsRCT2() { return { "/sdcard/rct2" }; } } // namespace OpenRCT2::Platform JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* pjvm, void* reserved) { // Due to an issue where JNI_OnLoad could be called multiple times, we need // to make sure it is only initialized once. // https://issuetracker.google.com/issues/220523932 // Otherwise JVM complains about jobject-s having incorrect serial numbers. if (!acl) { acl = std::make_shared(); } return JNI_VERSION_1_6; } AndroidClassLoader::AndroidClassLoader() { LOG_INFO("Obtaining JNI class loader"); // This is a workaround to be able to call JNI's ClassLoader from non-main // thread, based on https://stackoverflow.com/a/16302771 // Apparently it's OK to use it from across different thread, but JNI // only looks for ClassLoader in the _current_ thread and fails to find // it when searched for from a native library's non-main thread. // The solution below works by obtaining a ClassLoader reference in main // thread and caching it for future use from any thread, instead of using // it via env->FindClass(). ClassLoader itself is abstract, so we cannot // create it directly; instead we take an arbitrary class and call // getClassLoader() on it to create a reference that way. // If we're here, SDL's JNI_OnLoad has already been called and set env JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); // Take an arbitrary class. While the class does not really matter, it // makes sense to use one that's most likely already loaded and is unlikely // to be removed from code. auto randomClass = env->FindClass("io/openrct2/MainActivity"); jclass classClass = env->GetObjectClass(randomClass); // Get its class loader auto classLoaderClass = env->FindClass("java/lang/ClassLoader"); auto getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader", "()Ljava/lang/ClassLoader;"); // Store the class loader and its findClass method for future use _classLoader = env->NewGlobalRef(env->CallObjectMethod(randomClass, getClassLoaderMethod)); _findClassMethod = env->GetMethodID(classLoaderClass, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); } #endif