diff --git a/src/Magnum/Platform/CMakeLists.txt b/src/Magnum/Platform/CMakeLists.txt index e6a474796..93091bb90 100644 --- a/src/Magnum/Platform/CMakeLists.txt +++ b/src/Magnum/Platform/CMakeLists.txt @@ -508,6 +508,50 @@ if(WITH_WINDOWLESSEGLAPPLICATION) add_library(Magnum::WindowlessEglApplication ALIAS MagnumWindowlessEglApplication) endif() +# Windowless android application +if(WITH_WINDOWLESSANDROIDAPPLICATION) + if(NOT ${CMAKE_SYSTEM_NAME} STREQUAL Android) + message(FATAL_ERROR "WindowlessAndroidApplication is available only when targeting Android. Set WITH_WINDOWLESSANDROIDAPPLICATION to OFF to skip building it.") + endif() + + find_package(EGL REQUIRED) + set(NEED_EGLCONTEXT 1) + + set(MagnumWindowlessAndroidApplication_SRCS + WindowlessAndroidApplication.cpp + MotionEventSerializer.cpp + Implementation/Egl.cpp + $) + set(MagnumWindowlessAndroidApplication_HEADERS + WindowlessAndroidApplication.h + MotionEventSerializer.h) + set(MagnumWindowlessAndroidApplication_PRIVATE_HEADERS + Implementation/Egl.h) + + add_library(MagnumWindowlessAndroidApplication STATIC + ${MagnumWindowlessAndroidApplication_SRCS} + ${MagnumWindowlessAndroidApplication_HEADERS} + ${MagnumWindowlessAndroidApplication_PRIVATE_HEADERS}) + set_target_properties(MagnumWindowlessAndroidApplication PROPERTIES + DEBUG_POSTFIX "-d" + FOLDER "Magnum/Platform") + # Assuming that PIC is not needed because the Application lib is always + # linked to the executable and not to any intermediate shared lib + target_link_libraries(MagnumWindowlessAndroidApplication PUBLIC + MagnumGL + android + EGL::EGL) + + install(FILES ${MagnumWindowlessAndroidApplication_HEADERS} DESTINATION ${MAGNUM_INCLUDE_INSTALL_DIR}/Platform) + install(TARGETS MagnumWindowlessAndroidApplication + RUNTIME DESTINATION ${MAGNUM_BINARY_INSTALL_DIR} + LIBRARY DESTINATION ${MAGNUM_LIBRARY_INSTALL_DIR} + ARCHIVE DESTINATION ${MAGNUM_LIBRARY_INSTALL_DIR}) + + # Magnum AndroidApplication target alias for superprojects + add_library(Magnum::WindowlessAndroidApplication ALIAS MagnumWindowlessAndroidApplication) +endif() + # Windowless GLX application if(WITH_WINDOWLESSGLXAPPLICATION) if(NOT TARGET_GL) diff --git a/src/Magnum/Platform/MotionEventSerializer.cpp b/src/Magnum/Platform/MotionEventSerializer.cpp new file mode 100644 index 000000000..4080c031a --- /dev/null +++ b/src/Magnum/Platform/MotionEventSerializer.cpp @@ -0,0 +1,88 @@ +// +// Created by Matthew Good on 7/6/21. +// + +#include "MotionEventSerializer.h" + +MotionEventSerializer::IndexInfo::IndexInfo() {} + +MotionEventSerializer::IndexInfo::IndexInfo(int pointerIndex, int segment_length, int reserved) { + idx_pointerId = (pointerIndex*segment_length)+reserved; + idx_ptrIndex = (pointerIndex*segment_length)+reserved+1; + idx_active = (pointerIndex*segment_length)+reserved+2; + idx_action = (pointerIndex*segment_length)+reserved+3; + idx_moved = (pointerIndex*segment_length)+reserved+4; + idx_x = (pointerIndex*segment_length)+reserved+5; + idx_y = (pointerIndex*segment_length)+reserved+6; + idx_pressure = (pointerIndex*segment_length)+reserved+7; + idx_size = (pointerIndex*segment_length)+reserved+8; +} + +void MotionEventSerializer::acquire(JNIEnv *jenv, jfloatArray motionEventData) { + jboolean isCopy; + data = jenv->GetFloatArrayElements(motionEventData, &isCopy); + + dataLength = data[idx_dataLength]; + reserved = data[idx_dataReserved]; + segment_length = data[idx_dataSegmentLength]; + maxSupportedPointers = data[idx_maxSupportedPointers]; + int c = pointerCount(); + indexInfo = new IndexInfo[c]; + for (int i = 0; i < c; i++) { + indexInfo[i] = IndexInfo(i, segment_length, reserved); + } +} + +void MotionEventSerializer::release(JNIEnv *jenv, jfloatArray motionEventData) { + delete[] indexInfo; + jenv->ReleaseFloatArrayElements(motionEventData, data, 0); + data = nullptr; +} + +int MotionEventSerializer::pointerCount() { + return data[idx_ptrCount]; +} + +int MotionEventSerializer::currentPointerIndex() { + return data[idx_ptrCurrentIdx]; +} + +int MotionEventSerializer::getActionIndex() { + return currentPointerIndex(); +} + +int MotionEventSerializer::getPointerId(int pointerIndex) { + return data[indexInfo[pointerIndex].idx_pointerId]; +} + +bool MotionEventSerializer::isPointerActive(int pointerIndex) { + return data[indexInfo[pointerIndex].idx_active] == MOTION_EVENT_ACTIVE; +} + +int MotionEventSerializer::getAction(int pointerIndex) { + return data[indexInfo[pointerIndex].idx_action]; +} + +bool MotionEventSerializer::didPointerMove(int pointerIndex) { + return data[indexInfo[pointerIndex].idx_moved] == MOTION_EVENT_MOVED; +} + +float MotionEventSerializer::getX(int pointerIndex) { + return data[indexInfo[pointerIndex].idx_x]; +} + +float MotionEventSerializer::getY(int pointerIndex) { + return data[indexInfo[pointerIndex].idx_y]; +} + +float MotionEventSerializer::getPressure(int pointerIndex) { + return data[indexInfo[pointerIndex].idx_pressure]; +} + +float MotionEventSerializer::getSize(int pointerIndex) { + return data[indexInfo[pointerIndex].idx_size]; +} + +int MotionEventSerializer::getButtonState() { + return data[idx_buttonState]; +} diff --git a/src/Magnum/Platform/MotionEventSerializer.h b/src/Magnum/Platform/MotionEventSerializer.h new file mode 100644 index 000000000..f82c42af5 --- /dev/null +++ b/src/Magnum/Platform/MotionEventSerializer.h @@ -0,0 +1,92 @@ +// +// Created by Matthew Good on 7/6/21. +// + +#ifndef CRAFTER_MOTIONEVENTSERIALIZER_H +#define CRAFTER_MOTIONEVENTSERIALIZER_H + +#include + +class MotionEventSerializer { +private: + class IndexInfo { + public: + int idx_pointerId; + int idx_ptrIndex; + int idx_active; + int idx_action; + int idx_moved; + int idx_x; + int idx_y; + int idx_pressure; + int idx_size; + IndexInfo(); + IndexInfo(int pointerIndex, int segment_length, int reserved); + }; + + IndexInfo * indexInfo; + + int reserved; + int segment_length; + int maxSupportedPointers; + int dataLength; + float * data = nullptr; + // THESE MUSE BE KEPT IN SYNC WITH JAVA SIDE + static const int idx_dataLength = 0; + static const int idx_dataReserved = 1; + static const int idx_dataSegmentLength = 2; + static const int idx_maxSupportedPointers = 3; + static const int idx_ptrCount = 4; + static const int idx_ptrCurrentIdx = 5; + static const int idx_buttonState = 6; +public: + // THESE MUSE BE KEPT IN SYNC WITH JAVA SIDE + static const int MOTION_EVENT_ACTIVE = 1; + static const int MOTION_EVENT_NOT_ACTIVE = 2; + static const int MOTION_EVENT_ACTION_UNKNOWN = -1; + static const int MOTION_EVENT_ACTION_DOWN = 1; + static const int MOTION_EVENT_ACTION_UP = 2; + static const int MOTION_EVENT_ACTION_MOVE = 3; + static const int MOTION_EVENT_MOVED = 1; + static const int MOTION_EVENT_DID_NOT_MOVE = 2; + + /** + * Constants that identify buttons that are associated with motion events. + * Refer to the documentation on the MotionEvent class for descriptions of each button. + */ + enum { + /** primary */ + MOTION_EVENT_BUTTON_PRIMARY = 1 << 0, + /** secondary */ + MOTION_EVENT_BUTTON_SECONDARY = 1 << 1, + /** tertiary */ + MOTION_EVENT_BUTTON_TERTIARY = 1 << 2, + /** back */ + MOTION_EVENT_BUTTON_BACK = 1 << 3, + /** forward */ + MOTION_EVENT_BUTTON_FORWARD = 1 << 4, + MOTION_EVENT_BUTTON_STYLUS_PRIMARY = 1 << 5, + MOTION_EVENT_BUTTON_STYLUS_SECONDARY = 1 << 6, + }; + + void acquire(JNIEnv *jenv, jfloatArray motionEventData); + void release(JNIEnv *jenv, jfloatArray motionEventData); + + int pointerCount(); + int currentPointerIndex(); + + bool isPointerActive(int pointerIndex); + bool didPointerMove(int pointerIndex); + + int getPointerId(int pointerIndex); + int getAction(int pointerIndex); + int getActionIndex(); + float getX(int pointerIndex); + float getY(int pointerIndex); + float getPressure(int pointerIndex); + float getSize(int pointerIndex); + + int getButtonState(); +}; + +#endif //CRAFTER_MOTIONEVENTSERIALIZER_H diff --git a/src/Magnum/Platform/WindowlessAndroidApplication.cpp b/src/Magnum/Platform/WindowlessAndroidApplication.cpp new file mode 100644 index 000000000..92df67adf --- /dev/null +++ b/src/Magnum/Platform/WindowlessAndroidApplication.cpp @@ -0,0 +1,221 @@ +#include "WindowlessAndroidApplication.h" + +#include +#include +#include + +#include "Magnum/GL/Version.h" + +#include "Magnum/Platform/Implementation/Egl.h" + +#define M_LOG(TYPE, func_name) TYPE{Corrade::Utility::TYPE::Flags::Type::NoSpace} << "Platform::WindowlessAndroidApplication::" << #func_name << "(): " +#define CHECK_EGL(name, func_name) \ +{ \ + EGLint e = eglGetError(); \ + if (e == EGL_SUCCESS) { \ + M_LOG(Debug, func_name) << name << " succeeded"; \ + } else { \ + M_LOG(Error, func_name) << "cannot create EGL context: " << name << \ + " returned error: " << Magnum::Platform::Implementation::eglErrorString(e); \ + } \ +} +#define CHECK_PTR(ptr) (ptr == 0 ? "(nullptr)" : ptr) + +namespace Magnum { namespace Platform { + +WindowlessAndroidApplication::WindowlessAndroidApplication(JNIEnv *jenv, jobject classInstance, jstring name, jstring signature) { + if (!jvmManager.getJVM(jenv)) { + Error{} << "failed to get JavaVM"; + return; + } + if (!jvmManager.attachJVM()) { + Error() << "failed to attach JVM"; + return; + } + jObject = jvmManager.globalRef(jenv, classInstance); + jClass = jvmManager.globalRef(jenv, jenv->GetObjectClass(jObject)); + jboolean isCopy1, isCopy2; + + if (name == nullptr) { + Error{} << "cannot use a null name (0x0)"; + return; + } + + if (signature == nullptr) { + Error{} << "cannot use a null signature (0x0)"; + return; + } + + const char * n = jvmManager.jenv->GetStringUTFChars(name, &isCopy1); + if (n == nullptr) { + Error{} << "cannot get UTF chars from name"; + return; + } + + const char * s = jvmManager.jenv->GetStringUTFChars(signature, &isCopy2); + if (s == nullptr) { + Error{} << "cannot get UTF chars from signature"; + jvmManager.jenv->ReleaseStringUTFChars(name, n); + return; + } + + jSwapBuffers = jvmManager.jenv->GetMethodID(jClass, n, s); + + jvmManager.jenv->ReleaseStringUTFChars(name, n); + jvmManager.jenv->ReleaseStringUTFChars(name, s); + + if (jSwapBuffers == nullptr) { + Error{} << "cannot find method with name '" << name << "', " + << "and signature '" << signature << "'"; + } +} + +WindowlessAndroidApplication::~WindowlessAndroidApplication() { + jvmManager.jenv->DeleteGlobalRef(jClass); + jvmManager.jenv->DeleteGlobalRef(jObject); + jvmManager.detachJVM(); +} + +void WindowlessAndroidApplication::swapBuffers() { + if (jSwapBuffers != nullptr) { + jvmManager.jenv->CallVoidMethod(jObject, jSwapBuffers); + } +} + +enum class WindowlessAndroidApplication::Flag: UnsignedByte { + Redraw = 1 << 0 +}; + +void WindowlessAndroidApplication::redraw() { + _flags |= Flag::Redraw; +} + +void WindowlessAndroidApplication::onDraw() { + if ( + magnum_context != nullptr && + Platform::GLContext::hasCurrent() && + &Platform::GLContext::current() == &magnum_context->context->current() + ) { + if(_flags & Flag::Redraw) { + _flags &= ~Flag::Redraw; + drawEvent(); + } + } +} + +WindowlessAndroidApplication::GLConfiguration::GLConfiguration(): + _colorBufferSize{8, 8, 8, 8}, _depthBufferSize{24}, _stencilBufferSize{0} {} + +Vector2i WindowlessAndroidApplication::framebufferSize() const { + return {width,height}; +} + +void WindowlessAndroidApplication::surfaceChanged(int w, int h) { + width = w; + height = h; + ViewportEvent e{{w, h}}; + viewportEvent(e); +} + +void WindowlessAndroidApplication::viewportEvent(ViewportEvent& event) { + static_cast(event); +} + +static MotionEventSerializer motionEvent; + +bool WindowlessAndroidApplication::onTouchEvent(JNIEnv *jenv, jfloatArray motionEventData) { + motionEvent.acquire(jenv, motionEventData); + bool r; + int action = motionEvent.getAction(0); + switch (action) { + case MotionEventSerializer::MOTION_EVENT_ACTION_DOWN: + case MotionEventSerializer::MOTION_EVENT_ACTION_UP: { + /* On a touch screen move events aren't reported when the + finger is moving above (of course), so remember the position + always */ + float x = motionEvent.getX(0); + float y = motionEvent.getX(0); + _previousMouseMovePosition = {Int(x), Int(y)}; + MouseEvent e(motionEvent, x, y, motionEvent.getButtonState()); + if (action == MotionEventSerializer::MOTION_EVENT_ACTION_DOWN) { + mousePressEvent(e); + } else { + mouseReleaseEvent(e); + // reset the relative position + // if the relative position is not reset, then + // the relative position of mouse press will be + // relative to the last location of mouse release + // which differs from desktop behaviour + _previousMouseMovePosition = Vector2i{-1}; + } + r = e.isAccepted(); + break; + } + + case MotionEventSerializer::MOTION_EVENT_ACTION_MOVE: { + float x = motionEvent.getX(0); + float y = motionEvent.getY(0); + Vector2i position{Int(x), Int(y)}; + MouseMoveEvent e{motionEvent, x, y, motionEvent.getButtonState(), + _previousMouseMovePosition == Vector2i{-1} ? Vector2i{} : + position - _previousMouseMovePosition}; + _previousMouseMovePosition = position; + mouseMoveEvent(e); + r = e.isAccepted(); + break; + } + default: + r = false; + break; + } + + /** @todo Implement also other input events */ + + motionEvent.release(jenv, motionEventData); + return r; +} + +void WindowlessAndroidApplication::mousePressEvent(MouseEvent&) {} +void WindowlessAndroidApplication::mouseReleaseEvent(MouseEvent&) {} +void WindowlessAndroidApplication::mouseMoveEvent(MouseMoveEvent&) {} + +WindowlessAndroidApplication::JVM_MANAGER::JVM_MANAGER() { + jvm = nullptr; + jenv = nullptr; + needsToDetach = false; + jvmArgs.version = JNI_VERSION_1_6; +} + +bool WindowlessAndroidApplication::JVM_MANAGER::getJVM(JNIEnv *env) { + return env->GetJavaVM(&jvm) == JNI_OK; +} + +bool WindowlessAndroidApplication::JVM_MANAGER::attachJVM() { + // checks if current env needs attaching or it is already attached + jint res = jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6); + if (res == JNI_EDETACHED) { + // Supported but not attached yet, needs to call AttachCurrentThread + res = jvm->AttachCurrentThread(&jenv, &jvmArgs); + if (res == JNI_OK) { + needsToDetach = true; + } else { + // Failed to attach, cancel + return false; + } + } else if (JNI_OK == res) { + // Current thread already attached, do not attach 'again' (just to save the attachedHere flag) + // We make sure to keep attached = 0 + needsToDetach = false; + } else { + // JNI_EVERSION, specified version is not supported cancel this.. + return false; + } + return true; +} + +void WindowlessAndroidApplication::JVM_MANAGER::detachJVM() { + if (needsToDetach) { + jvm->DetachCurrentThread(); + } +} +}} diff --git a/src/Magnum/Platform/WindowlessAndroidApplication.h b/src/Magnum/Platform/WindowlessAndroidApplication.h new file mode 100644 index 000000000..8955734bd --- /dev/null +++ b/src/Magnum/Platform/WindowlessAndroidApplication.h @@ -0,0 +1,839 @@ +#ifndef Magnum_Platform_WindowlessAndroidApplication_h +#define Magnum_Platform_WindowlessAndroidApplication_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + 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. +*/ + +#if defined(CORRADE_TARGET_ANDROID) || defined(DOXYGEN_GENERATING_OUTPUT) +/** @file + * @brief Class @ref Magnum::Platform::WindowlessAndroidApplication, macro @ref MAGNUM_WindowlessAndroidApplication_MAIN() + */ +#endif + +#include +#include +#include + +#include "Magnum/Magnum.h" +#include "Magnum/Tags.h" +#include "Magnum/Math/Vector4.h" +#include "Magnum/Platform/Platform.h" +#include "Magnum/Platform/GLContext.h" +#include "MotionEventSerializer.h" + +#if defined(CORRADE_TARGET_ANDROID) || defined(DOXYGEN_GENERATING_OUTPUT) +#include +#include +#include +#include +#include + +/* Undef Xlib nonsense which might get pulled in by EGL */ +#undef None + +namespace Magnum { namespace Platform { + +/** @nosubgrouping +@brief Android application +@m_keywords{Application} +Application running on Android. Available only on +@ref CORRADE_TARGET_ANDROID "Android", see respective sections +in the @ref building-corrade-cross-android "Corrade" and +@ref building-cross-android "Magnum" building documentation. +@section Platform-WindowlessAndroidApplication-bootstrap Bootstrap application +Fully contained base application using @ref Sdl2Application for desktop build +and @ref WindowlessAndroidApplication for Android build along with full Android packaging +stuff and CMake setup is available in `base-android` branch of +[Magnum Bootstrap](https://github.com/mosra/magnum-bootstrap) repository, +download it as [tar.gz](https://github.com/mosra/magnum-bootstrap/archive/base-android.tar.gz) +or [zip](https://github.com/mosra/magnum-bootstrap/archive/base-android.zip) +file. After extracting the downloaded archive, you can do the desktop build in +the same way as with @ref Sdl2Application. +In order to build the application, you need Gradle and Android build of Corrade +and Magnum. Gradle is usually able to download all SDK dependencies on its own +and then you can just build and install the app on a connected device or +emulator like this: +@code{.sh} +gradle build +gradle installDebug +@endcode +Detailed information about deployment for Android and all needed boilerplate +together with a troubleshooting guide is available in @ref platforms-android. +@section Platform-WindowlessAndroidApplication-usage General usage +This application library is built if `WITH_WindowlessAndroidApplication` is enabled when +building Magnum. To use this library with CMake, put [FindEGL.cmake](https://github.com/mosra/magnum/blob/master/modules/FindEGL.cmake) +and [FindOpenGLES2.cmake](https://github.com/mosra/magnum/blob/master/modules/FindOpenGLES2.cmake) (or +[FindOpenGLES3.cmake](https://github.com/mosra/magnum/blob/master/modules/FindOpenGLES3.cmake)) +into your `modules/` directory, request the `WindowlessAndroidApplication` component of +the `Magnum` package and link to the `Magnum::WindowlessAndroidApplication` target: +@code{.cmake} +find_package(Magnum REQUIRED) +if(CORRADE_TARGET_ANDROID) + find_package(Magnum REQUIRED WindowlessAndroidApplication) +endif() +# ... +if(CORRADE_TARGET_ANDROID) + target_link_libraries(your-app PRIVATE Magnum::WindowlessAndroidApplication) +endif() +@endcode +Additionally, if you're using Magnum as a CMake subproject, do the following +* *before* calling @cmake find_package() @ce to ensure it's enabled, as the +library is not built by default: +@code{.cmake} +set(WITH_WindowlessAndroidApplication ON CACHE BOOL "" FORCE) +add_subdirectory(magnum EXCLUDE_FROM_ALL) +@endcode +If no other application is requested, you can also use the generic +`Magnum::Application` alias to simplify porting. Again, see @ref building and +@ref cmake for more information. Note that unlike on other platforms you need +to create a *shared library* instead of an executable: +@code{.cmake} +if(NOT CORRADE_TARGET_ANDROID) + add_executable(your-app ...) +else() + add_library(your-app SHARED ...) +endif() +@endcode +In C++ code you need to implement at least @ref drawEvent() to be able to draw +on the screen. The subclass must be then made accessible from JNI using +@ref MAGNUM_WindowlessAndroidApplication_MAIN() macro. See @ref platform for more +information. +@code{.cpp} +class MyApplication: public Platform::WindowlessAndroidApplication { + // implement required methods... +}; +MAGNUM_WindowlessAndroidApplication_MAIN(MyApplication) +@endcode +If no other application header is included, this class is also aliased to +@cpp Platform::Application @ce and the macro is aliased to @cpp MAGNUM_APPLICATION_MAIN() @ce +to simplify porting. +@section Platform-WindowlessAndroidApplication-resizing Responding to viewport events +Unlike in desktop application implementations, where this is controlled via +@ref Sdl2Application::Configuration::WindowFlag::Resizable, for example, on +Android you have to describe this in the `AndroidManifest.xml` file, as by +default the application gets killed and relaunched on screen orientation +change. See the @ref platforms-android-apps-manifest-screen-resize "manifest file docs" +for more information. +@section Platform-WindowlessAndroidApplication-output-redirection Redirecting output to Android log buffer +The application by default redirects @ref Corrade::Utility::Debug "Debug", +@ref Corrade::Utility::Warning "Warning" and @ref Corrade::Utility::Error "Error" +output to Android log buffer with tag `"magnum"`, which can be then accessed +through `logcat` utility. See also @ref Corrade::Utility::AndroidLogStreamBuffer +for more information. +*/ +class WindowlessAndroidApplication { +public: + + class Configuration; + class GLConfiguration; + class ViewportEvent; + class InputEvent; + class MouseEvent; + class MouseMoveEvent; + + /** + * @brief Construct without creating a window + * + * The window must be created later + * with @ref tryCreate(). + */ + explicit WindowlessAndroidApplication() = default; + explicit WindowlessAndroidApplication(JNIEnv *jenv, jobject classInstance, jstring name, jstring signature); + ~WindowlessAndroidApplication(); + + /** @brief Copying is not allowed */ + WindowlessAndroidApplication(const WindowlessAndroidApplication&) = delete; + + /** @brief Moving is not allowed */ + WindowlessAndroidApplication(WindowlessAndroidApplication&&) = delete; + + /** @brief Copying is not allowed */ + WindowlessAndroidApplication& operator=(const WindowlessAndroidApplication&) = delete; + + /** @brief Moving is not allowed */ + WindowlessAndroidApplication& operator=(WindowlessAndroidApplication&&) = delete; + + /** @{ @name Screen handling */ + + /** + * @brief Window size + * + * Window size to which all input event coordinates can be related. + * Expects that a window is already created, equivalent to + * @ref framebufferSize(). + * + * @attention The reported value will be incorrect in case you use + * the [Screen Compatibility Mode](http://www.androiddocs.com/guide/practices/screen-compat-mode.html). + * See @ref platforms-android-apps-manifest-screen-compatibility-mode + * for details. + */ + Vector2i windowSize() const { return framebufferSize(); } + + /** + * @brief Framebuffer size + * + * Size of the default framebuffer, equivalent to @ref windowSize(). + * Expects that a window is already created. + * @see @ref dpiScaling() + */ + Vector2i framebufferSize() const; + + /** + * @brief DPI scaling + * + * Provided only for compatibility with other toolkits. Returns always + * @cpp {1.0f, 1.0f} @ce. + * @see @ref framebufferSize() + */ + Vector2 dpiScaling() const { return Vector2{1.0f}; } + + /** + * @brief DPI scaling for given configuration + * + * Provided only for compatibility with other toolkits. Returns always + * @cpp {1.0f, 1.0f} @ce. + * @see @ref framebufferSize() + */ + Vector2 dpiScaling(const Configuration& configuration) const { + static_cast(configuration); + return Vector2{1.0f}; + } + + /** + * @brief Swap buffers + * + * Paints currently rendered framebuffer on screen. + */ + void swapBuffers(); + + /** + * @brief Redraw immediately + * + * Marks the window for redrawing, resulting in call to @ref drawEvent() + * in the next iteration. You can call it from @ref drawEvent() itself + * to redraw immediately without waiting for user input. + */ + void redraw(); + + /** + * @brief Draw event + * + * Called when the screen is redrawn. You should clean the framebuffer + * using @ref GL::DefaultFramebuffer::clear() (if using OpenGL) and + * then add your own drawing functions. After drawing is finished, call + * @ref swapBuffers(). If you want to draw immediately again, call also + * @ref redraw(). + */ + virtual void drawEvent() = 0; + + /** + * @brief native onDraw + * + * calls drawEvent if it should be redrawn + * + * call this from java jni onDraw method/native + */ + void onDraw(); + + bool onTouchEvent(JNIEnv *jenv, jfloatArray motionEventData); + void surfaceChanged(int w, int h); + +private: + /** + * @brief Viewport event + * + * Called when window size changes, for example after device + * orientation change. The default implementation does nothing. If you + * want to respond to size changes, you should pass the new size to + * @ref GL::DefaultFramebuffer::setViewport() (if using OpenGL) and + * possibly elsewhere (to @ref SceneGraph::Camera::setViewport(), other + * framebuffers...). + * + * @attention Android by default kills and fully recreates the + * application on device orientation change instead of calling the + * viewport event. To prevent that, you need to modify the + * `AndroidManifest.xml` file. See the + * @ref platforms-android-apps-manifest-screen-resize "manifest file docs" + * for more information. + * + * Note that this function might not get called at all if the window + * size doesn't change. You should configure the initial state of your + * cameras, framebuffers etc. in application constructor rather than + * relying on this function to be called. Size of the window can be + * retrieved using @ref windowSize(), size of the backing framebuffer + * via @ref framebufferSize() and DPI scaling using @ref dpiScaling(). + * See @ref Platform-GlfwApplication-dpi for detailed info about these + * values. + */ + virtual void viewportEvent(ViewportEvent& event); + + /* Since 1.8.17, the original short-hand group closing doesn't work + anymore. FFS. */ + /** + * @} + */ + + /** @{ @name Mouse handling */ + + /** + * @brief Mouse press event + * + * Called when mouse button is pressed. Default implementation does + * nothing. + */ + virtual void mousePressEvent(MouseEvent& event); + + /** + * @brief Mouse release event + * + * Called when mouse button is released. Default implementation does + * nothing. + */ + virtual void mouseReleaseEvent(MouseEvent& event); + + /** + * @brief Mouse move event + * + * Called when mouse is moved. Default implementation does nothing. + */ + virtual void mouseMoveEvent(MouseMoveEvent& event); + + /* Since 1.8.17, the original short-hand group closing doesn't work + anymore. FFS. */ + /** + * @} + */ + + enum class Flag: UnsignedByte; + typedef Containers::EnumSet Flags; + Flags _flags; + int width, height; + + class JVM_MANAGER { + public: + JavaVM * jvm; + JNIEnv* jenv; + JavaVMAttachArgs jvmArgs; + bool needsToDetach; + + JVM_MANAGER(); + bool getJVM(JNIEnv * env); + bool attachJVM(); + void detachJVM(); + + template O globalRef(J j, O o) { + return reinterpret_cast(j->NewGlobalRef(o)); + } + }; + + + JVM_MANAGER jvmManager; + jobject jObject; + jclass jClass; + jmethodID jSwapBuffers; + + Vector2i _previousMouseMovePosition{-1}; + CORRADE_ENUMSET_FRIEND_OPERATORS(Flags) +public: + /* workaround for error "undefined reference to vtable for ..." */ + /** + * @brief Context Container + * + * wraps @ref Containers::Optional + * inside of a container to enable context creation and cleanup + * as surface is created and destroyed. + */ + struct ContextContainer { + Containers::Optional context{InPlaceInit, NoCreate}; + }; + ContextContainer * magnum_context = nullptr; +}; + +/** +@brief Configuration + +@see @ref WindowlessAndroidApplication(), @ref GLConfiguration, @ref create(), + @ref tryCreate() +*/ +class WindowlessAndroidApplication::Configuration { + public: + constexpr /*implicit*/ Configuration() {} + + /** + * @brief Set window title + * @return Reference to self (for method chaining) + * + * @note This function does nothing and is included only for + * compatibility with other toolkits. You need to set the title + * separately in the `AndroidManifest.xml` file. + */ + template Configuration& setTitle(const T&) { return *this; } + + /** @brief Window size */ + Vector2i size() const { return _size; } + + /** + * @brief Set window size + * @return Reference to self (for method chaining) + * + * Default is @cpp {0, 0} @ce, which means that the size of the + * physical window will be used. If set to different value than the + * physical size, the surface will be scaled. + */ + Configuration& setSize(const Vector2i& size) { + _size = size; + return *this; + } + + private: + Vector2i _size; +}; + +/** +@brief OpenGL context configuration + +Double-buffered RGBA canvas with depth and stencil buffers. +@see @ref WindowlessAndroidApplication(), @ref Configuration, @ref create(), + @ref tryCreate() +*/ +class WindowlessAndroidApplication::GLConfiguration: public GL::Context::Configuration { + public: + /** + * @brief Context flag + * + * Includes also everything from @ref GL::Context::Configuration::Flag + * except for @relativeref{GL::Context::Configuration,Flag::Windowless}, + * which is not meant to be enabled for windowed apps. + * @see @ref Flags, @ref setFlags(), @ref GL::Context::Flag + */ + enum class Flag: UnsignedLong { + /** + * @copydoc GL::Context::Configuration::Flag::QuietLog + * @m_since_latest + */ + QuietLog = UnsignedLong(GL::Context::Configuration::Flag::QuietLog), + + /** + * @copydoc GL::Context::Configuration::Flag::VerboseLog + * @m_since_latest + */ + VerboseLog = UnsignedLong(GL::Context::Configuration::Flag::VerboseLog), + + /** + * @copydoc GL::Context::Configuration::Flag::GpuValidation + * @m_since_latest + */ + GpuValidation = UnsignedLong(GL::Context::Configuration::Flag::GpuValidation), + + /** + * @copydoc GL::Context::Configuration::Flag::GpuValidationNoError + * @m_since_latest + */ + GpuValidationNoError = UnsignedLong(GL::Context::Configuration::Flag::GpuValidationNoError) + }; + + /** + * @brief Context flags + * + * @see @ref setFlags(), @ref Context::Flags + */ + typedef Containers::EnumSet Flags; + + /*implicit*/ GLConfiguration(); + + /** @brief Context flags */ + Flags flags() const { + return Flag(UnsignedLong(GL::Context::Configuration::flags())); + } + + /** + * @brief Set context flags + * @return Reference to self (for method chaining) + * + * Default is no flag. To avoid clearing default flags by accident, + * prefer to use @ref addFlags() and @ref clearFlags() instead. + * @see @ref GL::Context::flags() + */ + GLConfiguration& setFlags(Flags flags) { + GL::Context::Configuration::setFlags(GL::Context::Configuration::Flag(UnsignedLong(flags))); + return *this; + } + + /** + * @brief Add context flags + * @return Reference to self (for method chaining) + * + * Unlike @ref setFlags(), ORs the flags with existing instead of + * replacing them. Useful for preserving the defaults. + * @see @ref clearFlags() + */ + GLConfiguration& addFlags(Flags flags) { + GL::Context::Configuration::addFlags(GL::Context::Configuration::Flag(UnsignedLong(flags))); + return *this; + } + + /** + * @brief Clear context flags + * @return Reference to self (for method chaining) + * + * Unlike @ref setFlags(), ANDs the inverse of @p flags with existing + * instead of replacing them. Useful for removing default flags. + * @see @ref addFlags() + */ + GLConfiguration& clearFlags(Flags flags) { + GL::Context::Configuration::clearFlags(GL::Context::Configuration::Flag(UnsignedLong(flags))); + return *this; + } + + /** + * @brief Set context version + * + * @note This function does nothing and is included only for + * compatibility with other toolkits. @ref GL::Version::GLES200 or + * @ref GL::Version::GLES300 is used based on engine compile-time + * settings. + */ + GLConfiguration& setVersion(GL::Version) { return *this; } + + /** @brief Color buffer size */ + Vector4i colorBufferSize() const { return _colorBufferSize; } + + /** + * @brief Set color buffer size + * + * Default is @cpp {8, 8, 8, 8} @ce (8-bit-per-channel RGBA). + * @see @ref setDepthBufferSize(), @ref setStencilBufferSize() + */ + GLConfiguration& setColorBufferSize(const Vector4i& size) { + _colorBufferSize = size; + return *this; + } + + /** @brief Depth buffer size */ + Int depthBufferSize() const { return _depthBufferSize; } + + /** + * @brief Set depth buffer size + * + * Default is @cpp 24 @ce bits. + * @see @ref setColorBufferSize(), @ref setStencilBufferSize() + */ + GLConfiguration& setDepthBufferSize(Int size) { + _depthBufferSize = size; + return *this; + } + + /** @brief Stencil buffer size */ + Int stencilBufferSize() const { return _stencilBufferSize; } + + /** + * @brief Set stencil buffer size + * + * Default is @cpp 0 @ce bits (i.e., no stencil buffer). + * @see @ref setColorBufferSize(), @ref setDepthBufferSize() + */ + GLConfiguration& setStencilBufferSize(Int size) { + _stencilBufferSize = size; + return *this; + } + + /* Overloads to remove WTF-factor from method chaining order */ + #ifndef DOXYGEN_GENERATING_OUTPUT + MAGNUM_GL_CONTEXT_CONFIGURATION_SUBCLASS_IMPLEMENTATION(GLConfiguration) + #endif + + private: + Vector4i _colorBufferSize; + Int _depthBufferSize, _stencilBufferSize; +}; + +/** +@brief Viewport event +@see @ref viewportEvent() +*/ +class WindowlessAndroidApplication::ViewportEvent { + public: + /** @brief Copying is not allowed */ + ViewportEvent(const ViewportEvent&) = delete; + + /** @brief Moving is not allowed */ + ViewportEvent(ViewportEvent&&) = delete; + + /** @brief Copying is not allowed */ + ViewportEvent& operator=(const ViewportEvent&) = delete; + + /** @brief Moving is not allowed */ + ViewportEvent& operator=(ViewportEvent&&) = delete; + + /** + * @brief Window size + * + * The same as @ref framebufferSize(). See + * @ref WindowlessAndroidApplication::windowSize() for possible caveats. + */ + Vector2i windowSize() const { return _windowSize; } + + /** + * @brief Framebuffer size + * + * The same as @ref windowSize(). See + * @ref WindowlessAndroidApplication::framebufferSize() for possible caveats. + */ + Vector2i framebufferSize() const { return _windowSize; } + + /** + * @brief DPI scaling + * + * Always @cpp {1.0f, 1.0f} @ce. + */ + Vector2 dpiScaling() const { return Vector2{1.0f}; } + + private: + friend WindowlessAndroidApplication; + + explicit ViewportEvent(const Vector2i& windowSize): _windowSize{windowSize} {} + + const Vector2i _windowSize; +}; + +/** +@brief Base for input events +@see @ref MouseEvent, @ref MouseMoveEvent, @ref mousePressEvent(), + @ref mouseReleaseEvent(), @ref mouseMoveEvent() +*/ +class WindowlessAndroidApplication::InputEvent { + public: + /** @brief Copying is not allowed */ + InputEvent(const InputEvent&) = delete; + + /** @brief Moving is not allowed */ + InputEvent(InputEvent&&) = delete; + + /** @brief Copying is not allowed */ + InputEvent& operator=(const InputEvent&) = delete; + + /** @brief Moving is not allowed */ + InputEvent& operator=(InputEvent&&) = delete; + + /** + * @brief Set event as accepted + * + * If the event is ignored (i.e., not set as accepted), it will be + * propagated elsewhere, for example to the Android system or to + * another screen when using @ref BasicScreenedApplication "ScreenedApplication". + * By default is each event ignored and thus propagated. + */ + void setAccepted(bool accepted = true) { _accepted = accepted; } + + /** @brief Whether the event is accepted */ + bool isAccepted() const { return _accepted; } + + #ifndef DOXYGEN_GENERATING_OUTPUT + protected: + explicit InputEvent(MotionEventSerializer & event): _event(event), _accepted(false) {} + + ~InputEvent() = default; + + MotionEventSerializer & _event; + #endif + + private: + bool _accepted; +}; + +/** +@brief Mouse event +@see @ref MouseMoveEvent, @ref mousePressEvent(), @ref mouseReleaseEvent() +*/ +class WindowlessAndroidApplication::MouseEvent: public InputEvent { + friend WindowlessAndroidApplication; + + public: + /** + * @brief Mouse button + * + * @see @ref button() + */ + enum class Button: std::int32_t { + /** No button was pressed (touch or stylus event) */ + None = 0, + + /** + * Left mouse button. Note that this button is not set if only + * touch or stylus event occured. + * @attention Available since Android 4.0 (API level 14), not + * detectable in earlier versions. + */ + #if defined(DOXYGEN_GENERATING_OUTPUT) || __ANDROID_API__ >= 14 + Left = MotionEventSerializer::MOTION_EVENT_BUTTON_PRIMARY, + #else + Left = 1 << 0, + #endif + + /** + * Middle mouse button or second stylus button + * @attention Available since Android 4.0 (API level 14), not + * detectable in earlier versions. + */ + #if defined(DOXYGEN_GENERATING_OUTPUT) || __ANDROID_API__ >= 14 + Middle = MotionEventSerializer::MOTION_EVENT_BUTTON_TERTIARY, + #else + Middle = 1 << 1, + #endif + + /** + * Right mouse button or first stylus button + * @attention Available since Android 4.0 (API level 14), not + * detectable in earlier versions. + */ + #if defined(DOXYGEN_GENERATING_OUTPUT) || __ANDROID_API__ >= 14 + Right = MotionEventSerializer::MOTION_EVENT_BUTTON_SECONDARY + #else + Right = 1 << 2 + #endif + }; + + /** @brief Button */ + Button button() { + #if __ANDROID_API__ >= 14 + return Button(button_state); + #else + return Button::None; + #endif + } + + /** @brief Position */ + Vector2i position() { + return {Int(x),Int(y)}; + } + + private: + float x; + float y; + int button_state; + explicit MouseEvent(MotionEventSerializer & event, float x_, float y_, int button_state_): + InputEvent(event), x(x_), y(y_), button_state(button_state_) {} +}; + +/** +@brief Mouse move event +@see @ref MouseEvent, @ref mouseMoveEvent() +*/ +class WindowlessAndroidApplication::MouseMoveEvent: public InputEvent { + friend WindowlessAndroidApplication; + + public: + /** + * @brief Mouse button + * + * @see @ref buttons() + */ + enum class Button: std::int32_t { + /** + * Left mouse button. Note that this button is not set if only + * touch or stylus event occured. + * @attention Available since Android 4.0 (API level 14), not + * detectable in earlier versions. + */ + #if defined(DOXYGEN_GENERATING_OUTPUT) || __ANDROID_API__ >= 14 + Left = MotionEventSerializer::MOTION_EVENT_BUTTON_PRIMARY, + #else + Left = 1 << 0, + #endif + + /** + * Middle mouse button or second stylus button + * @attention Available since Android 4.0 (API level 14), not + * detectable in earlier versions. + */ + #if defined(DOXYGEN_GENERATING_OUTPUT) || __ANDROID_API__ >= 14 + Middle = MotionEventSerializer::MOTION_EVENT_BUTTON_TERTIARY, + #else + Middle = 1 << 1, + #endif + + /** + * Right mouse button or first stylus button + * @attention Available since Android 4.0 (API level 14), not + * detectable in earlier versions. + */ + #if defined(DOXYGEN_GENERATING_OUTPUT) || __ANDROID_API__ >= 14 + Right = MotionEventSerializer::MOTION_EVENT_BUTTON_SECONDARY + #else + Right = 1 << 2 + #endif + }; + + /** + * @brief Set of mouse buttons + * + * @see @ref buttons() + */ + typedef Containers::EnumSet