diff --git a/modules/camera/SCsub b/modules/camera/SCsub index aed5efd0d2..750dc7c368 100644 --- a/modules/camera/SCsub +++ b/modules/camera/SCsub @@ -6,7 +6,7 @@ Import("env_modules") env_camera = env_modules.Clone() -if env["platform"] in ["windows", "macos", "linuxbsd"]: +if env["platform"] in ["windows", "macos", "linuxbsd", "android"]: env_camera.add_source_files(env.modules_sources, "register_types.cpp") if env["platform"] == "windows": @@ -15,6 +15,10 @@ if env["platform"] == "windows": elif env["platform"] == "macos": env_camera.add_source_files(env.modules_sources, "camera_macos.mm") +elif env["platform"] == "android": + env_camera.add_source_files(env.modules_sources, "camera_android.cpp") + env.Append(LIBS=["camera2ndk", "mediandk"]) + elif env["platform"] == "linuxbsd": env_camera.add_source_files(env.modules_sources, "camera_linux.cpp") env_camera.add_source_files(env.modules_sources, "camera_feed_linux.cpp") diff --git a/modules/camera/camera_android.cpp b/modules/camera/camera_android.cpp new file mode 100644 index 0000000000..60ff495290 --- /dev/null +++ b/modules/camera/camera_android.cpp @@ -0,0 +1,511 @@ +/**************************************************************************/ +/* camera_android.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "camera_android.h" + +#include "core/os/os.h" +#include "platform/android/display_server_android.h" + +////////////////////////////////////////////////////////////////////////// +// Helper functions +// +// The following code enables you to view the contents of a media type while +// debugging. + +#ifndef IF_EQUAL_RETURN +#define MAKE_FORMAT_CONST(suffix) AIMAGE_FORMAT_##suffix +#define IF_EQUAL_RETURN(param, val) \ + if (MAKE_FORMAT_CONST(val) == param) \ + return #val +#endif + +String GetFormatName(const int32_t &format) { + IF_EQUAL_RETURN(format, YUV_420_888); + IF_EQUAL_RETURN(format, RGB_888); + IF_EQUAL_RETURN(format, RGBA_8888); + + return "Unsupported"; +} + +////////////////////////////////////////////////////////////////////////// +// CameraFeedAndroid - Subclass for our camera feed on Android + +CameraFeedAndroid::CameraFeedAndroid(ACameraManager *manager, ACameraMetadata *metadata, const char *id, + CameraFeed::FeedPosition position, int32_t orientation) : + CameraFeed() { + this->manager = manager; + this->metadata = metadata; + this->orientation = orientation; + _add_formats(); + camera_id = id; + set_position(position); + + // Position + switch (position) { + case CameraFeed::FEED_BACK: + name = vformat("%s | BACK", id); + break; + case CameraFeed::FEED_FRONT: + name = vformat("%s | FRONT", id); + break; + default: + name = vformat("%s", id); + break; + } + + image_y.instantiate(); + image_uv.instantiate(); +} + +CameraFeedAndroid::~CameraFeedAndroid() { + if (is_active()) { + deactivate_feed(); + } + if (metadata != nullptr) { + ACameraMetadata_free(metadata); + } +} + +void CameraFeedAndroid::_set_rotation() { + int display_rotation = DisplayServerAndroid::get_singleton()->get_display_rotation(); + // reverse rotation + switch (display_rotation) { + case 90: + display_rotation = 270; + break; + case 270: + display_rotation = 90; + break; + default: + break; + } + + int sign = position == CameraFeed::FEED_FRONT ? 1 : -1; + float imageRotation = (orientation - display_rotation * sign + 360) % 360; + transform.set_rotation(real_t(Math::deg_to_rad(imageRotation))); +} + +void CameraFeedAndroid::_add_formats() { + // Get supported formats + ACameraMetadata_const_entry formats; + camera_status_t status = ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &formats); + + if (status == ACAMERA_OK) { + for (uint32_t f = 0; f < formats.count; f += 4) { + // Only support output streams + int32_t input = formats.data.i32[f + 3]; + if (input) { + continue; + } + + // Get format and resolution + int32_t format = formats.data.i32[f + 0]; + if (format == AIMAGE_FORMAT_YUV_420_888 || + format == AIMAGE_FORMAT_RGBA_8888 || + format == AIMAGE_FORMAT_RGB_888) { + CameraFeed::FeedFormat feed_format; + feed_format.width = formats.data.i32[f + 1]; + feed_format.height = formats.data.i32[f + 2]; + feed_format.format = GetFormatName(format); + feed_format.pixel_format = format; + this->formats.append(feed_format); + } + } + } +} + +bool CameraFeedAndroid::activate_feed() { + ERR_FAIL_COND_V_MSG(selected_format == -1, false, "CameraFeed format needs to be set before activating."); + if (is_active()) { + deactivate_feed(); + }; + + // Request permission + if (!OS::get_singleton()->request_permission("CAMERA")) { + return false; + } + + // Open device + static ACameraDevice_stateCallbacks deviceCallbacks = { + .context = this, + .onDisconnected = onDisconnected, + .onError = onError, + }; + camera_status_t c_status = ACameraManager_openCamera(manager, camera_id.utf8().get_data(), &deviceCallbacks, &device); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + // Create image reader + const FeedFormat &feed_format = formats[selected_format]; + media_status_t m_status = AImageReader_new(feed_format.width, feed_format.height, feed_format.pixel_format, 1, &reader); + if (m_status != AMEDIA_OK) { + onError(this, device, m_status); + return false; + } + + // Get image listener + static AImageReader_ImageListener listener{ + .context = this, + .onImageAvailable = onImage, + }; + m_status = AImageReader_setImageListener(reader, &listener); + if (m_status != AMEDIA_OK) { + onError(this, device, m_status); + return false; + } + + // Get image surface + ANativeWindow *surface; + m_status = AImageReader_getWindow(reader, &surface); + if (m_status != AMEDIA_OK) { + onError(this, device, m_status); + return false; + } + + // Prepare session outputs + ACaptureSessionOutput *output = nullptr; + c_status = ACaptureSessionOutput_create(surface, &output); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + ACaptureSessionOutputContainer *outputs = nullptr; + c_status = ACaptureSessionOutputContainer_create(&outputs); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + c_status = ACaptureSessionOutputContainer_add(outputs, output); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + // Create capture session + static ACameraCaptureSession_stateCallbacks sessionStateCallbacks{ + .context = this, + .onClosed = onSessionClosed, + .onReady = onSessionReady, + .onActive = onSessionActive + }; + c_status = ACameraDevice_createCaptureSession(device, outputs, &sessionStateCallbacks, &session); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + // Create capture request + c_status = ACameraDevice_createCaptureRequest(device, TEMPLATE_PREVIEW, &request); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + // Set capture target + ACameraOutputTarget *imageTarget = nullptr; + c_status = ACameraOutputTarget_create(surface, &imageTarget); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + c_status = ACaptureRequest_addTarget(request, imageTarget); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + // Start capture + c_status = ACameraCaptureSession_setRepeatingRequest(session, nullptr, 1, &request, nullptr); + if (c_status != ACAMERA_OK) { + onError(this, device, c_status); + return false; + } + + return true; +} + +bool CameraFeedAndroid::set_format(int p_index, const Dictionary &p_parameters) { + ERR_FAIL_COND_V_MSG(active, false, "Feed is active."); + ERR_FAIL_INDEX_V_MSG(p_index, formats.size(), false, "Invalid format index."); + + selected_format = p_index; + return true; +} + +Array CameraFeedAndroid::get_formats() const { + Array result; + for (const FeedFormat &feed_format : formats) { + Dictionary dictionary; + dictionary["width"] = feed_format.width; + dictionary["height"] = feed_format.height; + dictionary["format"] = feed_format.format; + result.push_back(dictionary); + } + return result; +} + +CameraFeed::FeedFormat CameraFeedAndroid::get_format() const { + CameraFeed::FeedFormat feed_format = {}; + return selected_format == -1 ? feed_format : formats[selected_format]; +} + +void CameraFeedAndroid::onImage(void *context, AImageReader *p_reader) { + CameraFeedAndroid *feed = static_cast(context); + Vector data_y = feed->data_y; + Vector data_uv = feed->data_uv; + Ref image_y = feed->image_y; + Ref image_uv = feed->image_uv; + + // Get image + AImage *image = nullptr; + media_status_t status = AImageReader_acquireNextImage(p_reader, &image); + ERR_FAIL_COND(status != AMEDIA_OK); + + // Get image data + uint8_t *data = nullptr; + int len = 0; + int32_t pixel_stride, row_stride; + FeedFormat format = feed->get_format(); + int width = format.width; + int height = format.height; + switch (format.pixel_format) { + case AIMAGE_FORMAT_YUV_420_888: + AImage_getPlaneData(image, 0, &data, &len); + if (len <= 0) { + return; + } + if (len != data_y.size()) { + int64_t size = Image::get_image_data_size(width, height, Image::FORMAT_R8, false); + data_y.resize(len > size ? len : size); + } + memcpy(data_y.ptrw(), data, len); + + AImage_getPlanePixelStride(image, 1, &pixel_stride); + AImage_getPlaneRowStride(image, 1, &row_stride); + AImage_getPlaneData(image, 1, &data, &len); + if (len <= 0) { + return; + } + if (len != data_uv.size()) { + int64_t size = Image::get_image_data_size(width / 2, height / 2, Image::FORMAT_RG8, false); + data_uv.resize(len > size ? len : size); + } + memcpy(data_uv.ptrw(), data, len); + + image_y->initialize_data(width, height, false, Image::FORMAT_R8, data_y); + image_uv->initialize_data(width / 2, height / 2, false, Image::FORMAT_RG8, data_uv); + + feed->set_ycbcr_images(image_y, image_uv); + break; + case AIMAGE_FORMAT_RGBA_8888: + AImage_getPlaneData(image, 0, &data, &len); + if (len <= 0) { + return; + } + if (len != data_y.size()) { + int64_t size = Image::get_image_data_size(width, height, Image::FORMAT_RGBA8, false); + data_y.resize(len > size ? len : size); + } + memcpy(data_y.ptrw(), data, len); + + image_y->initialize_data(width, height, false, Image::FORMAT_RGBA8, data_y); + + feed->set_rgb_image(image_y); + break; + case AIMAGE_FORMAT_RGB_888: + AImage_getPlaneData(image, 0, &data, &len); + if (len <= 0) { + return; + } + if (len != data_y.size()) { + int64_t size = Image::get_image_data_size(width, height, Image::FORMAT_RGB8, false); + data_y.resize(len > size ? len : size); + } + memcpy(data_y.ptrw(), data, len); + + image_y->initialize_data(width, height, false, Image::FORMAT_RGB8, data_y); + + feed->set_rgb_image(image_y); + break; + default: + return; + } + + // Rotation + feed->_set_rotation(); + + // Release image + AImage_delete(image); + + feed->emit_signal(SNAME("frame_changed")); +} + +void CameraFeedAndroid::onSessionReady(void *context, ACameraCaptureSession *session) { + print_verbose("Capture session ready"); +} + +void CameraFeedAndroid::onSessionActive(void *context, ACameraCaptureSession *session) { + print_verbose("Capture session active"); +} + +void CameraFeedAndroid::onSessionClosed(void *context, ACameraCaptureSession *session) { + print_verbose("Capture session closed"); +} + +void CameraFeedAndroid::deactivate_feed() { + if (session != nullptr) { + ACameraCaptureSession_stopRepeating(session); + ACameraCaptureSession_close(session); + session = nullptr; + } + + if (request != nullptr) { + ACaptureRequest_free(request); + request = nullptr; + } + + if (reader != nullptr) { + AImageReader_delete(reader); + reader = nullptr; + } + + if (device != nullptr) { + ACameraDevice_close(device); + device = nullptr; + } +} + +void CameraFeedAndroid::onError(void *context, ACameraDevice *p_device, int error) { + print_error(vformat("Camera error: %d", error)); + onDisconnected(context, p_device); +} + +void CameraFeedAndroid::onDisconnected(void *context, ACameraDevice *p_device) { + print_verbose("Camera disconnected"); + auto *feed = static_cast(context); + feed->set_active(false); +} + +////////////////////////////////////////////////////////////////////////// +// CameraAndroid - Subclass for our camera server on Android + +void CameraAndroid::update_feeds() { + ACameraIdList *cameraIds = nullptr; + camera_status_t c_status = ACameraManager_getCameraIdList(cameraManager, &cameraIds); + ERR_FAIL_COND(c_status != ACAMERA_OK); + + // remove existing devices + for (int i = feeds.size() - 1; i >= 0; i--) { + remove_feed(feeds[i]); + } + + for (int c = 0; c < cameraIds->numCameras; ++c) { + const char *id = cameraIds->cameraIds[c]; + ACameraMetadata *metadata = nullptr; + ACameraManager_getCameraCharacteristics(cameraManager, id, &metadata); + if (!metadata) { + continue; + } + + // Get sensor orientation + ACameraMetadata_const_entry orientation; + c_status = ACameraMetadata_getConstEntry(metadata, ACAMERA_SENSOR_ORIENTATION, &orientation); + int32_t cameraOrientation; + if (c_status == ACAMERA_OK) { + cameraOrientation = orientation.data.i32[0]; + } else { + cameraOrientation = 0; + print_error(vformat("Unable to get sensor orientation: %s", id)); + } + + // Get position + ACameraMetadata_const_entry lensInfo; + CameraFeed::FeedPosition position = CameraFeed::FEED_UNSPECIFIED; + camera_status_t status; + status = ACameraMetadata_getConstEntry(metadata, ACAMERA_LENS_FACING, &lensInfo); + if (status != ACAMERA_OK) { + ACameraMetadata_free(metadata); + continue; + } + uint8_t lens_facing = static_cast(lensInfo.data.u8[0]); + if (lens_facing == ACAMERA_LENS_FACING_FRONT) { + position = CameraFeed::FEED_FRONT; + } else if (lens_facing == ACAMERA_LENS_FACING_BACK) { + position = CameraFeed::FEED_BACK; + } else { + ACameraMetadata_free(metadata); + continue; + } + + Ref feed = memnew(CameraFeedAndroid(cameraManager, metadata, id, position, cameraOrientation)); + add_feed(feed); + } + + ACameraManager_deleteCameraIdList(cameraIds); +} + +void CameraAndroid::remove_all_feeds() { + // remove existing devices + for (int i = feeds.size() - 1; i >= 0; i--) { + remove_feed(feeds[i]); + } + + if (cameraManager != nullptr) { + ACameraManager_delete(cameraManager); + cameraManager = nullptr; + } +} + +void CameraAndroid::set_monitoring_feeds(bool p_monitoring_feeds) { + if (p_monitoring_feeds == monitoring_feeds) { + return; + } + + CameraServer::set_monitoring_feeds(p_monitoring_feeds); + if (p_monitoring_feeds) { + if (cameraManager == nullptr) { + cameraManager = ACameraManager_create(); + } + + // Update feeds + update_feeds(); + } else { + remove_all_feeds(); + } +} + +CameraAndroid::~CameraAndroid() { + remove_all_feeds(); +} diff --git a/modules/camera/camera_android.h b/modules/camera/camera_android.h new file mode 100644 index 0000000000..0840b7179d --- /dev/null +++ b/modules/camera/camera_android.h @@ -0,0 +1,96 @@ +/**************************************************************************/ +/* camera_android.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "servers/camera/camera_feed.h" +#include "servers/camera_server.h" + +#include +#include +#include +#include +#include + +class CameraFeedAndroid : public CameraFeed { + GDSOFTCLASS(CameraFeedAndroid, CameraFeed); + +private: + String camera_id; + int32_t orientation; + Ref image_y; + Ref image_uv; + Vector data_y; + Vector data_uv; + + ACameraManager *manager = nullptr; + ACameraMetadata *metadata = nullptr; + ACameraDevice *device = nullptr; + AImageReader *reader = nullptr; + ACameraCaptureSession *session = nullptr; + ACaptureRequest *request = nullptr; + + void _add_formats(); + void _set_rotation(); + + static void onError(void *context, ACameraDevice *p_device, int error); + static void onDisconnected(void *context, ACameraDevice *p_device); + static void onImage(void *context, AImageReader *p_reader); + static void onSessionReady(void *context, ACameraCaptureSession *session); + static void onSessionActive(void *context, ACameraCaptureSession *session); + static void onSessionClosed(void *context, ACameraCaptureSession *session); + +protected: +public: + bool activate_feed() override; + void deactivate_feed() override; + bool set_format(int p_index, const Dictionary &p_parameters) override; + Array get_formats() const override; + FeedFormat get_format() const override; + + CameraFeedAndroid(ACameraManager *manager, ACameraMetadata *metadata, const char *id, + CameraFeed::FeedPosition position, int32_t orientation); + ~CameraFeedAndroid() override; +}; + +class CameraAndroid : public CameraServer { + GDSOFTCLASS(CameraAndroid, CameraServer); + +private: + ACameraManager *cameraManager = nullptr; + + void update_feeds(); + void remove_all_feeds(); + +public: + void set_monitoring_feeds(bool p_monitoring_feeds) override; + + ~CameraAndroid(); +}; diff --git a/modules/camera/config.py b/modules/camera/config.py index fa229ef2f5..b543dc74fa 100644 --- a/modules/camera/config.py +++ b/modules/camera/config.py @@ -3,7 +3,7 @@ def can_build(env, platform): if sys.platform.startswith("freebsd"): return False - return platform == "macos" or platform == "windows" or platform == "linuxbsd" + return platform == "macos" or platform == "windows" or platform == "linuxbsd" or platform == "android" def configure(env): diff --git a/modules/camera/register_types.cpp b/modules/camera/register_types.cpp index 666ea8ba65..c56ddfe862 100644 --- a/modules/camera/register_types.cpp +++ b/modules/camera/register_types.cpp @@ -39,6 +39,9 @@ #if defined(MACOS_ENABLED) #include "camera_macos.h" #endif +#if defined(ANDROID_ENABLED) +#include "camera_android.h" +#endif void initialize_camera_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { @@ -54,6 +57,9 @@ void initialize_camera_module(ModuleInitializationLevel p_level) { #if defined(MACOS_ENABLED) CameraServer::make_default(); #endif +#if defined(ANDROID_ENABLED) + CameraServer::make_default(); +#endif } void uninitialize_camera_module(ModuleInitializationLevel p_level) { diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index b174b17855..4f0c15eb0c 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -279,6 +279,13 @@ DisplayServer::ScreenOrientation DisplayServerAndroid::screen_get_orientation(in return (ScreenOrientation)orientation; } +int DisplayServerAndroid::get_display_rotation() const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, 0); + + return godot_io_java->get_display_rotation(); +} + int DisplayServerAndroid::get_screen_count() const { return 1; } diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index fb1212141c..0b650f84fd 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -144,6 +144,7 @@ public: virtual void screen_set_orientation(ScreenOrientation p_orientation, int p_screen = SCREEN_OF_MAIN_WINDOW) override; virtual ScreenOrientation screen_get_orientation(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + int get_display_rotation() const; virtual int get_screen_count() const override; virtual int get_primary_screen() const override; diff --git a/platform/android/java/editor/src/horizonos/AndroidManifest.xml b/platform/android/java/editor/src/horizonos/AndroidManifest.xml index 3db83340aa..70453e4922 100644 --- a/platform/android/java/editor/src/horizonos/AndroidManifest.xml +++ b/platform/android/java/editor/src/horizonos/AndroidManifest.xml @@ -37,6 +37,12 @@ + + + + + + GetMethodID(cls, "setScreenOrientation", "(I)V"); _get_screen_orientation = p_env->GetMethodID(cls, "getScreenOrientation", "()I"); _get_system_dir = p_env->GetMethodID(cls, "getSystemDir", "(IZ)Ljava/lang/String;"); + _get_display_rotation = p_env->GetMethodID(cls, "getDisplayRotation", "()I"); } } @@ -289,6 +290,16 @@ String GodotIOJavaWrapper::get_system_dir(int p_dir, bool p_shared_storage) { } } +int GodotIOJavaWrapper::get_display_rotation() { + if (_get_display_rotation) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, 0); + return env->CallIntMethod(godot_io_instance, _get_display_rotation); + } else { + return 0; + } +} + // SafeNumeric because it can be changed from non-main thread and we need to // ensure the change is immediately visible to other threads. static SafeNumeric virtual_keyboard_height; diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index f74be8ddbf..6fe8e05010 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -49,6 +49,7 @@ private: jmethodID _get_data_dir = 0; jmethodID _get_temp_dir = 0; jmethodID _get_display_cutouts = 0; + jmethodID _get_display_rotation = 0; jmethodID _get_display_safe_area = 0; jmethodID _get_locale = 0; jmethodID _get_model = 0; @@ -90,4 +91,5 @@ public: void set_screen_orientation(int p_orient); int get_screen_orientation(); String get_system_dir(int p_dir, bool p_shared_storage); + int get_display_rotation(); };