From 221877fa33a04d18e6a878accf9f69cd5f7899b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Janiszewski?= Date: Sun, 17 Mar 2024 23:30:05 +0100 Subject: [PATCH] Android: Replace curl dependency with HttpAndroid Java implementation --- .../app/src/main/CMakeLists.txt | 10 +- .../main/java/io/openrct2/HttpAndroid.java | 122 ++++++++++++ src/openrct2/core/Http.Android.cpp | 179 ++++++++++++++++++ src/openrct2/core/Http.cURL.cpp | 2 +- 4 files changed, 303 insertions(+), 10 deletions(-) create mode 100644 src/openrct2-android/app/src/main/java/io/openrct2/HttpAndroid.java create mode 100644 src/openrct2/core/Http.Android.cpp diff --git a/src/openrct2-android/app/src/main/CMakeLists.txt b/src/openrct2-android/app/src/main/CMakeLists.txt index 076a16bff7..bac4e35970 100644 --- a/src/openrct2-android/app/src/main/CMakeLists.txt +++ b/src/openrct2-android/app/src/main/CMakeLists.txt @@ -29,7 +29,6 @@ ExternalProject_Add(libs INSTALL_COMMAND "" BUILD_BYPRODUCTS - ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}curl${CMAKE_SHARED_LIBRARY_SUFFIX} ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}crypto${CMAKE_SHARED_LIBRARY_SUFFIX} ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}vorbis${CMAKE_SHARED_LIBRARY_SUFFIX} ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}vorbisfile${CMAKE_SHARED_LIBRARY_SUFFIX} @@ -62,7 +61,6 @@ ExternalProject_Add(libs add_custom_command(TARGET libs POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} # COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/*.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} - COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/libcurl.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/libcrypto.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/libvorbis.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/libvorbisfile.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} @@ -142,12 +140,6 @@ set_target_properties(ssl PROPERTIES IMPORTED_LOCATION ) add_dependencies(ssl libs) -add_library(curl SHARED IMPORTED) -set_target_properties(curl PROPERTIES IMPORTED_LOCATION - ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}curl${CMAKE_SHARED_LIBRARY_SUFFIX} -) -add_dependencies(curl libs) - add_library(crypto SHARED IMPORTED) set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}crypto${CMAKE_SHARED_LIBRARY_SUFFIX} @@ -255,7 +247,7 @@ file(GLOB_RECURSE OPENRCT2_CLI_SOURCES "${ORCT2_ROOT}/src/openrct2-cli/*.hpp") add_library(openrct2 SHARED ${LIBOPENRCT2_SOURCES}) -target_link_libraries(openrct2 android stdc++ log dl SDL2 png z icu icuuc icudata curl crypto ssl) +target_link_libraries(openrct2 android stdc++ log dl SDL2 png z icu icuuc icudata crypto ssl) add_library(openrct2-ui SHARED ${OPENRCT2_GUI_SOURCES}) target_link_libraries(openrct2-ui openrct2 android stdc++ GLESv1_CM GLESv2 SDL2main speexdsp brotlicommon brotlidec bz2 freetype ogg vorbis vorbisfile FLAC) diff --git a/src/openrct2-android/app/src/main/java/io/openrct2/HttpAndroid.java b/src/openrct2-android/app/src/main/java/io/openrct2/HttpAndroid.java new file mode 100644 index 0000000000..ffb58a1062 --- /dev/null +++ b/src/openrct2-android/app/src/main/java/io/openrct2/HttpAndroid.java @@ -0,0 +1,122 @@ +package io.openrct2; + +import android.util.Log; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class HttpAndroid { + private static final String TAG = "HttpAndroid"; + + // Corresponding Java enum for the C++ 'Status' enum + public enum Status { + Invalid(0), + Ok(200), + NotFound(404); + + private final int code; + Status(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + } + + // Corresponding Java enum for the C++ 'Method' enum + public enum Method { + GET, + POST, + PUT + } + + // Java class equivalent to the C++ 'Response' struct + public static class Response { + Status status; + String contentType; + String body; + Map> headers; + String error; + + } + + // Java class equivalent to the C++ 'Request' struct + public static class Request { + String url; + Map headers; + Method method; + String body; + boolean forceIPv4; + + public Request() { + this.method = Method.GET; // Default method + } + } + + public static Response request(Request request) { + Response response = new Response(); + response.status = Status.Invalid; + response.error = "Request failed"; + try { + InputStream inputStream = null; + try { + URL url = new URL(request.url); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod(request.method.toString()); + connection.setConnectTimeout(10000); + connection.setReadTimeout(10000); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setInstanceFollowRedirects(true); + if (request.headers != null) { + for (Map.Entry entry : request.headers.entrySet()) { + connection.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + connection.connect(); + if (request.body!= null) { + OutputStream os = connection.getOutputStream(); + os.write(request.body.getBytes()); + os.flush(); + os.close(); + } + int responseCode = connection.getResponseCode(); + if (responseCode == 200) { + inputStream = connection.getInputStream(); + } else { + inputStream = connection.getErrorStream(); + } + // Log + Log.d(TAG, "Request: " + request.url + ", response code: " + responseCode); + response.status = Status.Ok; + response.contentType = connection.getContentType(); + response.headers = connection.getHeaderFields(); + response.body = IOUtils.toString(inputStream, "UTF-8"); + response.error = null; + return response; + } catch (IOException e) { + Log.e(TAG, "Error while requesting " + request.url + ", error: " + e.getMessage(), e); + response.error = e.getMessage(); + return response; + } finally { + if (inputStream != null) { + inputStream.close(); + } + } + } catch (IOException e) { + Log.e(TAG, "Error while requesting " + request.url, e); + response.error = e.getMessage(); + return response; + } + } +} diff --git a/src/openrct2/core/Http.Android.cpp b/src/openrct2/core/Http.Android.cpp new file mode 100644 index 0000000000..67cb15eafc --- /dev/null +++ b/src/openrct2/core/Http.Android.cpp @@ -0,0 +1,179 @@ +/***************************************************************************** + * Copyright (c) 2014-2024 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. + *****************************************************************************/ + +#if !defined(DISABLE_HTTP) && defined(__ANDROID__) + +# include "Http.h" + +# include "../Version.h" +# include "../platform/Platform.h" + +# include +# include +# include + +# define OPENRCT2_USER_AGENT "OpenRCT2/" OPENRCT2_VERSION + +namespace Http +{ + Response Do(const Request& req) + { + std::map headers = req.header; + headers["User-Agent"] = OPENRCT2_USER_AGENT; + // Lambda to convert jstring to string + auto jstringToString = [](JNIEnv* env, jstring jstr) -> std::string { + if (jstr == nullptr) + { + return ""; + } + const char* cstr = env->GetStringUTFChars(jstr, nullptr); + std::string str = cstr; + env->ReleaseStringUTFChars(jstr, cstr); + return str; + }; + + JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + + // Create request object + + jclass HttpAndroidClass = Platform::AndroidFindClass(env, "io/openrct2/HttpAndroid"); + jclass req_class = Platform::AndroidFindClass(env, "io/openrct2/HttpAndroid$Request"); + // New request object + jobject jniRequest = env->NewObject(req_class, env->GetMethodID(req_class, "", "()V")); + // Create request's headers map + jobject jniHeaders = env->NewObject( + env->FindClass("java/util/HashMap"), + env->GetMethodID(env->GetObjectClass(env->FindClass("java/util/HashMap")), "", "()V")); + // HashMap's put method + jmethodID putMethod = env->GetMethodID( + env->FindClass("java/util/HashMap"), "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); + for (const auto& header : headers) + { + jstring jniKey = env->NewStringUTF(header.first.c_str()); + jstring jniValue = env->NewStringUTF(header.second.c_str()); + env->CallObjectMethod(jniHeaders, putMethod, jniKey, jniValue); + } + env->SetObjectField(jniRequest, env->GetFieldID(req_class, "headers", "Ljava/util/Map;"), jniHeaders); + + jstring req_body = req.body.empty() ? nullptr : env->NewStringUTF(req.body.c_str()); + jstring url = req.url.empty() ? nullptr : env->NewStringUTF(req.url.c_str()); + env->SetObjectField(jniRequest, env->GetFieldID(req_class, "body", "Ljava/lang/String;"), req_body); + env->SetObjectField(jniRequest, env->GetFieldID(req_class, "url", "Ljava/lang/String;"), url); + if (req_body != nullptr) + { + env->ReleaseStringUTFChars(req_body, env->GetStringUTFChars(req_body, nullptr)); + } + if (url != nullptr) + { + env->ReleaseStringUTFChars(url, env->GetStringUTFChars(url, nullptr)); + } + std::string method = "GET"; + switch (req.method) + { + case Method::GET: + method = "GET"; + break; + case Method::POST: + method = "POST"; + break; + case Method::PUT: + method = "PUT"; + break; + } + env->SetObjectField( + jniRequest, env->GetFieldID(req_class, "method", "Lio/openrct2/HttpAndroid$Method;"), + env->GetStaticObjectField( + Platform::AndroidFindClass(env, "io/openrct2/HttpAndroid$Method"), + env->GetStaticFieldID( + Platform::AndroidFindClass(env, "io/openrct2/HttpAndroid$Method"), method.c_str(), + "Lio/openrct2/HttpAndroid$Method;"))); + // Call request method + jmethodID requestMethod = env->GetStaticMethodID( + HttpAndroidClass, "request", "(Lio/openrct2/HttpAndroid$Request;)Lio/openrct2/HttpAndroid$Response;"); + jobject jniResponse = env->CallStaticObjectMethod(HttpAndroidClass, requestMethod, jniRequest); + jfieldID statusField = env->GetFieldID(env->GetObjectClass(jniResponse), "status", "Lio/openrct2/HttpAndroid$Status;"); + jobject jniStatus = env->GetObjectField(jniResponse, statusField); + jclass statusClass = Platform::AndroidFindClass(env, "io/openrct2/HttpAndroid$Status"); + jmethodID getCodeMethod = env->GetMethodID(statusClass, "getCode", "()I"); + int code = env->CallIntMethod(jniStatus, getCodeMethod); + + Response response; + + // Get response's headers field + jclass responseClass = env->GetObjectClass(jniResponse); + jfieldID headersField = env->GetFieldID(responseClass, "headers", "Ljava/util/Map;"); + jobject jniHeadersMap = env->GetObjectField(jniResponse, headersField); + + jmethodID entrySetMethod = env->GetMethodID(env->GetObjectClass(jniHeadersMap), "entrySet", "()Ljava/util/Set;"); + jobject jniEntrySet = env->CallObjectMethod(jniHeadersMap, entrySetMethod); + jmethodID iteratorMethod = env->GetMethodID(env->GetObjectClass(jniEntrySet), "iterator", "()Ljava/util/Iterator;"); + jobject jniIterator = env->CallObjectMethod(jniEntrySet, iteratorMethod); + jmethodID hasNextMethod = env->GetMethodID(env->GetObjectClass(jniIterator), "hasNext", "()Z"); + jmethodID nextMethod = env->GetMethodID(env->GetObjectClass(jniIterator), "next", "()Ljava/lang/Object;"); + while (env->CallBooleanMethod(jniIterator, hasNextMethod)) + { + jobject jniNext = env->CallObjectMethod(jniIterator, nextMethod); + jmethodID getKeyMethod = env->GetMethodID(env->GetObjectClass(jniNext), "getKey", "()Ljava/lang/Object;"); + jmethodID getValueMethod = env->GetMethodID(env->GetObjectClass(jniNext), "getValue", "()Ljava/lang/Object;"); + jobject jniKey = env->CallObjectMethod(jniNext, getKeyMethod); + // The first key is always null for some reason, its value contains the status code + if (jniKey == nullptr) + { + env->DeleteLocalRef(jniKey); + env->DeleteLocalRef(jniNext); + continue; + } + jobject jniValue = env->CallObjectMethod(jniNext, getValueMethod); + // convert jniValue to string + jmethodID valueToStringMethod = env->GetMethodID(env->GetObjectClass(jniValue), "toString", "()Ljava/lang/String;"); + jstring jniValueString = (jstring)env->CallObjectMethod(jniValue, valueToStringMethod); + jstring jniKeyString = (jstring)jniKey; + const char* jniKeyChars = env->GetStringUTFChars(jniKeyString, nullptr); + const char* jniValueChars = env->GetStringUTFChars(jniValueString, nullptr); + std::string value = jniValueChars; + std::string key = jniKeyChars; + env->ReleaseStringUTFChars(jniKeyString, jniKeyChars); + env->ReleaseStringUTFChars(jniValueString, jniValueChars); + response.header[key] = value; + + // Cleanup + env->DeleteLocalRef(jniValue); + env->DeleteLocalRef(jniKey); + env->DeleteLocalRef(jniNext); + } + // Get response's body field and convert it to string + jfieldID bodyField = env->GetFieldID(responseClass, "body", "Ljava/lang/String;"); + jstring jniBody = (jstring)env->GetObjectField(jniResponse, bodyField); + // Get response's error field and convert it to string + jfieldID errorField = env->GetFieldID(responseClass, "error", "Ljava/lang/String;"); + jstring jniError = (jstring)env->GetObjectField(jniResponse, errorField); + // Get response's content type field and convert it to string + jfieldID contentTypeField = env->GetFieldID(responseClass, "contentType", "Ljava/lang/String;"); + jstring jniContentType = (jstring)env->GetObjectField(jniResponse, contentTypeField); + // Prepare response + response.error = jstringToString(env, jniError); + response.content_type = jstringToString(env, jniContentType); + response.status = static_cast(code); + response.body = jstringToString(env, jniBody); + + env->DeleteLocalRef(jniResponse); + env->DeleteLocalRef(jniStatus); + env->DeleteLocalRef(jniHeadersMap); + env->DeleteLocalRef(jniBody); + env->DeleteLocalRef(jniContentType); + env->DeleteLocalRef(jniError); + env->DeleteLocalRef(jniHeaders); + env->DeleteLocalRef(jniRequest); + + return response; + } + +} // namespace Http + +#endif diff --git a/src/openrct2/core/Http.cURL.cpp b/src/openrct2/core/Http.cURL.cpp index aef20b4127..67603998f9 100644 --- a/src/openrct2/core/Http.cURL.cpp +++ b/src/openrct2/core/Http.cURL.cpp @@ -7,7 +7,7 @@ * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ -#if !defined(DISABLE_HTTP) && !defined(_WIN32) +#if !defined(DISABLE_HTTP) && !defined(_WIN32) && !defined(__ANDROID__) # include "Http.h"