diff --git a/CMakeLists.txt b/CMakeLists.txt index 417111e6c..24909f7d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,8 @@ if(EMSCRIPTEN) add_compile_options(-O1) endif() + set(INCLUDE_NATIVE_COMPONENTS OFF) +elseif(ANDROID) set(INCLUDE_NATIVE_COMPONENTS OFF) else() set(INCLUDE_NATIVE_COMPONENTS ON) @@ -78,8 +80,28 @@ option(FORCE_VENDORED_Eigen3 set(OPENGL 3 CACHE STRING "OpenGL version to use (one of: 1 3)") +if(ANDROID) + set(ANDROID_JAR CACHE STRING "Path to android.jar") + set(BUILD_TOOL CACHE STRING "Path to build tool root") + if(NOT BUILD_TOOL) + message(FATAL_ERROR "Require BUILD_TOOL") + endif() + if(NOT ANDROID_JAR) + message(FATAL_ERROR "Require ANDROID_JAR") + endif() + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + set(CMAKE_SHARED_LINKER_FLAGS "-static-libstdc++") + # for termux to find liblog, libandroid + set(CMAKE_PATH_PREFIX "/system/lib64;${CMAKE_PATH_PREFIX}") +endif() + set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) -set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) +if(ANDROID) + set(ANDROID_ABI arm64-v8a CACHE STRING "Android ABI") + set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib/${ANDROID_ABI}) +else() + set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) +endif() if("${CMAKE_GENERATOR}" STREQUAL "Xcode") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_BINARY_DIR}/bin>) endif() @@ -139,6 +161,9 @@ if(ENABLE_OPENMP) if(OPENMP_FOUND) add_library(slvs_openmp INTERFACE) target_compile_options(slvs_openmp INTERFACE "SHELL:${OpenMP_CXX_FLAGS}") + if(ANDROID) + set(OpenMP_CXX_LIBRARIES -fopenmp -static-openmp) + endif() target_link_libraries(slvs_openmp INTERFACE ${OpenMP_CXX_LIBRARIES}) target_include_directories(slvs_openmp SYSTEM INTERFACE @@ -278,7 +303,7 @@ endif() if(ENABLE_GUI OR ENABLE_CLI) - if(WIN32 OR APPLE OR EMSCRIPTEN) + if(WIN32 OR APPLE OR EMSCRIPTEN OR ANDROID) # On Win32 and macOS we use vendored packages, since there is little to no benefit # to trying to find system versions. In particular, trying to link to libraries from # Homebrew or macOS system libraries into the .app file is highly likely to result @@ -287,7 +312,9 @@ if(ENABLE_GUI OR ENABLE_CLI) include(FindVendoredPackage) include(AddVendoredSubdirectory) - set(FORCE_VENDORED_ZLIB ON) + if(NOT ANDROID) + set(FORCE_VENDORED_ZLIB ON) + endif() set(FORCE_VENDORED_PNG ON) set(FORCE_VENDORED_Freetype ON) @@ -300,14 +327,19 @@ if(ENABLE_GUI OR ENABLE_CLI) # generate an imported target since we'll have proper targets, but libpng would # still need to be updated to use ZLIB::ZLIBSTATIC instead of ZLIB::ZLIB, so this # ugly hack will have to stay until both libraries release fixed versions. - add_vendored_subdirectory(extlib/zlib) - set(ZLIB_LIBRARY zlibstatic CACHE INTERNAL "") - set(ZLIB_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/zlib CACHE INTERNAL "") - add_library(ZLIB::ZLIB ALIAS ${ZLIB_LIBRARY}) - find_package(ZLIB REQUIRED) - set(ZLIB_FOUND ${ZLIB_FOUND} CACHE INTERNAL "") - - list(APPEND ZLIB_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/zlib) + if(ANDROID) + find_library(log-lib log) + find_library(droid-lib android) + else() + add_vendored_subdirectory(extlib/zlib) + set(ZLIB_LIBRARY zlibstatic CACHE INTERNAL "") + set(ZLIB_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/zlib CACHE INTERNAL "") + add_library(ZLIB::ZLIB ALIAS ${ZLIB_LIBRARY}) + find_package(ZLIB REQUIRED) + set(ZLIB_FOUND ${ZLIB_FOUND} CACHE INTERNAL "") + + list(APPEND ZLIB_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/zlib) + endif() # There's an issue with the zlib configuration, which doesn't provide a way to # disable the shared or static library builds, and on UNIX sets the base name of @@ -343,22 +375,24 @@ if(ENABLE_GUI OR ENABLE_CLI) FREETYPE_LIBRARY freetype FREETYPE_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/freetype/include) - message(STATUS "Using in-tree pixman") - set(PIXMAN_FOUND YES) - set(PIXMAN_LIBRARY pixman) - set(PIXMAN_BUILD_TESTS OFF CACHE BOOL "") - set(PIXMAN_BUILD_DEMOS OFF CACHE BOOL "") - - set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) - list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/pixman) - add_vendored_subdirectory(extlib/pixman) - - message(STATUS "Using in-tree cairo") - add_vendored_subdirectory(extlib/cairo) - set(CAIRO_FOUND YES) - set(CAIRO_LIBRARIES cairo) - set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src) - list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src) + if(NOT ANDROID) + message(STATUS "Using in-tree pixman") + set(PIXMAN_FOUND YES) + set(PIXMAN_LIBRARY pixman) + set(PIXMAN_BUILD_TESTS OFF CACHE BOOL "") + set(PIXMAN_BUILD_DEMOS OFF CACHE BOOL "") + + set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) + list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/pixman) + add_vendored_subdirectory(extlib/pixman) + + message(STATUS "Using in-tree cairo") + add_vendored_subdirectory(extlib/cairo) + set(CAIRO_FOUND YES) + set(CAIRO_LIBRARIES cairo) + set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src) + list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src) + endif() else() # On Linux and BSDs we're a good citizen and link to system libraries. find_package(Backtrace) @@ -405,6 +439,8 @@ if(ENABLE_GUI) find_library(APPKIT_LIBRARY AppKit REQUIRED) elseif(EMSCRIPTEN) # Everything is built in + elseif(ANDROID) + set(OPENGL_LIBRARIES EGL GLESv2) else() find_package(OpenGL REQUIRED) find_package(SpaceWare) @@ -533,7 +569,7 @@ add_subdirectory(src) if(ENABLE_TESTS) add_subdirectory(test) endif() -if((ENABLE_CLI OR ENABLE_GUI) AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) +if((ENABLE_CLI OR ENABLE_GUI) AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") AND (NOT ANDROID)) add_subdirectory(bench) else() message(STATUS "Benchmarking disabled in debug and library builds.") diff --git a/README.md b/README.md index a4513b922..0f29a0dd4 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,70 @@ ninja [mingw]: http://www.mingw.org/ [msys2]: https://www.msys2.org/ +## Building for Android (Experimental) + +> [!NOTE] +> This port contains many critical bugs and unimplemented core functions. + +Before building, [check out the project and the necessary submodules](#via-source-code). + +> [!TIP] +> Android port doesn't need extlib/angle, extlib/cairo, extlib/pixman, extlib/zlib submodules. + +### Building with Android NDK + +First, install Android NDK and Build-Tools via sdkmanager(CLI or Android Studio) or download from [website](https://developer.android.com/ndk/downloads/) + +and install Java, make/ninja and cmake: + +```sh +# Ubuntu/Debian +sudo apt install default-jre cmake make +# macOS +brew install openjdk@17 cmake make +# Windows +winget install --id EclipseAdoptium.Temurin.17.JDK -e -h +winget install --id Kitware.CMake -e -h +winget install --id Ninja-build.Ninja -e -h +``` + +Also keep an `android.jar` file from Android Platforms or elsewhere like +[Sable/android-platforms](//github.com/Sable/android-platforms). + +After that, build SolveSpace as following: + +```sh +cmake -Bbuild -S. -DCMAKE_TOOLCHAIN_FILE=$ANDROID_SDK_ROOT/ndk/xx.xx/build/cmake/android.toolchain.cmake + -DANDROID_JAR=/path/to/android.jar -DBUILD_TOOL=$ANDROID_SDK_ROOT/build-tool/xx.xx/ + -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-21 + -DENABLE_LTO=ON -DENABLE_OPENMP=ON -DCMAKE_BUILD_TYPE=Debug +cmake --build build -j8 +``` + +This will build an app.apk in the `build/bin` directory, and then install it with +`adb install --no-streaming build/bin/app.apk`. + +### Building on Termux + +First, install tools for building apk: + +```sh +pkg i openjdk-21 d8 aapt apksigner cmake make clang ndk-multilib eigen zlib +wget -c -k -O /path/to/android.jar https://raw.githubusercontent.com/Sable/android-platforms/master/android-36/android.jar +``` + +After that, build SolveSpace as following: + +```sh +cmake -Bbuild -S. + -DANDROID_JAR=/path/to/android.jar -DBUILD_TOOL=$PREFIX/bin + -DANDROID_ABI=arm64-v8a -DENABLE_OPENMP=ON -DENABLE_LTO=ON -DCMAKE_BUILD_TYPE=Debug + -DZLIB_ROOT=/system/lib64 -DEIGEN3_INCLUDE_DIRS=$PREFIX/include/eigen3 +make -Cbuild -j8 +``` + +To install the generated apk, run `xdg-open build/bin/app.apk`. + ## Contributing See the [guide for contributors](CONTRIBUTING.md) for the best way to file issues, contribute code, diff --git a/res/AndroidManifest.xml b/res/AndroidManifest.xml new file mode 100644 index 000000000..6c8d5e9fd --- /dev/null +++ b/res/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/CMakeLists.txt b/res/CMakeLists.txt index c4f958ed4..e63fb25e1 100644 --- a/res/CMakeLists.txt +++ b/res/CMakeLists.txt @@ -88,10 +88,13 @@ elseif(EMSCRIPTEN) set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name}) set(resource_list "${resource_list};${source}" PARENT_SCOPE) endfunction() -else() # Unix - include(GNUInstallDirs) - - set(app_resource_dir ${CMAKE_BINARY_DIR}/res) +else() # Unix & Android + if(ANDROID) + set(app_resource_dir ${CMAKE_BINARY_DIR}/assets) + else() + include(GNUInstallDirs) + set(app_resource_dir ${CMAKE_BINARY_DIR}/res) + endif() function(add_resource name) set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name}) @@ -107,9 +110,11 @@ else() # Unix DEPENDS ${source} VERBATIM) - get_filename_component(name_dir ${name} DIRECTORY) - install(FILES ${source} - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/solvespace/${name_dir}) + if (NOT ANDROID) + get_filename_component(name_dir ${name} DIRECTORY) + install(FILES ${source} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/solvespace/${name_dir}) + endif() endfunction() endif() @@ -128,7 +133,7 @@ elseif(APPLE) add_iconset (cocoa/AppIcon.iconset) add_xib (cocoa/MainMenu.xib) add_xib (cocoa/SaveFormatAccessory.xib) -else() +elseif(NOT ANDROID) add_resource(freedesktop/solvespace-48x48.png) if(FLATPAK) @@ -324,4 +329,33 @@ add_custom_target(resources SOURCES ${resource_list}) if(WIN32) set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file}) +elseif(ANDROID) + set(droid_res ${resource_root}/android) + file(GLOB_RECURSE res_list ${droid_res}/*) + set(MIN_SDK 21) + set(TAG_SDK 34) + math(EXPR VERCODE "${PROJECT_VERSION_MAJOR}*100000+${PROJECT_VERSION_MINOR}*1000") + set(VERNAME "${PROJECT_VERSION}~${solvespace_GIT_HASH}") + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(debugmode --debug-mode) + else() + set(debugmode "") + endif() + add_custom_target(aapt_pre + BYPRODUCTS ${CMAKE_BINARY_DIR}/bin/res.apk ${CMAKE_BINARY_DIR}/gen/com/solvespace/R.java + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/gen + COMMAND ${BUILD_TOOL}/aapt p -f -m + -F ${CMAKE_BINARY_DIR}/bin/res.apk + -I ${ANDROID_JAR} + -S ${droid_res} + -A ${app_resource_dir} + -M ${resource_root}/AndroidManifest.xml + ${debugmode} + --version-code ${VERCODE} + --version-name ${VERNAME} + --min-sdk-version ${MIN_SDK} + --target-sdk-version ${TAG_SDK} + -J ${CMAKE_BINARY_DIR}/gen + COMMENT "aapt pack" + DEPENDS ${res_list} ${resource_root}/AndroidManifest.xml) endif() diff --git a/res/android/drawable-hdpi/ic_launcher.png b/res/android/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..99847c219 Binary files /dev/null and b/res/android/drawable-hdpi/ic_launcher.png differ diff --git a/res/android/drawable-mdpi/ic_launcher.png b/res/android/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..cd22c42d8 Binary files /dev/null and b/res/android/drawable-mdpi/ic_launcher.png differ diff --git a/res/android/drawable-xhdpi/ic_launcher.png b/res/android/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..a2545e12d Binary files /dev/null and b/res/android/drawable-xhdpi/ic_launcher.png differ diff --git a/res/android/drawable-xxhdpi/ic_launcher.png b/res/android/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..00ce5024b Binary files /dev/null and b/res/android/drawable-xxhdpi/ic_launcher.png differ diff --git a/res/android/layout/main.xml b/res/android/layout/main.xml new file mode 100644 index 000000000..3e4f1ca7b --- /dev/null +++ b/res/android/layout/main.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/res/android/values/ids.xml b/res/android/values/ids.xml new file mode 100644 index 000000000..ccb4e5fa7 --- /dev/null +++ b/res/android/values/ids.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/res/android/values/string.xml b/res/android/values/string.xml new file mode 100644 index 000000000..ebe2bb60b --- /dev/null +++ b/res/android/values/string.xml @@ -0,0 +1,4 @@ + + Yes + No + \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3af207599..3227730bf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,7 +38,7 @@ target_link_libraries(slvs_deps INTERFACE ${FREETYPE_LIBRARY} mimalloc-static) -if(NOT USE_QT_GUI OR ENABLE_CLI) +if(NOT USE_QT_GUI AND NOT ANDROID OR ENABLE_CLI) target_include_directories(slvs_deps SYSTEM INTERFACE ${CAIRO_INCLUDE_DIRS}) target_link_libraries(slvs_deps INTERFACE ${CAIRO_LIBRARIES}) endif() @@ -106,7 +106,8 @@ if(ENABLE_GUI OR ENABLE_CLI) platform/guiwin.cpp platform/guigtk.cpp platform/guimac.mm - platform/guihtml.cpp) + platform/guihtml.cpp + platform/guidroid.cpp) # solvespace library @@ -172,7 +173,14 @@ if(ENABLE_GUI OR ENABLE_CLI) ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(solvespace-core PRIVATE slvs-solver) target_link_libraries(solvespace-core PUBLIC slvs_deps) - + if(ANDROID) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + set_target_properties(solvespace-core PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_link_libraries(solvespace-core + PUBLIC + ${droid-lib} + ${log-lib}) + endif() # solvespace translations if(HAVE_GETTEXT) @@ -260,11 +268,18 @@ endif() # solvespace graphical executable if(ENABLE_GUI) - add_executable(solvespace WIN32 MACOSX_BUNDLE - ${solvespace_core_gl_SOURCES} - $) + if(ANDROID) + add_library(solvespace + SHARED + ${solvespace_core_gl_SOURCES} + $) + else() + add_executable(solvespace WIN32 MACOSX_BUNDLE + ${solvespace_core_gl_SOURCES} + $) + endif() - if(NOT USE_QT_GUI) + if(NOT USE_QT_GUI AND NOT ANDROID) target_sources(solvespace PRIVATE platform/entrygui.cpp) endif() @@ -400,6 +415,55 @@ if(ENABLE_GUI) Qt6::Core Qt6::Gui Qt6::OpenGLWidgets Qt6::Widgets # Qt5::Core Qt5::Gui Qt5::Widgets ) + elseif(ANDROID) + target_sources(solvespace PRIVATE + platform/guidroid.cpp) + file(GLOB_RECURSE java_src platform/java/*.java) + set(LIBDIR lib/${ANDROID_ABI}) + if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + set(HOME $ENV{USERPROFILE}) + set(LIST_CLS dir /s /b classes\\*.class) + else() + set(HOME $ENV{HOME}) + set(LIST_CLS find classes -name *.class) + endif() + if(CMAKE_BUILD_TYPE STREQUAL "Release") + set(strip ${CMAKE_STRIP} -s ${LIBDIR}/libsolvespace.so) + else() + set(strip "") + endif() + file(TO_NATIVE_PATH ${HOME}/.android/debug.keystore kspath) + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/cls.txt + COMMAND javac -cp ${ANDROID_JAR} -source 1.8 -target 1.8 + -d classes + ${java_src} gen/com/solvespace/R.java + COMMAND ${LIST_CLS} > cls.txt + DEPENDS ${java_src} aapt_pre + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Compiling java" + VERBATIM) + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/classes.dex + COMMAND ${BUILD_TOOL}/d8 --output ./ --lib ${ANDROID_JAR} --min-api 21 @cls.txt + DEPENDS ${CMAKE_BINARY_DIR}/cls.txt + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Dexing classes to dex" + VERBATIM) + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/bin/app.apk + COMMAND ${strip} + COMMAND jar -u -f bin/res.apk classes.dex ${LIBDIR}/libsolvespace.so + COMMAND ${BUILD_TOOL}/zipalign -f -p 4 bin/res.apk bin/app.apk + COMMAND ${BUILD_TOOL}/apksigner sign --ks ${kspath} --ks-pass pass:android --v2-signing-enabled false bin/app.apk + DEPENDS ${CMAKE_BINARY_DIR}/classes.dex solvespace + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Packaging" + VERBATIM) + add_custom_target(gen_apk ALL + DEPENDS ${CMAKE_BINARY_DIR}/bin/app.apk) + set_directory_properties(PROPERTIES ADDITIONAL_CLEAN_FILES + ${CMAKE_BINARY_DIR}/classes/) else() target_sources(solvespace PRIVATE platform/guigtk.cpp) diff --git a/src/mouse.cpp b/src/mouse.cpp index 76173bf6a..dd587cf83 100644 --- a/src/mouse.cpp +++ b/src/mouse.cpp @@ -810,9 +810,10 @@ void GraphicsWindow::MouseRightUp(double x, double y) { } menu->PopUp(); - +#ifndef __ANDROID__ context.active = false; SS.ScheduleShowTW(); +#endif } hRequest GraphicsWindow::AddRequest(Request::Type type) { @@ -961,8 +962,8 @@ void GraphicsWindow::MouseLeftDown(double mx, double my, bool shiftDown, bool ct } SS.TW.HideEditControl(); - if(SS.showToolbar) { - if(ToolbarMouseDown((int)mx, (int)my)) return; + if(SS.showToolbar && ToolbarMouseDown((int)mx, (int)my)) { + return; } // This will be clobbered by MouseMoved below. diff --git a/src/platform/guidroid.cpp b/src/platform/guidroid.cpp new file mode 100644 index 000000000..e839a0742 --- /dev/null +++ b/src/platform/guidroid.cpp @@ -0,0 +1,1382 @@ +//----------------------------------------------------------------------------- +// The Android-based implementation of platform-dependent GUI functionality. +// +// Copyright 2025 +//----------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gui.h" +#include "solvespace.h" + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Fatal errors +//----------------------------------------------------------------------------- + +void FatalError(const std::string &message) { + android_set_abort_message(message.c_str()); + abort(); +} + +//----------------------------------------------------------------------------- +// Android-specific JNI interface +//----------------------------------------------------------------------------- + +// Global JNI references +static JavaVM* g_JavaVM = nullptr; +static jobject g_Activity = nullptr; + +// JNI method IDs +// SolveSpaceActivity +static jmethodID g_ShowEditorMethod = nullptr; +static jmethodID g_HideEditorMethod = nullptr; +static jmethodID g_IsEditorVisibleMethod = nullptr; +static jmethodID g_SendDelayedMethod = nullptr; +static jmethodID g_ShowMessageDialogMethod = nullptr; +static jmethodID g_ShowFileDialogMethod = nullptr; +static jmethodID g_SetTitle = nullptr; +static jmethodID g_OnWinAdded = nullptr; +static jmethodID g_GetDensity = nullptr; +static jmethodID g_SetScrollbar = nullptr; +static jmethodID g_Popup = nullptr; +static jmethodID g_OpenContentFileMethod = nullptr; +static jmethodID g_DeleteContentFileMethod = nullptr; +static jmethodID g_GetInternalStoragePathMethod = nullptr; +static jmethodID g_Finish = nullptr; +static jmethodID g_GetSysFonts = nullptr; +static jmethodID g_InvalidateMenu = nullptr; +static jmethodID g_FileName = nullptr; +// Menu +static jmethodID g_Add = nullptr; +static jmethodID g_AddSubMenu = nullptr; +static jmethodID g_Clear = nullptr; +// MenuItem +static jmethodID g_GetItemId = nullptr; +static jmethodID g_SetAlphabeticShortcut = nullptr; +static jmethodID g_SetCheckable = nullptr; +static jmethodID g_SetChecked = nullptr; +static jmethodID g_SetEnabled = nullptr; +static jmethodID g_SetShowAsAction = nullptr; +// SharedPreferences +static jmethodID g_GetPreferences = nullptr; +static jmethodID g_GetInt = nullptr; +static jmethodID g_GetFloat = nullptr; +static jmethodID g_GetString = nullptr; +static jmethodID g_Edit = nullptr; +// SharedPreferences$Editor +static jmethodID g_PutInt = nullptr; +static jmethodID g_PutFloat = nullptr; +static jmethodID g_PutString = nullptr; +static jmethodID g_Commit = nullptr; + +static EGLDisplay gDisplay = EGL_NO_DISPLAY; + +JNIEnv* GetJNIEnv() { + ssassert(g_JavaVM != NULL, "g_JavaVM is NULL"); + + JNIEnv* env; + int status = g_JavaVM->GetEnv((void**)&env, JNI_VERSION_1_6); + if (status == JNI_EDETACHED) { + ssassert(g_JavaVM->AttachCurrentThread(&env, nullptr) == JNI_OK, "Attach Failed"); + } else { + char str[20]; + sprintf(str, "GetEnv error: %d", status); + ssassert(status == JNI_OK, str); + } + return env; +} + +// Opens a Storage Access Framework content URI through the Java activity's +// ContentResolver. The Java side detaches the descriptor from its +// ParcelFileDescriptor, so ownership passes to the caller (which must close +// it, e.g. via fdopen + fclose). Returns -1 on failure. +int AndroidOpenContentFile(const char *uri, const char *mode) { + JNIEnv *env = GetJNIEnv(); + if (!(env && g_Activity && g_OpenContentFileMethod)) return -1; + + jstring juri = env->NewStringUTF(uri); + jstring jmode = env->NewStringUTF(mode); + jint fd = env->CallIntMethod(g_Activity, g_OpenContentFileMethod, juri, jmode); + env->DeleteLocalRef(juri); + env->DeleteLocalRef(jmode); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + return -1; + } + return fd; +} + +// Deletes a Storage Access Framework content URI through the Java activity. +void AndroidDeleteContentFile(const char *uri) { + JNIEnv *env = GetJNIEnv(); + if (!(env && g_Activity && g_DeleteContentFileMethod)) return; + + jstring juri = env->NewStringUTF(uri); + env->CallBooleanMethod(g_Activity, g_DeleteContentFileMethod, juri); + env->DeleteLocalRef(juri); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + } +} + +// Returns the absolute path of the app's private files directory, or an empty +// string if the Java activity is not available. +std::string AndroidInternalStoragePath() { + JNIEnv *env = GetJNIEnv(); + if (!(env && g_Activity && g_GetInternalStoragePathMethod)) return ""; + + jstring ret = (jstring)env->CallObjectMethod(g_Activity, g_GetInternalStoragePathMethod); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return ""; + } + if (!ret) return ""; + const char *str = env->GetStringUTFChars(ret, NULL); + std::string result = str ? str : ""; + if (str) env->ReleaseStringUTFChars(ret, str); + env->DeleteLocalRef(ret); + return result; +} + +// Maps a content URI to a filesystem-safe string, so it can be used as a +// filename/key in private storage (e.g. for autosave companions). +std::string AndroidContentKey(const char *uri) { + std::string key; + for (const char *p = uri; *p; ++p) { + unsigned char c = (unsigned char)*p; + if (isalnum(c) || c == '.' || c == '-' || c == '_') { + key += (char)c; + } else { + key += '_'; + } + } + return key; +} + +std::string DroidFileName(const char *uri) { + JNIEnv *env = GetJNIEnv(); + if (!env || !g_Activity) { + return ""; + } + jstring juri = env->NewStringUTF(uri); + jstring jstr = (jstring)env->CallObjectMethod(g_Activity, g_FileName, juri); + env->DeleteLocalRef(juri); + const char *str = env->GetStringUTFChars(jstr, NULL); + std::string ret = str ? str : ""; + if (str) env->ReleaseStringUTFChars(jstr, str); + env->DeleteLocalRef(jstr); + return ret; +} +//----------------------------------------------------------------------------- +// EGL and GL context management +//----------------------------------------------------------------------------- + +class EGLContextManager { +private: + EGLContext m_Context; + EGLSurface m_Surf = EGL_NO_SURFACE; + static EGLConfig m_Config; + +public: + ANativeWindow *m_NativeWindow; + EGLContextManager() : + m_Context(EGL_NO_CONTEXT), + m_Surf(EGL_NO_SURFACE), + m_NativeWindow(nullptr) {} + + bool Initialize() { + if (gDisplay == EGL_NO_DISPLAY) { + // Get display + gDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (gDisplay == EGL_NO_DISPLAY) { + ALOGE("Failed to get EGL display"); + return false; + } + + // Initialize EGL + if (!eglInitialize(gDisplay, 0, 0)) { + ALOGE("Failed to initialize EGL"); + return false; + } + + // Choose config + const EGLint configAttribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_STENCIL_SIZE, 8, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_NONE + }; + + EGLint numConfigs; + if (!eglChooseConfig(gDisplay, configAttribs, &m_Config, 1, &numConfigs)) { + ALOGE("Failed to choose EGL config"); + return false; + } + + EGLint format; + eglGetConfigAttrib(gDisplay, m_Config, EGL_NATIVE_VISUAL_ID, &format); + //NativeWindow_setBuffersGeometry(g_NativeWindow, 0, 0, format); + } + + // Create context + const EGLint contextAttribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + + m_Context = eglCreateContext(gDisplay, m_Config, EGL_NO_CONTEXT, contextAttribs); + dbp("CreateContext"); + if (m_Context == EGL_NO_CONTEXT) { + ALOGE("Failed to create EGL context"); + return false; + } + + // Create surface + return CreateSurface(); + } + + bool CreateSurface() { + if (!m_NativeWindow) return false; + if (m_Surf == EGL_NO_SURFACE) { + m_Surf = eglCreateWindowSurface(gDisplay, m_Config, (EGLNativeWindowType)m_NativeWindow, NULL); + dbp("create surf %p", m_Surf); + } else dbp("surf %p", m_Surf); + + // Make current + if (!eglMakeCurrent(gDisplay, m_Surf, m_Surf, m_Context)) { + ALOGE("Failed to make EGL context current"); + return false; + } + + return true; + } + + void DestroySurface() { + if (gDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(gDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (m_Surf != EGL_NO_SURFACE) { + eglDestroySurface(gDisplay, m_Surf); + m_Surf = EGL_NO_SURFACE; + } + } + } + + void Terminate() { + DestroySurface(); + if (gDisplay != EGL_NO_DISPLAY) { + if (m_Context != EGL_NO_CONTEXT) { + eglDestroyContext(gDisplay, m_Context); + m_Context = EGL_NO_CONTEXT; + } + eglTerminate(gDisplay); + gDisplay = EGL_NO_DISPLAY; + } + ANativeWindow_release(m_NativeWindow); + } + + __inline__ void SwapBuffers() { + if (gDisplay != EGL_NO_DISPLAY && m_Surf != EGL_NO_SURFACE) { + eglSwapBuffers(gDisplay, m_Surf); + } + } + + __inline__ bool MakeCurrent() { + return eglMakeCurrent(gDisplay, m_Surf, m_Surf, m_Context); + } +}; + +EGLConfig EGLContextManager::m_Config = nullptr; + +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- +class Attachable { +public: + virtual void Attach(JNIEnv *env, jobject menu) = 0; +}; + +#define MITM_ENABLED 1 +#define MITM_CHECKABLE 2 +#define MITM_CHECKED 4 +class MenuItemImplAndroid final : public MenuItem, public Attachable { +public: + static std::unordered_map item_map; + static jint nextId; + jint id, stat = MITM_ENABLED; + jchar alt = '\0'; + jweak ref = nullptr; + std::string title; + + ~MenuItemImplAndroid() { + if (item_map.find(id)!=item_map.end()) { + item_map.erase(id); + } + JNIEnv *env = GetJNIEnv(); + if (env && ref) { + env->DeleteWeakGlobalRef(ref); + ref = nullptr; + } + } + + void SetAccelerator(KeyboardEvent accel) override { + } + + void SetIndicator(Indicator type) override { + switch(type) { + case Indicator::NONE: + stat &= ~MITM_CHECKABLE; + break; + case Indicator::RADIO_MARK: + case Indicator::CHECK_MARK: + stat |= MITM_CHECKABLE; + break; + } + } + + void SetActive(bool active) override { + if (active) { + stat |= MITM_CHECKED; + } else { + stat &= ~MITM_CHECKED; + } + JNIEnv *env = GetJNIEnv(); + if (env && ref && !env->IsSameObject(ref, NULL)) { + env->CallObjectMethod(ref, g_SetChecked, active); + } + } + + void SetEnabled(bool enabled) override { + if(enabled) { + stat |= MITM_ENABLED; + } else { + stat &= ~MITM_ENABLED; + } + JNIEnv *env = GetJNIEnv(); + if (env && ref && !env->IsSameObject(ref, NULL)) { + env->CallObjectMethod(ref, g_SetEnabled, enabled); + } + } + + void Attach(JNIEnv *env, jobject menu) override { + jstring str = env->NewStringUTF(title.c_str()); + jobject item = env->CallObjectMethod(menu, g_Add, 0, id, 0, str); + env->DeleteLocalRef(str); + if (alt) { + // TODO: verify whether the Alt key works + env->CallObjectMethod(item, g_SetAlphabeticShortcut, alt, AMETA_ALT_ON); + } + ref = env->NewWeakGlobalRef(item); + env->CallObjectMethod(item, g_SetCheckable, 0!=(stat&MITM_CHECKABLE)); + env->CallObjectMethod(item, g_SetEnabled, 0!=(stat&MITM_ENABLED)); + env->CallObjectMethod(item, g_SetChecked, 0!=(stat&MITM_CHECKED)); + env->CallVoidMethod(item, g_SetShowAsAction, 1); + } + + static TimerRef GetTimer() { + static TimerRef ref = CreateTimer(); + return ref; + } +}; + +std::unordered_map MenuItemImplAndroid::item_map; +jint MenuItemImplAndroid::nextId = 1; + +class MenuImplAndroid final : public Menu, public Attachable, public std::enable_shared_from_this { +public: + std::vector> menus; + std::string title; + jchar alt = '\0'; + + MenuImplAndroid() { + } + + MenuItemRef AddItem(const std::string &label, + std::function onTrigger = NULL, + bool mnemonics = true) override { + auto menuItem = std::make_shared(); + menuItem->id = MenuItemImplAndroid::nextId++; + menuItem->onTrigger = onTrigger; + auto pAlt = label.find('&'); + menuItem->title.assign(label); + if (pAlt != std::string::npos && pAlt+1alt = label[pAlt+1]; + menuItem->title.erase(pAlt, 1); + } + MenuItemImplAndroid::item_map[menuItem->id] = menuItem.get(); + menus.push_back(menuItem); + return menuItem; + } + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + subMenu->title.assign(label); + auto pAlt = label.find('&'); + if (pAlt != std::string::npos && pAlt+1 < label.size()) { + subMenu->alt = label[pAlt+1]; + subMenu->title.erase(pAlt, 1); + } + menus.push_back(subMenu); + return subMenu; + } + + void AddSeparator() override { + // Android do not support separator + } + + void PopUp() override { + JNIEnv *env = GetJNIEnv(); + if (env && g_Activity) { + MenuRef ref = shared_from_this(); + MenuRef *pref = new MenuRef(std::move(ref)); + env->CallVoidMethod(g_Activity, g_Popup, (jlong)pref); + } + } + + void Clear() override { + menus.clear(); + } + + void Attach(JNIEnv *env, jobject jmenu) override { + jstring str = env->NewStringUTF(title.c_str()); + jobject subMenu = env->CallObjectMethod(jmenu, g_AddSubMenu, str); + env->DeleteLocalRef(str); + for (const auto& m : menus) { + m->Attach(env, subMenu); + } + } +}; + +class MenuBarImplAndroid final : public MenuBar { +public: + std::vector> subMenus; + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + auto pAlt = label.find('&'); + subMenu->title.assign(label); + if (pAlt != std::string::npos && pAlt+1alt = label[pAlt+1]; + subMenu->title.erase(pAlt, 1); + } + subMenus.push_back(subMenu); + + return subMenu; + } + + void Clear() override { + subMenus.clear(); + } +}; + +MenuRef CreateMenu() { + return std::make_shared(); +} + +MenuBarRef GetOrCreateMainMenu(bool *unique) { + static std::shared_ptr mainMenu; + if(!mainMenu) { + mainMenu = std::make_shared(); + } + *unique = true; + return mainMenu; +} + +//----------------------------------------------------------------------------- +// Android Window Implementation +//----------------------------------------------------------------------------- +// Global window map (Android typically has one main window) +static int g_NextWindowId = 0; +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeOnWindowChanged(JNIEnv *env, jclass jc, jint winId); +class WindowImplAndroid final : public Window { +private: + bool m_Visible; + bool m_FullScreen; + std::string m_Title; + MenuBarRef m_MenuBar; + float m_ScrollMin = 0.0; + float m_ScrollMax = 1.0; + float m_ScrollPage = 0.0; + float m_ScrollPos = 0.0; + bool m_ScrollVis = false; + TimerRef m_Inval; + +public: + int id; + EGLContextManager m_EGLContext; + + WindowImplAndroid(Window::Kind kind) : + m_Visible(false), + m_FullScreen(false) { + id = g_NextWindowId; + // Android typically has only one window (the activity) + JNIEnv *env = GetJNIEnv(); + if (env && g_Activity) { + jobject jsurf = env->CallObjectMethod(g_Activity, g_OnWinAdded, id); + ANativeWindow *navWin = ANativeWindow_fromSurface(env, jsurf); + ANativeWindow_acquire(navWin); + m_EGLContext.m_NativeWindow = navWin; + m_EGLContext.Initialize(); + } + g_NextWindowId++; + } + + ~WindowImplAndroid() override { + m_EGLContext.Terminate(); + } + + void CreateSurface() { + m_EGLContext.CreateSurface(); + } + + // Window interface implementation + double GetDevicePixelRatio() override { + // Get density from Android display metrics + JNIEnv* env = GetJNIEnv(); + if (!env || !g_Activity) return 1.0; + + return env->CallFloatMethod(g_Activity, g_GetDensity); + } + + double GetPixelDensity() override { + return GetDevicePixelRatio() * 160; + } + + bool IsVisible() override { + return m_Visible; + } + + void SetVisible(bool visible) override { + m_Visible = visible; + // Visibility is controlled by Android activity lifecycle + } + + void Focus() override { + // Android windows are always focused when visible + } + + bool IsFullScreen() override { + return m_FullScreen; + } + + void SetFullScreen(bool fullScreen) override { + m_FullScreen = fullScreen; + // Request fullscreen through JNI + JNIEnv* env = GetJNIEnv(); + if (env && g_Activity) { + jclass activityClass = env->GetObjectClass(g_Activity); + jmethodID setFullscreenMethod = env->GetMethodID(activityClass, "setFullScreen", "(Z)V"); + if (setFullscreenMethod) { + env->CallVoidMethod(g_Activity, setFullscreenMethod, fullScreen); + } + } + } + + void SetTitle(const std::string &title) override { + m_Title = title; + if (id != 0) return; + JNIEnv* env = GetJNIEnv(); + if (env && g_Activity) { + jstring jTitle = env->NewStringUTF(title.c_str()); + env->CallVoidMethod(g_Activity, g_SetTitle, jTitle); + env->DeleteLocalRef(jTitle); + } + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + m_MenuBar = newMenuBar; + if (!m_Visible || id != 0) + return; + JNIEnv *env = GetJNIEnv(); + if (env && g_Activity) { + env->CallVoidMethod(g_Activity, g_InvalidateMenu); + } + } + + void GetContentSize(double *width, double *height) override { + float pixelRatio = GetDevicePixelRatio(); + *width = ANativeWindow_getWidth(m_EGLContext.m_NativeWindow) / pixelRatio; + *height = ANativeWindow_getHeight(m_EGLContext.m_NativeWindow) / pixelRatio; + } + + void SetMinContentSize(double width, double height) override { + // Minimum size is handled by Android + // dbp("minsize %lf %lf", width, height); + } + + void FreezePosition(SettingsRef settings, const std::string &key) override { + // Window position is managed by Android + } + + void ThawPosition(SettingsRef settings, const std::string &key) override { + // Window position is managed by Android + } + + void SetCursor(Cursor cursor) override { + // Cursors are not typically used on Android touch devices + } + + void SetTooltip(const std::string &text, double x, double y, + double width, double height) override { + // Tooltips are not commonly used in Android apps + } + + bool IsEditorVisible() override { + JNIEnv *env = GetJNIEnv(); + if (!(env && g_Activity)) return false; + return env->CallBooleanMethod(g_Activity, g_IsEditorVisibleMethod); + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + // Show software keyboard through JNI + JNIEnv* env = GetJNIEnv(); + if (!(env && g_Activity)) return; + jstring jText = env->NewStringUTF(text.c_str()); + env->CallVoidMethod(g_Activity, g_ShowEditorMethod, + id, (float)x, (float)y, (float)fontHeight, (float)minWidth, isMonospace, jText); + env->DeleteLocalRef(jText); + } + + void HideEditor() override { + // Hide software keyboard through JNI + JNIEnv* env = GetJNIEnv(); + if (!(env && g_Activity)) return; + env->CallVoidMethod(g_Activity, g_HideEditorMethod); + } + + void SetScrollbarVisible(bool visible) override { + m_ScrollVis = visible; + UpdateScrollbar(); + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + m_ScrollMin = min; + m_ScrollMax = max; + m_ScrollPage = pageSize; + UpdateScrollbar(); + } + + double GetScrollbarPosition() override { + return m_ScrollPos; + } + + void SetScrollbarPosition(double pos) override { + m_ScrollPos = pos; + UpdateScrollbar(); + } + + // Pushes the current scrollbar geometry to the Java layer, which draws the + // thumb as the SurfaceView foreground (a LayerDrawable item pinned right). + void UpdateScrollbar() { + JNIEnv *env = GetJNIEnv(); + if (!(env && g_Activity) || !g_SetScrollbar) return; + env->CallVoidMethod(g_Activity, g_SetScrollbar, + id, m_ScrollMin, m_ScrollMax, m_ScrollPage, + m_ScrollPos, m_ScrollVis); + } + + void Render() { + if (onRender) { + m_EGLContext.MakeCurrent(); + onRender(); + m_EGLContext.SwapBuffers(); + } + } + + void Invalidate() override { + JNIEnv *env = GetJNIEnv(); + if (env && g_Activity) { + if (!m_Inval) { + m_Inval = CreateTimer(); + m_Inval->onTimeout = std::bind(&WindowImplAndroid::Render, this); + } + m_Inval->RunAfterNextFrame(); + } + } + + __inline__ void InvalidateMenu(JNIEnv *env, jobject jmenu) { + if (!m_MenuBar) return; + auto menuBar = (MenuBarImplAndroid*)m_MenuBar.get(); + for (auto &menu : menuBar->subMenus) { + menu->Attach(env, jmenu); + } + } + + __inline__ bool MakeCurrent() { + UpdateScrollbar(); + return m_EGLContext.MakeCurrent(); + } +}; +static std::unordered_map> g_Windows; + +//----------------------------------------------------------------------------- +// Window creation +//----------------------------------------------------------------------------- + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + auto window = std::make_shared(kind); + g_Windows[window->id] = window; + return window; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeInit(JNIEnv* env, jobject thiz, jobject jamgr, jobjectArray strs) { + g_Activity = env->NewGlobalRef(thiz); + amgr = AAssetManager_fromJava(env, jamgr); + jsize siz = 0, i = 0; + if (strs && (siz=env->GetArrayLength(strs))) { + for (i=0; iGetObjectArrayElement(strs, i); + const char *chars = env->GetStringUTFChars(str, nullptr); + if (chars != nullptr) { + if (SetLocale(chars)) { + i = siz; // after this loop, i = siz+1 + } + env->ReleaseStringUTFChars(str, chars); + } + env->DeleteLocalRef(str); + } + } + if (i <= siz) { + SetLocale("en_US"); + } + SS.Init(); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeClear(JNIEnv* env, jobject thiz) { + SS.Clear(); + SK.Clear(); + g_Windows.clear(); + g_NextWindowId = 0; + MenuItemImplAndroid::nextId = 1; + MenuItemImplAndroid::item_map.clear(); + env->DeleteGlobalRef(g_Activity); + g_Activity = nullptr; + amgr = nullptr; +} + +void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeOnWindowChanged(JNIEnv *env, jclass jc, jint winId) { + if (winId < g_NextWindowId) { + g_Windows[winId]->Invalidate(); + } +} + +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeSetSurface(JNIEnv *env, jclass jc, jint wid, jobject surf) { + auto win = g_Windows[wid].get(); + auto eglCont = &win->m_EGLContext; + if (eglCont->m_NativeWindow) { + ANativeWindow_release(eglCont->m_NativeWindow); + } + eglCont->m_NativeWindow = ANativeWindow_fromSurface(env, surf); + ANativeWindow_acquire(eglCont->m_NativeWindow); + eglCont->DestroySurface(); + eglCont->CreateSurface(); + win->UpdateScrollbar(); + win->Invalidate(); +} + +static time_t lastDown = 0; +static double lastDist = 0.; +static bool doubleClicked = false; +#define DBL_TIMEOUT 300L + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeOnMotionEvent(JNIEnv *env, jclass jc, jint wid, jint act, jfloat x, jfloat y, jdouble dist, jint button, jint state) { + auto win = g_Windows[wid]; + MouseEvent event = {.x = x, .y = y}; + event.shiftDown = state & AMETA_SHIFT_ON; + event.controlDown = state & AMETA_CTRL_ON; + if (act != AMOTION_EVENT_ACTION_MOVE) + doubleClicked = false; + switch (act) { + case AMOTION_EVENT_ACTION_DOWN: { + struct timespec ts; + clock_gettime(CLOCK_BOOTTIME, &ts); + time_t tm = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + if (tm - lastDown < DBL_TIMEOUT) { + event.type = MouseEvent::Type::DBL_PRESS; + doubleClicked = true; + } else { + event.type = MouseEvent::Type::PRESS; + } + lastDown = tm; + lastDist = dist; + break; + } + case AMOTION_EVENT_ACTION_POINTER_DOWN: + lastDist = dist; + event.type = MouseEvent::Type::PRESS; + break; + case AMOTION_EVENT_ACTION_MOVE: + if (lastDist != 0. && dist != 0.) { + event.type = MouseEvent::Type::SCROLL_VERT; + event.scrollDelta = fmax(-2.0, fmin((dist - lastDist) / 25.0, 2.0)); + lastDist = dist; + auto fun = win->onRender; + win->onRender = nullptr; + win->onMouseEvent(event); + win->onRender = fun; + } + event.type = MouseEvent::Type::MOTION; + // map doubleClicked move to middle clicked move for orbiting + if (doubleClicked) + button = AMOTION_EVENT_BUTTON_TERTIARY; + break; + case AMOTION_EVENT_ACTION_UP: + case AMOTION_EVENT_ACTION_POINTER_UP: + event.type = MouseEvent::Type::RELEASE; + lastDist = 0.; + break; + case AMOTION_EVENT_ACTION_SCROLL: + event.type = MouseEvent::Type::SCROLL_VERT; + event.scrollDelta = dist; + break; + case AMOTION_EVENT_ACTION_CANCEL: + case AMOTION_EVENT_ACTION_OUTSIDE: + event.type = MouseEvent::Type::LEAVE; + lastDist = 0.; + break; + } + switch (button) { + case AMOTION_EVENT_BUTTON_PRIMARY: + event.button = MouseEvent::Button::LEFT; + break; + case AMOTION_EVENT_BUTTON_SECONDARY: + event.button = MouseEvent::Button::RIGHT; + break; + case AMOTION_EVENT_BUTTON_TERTIARY: + event.button = MouseEvent::Button::MIDDLE; + break; + default: + event.button = MouseEvent::Button::NONE; + } + return win->onMouseEvent(event); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeOnKeyEvent(JNIEnv *env, jclass jc, jint wid, jint keyact, jint keyenc, jint metastate) { + if (keyact == 0) + return false; // key char from getUnicodeChar() would failed sometimes and trigger the command::none + auto win = g_Windows[wid]; + KeyboardEvent event = {}; + if (keyact == AKEY_EVENT_ACTION_DOWN) + event.type = KeyboardEvent::Type::PRESS; + else if (keyact == AKEY_EVENT_ACTION_UP) + event.type = KeyboardEvent::Type::RELEASE; + if (keyenc < 0) { + event.key = KeyboardEvent::Key::FUNCTION; + event.num = -keyenc - AKEYCODE_F1 + 1; + } else { + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = keyenc; + } + event.shiftDown = metastate & AMETA_SHIFT_ON; + event.controlDown = metastate & AMETA_CTRL_ON; + return win->onKeyboardEvent(event); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeOnEditorDone(JNIEnv *env, jclass clz, jint wid, jstring str) { + auto win = g_Windows[wid]; + const char * cstr = env->GetStringUTFChars(str, NULL); + win->onEditingDone(cstr); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeOnScrollbarAdjusted(JNIEnv *env, jobject thiz, jint wid, jdouble pos) { + auto win = g_Windows[wid]; + auto adjust = win->onScrollbarAdjusted; + if (adjust) { + adjust(pos); + } +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_solvespace_SolveSpaceActivity_onCreateOptionsMenu(JNIEnv *env, jobject thiz, jobject menu) { + if (g_NextWindowId == 0) return false; + env->CallVoidMethod(menu, g_Clear); + g_Windows[0]->InvalidateMenu(env, menu); + return true; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_solvespace_SolveSpaceActivity_onOptionsItemSelected(JNIEnv *env, jobject thiz, jobject jitem) { + jint id = env->CallIntMethod(jitem, g_GetItemId); + auto pfunc = MenuItemImplAndroid::item_map.find(id); + if (pfunc == MenuItemImplAndroid::item_map.end()) { + return false; + } + auto item = pfunc->second; + if (item->onTrigger){ + auto timer = MenuItemImplAndroid::GetTimer(); + timer->onTimeout = item->onTrigger; + timer->RunAfter(0); + return true; + } + return false; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeOnCreateContextMenu(JNIEnv *env, jclass clz, jobject jmenu, std::shared_ptr *menu) { + if (menu && menu->get()) + for (auto m:menu->get()->menus) + m->Attach(env, jmenu); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_SolveSpaceActivity_nativeOnContextMenuClosed(JNIEnv *env, jclass, std::shared_ptr *pref) { + SS.GW.context.active = false; + SS.ScheduleShowTW(); + delete pref; +} + +//----------------------------------------------------------------------------- +// JNI initialization +//----------------------------------------------------------------------------- + +extern "C" JNIEXPORT jint JNICALL +JNI_OnLoad(JavaVM* vm, void* reserved) { + g_JavaVM = vm; + + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + + // Find our activity class + jclass activityClass = env->FindClass("com/solvespace/SolveSpaceActivity"); + if (!activityClass) { + ALOGE("Failed to find activity class"); + return -1; + } + + // Cache method IDs + g_IsEditorVisibleMethod = env->GetMethodID(activityClass, "isEditorVisible", "()Z"); + g_ShowEditorMethod = env->GetMethodID(activityClass, "showEditor", + "(IFFFFZLjava/lang/String;)V"); + g_HideEditorMethod = env->GetMethodID(activityClass, "hideEditor", "()V"); + g_ShowMessageDialogMethod = env->GetMethodID(activityClass, "showMessageDialog", + "(Ljava/lang/String;Ljava/lang/String;I)I"); + g_ShowFileDialogMethod = env->GetMethodID(activityClass, "showFileDialog", + "(ZLjava/lang/String;[Ljava/lang/String;)Ljava/lang/String;"); + g_SendDelayedMethod = env->GetMethodID(activityClass, "sendDelayed", + "(JJ)V"); + g_SetTitle = env->GetMethodID(activityClass, "setWinTitle", + "(Ljava/lang/String;)V"); + g_OnWinAdded = env->GetMethodID(activityClass, "onWinAdded", "(I)Landroid/view/Surface;"); + g_GetPreferences = env->GetMethodID(activityClass, "getPreferences", "(I)Landroid/content/SharedPreferences;"); + g_GetDensity = env->GetMethodID(activityClass, "getDensity", "()F"); + g_SetScrollbar = env->GetMethodID(activityClass, "setScrollbar", "(IFFFFZ)V"); + g_Popup = env->GetMethodID(activityClass, "popup", "(J)V"); + g_OpenContentFileMethod = env->GetMethodID(activityClass, "openContentFile", + "(Ljava/lang/String;Ljava/lang/String;)I"); + g_DeleteContentFileMethod = env->GetMethodID(activityClass, "deleteContentFile", + "(Ljava/lang/String;)Z"); + g_GetInternalStoragePathMethod = env->GetMethodID(activityClass, "getInternalStoragePath", + "()Ljava/lang/String;"); + g_Finish = env->GetMethodID(activityClass, "finish", "()V"); + g_GetSysFonts = env->GetStaticMethodID(activityClass, "getSysFonts", "()[Ljava/lang/String;"); + g_InvalidateMenu = env->GetMethodID(activityClass, "invalidateOptionsMenu", "()V"); + g_FileName = env->GetMethodID(activityClass, "fileName", "(Ljava/lang/String;)Ljava/lang/String;"); + + jclass menuClz = env->FindClass("android/view/Menu"); + ssassert(menuClz != NULL, "Failed to find Menu class"); + g_Add = env->GetMethodID(menuClz, "add", "(IIILjava/lang/CharSequence;)Landroid/view/MenuItem;"); + g_AddSubMenu = env->GetMethodID(menuClz, "addSubMenu", "(Ljava/lang/CharSequence;)Landroid/view/SubMenu;"); + g_Clear = env->GetMethodID(menuClz, "clear", "()V"); + jclass itemClz = env->FindClass("android/view/MenuItem"); + ssassert(itemClz != NULL, "Failed to find MenuItem class"); + g_GetItemId = env->GetMethodID(itemClz, "getItemId", "()I"); + jmethodID setAlphaShort = env->GetMethodID(itemClz, "setAlphabeticShortcut", "(CI)Landroid/view/MenuItem;"); + if (setAlphaShort == nullptr) { + setAlphaShort = env->GetMethodID(itemClz, "setAlphabeticShortcut", "(C)Landroid/view/MenuItem;"); + } + g_SetAlphabeticShortcut = setAlphaShort; + g_SetCheckable = env->GetMethodID(itemClz, "setCheckable", "(Z)Landroid/view/MenuItem;"); + g_SetChecked = env->GetMethodID(itemClz, "setChecked", "(Z)Landroid/view/MenuItem;"); + g_SetEnabled = env->GetMethodID(itemClz, "setEnabled", "(Z)Landroid/view/MenuItem;"); + g_SetShowAsAction = env->GetMethodID(itemClz, "setShowAsAction", "(I)V"); + jclass prefClass = env->FindClass("android/content/SharedPreferences"); + ssassert(prefClass != NULL , "Failed to find SharedPreferences class"); +#define BL "(" +#define BR ")" +#define JSTR "Ljava/lang/String;" +#define EDIT "Landroid/content/SharedPreferences$Editor;" + g_GetInt = env->GetMethodID(prefClass, "getInt", BL JSTR "I" BR "I"); + g_GetFloat = env->GetMethodID(prefClass, "getFloat", BL JSTR "F" BR "F"); + g_GetString = env->GetMethodID(prefClass, "getString", BL JSTR JSTR BR JSTR); + g_Edit = env->GetMethodID(prefClass, "edit", "()" EDIT); + jclass editClass = env->FindClass("android/content/SharedPreferences$Editor"); + ssassert(editClass != NULL, "Failed to find SharedPrefereces$Editor class"); + + g_PutInt = env->GetMethodID(editClass, "putInt", BL JSTR "I" BR EDIT); + g_PutFloat = env->GetMethodID(editClass, "putFloat", BL JSTR "F" BR EDIT); + g_PutString = env->GetMethodID(editClass, "putString", BL JSTR JSTR BR EDIT); + g_Commit = env->GetMethodID(editClass, "commit", "()Z"); +#undef BL +#undef BR +#undef JSTR +#undef EDIT + return JNI_VERSION_1_6; +} + +//----------------------------------------------------------------------------- +// Android-specific implementations of other platform functions +//----------------------------------------------------------------------------- + +// Settings, Timers, Menus, Dialogs etc. would need similar Android implementations +// For brevity, I'm showing the core window implementation + +class SettingsImplAndroid final : public Settings { +public: + jobject prefs; + + SettingsImplAndroid() { + JNIEnv *env = GetJNIEnv(); + prefs = env->NewGlobalRef(env->CallObjectMethod(g_Activity, g_GetPreferences, 0)); + } + + ~SettingsImplAndroid() { + JNIEnv *env = GetJNIEnv(); + if (env) { + env->DeleteGlobalRef(prefs); + } + } + + void FreezeInt(const std::string &key, uint32_t value) override { + JNIEnv *env = GetJNIEnv(); + if (!env) return; + jobject edit = env->CallObjectMethod(prefs, g_Edit); + jstring jkey = env->NewStringUTF(key.c_str()); + env->CallObjectMethod(edit, g_PutInt, jkey, value); + env->CallBooleanMethod(edit, g_Commit); + env->DeleteLocalRef(jkey); + } + + uint32_t ThawInt(const std::string &key, uint32_t defaultValue) override { + JNIEnv *env = GetJNIEnv(); + if (!env) return defaultValue; + jstring jkey = env->NewStringUTF(key.c_str()); + uint32_t t = env->CallIntMethod(prefs, g_GetInt, jkey, defaultValue); + env->DeleteLocalRef(jkey); + return t; + } + + void FreezeFloat(const std::string &key, double value) override { + JNIEnv *env = GetJNIEnv(); + if (!env) return; + jobject edit = env->CallObjectMethod(prefs, g_Edit); + jstring jkey = env->NewStringUTF(key.c_str()); + env->CallObjectMethod(edit, g_PutFloat, jkey, (float)value); + env->CallBooleanMethod(edit, g_Commit); + env->DeleteLocalRef(jkey); + } + + double ThawFloat(const std::string &key, double defaultValue) override { + JNIEnv *env = GetJNIEnv(); + if (!env) return defaultValue; + jstring jkey = env->NewStringUTF(key.c_str()); + double t = env->CallFloatMethod(prefs, g_GetFloat, jkey, (float)defaultValue); + env->DeleteLocalRef(jkey); + return t; + } + + void FreezeString(const std::string &key, const std::string &value) override { + JNIEnv *env = GetJNIEnv(); + if (!env) return; + jobject edit = env->CallObjectMethod(prefs, g_Edit); + jstring jkey = env->NewStringUTF(key.c_str()); + jstring jval = env->NewStringUTF(value.c_str()); + env->CallObjectMethod(edit, g_PutString, jkey, jval); + env->CallBooleanMethod(edit, g_Commit); + env->DeleteLocalRef(jkey); + env->DeleteLocalRef(jval); + } + + std::string ThawString(const std::string &key, const std::string &defaultValue = "") override { + JNIEnv *env = GetJNIEnv(); + if (!env) return defaultValue; + jstring jkey = env->NewStringUTF(key.c_str()); + jstring jdval = env->NewStringUTF(defaultValue.c_str()); + jstring jret = (jstring)env->CallObjectMethod(prefs, g_GetString, jkey, jdval); + env->DeleteLocalRef(jkey); + env->DeleteLocalRef(jdval); + return env->GetStringUTFChars(jret, NULL); + } +}; + +SettingsRef GetSettings() { + static std::shared_ptr settings; + if(!settings) { + settings = std::make_shared(); + } + return settings; +} + +class TimerImplAndroid final : public Timer { +private: + void post(jlong milliseconds) { + JNIEnv *env = GetJNIEnv(); + if (!(env && g_Activity)) { + return; + } + env->CallVoidMethod(g_Activity, g_SendDelayedMethod, this, milliseconds); + } +public: + void RunAfter(unsigned int milliseconds) override { + post(milliseconds); + } + void RunAfterNextFrame() override { + post(-1L); + } +}; + +TimerRef CreateTimer() { + return std::make_shared(); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_solvespace_MainHandler_nativeRun(JNIEnv* env, jclass clazz, Timer *timer) { + auto func = timer->onTimeout; + if (func) func(); +} + +//----------------------------------------------------------------------------- +// 3DConnexion support +//----------------------------------------------------------------------------- + +void Open3DConnexion() {} +void Close3DConnexion() {} +void Request3DConnexionEventsForWindow(WindowRef window) {} + +#define BTN_NO 1 +#define BTN_YES 2 +#define BTN_CANCEL 4 +#define BTN_OK 8 +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- +class MessageDialogImplAndroid final : public MessageDialog { +public: + std::string tit; + std::string msg; + int buttons = 0; + + MessageDialogImplAndroid() { + SetTitle("Message"); + } + + void SetType(Type type) override { + } + + void SetTitle(std::string title) override { + tit = title; + } + + void SetMessage(std::string message) override { + msg = message; + } + + void SetDescription(std::string description) override { + } + + void AddButton(std::string _label, Response response, bool isDefault) override { + int btn = 0; + switch (response) { + case Response::OK: btn = BTN_OK; break; + case Response::YES: btn = BTN_YES; break; + case Response::NO: btn = BTN_NO; break; + case Response::CANCEL: btn = BTN_CANCEL; break; + default: ssassert(false, "Invalid response"); break; + } + buttons |= btn; + } + + Response RunModal() override { + char buf[20]; + sprintf(buf, "Err btns %d", buttons); + ssassert(buttons == BTN_OK + || buttons == (BTN_OK|BTN_CANCEL) + || buttons == (BTN_YES|BTN_NO) + || buttons == (BTN_CANCEL|BTN_YES|BTN_NO), + buf); + auto env = GetJNIEnv(); + ssassert(env != NULL, "GetEnv Failed"); + auto jtit = env->NewStringUTF(tit.c_str()); + auto jmsg = env->NewStringUTF(msg.c_str()); + int ret = env->CallIntMethod(g_Activity, g_ShowMessageDialogMethod, jtit, jmsg, buttons); + env->DeleteLocalRef(jtit); + env->DeleteLocalRef(jmsg); + switch (ret) { + case BTN_OK: return Response::OK; + case BTN_YES: return Response::YES; + case BTN_NO: return Response::NO; + case BTN_CANCEL: return Response::CANCEL; + default: ssassert(false, "Invalid response");break; + } + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- +class FileDialogImplAndroid final : public FileDialog { +public: + + jboolean isSaveDialog; + std::string uri; + std::string sugName; + std::vector exts; + + FileDialogImplAndroid(bool isSaveDialog) { + this->isSaveDialog = isSaveDialog; + } + + void SetTitle(std::string title) override { + } + + void SetCurrentName(std::string name) override { + sugName = name; + } + + void SetFilename(Path path) override { + uri = path.raw; + sugName = path.FileName(); + } + + Path GetFilename() override { + return Path::From(uri); + } + + void SuggestFilename(Platform::Path path) override { + sugName = path.FileName(); + } + + void AddFilter(std::string name, std::vector extensions) override { + exts.insert(exts.end(), extensions.begin(), extensions.end()); + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + } + + bool RunModal() override { + JNIEnv *env = GetJNIEnv(); + if (env && g_Activity) { + jstring tit = nullptr; + if (isSaveDialog) + tit = env->NewStringUTF(sugName.empty() ? _("untitled") : sugName.c_str()); + jclass strClz = env->FindClass("java/lang/String"); + jsize siz = (jsize)exts.size(); + jobjectArray strArr = env->NewObjectArray(siz, strClz, nullptr); + for (jsize i=0; iNewStringUTF(exts[i].c_str()); + env->SetObjectArrayElement(strArr, i, jstr); + env->DeleteLocalRef(jstr); + } + jstring ret = (jstring)env->CallObjectMethod(g_Activity, g_ShowFileDialogMethod, isSaveDialog, tit, strArr); + if (tit) env->DeleteLocalRef(tit); + env->DeleteLocalRef(strArr); + if (ret) { + uri = env->GetStringUTFChars(ret, nullptr); + return true; + } + } + return false; + } +}; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + return std::make_shared(false); +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + return std::make_shared(true); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + std::vector fonts; + JNIEnv *env = GetJNIEnv(); + if (!env || !g_Activity) { + return fonts; + } + jobjectArray jfonts = (jobjectArray)env->CallStaticObjectMethod(env->GetObjectClass(g_Activity), g_GetSysFonts); + jsize length = env->GetArrayLength(jfonts); + if (length <= 0) { + return fonts; + } + fonts.reserve(length); + for (jsize i = 0; i < length; ++i) { + jstring jStr = (jstring)env->GetObjectArrayElement(jfonts, i); + + const char* utfChars = env->GetStringUTFChars(jStr, nullptr); + if (utfChars != nullptr) { + fonts.emplace_back(Path::From(utfChars)); + env->ReleaseStringUTFChars(jStr, utfChars); + } + + env->DeleteLocalRef(jStr); + } + + return fonts; +} + +void OpenInBrowser(const std::string &url) { + JNIEnv *env = GetJNIEnv(); + if (!(env && g_Activity)) return; + + jmethodID openUrl = env->GetMethodID(env->GetObjectClass(g_Activity), "openUrl", "(Ljava/lang/String;)V"); + if (openUrl) { + jstring jurl = env->NewStringUTF(url.c_str()); + env->CallVoidMethod(g_Activity, openUrl, jurl); + env->DeleteLocalRef(jurl); + } +} + +void ExitGui() { + JNIEnv *env = GetJNIEnv(); + if (env && g_Activity) { + env->CallVoidMethod(g_Activity, g_Finish); + } +} + +} // namespace Platform +} // namespace SolveSpace diff --git a/src/platform/java/com/solvespace/MainHandler.java b/src/platform/java/com/solvespace/MainHandler.java new file mode 100644 index 000000000..0e32cb154 --- /dev/null +++ b/src/platform/java/com/solvespace/MainHandler.java @@ -0,0 +1,17 @@ +package com.solvespace; +import android.os.*; + +public class MainHandler extends Handler +{ + + public static final int HD_NRUN = 1, HD_RET = 2; + @Override + public void handleMessage(Message msg) { + if (msg.what == HD_NRUN) + nativeRun((Long)msg.obj); + else if (msg.what == HD_RET) + throw new RuntimeException(); + } + + public final static native void nativeRun(long obj); +} diff --git a/src/platform/java/com/solvespace/SolveSpaceActivity.java b/src/platform/java/com/solvespace/SolveSpaceActivity.java new file mode 100644 index 000000000..32c2f139c --- /dev/null +++ b/src/platform/java/com/solvespace/SolveSpaceActivity.java @@ -0,0 +1,718 @@ +package com.solvespace; + +import android.app.*; +import android.content.*; +import android.content.res.*; +import android.database.*; +import android.graphics.*; +import android.graphics.drawable.*; +import android.graphics.drawable.shapes.*; +import android.graphics.fonts.*; +import android.net.*; +import android.os.*; +import android.provider.DocumentsContract; +import android.text.*; +import android.util.*; +import android.view.*; +import android.view.inputmethod.*; +import android.widget.*; +import java.io.*; +import java.util.*; + +public class SolveSpaceActivity extends Activity +implements SurfaceHolder.Callback2, ActionBar.OnNavigationListener, +View.OnTouchListener, View.OnGenericMotionListener, TextView.OnEditorActionListener, +DialogInterface.OnCancelListener, DialogInterface.OnClickListener, +PopupWindow.OnDismissListener, DialogInterface.OnDismissListener, View.OnClickListener +{ + private final static String TAG = "SolveSpace"; + private final static int + BTN_NO = 1, + BTN_YES = 2, + BTN_CANCEL = 4, + BTN_OK = 8; + static { + System.loadLibrary("solvespace"); + } + + private Handler hand = new MainHandler(); + private SurfaceView[] svs; + private PopupWindow pw; + private long pmenu; + private int inited; + + // Vertical scrollbar (rendered as the SurfaceView foreground). + private int mScrollbarWidth = 8; + // True while the user is actively dragging the scrollbar thumb. + private boolean mScrollDragging; + + // Mouse states + private float currX, currY; + private int mouseDown; + private int currWid; + + // Layout constants for the scrollbar. + private static final int SCROLLBAR_TRACK_PADDING = 8; // dp + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + View hand = findViewById(R.id.hand); + hand.setTag(0); + hand.setOnClickListener(this); + Configuration conf = getResources().getConfiguration(); + notifyOrientation(conf); + SurfaceView sv = findViewById(R.id.surf); + sv.setTag(0); + sv.setTag(R.id.scroll, new ScrollHolder()); + SurfaceView tv = findViewById(R.id.surf2); + tv.setTag(1); + tv.setTag(R.id.scroll, new ScrollHolder()); + svs = new SurfaceView[]{ + sv, tv + }; + + // Build the foreground as a LayerDrawable containing the vertical + // scrollbar thumb pinned to the right edge (gravity=right). The C++ + // side updates the thumb's top/bottom insets via JNI to reflect the + // current scroll position and length. + float density = getResources().getDisplayMetrics().density; + int sbWidth = (int)(SCROLLBAR_TRACK_PADDING * density); + mScrollbarWidth = sbWidth; + Shape rect = new RectShape(); + ShapeDrawable thumb = new ShapeDrawable(rect); + thumb.getPaint().setColor(0x80ffffff); + thumb.setAlpha(0); + sv.setForeground(thumb); + thumb = new ShapeDrawable(rect); + thumb.getPaint().setColor(0x80ffffff); + thumb.setAlpha(0); + tv.setForeground(thumb); + + sv.getHolder().addCallback(this); + tv.getHolder().addCallback(this); + sv.setOnGenericMotionListener(this); + tv.setOnGenericMotionListener(this); + sv.setOnTouchListener(this); + tv.setOnTouchListener(this); + registerForContextMenu(sv); + inited = 2; + } + + public final void sendDelayed(final long timer, long timeout) { + if (timeout < 0) { + svs[0].postOnAnimation(new Runnable(){ + public void run() { + MainHandler.nativeRun(timer); + } + }); + } else { + Message msg = Message.obtain(hand, MainHandler.HD_NRUN, timer); + hand.sendMessageDelayed(msg, timeout); + } + } + + @Override + public native boolean onCreateOptionsMenu(Menu menu); + + private void notifyOrientation(Configuration conf) { + View dv = getWindow().getDecorView(); + boolean isVert = conf.orientation == Configuration.ORIENTATION_PORTRAIT; + dv.setSystemUiVisibility( + isVert ? 0 : + (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN) + ); + LinearLayout v = findViewById(R.id.drawer); + View hand = findViewById(R.id.hand); + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)v.getLayoutParams(); + ViewGroup.LayoutParams handParams = hand.getLayoutParams(); + DisplayMetrics dm = getResources().getDisplayMetrics(); + int dp8 = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, dm); + int opening = 1-(Integer)hand.getTag(); + if (isVert) { + params.width = params.MATCH_PARENT; + params.height = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 320, dm); + params.removeRule(RelativeLayout.ALIGN_PARENT_END); + params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); + params.setMarginEnd(0); + handParams.height = dp8; + handParams.width = params.MATCH_PARENT; + params.bottomMargin = opening*(dp8 - params.height); + v.setOrientation(LinearLayout.VERTICAL); + } else { + params.width = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 400, dm); + params.height = params.MATCH_PARENT; + params.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM); + params.addRule(RelativeLayout.ALIGN_PARENT_END); + handParams.height = params.MATCH_PARENT; + handParams.width = dp8; + params.setMarginEnd(opening*(dp8 - params.width)); + params.bottomMargin = 0; + v.setOrientation(LinearLayout.HORIZONTAL); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) + { + super.onConfigurationChanged(newConfig); + notifyOrientation(newConfig); + } + + @Override + public void surfaceChanged(SurfaceHolder p1, int format, int width, int height) { + int i = p1==svs[0].getHolder() ? 0:1; + nativeOnWindowChanged(i); + } + + @Override + public void surfaceCreated(SurfaceHolder p1) { + if (inited >= 0) + inited--; + if (inited == 0) { + String[] lcs; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + LocaleList lclst = getResources().getConfiguration().getLocales(); + final int siz = lclst.size(); + lcs = new String[siz]; + for (int i=0; i= 0 && currY >= 0) + v.showContextMenu(currX, currY); + else + v.showContextMenu(); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + nativeOnCreateContextMenu(menu, pmenu); + } + + @Override + public void onContextMenuClosed(Menu menu) + { + nativeOnContextMenuClosed(pmenu); + pmenu = 0; + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + return onOptionsItemSelected(item); + } + + @Override + protected void onDestroy() { + nativeClear(); + super.onDestroy(); + } + + public void showEditor(int wid, float x, float y, float fontHeight, float minWidth, boolean isMono, String text) { + EditText ed = new EditText(this); + if (isMono) ed.setTypeface(Typeface.MONOSPACE); + ed.setImeOptions(EditorInfo.IME_ACTION_DONE); + ed.setText(text); + ed.setOnEditorActionListener(this); + ed.setTextColor(0xff000000); + ed.setFocusable(true); + ed.setTag(wid); + PopupWindow pw = new PopupWindow( + ed, + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + true); + this.pw = pw; + pw.setBackgroundDrawable(new ColorDrawable(0xffffffff)); + pw.setAnimationStyle(0); + pw.setOutsideTouchable(false); + pw.setTouchable(true); + pw.setTouchModal(false); + pw.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + pw.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + pw.setOnDismissListener(this); + float density = getDensity(); + int[] loc = new int[2]; + svs[wid].getLocationInWindow(loc); + loc[0] += (int)(x * density)-ed.getPaddingLeft(); + loc[1] += (int)(y * density)-ed.getPaddingTop(); + pw.showAtLocation(getWindow().getDecorView(), Gravity.TOP|Gravity.LEFT, loc[0], loc[1]); + ed.requestFocus(); + } + + // ---- Scrollbar geometry (called from C++ via JNI) ---- + + // Stores the scrollbar configuration and redraws the thumb. + // All geometry arguments are in normalized scrollbar units. + protected void setScrollbar(int wid, float min, float max, float page, float pos, boolean visible) { + SurfaceView v = svs[wid]; + ScrollHolder holder = (ScrollHolder)v.getTag(R.id.scroll); + holder.min = min; + holder.max = max; + holder.page = page; + holder.pos = pos; + holder.visible = visible; + Drawable scrollbar = v.getForeground(); + if (scrollbar == null) return; + int height = v.getHeight(); + if (!visible || height <= 0 || + max <= min) { + scrollbar.setAlpha(0); + return; + } + + double range = Math.max(1e-6, max - min); + // Thumb length is proportional to the page size within the range. + int thumbH = (int)(page / (max - min) * height); + + double ratio = (pos - min) / range; + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + int top = (int)(ratio * height); + int bottom = top + thumbH; + if (bottom > height) bottom = height; + + // Pin the thumb to the right edge; its width is controlled by the + // right inset relative to the right gravity of the layer. + int width = v.getWidth(); + scrollbar.setBounds(width - mScrollbarWidth, top, width, bottom); + scrollbar.setAlpha(0x80); + } + + // Callback from C++ that the scroll position was adjusted by the user. + private native void nativeOnScrollbarAdjusted(int wid, double pos); + + @Override + public void onDismiss() { + PopupWindow pw = this.pw; + if (pw != null) { + EditText ed = (EditText)pw.getContentView(); + nativeOnEditorDone((Integer)ed.getTag(), ed.getText().toString()); + } + } + + public void hideEditor() { + PopupWindow pw = this.pw; + this.pw = null; + if (pw != null) { + pw.dismiss(); + } + } + + public boolean isEditorVisible() { + return pw != null && pw.isShowing(); + } + + boolean showing = false; + int ret = BTN_CANCEL; + + public int showMessageDialog(String title, String msg, int btn) { + if (showing) { + return BTN_CANCEL; + } + showing = true; + AlertDialog.Builder bd = new AlertDialog.Builder(this); + bd.setTitle(title); + bd.setMessage(msg); + bd.setOnCancelListener(this); + bd.setOnDismissListener(this); + if ((btn&(BTN_OK|BTN_YES)) != 0) { + bd.setPositiveButton((btn&BTN_YES)!=0?R.string.yes:android.R.string.ok, this); + } + if ((btn&(BTN_NO|BTN_CANCEL)) != 0) { + bd.setNegativeButton((btn&BTN_NO)!=0?R.string.no:android.R.string.cancel, this); + } + if (btn == (BTN_CANCEL|BTN_YES|BTN_NO)) { + bd.setNeutralButton(android.R.string.cancel, this); + } + ret = btn; + bd.create().show(); + try {Looper.loop();} catch (RuntimeException re) {} + showing = false; + return ret; + } + + @Override + public void onClick(DialogInterface p1, int p2) { + int dret; + switch (p2) { + case DialogInterface.BUTTON_POSITIVE: + dret = (ret&BTN_OK)==0?BTN_YES:BTN_OK; + break; + case DialogInterface.BUTTON_NEGATIVE: + dret = (ret&BTN_NO)==0?BTN_CANCEL:BTN_NO; + break; + case DialogInterface.BUTTON_NEUTRAL: + default: + dret = BTN_CANCEL; + break; + } + ret = dret; + } + + @Override + public void onCancel(DialogInterface p1) { + ret = BTN_CANCEL; + } + + @Override + public void onDismiss(DialogInterface p1) { + hand.obtainMessage(MainHandler.HD_RET).sendToTarget(); + } + + private String fname; + public String showFileDialog(boolean isSave, String title, String[] exts) { + if (showing) return null; + showing = true; + Intent it = new Intent(isSave?Intent.ACTION_CREATE_DOCUMENT:Intent.ACTION_OPEN_DOCUMENT); + it.setType("*/*"); + it.addCategory(Intent.CATEGORY_OPENABLE); + if (isSave && title != null) { + it.putExtra(Intent.EXTRA_TITLE, title); + } + startActivityForResult(it, 0); + fname = null; + try { Looper.loop(); } catch (RuntimeException re) {} + showing = false; + String fnam = fname; + if (fnam == null) return null; + // Apply filters + String name = fileName(fnam); + int pt = name.lastIndexOf("."); + int l = exts == null ? 0 : exts.length; + if (pt >= 0 && l > 0) { + String ext = name.substring(pt+1, name.length()); + int i=0; + for (; i= 0 && y <= height)) { + mScrollDragging = true; + nativeOnScrollbarAdjusted(wid, (y - thumbH * 0.5)*range/height); + } + } else if (act == MotionEvent.ACTION_MOVE && mScrollDragging) { + nativeOnScrollbarAdjusted(wid, (y - thumbH * 0.5)*range/height); + } else if (act == MotionEvent.ACTION_UP || act == MotionEvent.ACTION_CANCEL) { + mScrollDragging = false; + } + } + + private boolean xInScrollbar(MotionEvent ev, int sbLeft, int width) { + return ev.getX() >= sbLeft && ev.getX() <= width; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent ev) { + if (!super.onKeyDown(keyCode, ev)) + svs[0].postOnAnimation(new Key(0, ev)); + return true; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent ev) { + if (!super.onKeyUp(keyCode, ev)) + svs[0].postOnAnimation(new Key(0, ev)); + return true; + } + + public Surface onWinAdded(int wid) { + return svs[wid].getHolder().getSurface(); + } + + public void setWinTitle(String title) { + getActionBar().setTitle(title); + } + + @Override + public boolean onEditorAction(TextView p1, int p2, KeyEvent p3) { + nativeOnEditorDone((Integer)p1.getTag(), p1.getText().toString()); + return true; + } + + private void toast(String tst) { + Toast.makeText(this, tst, 0).show(); + } + + public static String[] getSysFonts() { + Set fonts = SystemFonts.getAvailableFonts(); + String[] arr = new String[fonts.size()]; + int i=0; + for (Font f:fonts) { + arr[i++] = f.getFile().getPath(); + } + return arr; + } + + @Override + public void onClick(View v) { + int opening = (Integer)v.getTag(); + LinearLayout lr = findViewById(R.id.drawer); + ViewGroup.LayoutParams handParams = v.getLayoutParams(); + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)lr.getLayoutParams(); + DisplayMetrics dm = getResources().getDisplayMetrics(); + if (lr.getOrientation() == LinearLayout.VERTICAL) { + params.bottomMargin = opening*(handParams.height - (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 320, dm)); + } else { + params.setMarginEnd(opening*(handParams.width - (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 400, dm))); + } + lr.requestLayout(); + v.setTag(1-opening); + } + + private native void nativeInit(AssetManager amgr, String[] strs); + private native void nativeClear(); + private native void nativeSetSurface(int wid, Surface suf); + protected static native boolean nativeOnMotionEvent(int wid, int action, float x, float y, double dist, int button, int metastate); + protected static native boolean nativeOnKeyEvent(int wid, int keystate, int keyCode, int metastate); + private static native void nativeOnWindowChanged(int winId); + private static native void nativeOnEditorDone(int wid, String text); + private static native void nativeOnCreateContextMenu(Menu menu, long pemnu); + private static native void nativeOnContextMenuClosed(long pmenu); + + static class Motion implements Runnable { + int id, act, button, metastate; + float x, y; + double dist; + public Motion(int wid, int ac, float x, float y, double dis, int btn, int mt) { + id = wid; + act = ac; + this.x = x; + this.y = y; + dist = dis; + button = btn; + metastate = mt; + } + public void run() { + nativeOnMotionEvent(id, act, x, y, dist, button, metastate); + } + } + + static class Key implements Runnable { + int id, act, keyenc, meta; + public Key(int wid, KeyEvent ke) { + id = wid; + act = ke.getAction(); + int kcode = ke.getKeyCode(); + if (kcode >= KeyEvent.KEYCODE_F1 && kcode <= KeyEvent.KEYCODE_F12) + keyenc = -kcode; + else + keyenc = ke.getUnicodeChar(0); + meta = ke.getMetaState(); + } + public void run() { + nativeOnKeyEvent(id, act, keyenc, meta); + } + } + + static class ScrollHolder { + float min, max, page, pos; + boolean visible; + } +} diff --git a/src/platform/platform.cpp b/src/platform/platform.cpp index a57ff5ec3..dad98bf63 100644 --- a/src/platform/platform.cpp +++ b/src/platform/platform.cpp @@ -15,12 +15,14 @@ #include #include - #if defined(WIN32) // Conversely, include Microsoft headers after solvespace.h to avoid clashes. # include # include #else +#ifdef __ANDROID__ +# include +#endif # include # include #endif @@ -154,9 +156,14 @@ Path Path::CurrentDirectory() { std::string Path::FileName() const { std::string fileName = raw; +#ifdef __ANDROID__ + if (fileName.compare(0, 8, "content:") == 0) + return DroidFileName(fileName.c_str()); +#endif + constexpr size_t siz = 1; size_t slash = fileName.rfind(SEPARATOR); if(slash != std::string::npos) { - fileName = fileName.substr(slash + 1); + fileName = fileName.substr(slash + siz); } return fileName; } @@ -186,6 +193,21 @@ bool Path::HasExtension(std::string theirExt) const { } Path Path::WithExtension(std::string ext) const { +#ifdef __ANDROID__ + // A Storage Access Framework content URI is a document reference, not a + // filesystem path; rewriting it would break the URI. Companion files + // (e.g. autosave "*.slvs~") cannot live next to the document, so map them + // into the app's private storage, keyed by the URI. + if(raw.compare(0, 8, "content:") == 0) { + std::string dir = AndroidInternalStoragePath(); + if(dir.empty()) { + // No Java context available; leave the path untouched rather + // than fabricating a broken document URI. + return *this; + } + return From(dir + "/" + AndroidContentKey(raw.c_str()) + "." + ext); + } +#endif Path withExt = *this; size_t dot = withExt.raw.rfind('.'); if(dot != std::string::npos) { @@ -212,6 +234,14 @@ static void FindPrefix(const std::string &raw, size_t *pos) { *pos = raw.find('\\', slashAt + 1); } } +#elif defined(__ANDROID__) + if(strncmp("content:", raw.c_str(), 8) == 0) { + // A Storage Access Framework content URI acts as its own root; there + // are no path components below it that can be expanded/relativized. + *pos = 8; + } else if(!raw.empty() && raw[0] == '/') { + *pos = 1; + } #else if(!raw.empty() && raw[0] == '/') { *pos = 1; @@ -279,6 +309,13 @@ Path Path::Join(const Path &other) const { // On Windows, additionally prepends the UNC prefix to absolute paths without one. // Returns an empty path if a ".." component would escape from the root. Path Path::Expand(bool fromCurrentDirectory) const { +#ifdef __ANDROID__ + // Storage Access Framework content URIs are opaque to filesystem + // semantics (no "." / "..", no real parent); never mangle them. + if(raw.compare(0, 8, "content:") == 0) { + return *this; + } +#endif Path source; Path expanded; @@ -416,6 +453,31 @@ std::string Path::ToPortable() const { FILE *OpenFile(const Platform::Path &filename, const char *mode) { ssassert(filename.raw.length() == strlen(filename.raw.c_str()), "Unexpected null byte in middle of a path"); +#ifdef __ANDROID__ + if(filename.raw.compare(0, 8, "content:") == 0) { + // Storage Access Framework URIs cannot be opened with fopen(); open + // them through the Java activity's ContentResolver and wrap the + // returned descriptor in a FILE*. The Java side detaches the fd, so + // ownership transfers to us and fclose() will release it. + std::string jmode; + if(strcmp(mode, "rb") == 0) jmode = "r"; + else if(strcmp(mode, "wb") == 0) jmode = "w"; + else if(strcmp(mode, "ab") == 0) jmode = "wa"; + else if(strcmp(mode, "r+b") == 0 || strcmp(mode, "rb+") == 0) jmode = "rw"; + else if(strcmp(mode, "w+b") == 0 || strcmp(mode, "wb+") == 0) jmode = "rwt"; + else if(strcmp(mode, "a+b") == 0 || strcmp(mode, "ab+") == 0) jmode = "rwa"; + else jmode = mode; + + int fd = AndroidOpenContentFile(filename.raw.c_str(), jmode.c_str()); + if(fd < 0) return NULL; + FILE *f = fdopen(fd, mode); + if(f == NULL) { + close(fd); + return NULL; + } + return f; + } +#endif #if defined(WIN32) return _wfopen(Widen(filename.Expand(/*fromCurrentDirectory=*/true).raw).c_str(), Widen(mode).c_str()); #else @@ -433,6 +495,14 @@ bool FileExists(const Platform::Path &filename) { void RemoveFile(const Platform::Path &filename) { ssassert(filename.raw.length() == strlen(filename.raw.c_str()), "Unexpected null byte in middle of a path"); +#ifdef __ANDROID__ + if(filename.raw.compare(0, 8, "content:") == 0) { + // Storage Access Framework URIs must be deleted through the + // Java activity (DocumentsContract), not unlink(). + AndroidDeleteContentFile(filename.raw.c_str()); + return; + } +#endif #if defined(WIN32) _wremove(Widen(filename.Expand().raw).c_str()); #else @@ -536,7 +606,7 @@ static Platform::Path ResourcePath(const std::string &name) { return Path::From("res/" + name); } -#elif !defined(WIN32) +#elif !defined(WIN32) && !defined(__ANDROID__) # if defined(__linux__) static const char *selfSymlink = "/proc/self/exe"; @@ -594,13 +664,36 @@ static Platform::Path ResourcePath(const std::string &name) { #endif #if !defined(WIN32) - +#ifdef __ANDROID__ +AAssetManager * amgr; +static bool ReadAsset(const char *name, std::string *data) { + AAsset * asset = AAssetManager_open(amgr, name, AASSET_MODE_UNKNOWN); + if (!asset) { + return false; + } + off_t size = AAsset_getLength(asset); + data->resize(size); + char *buf = (char*)data->data(); + if (!AAsset_read(asset, buf, size)) { + AAsset_close(asset); + return false; + } + AAsset_close(asset); + return true; +} +#endif const void *LoadResource(const std::string &name, size_t *size) { static std::map cache; auto it = cache.find(name); if(it == cache.end()) { - ssassert(ReadFile(ResourcePath(name), &cache[name]), "Cannot read resource"); + ssassert( + #ifdef __ANDROID__ + ReadAsset(name.c_str(), &cache[name]), + #else + ReadFile(ResourcePath(name), &cache[name]), + #endif + ("Cannot read resource "+name).c_str()); it = cache.find(name); } diff --git a/src/platform/platform.h b/src/platform/platform.h index 771a4cf0b..13f2e9f05 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -6,6 +6,10 @@ #ifndef SOLVESPACE_PLATFORM_H #define SOLVESPACE_PLATFORM_H +#ifdef __ANDROID__ +#include +#include +#endif #include #include @@ -81,6 +85,24 @@ std::vector InitCli(int argc, char **argv); void *AllocTemporary(size_t size); void FreeAllTemporary(); +// Android Env +#ifdef __ANDROID__ +extern AAssetManager *amgr; +extern JNIEnv* GetJNIEnv(); +// Open a Storage Access Framework content URI through the Java activity's +// ContentResolver. Returns an owned file descriptor (>= 0) on success, or -1 +// on failure. The descriptor must be closed by the caller (e.g. via fclose). +int AndroidOpenContentFile(const char *uri, const char *mode); +// Delete a Storage Access Framework content URI through the Java activity. +void AndroidDeleteContentFile(const char *uri); +// Absolute path of the app's private files directory (getFilesDir()). +std::string AndroidInternalStoragePath(); +// Maps a content URI to a filesystem-safe name (used as a key for +// autosave/backup companions that must live in private storage). +std::string AndroidContentKey(const char *uri); +std::string DroidFileName(const char *uri); +#endif + } // namespace Platform } // namespace SolveSpace diff --git a/src/platform/platformbase.cpp b/src/platform/platformbase.cpp index 6e3bb8aa0..4c5268fe9 100644 --- a/src/platform/platformbase.cpp +++ b/src/platform/platformbase.cpp @@ -51,11 +51,16 @@ void DebugPrint(const char *fmt, ...) #endif +//----------------------------------------------------------------------------- +// Debug output, on Android +//----------------------------------------------------------------------------- +#if defined(__ANDROID__) + //----------------------------------------------------------------------------- // Debug output, on *nix. //----------------------------------------------------------------------------- -#if !defined(WIN32) +#elif !defined(WIN32) void DebugPrint(const char *fmt, ...) { va_list va; diff --git a/src/render/gl3shader.cpp b/src/render/gl3shader.cpp index 7c4552e61..ebf913cc0 100644 --- a/src/render/gl3shader.cpp +++ b/src/render/gl3shader.cpp @@ -113,7 +113,7 @@ precision highp float; if(infoLen > 1) { std::string infoStr(infoLen, '\0'); glGetShaderInfoLog(shader, infoLen, NULL, &infoStr[0]); - dbp(infoStr.c_str()); + dbp("%s", infoStr.c_str()); } GLint compiled = 0; @@ -150,7 +150,7 @@ void Shader::Init(const std::string &vertexRes, const std::string &fragmentRes, if(infoLen > 1) { std::string infoStr(infoLen, '\0'); glGetProgramInfoLog(program, infoLen, NULL, &infoStr[0]); - dbp(infoStr.c_str()); + dbp("%s", infoStr.c_str()); } GLint linked = 0; diff --git a/src/render/gl3shader.h b/src/render/gl3shader.h index 19f73128b..47797d0d2 100644 --- a/src/render/gl3shader.h +++ b/src/render/gl3shader.h @@ -6,7 +6,7 @@ #ifndef SOLVESPACE_GL3SHADER_H #define SOLVESPACE_GL3SHADER_H -#if defined(WIN32) || defined(__EMSCRIPTEN__) +#if defined(WIN32) || defined(__EMSCRIPTEN__) || defined(__ANDROID__) # define GL_APICALL /*static linkage*/ # define GL_GLEXT_PROTOTYPES # include diff --git a/src/resource.cpp b/src/resource.cpp index 8921d7abe..32b0e186b 100644 --- a/src/resource.cpp +++ b/src/resource.cpp @@ -3,7 +3,10 @@ // // Copyright 2016 whitequark //----------------------------------------------------------------------------- +#include "resource.h" +#ifndef __ANDROID__ #include +#endif #include #include #include "solvespace.h" @@ -29,6 +32,14 @@ std::string LoadString(const std::string &name) { } std::string LoadStringFromGzip(const std::string &name) { +#ifdef __ANDROID__ + // AAPT would uncompress gzip in assets when packaging, so redirect this to LoadString() + // TODO: define a macro LoadSringFromGzip redirecting to LoadString since all calls to + // this has a ".gz"-end name + const size_t siz = name.size(); + const bool endsWithGz = siz > 3 && !strncmp(".gz", name.c_str()+siz-3, 3); + return LoadString(endsWithGz ? name.substr(0, siz-3) : name); +#else size_t deflatedSize; const void *data = Platform::LoadResource(name, &deflatedSize); @@ -58,6 +69,7 @@ std::string LoadStringFromGzip(const std::string &name) { inflateEnd(&stream); return result; +#endif } std::shared_ptr LoadPng(const std::string &name) { diff --git a/src/slvs/CMakeLists.txt b/src/slvs/CMakeLists.txt index d931f8fa1..438a1bc73 100644 --- a/src/slvs/CMakeLists.txt +++ b/src/slvs/CMakeLists.txt @@ -67,10 +67,16 @@ target_include_directories(slvs PUBLIC ${CMAKE_SOURCE_DIR}/include) set_target_properties(slvs PROPERTIES PUBLIC_HEADER ${CMAKE_SOURCE_DIR}/include/slvs.h - VERSION ${PROJECT_VERSION} - SOVERSION 1 ) +if(ANDROID) + target_link_libraries(slvs PUBLIC ${log-lib}) +else() + set_target_properties(slvs PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 1 + ) + # if(NOT WIN32) install(TARGETS slvs COMPONENT slvs @@ -78,3 +84,4 @@ install(TARGETS slvs ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) # endif() +endif() \ No newline at end of file diff --git a/src/solvespace.cpp b/src/solvespace.cpp index 6c177c055..f4380aca4 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -677,7 +677,7 @@ void SolveSpaceUI::UpdateWindowTitles() { if(SS.showFullFilePath) { GW.window->SetTitle(saveFile.raw); } else { - GW.window->SetTitle(saveFile.raw.substr(saveFile.raw.find_last_of("/\\") + 1)); + GW.window->SetTitle(saveFile.FileName()); } } } diff --git a/src/util.h b/src/util.h index 585b5decd..660d2de39 100644 --- a/src/util.h +++ b/src/util.h @@ -40,15 +40,22 @@ #endif #define CO(v) (v).x, (v).y, (v).z - +#ifdef __ANDROID__ +#define LOG_TAG "SS" +#include +#define dbp ALOGD +#else #define dbp SolveSpace::Platform::DebugPrint +#endif namespace SolveSpace { namespace Platform { // Debug print function. +#ifndef __ANDROID__ void DebugPrint(const char *fmt, ...); +#endif } // namespace Platform