From e0011ba5e1a4906e23bc07d80af0d9bc6964fdcf Mon Sep 17 00:00:00 2001 From: Juliano Leal Goncalves Date: Mon, 20 Sep 2021 17:04:25 -0300 Subject: [PATCH 001/185] =?UTF-8?q?=F0=9F=94=A5=20Remove=20Android=20suppo?= =?UTF-8?q?rt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .circleci/config.yml | 11 - CMakeLists.txt | 38 +- Source/DiabloUI/fonts.cpp | 2 +- Source/init.cpp | 2 +- Source/main.cpp | 4 - Source/options.cpp | 24 +- Source/storm/storm_file_wrapper.h | 2 +- Source/storm/storm_sdl_rw.cpp | 11 - Source/utils/file_util.cpp | 2 +- android-project/.gitignore | 16 - android-project/3rdParty/SDL2/CMakeLists.txt | 11 - .../3rdParty/SDL2_ttf/CMakeLists.txt | 16 - android-project/CMake/FindFreetype.cmake | 9 - android-project/CMake/FindSDL2.cmake | 2 - android-project/CMake/android_defs.cmake | 22 - android-project/app/build.gradle | 64 - android-project/app/proguard-rules.pro | 17 - .../app/src/main/AndroidManifest.xml | 83 - android-project/app/src/main/assets/.gitkeep | 0 .../app/src/main/ic_launcher-playstore.png | Bin 68393 -> 0 bytes .../diasurgical/devilutionx/DataActivity.java | 126 - .../devilutionx/DevilutionXSDLActivity.java | 194 -- .../main/java/org/libsdl/app/HIDDevice.java | 22 - .../app/HIDDeviceBLESteamController.java | 650 ----- .../java/org/libsdl/app/HIDDeviceManager.java | 685 ----- .../java/org/libsdl/app/HIDDeviceUSB.java | 309 --- .../app/src/main/java/org/libsdl/app/SDL.java | 85 - .../main/java/org/libsdl/app/SDLActivity.java | 2323 ----------------- .../java/org/libsdl/app/SDLAudioManager.java | 390 --- .../org/libsdl/app/SDLControllerManager.java | 792 ------ .../app/src/main/res/layout/activity_data.xml | 78 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3951 -> 0 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 1970 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 6569 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 6070 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2383 -> 0 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 1299 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 3936 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3620 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 5787 -> 0 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 2754 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 9494 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 8909 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 9498 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 4402 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 16514 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 14765 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 13599 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 6335 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 24865 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 21644 -> 0 bytes .../app/src/main/res/values-bg/strings.xml | 12 - .../app/src/main/res/values-da/strings.xml | 7 - .../app/src/main/res/values-de/strings.xml | 15 - .../app/src/main/res/values-es/strings.xml | 4 - .../app/src/main/res/values-fr/strings.xml | 4 - .../app/src/main/res/values-hr/strings.xml | 4 - .../app/src/main/res/values-it/strings.xml | 14 - .../src/main/res/values-ko-rKR/strings.xml | 14 - .../src/main/res/values-pt-rBR/strings.xml | 15 - .../app/src/main/res/values-ru/strings.xml | 14 - .../app/src/main/res/values-sv/strings.xml | 4 - .../src/main/res/values-zh-rCN/strings.xml | 14 - .../src/main/res/values-zh-rTW/strings.xml | 14 - .../app/src/main/res/values/colors.xml | 7 - .../app/src/main/res/values/strings.xml | 14 - .../app/src/main/res/values/themes.xml | 9 - android-project/build.gradle | 25 - android-project/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - android-project/gradlew | 160 -- android-project/gradlew.bat | 90 - android-project/settings.gradle | 1 - docs/building.md | 14 - docs/installing.md | 7 - 78 files changed, 14 insertions(+), 6454 deletions(-) delete mode 100644 android-project/.gitignore delete mode 100644 android-project/3rdParty/SDL2/CMakeLists.txt delete mode 100644 android-project/3rdParty/SDL2_ttf/CMakeLists.txt delete mode 100644 android-project/CMake/FindFreetype.cmake delete mode 100644 android-project/CMake/FindSDL2.cmake delete mode 100644 android-project/CMake/android_defs.cmake delete mode 100644 android-project/app/build.gradle delete mode 100644 android-project/app/proguard-rules.pro delete mode 100644 android-project/app/src/main/AndroidManifest.xml delete mode 100644 android-project/app/src/main/assets/.gitkeep delete mode 100644 android-project/app/src/main/ic_launcher-playstore.png delete mode 100644 android-project/app/src/main/java/org/diasurgical/devilutionx/DataActivity.java delete mode 100644 android-project/app/src/main/java/org/diasurgical/devilutionx/DevilutionXSDLActivity.java delete mode 100644 android-project/app/src/main/java/org/libsdl/app/HIDDevice.java delete mode 100644 android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java delete mode 100644 android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java delete mode 100644 android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java delete mode 100644 android-project/app/src/main/java/org/libsdl/app/SDL.java delete mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLActivity.java delete mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLAudioManager.java delete mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java delete mode 100644 android-project/app/src/main/res/layout/activity_data.xml delete mode 100644 android-project/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 android-project/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 android-project/app/src/main/res/mipmap-hdpi/ic_launcher_background.png delete mode 100644 android-project/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 android-project/app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 android-project/app/src/main/res/mipmap-mdpi/ic_launcher_background.png delete mode 100644 android-project/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 android-project/app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 android-project/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png delete mode 100644 android-project/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 android-project/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png delete mode 100644 android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png delete mode 100644 android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png delete mode 100644 android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 android-project/app/src/main/res/values-bg/strings.xml delete mode 100644 android-project/app/src/main/res/values-da/strings.xml delete mode 100644 android-project/app/src/main/res/values-de/strings.xml delete mode 100644 android-project/app/src/main/res/values-es/strings.xml delete mode 100644 android-project/app/src/main/res/values-fr/strings.xml delete mode 100644 android-project/app/src/main/res/values-hr/strings.xml delete mode 100644 android-project/app/src/main/res/values-it/strings.xml delete mode 100644 android-project/app/src/main/res/values-ko-rKR/strings.xml delete mode 100644 android-project/app/src/main/res/values-pt-rBR/strings.xml delete mode 100644 android-project/app/src/main/res/values-ru/strings.xml delete mode 100644 android-project/app/src/main/res/values-sv/strings.xml delete mode 100644 android-project/app/src/main/res/values-zh-rCN/strings.xml delete mode 100644 android-project/app/src/main/res/values-zh-rTW/strings.xml delete mode 100644 android-project/app/src/main/res/values/colors.xml delete mode 100644 android-project/app/src/main/res/values/strings.xml delete mode 100644 android-project/app/src/main/res/values/themes.xml delete mode 100644 android-project/build.gradle delete mode 100644 android-project/gradle.properties delete mode 100644 android-project/gradle/wrapper/gradle-wrapper.jar delete mode 100644 android-project/gradle/wrapper/gradle-wrapper.properties delete mode 100755 android-project/gradlew delete mode 100644 android-project/gradlew.bat delete mode 100644 android-project/settings.gradle diff --git a/.circleci/config.yml b/.circleci/config.yml index 03540b8bf75..113f3ab5545 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -40,16 +40,6 @@ jobs: - run: /opt/devkitpro/portlibs/switch/bin/aarch64-none-elf-cmake -S. -Bbuild .. -DNIGHTLY_BUILD=ON - run: cmake --build build -j 2 - store_artifacts: {path: ./build/devilutionx.nro, destination: devilutionx.nro} - android: - docker: - - image: circleci/android:api-28-ndk - working_directory: ~/repo - steps: - - checkout - - run: sudo apt update -y - - run: sudo apt install -y g++ cmake ninja-build - - run: cd android-project && ./gradlew assembleDebug - - store_artifacts: {path: ./android-project/app/build/outputs/apk/debug/app-debug.apk, destination: devilutionx-debug.apk} 3ds: docker: - image: devkitpro/devkitarm:latest @@ -97,7 +87,6 @@ workflows: - linux_x86_64 - linux_x86_64_test - switch - - android - 3ds - amigaos-m68k - vita diff --git a/CMakeLists.txt b/CMakeLists.txt index 07034a4d2b3..ea305564aef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,11 +193,6 @@ if(NINTENDO_3DS) include(n3ds_defs) endif() -if(ANDROID) - list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android-project/CMake") - include(android_defs) -endif() - if(PIE) set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) endif() @@ -249,10 +244,7 @@ if(NOT PNG_FOUND) add_subdirectory(3rdParty/libpng) endif() -if(ANDROID) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/android-project/3rdParty/SDL2) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/android-project/3rdParty/SDL2_ttf) -elseif(USE_SDL1) +if(USE_SDL1) find_package(SDL REQUIRED) find_package(SDL_ttf REQUIRED) include_directories(${SDL_INCLUDE_DIR}) @@ -574,18 +566,14 @@ if(RUN_TESTS) endif() add_library(libdevilutionx OBJECT ${libdevilutionx_SRCS}) -if (ANDROID) - add_library(${BIN_TARGET} SHARED Source/main.cpp) -else() - add_executable(${BIN_TARGET} - WIN32 - MACOSX_BUNDLE - Source/main.cpp - Source/devilutionx.exe.manifest - Packaging/macOS/AppIcon.icns - Packaging/resources/CharisSILB.ttf - Packaging/windows/devilutionx.rc) -endif() +add_executable(${BIN_TARGET} + WIN32 + MACOSX_BUNDLE + Source/main.cpp + Source/devilutionx.exe.manifest + Packaging/macOS/AppIcon.icns + Packaging/resources/CharisSILB.ttf + Packaging/windows/devilutionx.rc) target_link_libraries(${BIN_TARGET} PRIVATE libdevilutionx) # Copy the font and devilutionx.mpq to the build directory to it works from the build directory @@ -649,14 +637,6 @@ if (Gettext_FOUND) list(APPEND VITA_TRANSLATIONS_LIST "FILE" "${CMAKE_CURRENT_BINARY_DIR}/${lang}.gmo" "${lang}.gmo") endif() endforeach(lang) - - if(ANDROID) - add_custom_target(copy_translations ALL - COMMAND ${CMAKE_COMMAND} -E copy ${devilutionx_TRANSLATIONS} ${DevilutionX_SOURCE_DIR}/android-project/app/src/main/assets - DEPENDS ${devilutionx_TRANSLATIONS}) - - add_dependencies(${BIN_TARGET} copy_translations) - endif() endif() target_include_directories(libdevilutionx PUBLIC diff --git a/Source/DiabloUI/fonts.cpp b/Source/DiabloUI/fonts.cpp index 7d55aa43754..e35bbeca6ce 100644 --- a/Source/DiabloUI/fonts.cpp +++ b/Source/DiabloUI/fonts.cpp @@ -23,7 +23,7 @@ void LoadTtfFont() } std::string ttfFontPath = paths::TtfPath() + paths::TtfName(); -#if defined(__linux__) && !defined(__ANDROID__) +#if defined(__linux__) if (!FileExists(ttfFontPath.c_str())) { ttfFontPath = "/usr/share/fonts/truetype/" + paths::TtfName(); } diff --git a/Source/init.cpp b/Source/init.cpp index c320224d268..c70e1ef87e9 100644 --- a/Source/init.cpp +++ b/Source/init.cpp @@ -147,7 +147,7 @@ void init_archives() if (paths[0] == paths[1]) paths.pop_back(); -#if defined(__linux__) && !defined(__ANDROID__) +#if defined(__linux__) paths.emplace_back("/usr/share/diasurgical/devilutionx/"); paths.emplace_back("/usr/local/share/diasurgical/devilutionx/"); #elif defined(__3DS__) diff --git a/Source/main.cpp b/Source/main.cpp index cdf4cf6a269..085c7234874 100644 --- a/Source/main.cpp +++ b/Source/main.cpp @@ -19,11 +19,7 @@ extern "C" const char *__asan_default_options() // NOLINT(bugprone-reserved-iden } #endif -#ifdef __ANDROID__ -int SDL_main(int argc, char **argv) -#else int main(int argc, char **argv) -#endif { #ifdef __SWITCH__ switch_enable_network(); diff --git a/Source/options.cpp b/Source/options.cpp index 0e0c6855dda..176fa6ce2b0 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -8,11 +8,6 @@ #include #include -#ifdef __ANDROID__ -#include "SDL.h" -#include -#endif - #ifdef __vita__ #include #include @@ -170,14 +165,9 @@ void SaveIni() #if SDL_VERSION_ATLEAST(2, 0, 0) bool HardwareCursorDefault() { -#ifdef __ANDROID__ - // See https://github.com/diasurgical/devilutionX/issues/2502 - return false; -#else SDL_version v; SDL_GetVersion(&v); return SDL_VERSIONNUM(v.major, v.minor, v.patch) >= SDL_VERSIONNUM(2, 0, 12); -#endif } #endif @@ -285,19 +275,7 @@ void LoadOptions() sgOptions.Controller.bRearTouch = GetIniBool("Controller", "Enable Rear Touchpad", true); #endif -#ifdef __ANDROID__ - JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv(); - jobject activity = (jobject)SDL_AndroidGetActivity(); - jclass clazz(env->GetObjectClass(activity)); - jmethodID method_id = env->GetMethodID(clazz, "getLocale", "()Ljava/lang/String;"); - jstring jLocale = (jstring)env->CallObjectMethod(activity, method_id); - const char *cLocale = env->GetStringUTFChars(jLocale, nullptr); - std::string locale = cLocale; - env->ReleaseStringUTFChars(jLocale, cLocale); - env->DeleteLocalRef(jLocale); - env->DeleteLocalRef(activity); - env->DeleteLocalRef(clazz); -#elif defined(__vita__) +#if defined(__vita__) int32_t language = SCE_SYSTEM_PARAM_LANG_ENGLISH_US; // default to english const char *vita_locales[] = { "ja_JP", diff --git a/Source/storm/storm_file_wrapper.h b/Source/storm/storm_file_wrapper.h index 3e204cbb3cc..7a83c24c927 100644 --- a/Source/storm/storm_file_wrapper.h +++ b/Source/storm/storm_file_wrapper.h @@ -1,6 +1,6 @@ /** A pointer to a Storm file as a `FILE *`. Only available on some platforms. */ #pragma once -#if (defined(__linux__) && !defined(__ANDROID__)) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__HAIKU__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__HAIKU__) #include #include "miniwin/miniwin.h" diff --git a/Source/storm/storm_sdl_rw.cpp b/Source/storm/storm_sdl_rw.cpp index 198c56e497f..2233f69a8bf 100644 --- a/Source/storm/storm_sdl_rw.cpp +++ b/Source/storm/storm_sdl_rw.cpp @@ -98,17 +98,6 @@ SDL_RWops *SFileRw_FromStormHandle(HANDLE handle) SDL_RWops *SFileOpenRw(const char *filename) { -#ifdef __ANDROID__ - std::string relativePath = filename; - for (std::size_t i = 0; i < relativePath.size(); ++i) { - if (relativePath[i] == '\\') - relativePath[i] = '/'; - } - SDL_RWops *rwops = SDL_RWFromFile(relativePath.c_str(), "rb"); - if (rwops != nullptr) - return rwops; -#endif - HANDLE handle; if (SFileOpenFile(filename, &handle)) return SFileRw_FromStormHandle(handle); diff --git a/Source/utils/file_util.cpp b/Source/utils/file_util.cpp index 0fad6cde146..ef353b358c1 100644 --- a/Source/utils/file_util.cpp +++ b/Source/utils/file_util.cpp @@ -62,7 +62,7 @@ bool FileExists(const char *path) return false; } return true; -#elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) && !defined(__ANDROID__) +#elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) return ::access(path, F_OK) == 0; #else SDL_RWops *file = SDL_RWFromFile(path, "r+b"); diff --git a/android-project/.gitignore b/android-project/.gitignore deleted file mode 100644 index 75fb9aeb26b..00000000000 --- a/android-project/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# Intellij -.idea/ - -# Gradle files -/.gradle/ -build/ - -# Android -/app/.cxx/ - -# Local configuration file (sdk path, etc) -/local.properties - -/app/src/main/assets/CharisSILB.ttf -/app/src/main/assets/*.gmo -/app/src/main/assets/ui_art/* diff --git a/android-project/3rdParty/SDL2/CMakeLists.txt b/android-project/3rdParty/SDL2/CMakeLists.txt deleted file mode 100644 index 04f49d90f2a..00000000000 --- a/android-project/3rdParty/SDL2/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -set(BUILD_SHARED_LIBS ON) - -include(FetchContent_MakeAvailableExcludeFromAll) -include(FetchContent) -FetchContent_Declare(SDL2 - URL https://github.com/libsdl-org/SDL/archive/4cd981609b50ed273d80c635c1ca4c1e5518fb21.tar.gz - URL_HASH MD5=b805579e8bf30dcc543eced3686ee72e -) -FetchContent_MakeAvailableExcludeFromAll(SDL2) - -add_library(SDL2::SDL2 ALIAS SDL2) diff --git a/android-project/3rdParty/SDL2_ttf/CMakeLists.txt b/android-project/3rdParty/SDL2_ttf/CMakeLists.txt deleted file mode 100644 index 17731d2f36b..00000000000 --- a/android-project/3rdParty/SDL2_ttf/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -set(BUILD_SHARED_LIBS ON) - -include(FetchContent_MakeAvailableExcludeFromAll) -include(FetchContent) - -FetchContent_Declare(SDL2_ttf - URL https://github.com/libsdl-org/SDL_ttf/archive/33cdd1881e31184b49a68b4890d1d256fc0c6dc1.tar.gz - URL_HASH MD5=7cfa28e6170618acf50d6a9cd27091ab -) -FetchContent_MakeAvailableExcludeFromAll(SDL2_ttf) - -# SDL2_ttf only provides an INSTALL_INTERFACE directory -# so use the source directory for the BUILD_INTERFACE -target_include_directories(SDL2_ttf PUBLIC $) - -add_library(SDL2::SDL2_ttf ALIAS SDL2_ttf) \ No newline at end of file diff --git a/android-project/CMake/FindFreetype.cmake b/android-project/CMake/FindFreetype.cmake deleted file mode 100644 index 51e6b1ee1f4..00000000000 --- a/android-project/CMake/FindFreetype.cmake +++ /dev/null @@ -1,9 +0,0 @@ -# Use globbing to find the source directory regardless of the version number -file(GLOB freetype_SOURCE_DIR ${SDL_ttf_SOURCE_DIR}/external/freetype*) -add_subdirectory(${freetype_SOURCE_DIR}) - -# freetype only provides an INSTALL_INTERFACE directory -# so use the source directory for the BUILD_INTERFACE -target_include_directories(freetype PUBLIC $) - -add_library(Freetype::Freetype ALIAS freetype) \ No newline at end of file diff --git a/android-project/CMake/FindSDL2.cmake b/android-project/CMake/FindSDL2.cmake deleted file mode 100644 index 8095f70786e..00000000000 --- a/android-project/CMake/FindSDL2.cmake +++ /dev/null @@ -1,2 +0,0 @@ -# This script exists for find_package() but SDL2 is provided by: -# .../devilutionX/android-project/3rdParty/SDL2/CMakeLists.txt \ No newline at end of file diff --git a/android-project/CMake/android_defs.cmake b/android-project/CMake/android_defs.cmake deleted file mode 100644 index ee0ccc3d0c8..00000000000 --- a/android-project/CMake/android_defs.cmake +++ /dev/null @@ -1,22 +0,0 @@ -#General compilation options -set(ASAN OFF) -set(UBSAN OFF) -set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) -set(DEVILUTIONX_SYSTEM_LIBPNG OFF) -set(DISABLE_ZERO_TIER ON) -set(VIRTUAL_GAMEPAD ON) - -if(BINARY_RELEASE OR CMAKE_BUILD_TYPE STREQUAL "Release") - # Workaroudn linker bug in CLang: https://github.com/android/ndk/issues/721 - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -flto=full") -endif() - -#additional compilation definitions -set(TTF_FONT_DIR \"\") - -file( - COPY "${DevilutionX_SOURCE_DIR}/Packaging/resources/CharisSILB.ttf" - DESTINATION "${DevilutionX_SOURCE_DIR}/android-project/app/src/main/assets") - -file(GLOB VirtualGamepadArt "${DevilutionX_SOURCE_DIR}/Packaging/resources/ui_art/*") -file(COPY ${VirtualGamepadArt} DESTINATION "${DevilutionX_SOURCE_DIR}/android-project/app/src/main/assets/ui_art") diff --git a/android-project/app/build.gradle b/android-project/app/build.gradle deleted file mode 100644 index 84eb5f702a9..00000000000 --- a/android-project/app/build.gradle +++ /dev/null @@ -1,64 +0,0 @@ -def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY'); -def buildAsApplication = !buildAsLibrary -if (buildAsApplication) { - apply plugin: 'com.android.application' -} else { - apply plugin: 'com.android.library' -} - -android { - compileSdkVersion 29 // Upgrade to 30 after november 2021 - aaptOptions { - noCompress 'mpq' - } - defaultConfig { - if (buildAsApplication) { - applicationId "org.diasurgical.devilutionx" - } - minSdkVersion 18 - targetSdkVersion 29 // Upgrade to 30 after november 2021 - versionCode 18 - versionName "1.2.1" - externalNativeBuild { - cmake { - arguments "-DANDROID_STL=c++_static" - abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - } - } - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) { - sourceSets.main { - jniLibs.srcDir 'libs' - } - externalNativeBuild { - cmake { - path '../../CMakeLists.txt' - version "3.13.0+" - } - } - - } - - if (buildAsLibrary) { - libraryVariants.all { variant -> - variant.outputs.each { output -> - def outputFile = output.outputFile - if (outputFile != null && outputFile.name.endsWith(".aar")) { - def fileName = "org.diasurgical.devilutionx.aar"; - output.outputFile = new File(outputFile.parent, fileName); - } - } - } - } -} - -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.android.support.constraint:constraint-layout:2.0.4' -} diff --git a/android-project/app/proguard-rules.pro b/android-project/app/proguard-rules.pro deleted file mode 100644 index eaf0e916cdf..00000000000 --- a/android-project/app/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in [sdk]/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/android-project/app/src/main/AndroidManifest.xml b/android-project/app/src/main/AndroidManifest.xml deleted file mode 100644 index 4e822598ee8..00000000000 --- a/android-project/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android-project/app/src/main/assets/.gitkeep b/android-project/app/src/main/assets/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/android-project/app/src/main/ic_launcher-playstore.png b/android-project/app/src/main/ic_launcher-playstore.png deleted file mode 100644 index 2935c73d2948952692dd35a31181e34d7546c290..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68393 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 diff --git a/android-project/app/src/main/java/org/diasurgical/devilutionx/DataActivity.java b/android-project/app/src/main/java/org/diasurgical/devilutionx/DataActivity.java deleted file mode 100644 index 570c215a94d..00000000000 --- a/android-project/app/src/main/java/org/diasurgical/devilutionx/DataActivity.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.diasurgical.devilutionx; - -import android.app.Activity; -import android.app.DownloadManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.text.method.LinkMovementMethod; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; - -import java.io.File; - -public class DataActivity extends Activity { - private String externalDir; - private DownloadReceiver mReceiver; - private boolean isDownloading = false; - - @Override - protected void onCreate(Bundle savedInstanceState) { - externalDir = getExternalFilesDir(null).getAbsolutePath(); - - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_data); - - ((TextView) findViewById(R.id.full_guide)).setMovementMethod(LinkMovementMethod.getInstance()); - ((TextView) findViewById(R.id.online_guide)).setMovementMethod(LinkMovementMethod.getInstance()); - } - - protected void onResume() { - super.onResume(); - startGame(); - } - - public void startGame(View view) { - startGame(); - } - - private void startGame() { - if (missingGameData()) { - Toast toast = Toast.makeText(getApplicationContext(), getString(R.string.missing_game_data), Toast.LENGTH_SHORT); - toast.show(); - return; - } - - Intent intent = new Intent(this, DevilutionXSDLActivity.class); - startActivity(intent); - this.finish(); - } - - protected void onDestroy() { - if (mReceiver != null) - unregisterReceiver(mReceiver); - - super.onDestroy(); - } - - /** - * Check if the game data is present - */ - private boolean missingGameData() { - File fileLower = new File(externalDir + "/diabdat.mpq"); - File fileUpper = new File(externalDir + "/DIABDAT.MPQ"); - File spawnFile = new File(externalDir + "/spawn.mpq"); - - return !fileUpper.exists() && !fileLower.exists() && (!spawnFile.exists() || isDownloading); - } - - /** - * Start downloading the shareware - */ - public void sendDownloadRequest(View view) { - String url = "https://github.com/d07RiV/diabloweb/raw/3a5a51e84d5dab3cfd4fef661c46977b091aaa9c/spawn.mpq"; - String fileName = "spawn.mpq"; - - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)) - .setTitle(fileName) - .setDescription(getString(R.string.shareware_data)) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE); - - request.setDestinationInExternalFilesDir(this, null, fileName); - - DownloadManager downloadManager = (DownloadManager)this.getSystemService(Context.DOWNLOAD_SERVICE); - downloadManager.enqueue(request); - - if (mReceiver == null) - mReceiver = new DownloadReceiver(); - registerReceiver(mReceiver, new IntentFilter("android.intent.action.DOWNLOAD_COMPLETE")); - - isDownloading = true; - view.setEnabled(false); - - Toast toast = Toast.makeText(getApplicationContext(), getString(R.string.download_started), Toast.LENGTH_SHORT); - toast.show(); - } - - /** - * Start game when download finishes - */ - private class DownloadReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - isDownloading = false; - - long receivedID = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L); - DownloadManager mgr = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - - DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterById(receivedID); - Cursor cur = mgr.query(query); - int index = cur.getColumnIndex(DownloadManager.COLUMN_STATUS); - if (cur.moveToFirst()) { - if (cur.getInt(index) == DownloadManager.STATUS_SUCCESSFUL) { - startGame(); - } - } - cur.close(); - findViewById(R.id.download_button).setEnabled(true); - } - } -} diff --git a/android-project/app/src/main/java/org/diasurgical/devilutionx/DevilutionXSDLActivity.java b/android-project/app/src/main/java/org/diasurgical/devilutionx/DevilutionXSDLActivity.java deleted file mode 100644 index bb0d88bf61d..00000000000 --- a/android-project/app/src/main/java/org/diasurgical/devilutionx/DevilutionXSDLActivity.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.diasurgical.devilutionx; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Rect; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.util.Log; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.ViewTreeObserver; - -import org.libsdl.app.SDLActivity; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Locale; - -public class DevilutionXSDLActivity extends SDLActivity { - private String externalDir; - - protected void onCreate(Bundle savedInstanceState) { - // windowSoftInputMode=adjustPan stopped working - // for fullscreen apps after Android 7.0 - if (Build.VERSION.SDK_INT >= 25) - trackVisibleSpace(); - - externalDir = getExternalFilesDir(null).getAbsolutePath(); - - migrateAppData(); - - super.onCreate(savedInstanceState); - } - - /** - * On app launch make sure the game data is present - */ - protected void onStart() { - super.onStart(); - - if (missingGameData()) { - Intent intent = new Intent(this, DataActivity.class); - startActivity(intent); - this.finish(); - } - } - - private void trackVisibleSpace() { - this.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - // Software keyboard may encroach on the app's visible space so - // force the drawing surface to fit in the visible display frame - Rect visibleSpace = new Rect(); - getWindow().getDecorView().getWindowVisibleDisplayFrame(visibleSpace); - - SurfaceView surface = mSurface; - SurfaceHolder holder = surface.getHolder(); - holder.setFixedSize(visibleSpace.width(), visibleSpace.height()); - } - }); - } - - private boolean missingGameData() { - File fileLower = new File(externalDir + "/diabdat.mpq"); - File fileUpper = new File(externalDir + "/DIABDAT.MPQ"); - File spawnFile = new File(externalDir + "/spawn.mpq"); - - return !fileUpper.exists() && !fileLower.exists() && !spawnFile.exists(); - } - - private boolean copyFile(File src, File dst) { - try { - InputStream in = new FileInputStream(src); - try { - OutputStream out = new FileOutputStream(dst); - try { - // Transfer bytes from in to out - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } finally { - out.close(); - } - } finally { - in.close(); - } - } catch (IOException exception) { - Log.e("copyFile", exception.getMessage()); - if (dst.exists()) { - //noinspection ResultOfMethodCallIgnored - dst.delete(); - } - return false; - } - - return true; - } - - private void migrateFile(File file) { - if (!file.exists() || !file.canRead()) { - return; - } - File newPath = new File(externalDir + "/" + file.getName()); - if (newPath.exists()) { - if (file.canWrite()) { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } - return; - } - if (!new File(newPath.getParent()).canWrite()) { - return; - } - if (!file.renameTo(newPath)) { - if (copyFile(file, newPath) && file.canWrite()) { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } - } - } - - /** - * This can be removed Nov 2021 and Google will no longer permit access to the old folder from that point on - */ - @SuppressWarnings("deprecation") - @SuppressLint("SdCardPath") - private void migrateAppData() { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { - if (PackageManager.PERMISSION_GRANTED != checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { - return; - } - } - - migrateFile(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/diabdat.mpq")); - migrateFile(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/DIABDAT.MPQ")); - - migrateFile(new File("/sdcard/diabdat.mpq")); - migrateFile(new File("/sdcard/devilutionx/diabdat.mpq")); - migrateFile(new File("/sdcard/devilutionx/spawn.mpq")); - - for (File internalFile : getFilesDir().listFiles()) { - migrateFile(internalFile); - } - } - - /** - * This method is called by SDL using JNI. - */ - public String getLocale() - { - return Locale.getDefault().toString(); - } - - protected String[] getArguments() { - if (BuildConfig.DEBUG) { - return new String[]{ - "--data-dir", - externalDir, - "--config-dir", - externalDir, - "--save-dir", - externalDir, - "--verbose", - }; - } - - return new String[]{ - "--data-dir", - externalDir, - "--config-dir", - externalDir, - "--save-dir", - externalDir - }; - } - - protected String[] getLibraries() { - return new String[]{ - "SDL2", - "SDL2_ttf", - "devilutionx" - }; - } -} diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java b/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java deleted file mode 100644 index 955df5d14c0..00000000000 --- a/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.libsdl.app; - -import android.hardware.usb.UsbDevice; - -interface HIDDevice -{ - public int getId(); - public int getVendorId(); - public int getProductId(); - public String getSerialNumber(); - public int getVersion(); - public String getManufacturerName(); - public String getProductName(); - public UsbDevice getDevice(); - public boolean open(); - public int sendFeatureReport(byte[] report); - public int sendOutputReport(byte[] report); - public boolean getFeatureReport(byte[] report); - public void setFrozen(boolean frozen); - public void close(); - public void shutdown(); -} diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java deleted file mode 100644 index 94a28189b8b..00000000000 --- a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ /dev/null @@ -1,650 +0,0 @@ -package org.libsdl.app; - -import android.content.Context; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattDescriptor; -import android.bluetooth.BluetoothManager; -import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothGattService; -import android.hardware.usb.UsbDevice; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.os.*; - -//import com.android.internal.util.HexDump; - -import java.lang.Runnable; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.UUID; - -class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { - - private static final String TAG = "hidapi"; - private HIDDeviceManager mManager; - private BluetoothDevice mDevice; - private int mDeviceId; - private BluetoothGatt mGatt; - private boolean mIsRegistered = false; - private boolean mIsConnected = false; - private boolean mIsChromebook = false; - private boolean mIsReconnecting = false; - private boolean mFrozen = false; - private LinkedList mOperations; - GattOperation mCurrentOperation = null; - private Handler mHandler; - - private static final int TRANSPORT_AUTO = 0; - private static final int TRANSPORT_BREDR = 1; - private static final int TRANSPORT_LE = 2; - - private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; - - static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); - static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); - static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); - static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; - - static class GattOperation { - private enum Operation { - CHR_READ, - CHR_WRITE, - ENABLE_NOTIFICATION - } - - Operation mOp; - UUID mUuid; - byte[] mValue; - BluetoothGatt mGatt; - boolean mResult = true; - - private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { - mGatt = gatt; - mOp = operation; - mUuid = uuid; - } - - private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { - mGatt = gatt; - mOp = operation; - mUuid = uuid; - mValue = value; - } - - public void run() { - // This is executed in main thread - BluetoothGattCharacteristic chr; - - switch (mOp) { - case CHR_READ: - chr = getCharacteristic(mUuid); - //Log.v(TAG, "Reading characteristic " + chr.getUuid()); - if (!mGatt.readCharacteristic(chr)) { - Log.e(TAG, "Unable to read characteristic " + mUuid.toString()); - mResult = false; - break; - } - mResult = true; - break; - case CHR_WRITE: - chr = getCharacteristic(mUuid); - //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value)); - chr.setValue(mValue); - if (!mGatt.writeCharacteristic(chr)) { - Log.e(TAG, "Unable to write characteristic " + mUuid.toString()); - mResult = false; - break; - } - mResult = true; - break; - case ENABLE_NOTIFICATION: - chr = getCharacteristic(mUuid); - //Log.v(TAG, "Writing descriptor of " + chr.getUuid()); - if (chr != null) { - BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); - if (cccd != null) { - int properties = chr.getProperties(); - byte[] value; - if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) { - value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; - } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) { - value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; - } else { - Log.e(TAG, "Unable to start notifications on input characteristic"); - mResult = false; - return; - } - - mGatt.setCharacteristicNotification(chr, true); - cccd.setValue(value); - if (!mGatt.writeDescriptor(cccd)) { - Log.e(TAG, "Unable to write descriptor " + mUuid.toString()); - mResult = false; - return; - } - mResult = true; - } - } - } - } - - public boolean finish() { - return mResult; - } - - private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { - BluetoothGattService valveService = mGatt.getService(steamControllerService); - if (valveService == null) - return null; - return valveService.getCharacteristic(uuid); - } - - static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) { - return new GattOperation(gatt, Operation.CHR_READ, uuid); - } - - static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) { - return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value); - } - - static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { - return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); - } - } - - public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { - mManager = manager; - mDevice = device; - mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); - mIsRegistered = false; - mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); - mOperations = new LinkedList(); - mHandler = new Handler(Looper.getMainLooper()); - - mGatt = connectGatt(); - // final HIDDeviceBLESteamController finalThis = this; - // mHandler.postDelayed(new Runnable() { - // @Override - // public void run() { - // finalThis.checkConnectionForChromebookIssue(); - // } - // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); - } - - public String getIdentifier() { - return String.format("SteamController.%s", mDevice.getAddress()); - } - - public BluetoothGatt getGatt() { - return mGatt; - } - - // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead - // of TRANSPORT_LE. Let's force ourselves to connect low energy. - private BluetoothGatt connectGatt(boolean managed) { - if (Build.VERSION.SDK_INT >= 23) { - try { - return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE); - } catch (Exception e) { - return mDevice.connectGatt(mManager.getContext(), managed, this); - } - } else { - return mDevice.connectGatt(mManager.getContext(), managed, this); - } - } - - private BluetoothGatt connectGatt() { - return connectGatt(false); - } - - protected int getConnectionState() { - - Context context = mManager.getContext(); - if (context == null) { - // We are lacking any context to get our Bluetooth information. We'll just assume disconnected. - return BluetoothProfile.STATE_DISCONNECTED; - } - - BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE); - if (btManager == null) { - // This device doesn't support Bluetooth. We should never be here, because how did - // we instantiate a device to start with? - return BluetoothProfile.STATE_DISCONNECTED; - } - - return btManager.getConnectionState(mDevice, BluetoothProfile.GATT); - } - - public void reconnect() { - - if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) { - mGatt.disconnect(); - mGatt = connectGatt(); - } - - } - - protected void checkConnectionForChromebookIssue() { - if (!mIsChromebook) { - // We only do this on Chromebooks, because otherwise it's really annoying to just attempt - // over and over. - return; - } - - int connectionState = getConnectionState(); - - switch (connectionState) { - case BluetoothProfile.STATE_CONNECTED: - if (!mIsConnected) { - // We are in the Bad Chromebook Place. We can force a disconnect - // to try to recover. - Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect."); - mIsReconnecting = true; - mGatt.disconnect(); - mGatt = connectGatt(false); - break; - } - else if (!isRegistered()) { - if (mGatt.getServices().size() > 0) { - Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover."); - probeService(this); - } - else { - Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover."); - mIsReconnecting = true; - mGatt.disconnect(); - mGatt = connectGatt(false); - break; - } - } - else { - Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!"); - return; - } - break; - - case BluetoothProfile.STATE_DISCONNECTED: - Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover."); - - mIsReconnecting = true; - mGatt.disconnect(); - mGatt = connectGatt(false); - break; - - case BluetoothProfile.STATE_CONNECTING: - Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer."); - break; - } - - final HIDDeviceBLESteamController finalThis = this; - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - finalThis.checkConnectionForChromebookIssue(); - } - }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); - } - - private boolean isRegistered() { - return mIsRegistered; - } - - private void setRegistered() { - mIsRegistered = true; - } - - private boolean probeService(HIDDeviceBLESteamController controller) { - - if (isRegistered()) { - return true; - } - - if (!mIsConnected) { - return false; - } - - Log.v(TAG, "probeService controller=" + controller); - - for (BluetoothGattService service : mGatt.getServices()) { - if (service.getUuid().equals(steamControllerService)) { - Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); - - for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { - if (chr.getUuid().equals(inputCharacteristic)) { - Log.v(TAG, "Found input characteristic"); - // Start notifications - BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); - if (cccd != null) { - enableNotification(chr.getUuid()); - } - } - } - return true; - } - } - - if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) { - Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us."); - mIsConnected = false; - mIsReconnecting = true; - mGatt.disconnect(); - mGatt = connectGatt(false); - } - - return false; - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - private void finishCurrentGattOperation() { - GattOperation op = null; - synchronized (mOperations) { - if (mCurrentOperation != null) { - op = mCurrentOperation; - mCurrentOperation = null; - } - } - if (op != null) { - boolean result = op.finish(); // TODO: Maybe in main thread as well? - - // Our operation failed, let's add it back to the beginning of our queue. - if (!result) { - mOperations.addFirst(op); - } - } - executeNextGattOperation(); - } - - private void executeNextGattOperation() { - synchronized (mOperations) { - if (mCurrentOperation != null) - return; - - if (mOperations.isEmpty()) - return; - - mCurrentOperation = mOperations.removeFirst(); - } - - // Run in main thread - mHandler.post(new Runnable() { - @Override - public void run() { - synchronized (mOperations) { - if (mCurrentOperation == null) { - Log.e(TAG, "Current operation null in executor?"); - return; - } - - mCurrentOperation.run(); - // now wait for the GATT callback and when it comes, finish this operation - } - } - }); - } - - private void queueGattOperation(GattOperation op) { - synchronized (mOperations) { - mOperations.add(op); - } - executeNextGattOperation(); - } - - private void enableNotification(UUID chrUuid) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); - queueGattOperation(op); - } - - public void writeCharacteristic(UUID uuid, byte[] value) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); - queueGattOperation(op); - } - - public void readCharacteristic(UUID uuid) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); - queueGattOperation(op); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////// BluetoothGattCallback overridden methods - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - public void onConnectionStateChange(BluetoothGatt g, int status, int newState) { - //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState); - mIsReconnecting = false; - if (newState == 2) { - mIsConnected = true; - // Run directly, without GattOperation - if (!isRegistered()) { - mHandler.post(new Runnable() { - @Override - public void run() { - mGatt.discoverServices(); - } - }); - } - } - else if (newState == 0) { - mIsConnected = false; - } - - // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent. - } - - public void onServicesDiscovered(BluetoothGatt gatt, int status) { - //Log.v(TAG, "onServicesDiscovered status=" + status); - if (status == 0) { - if (gatt.getServices().size() == 0) { - Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack."); - mIsReconnecting = true; - mIsConnected = false; - gatt.disconnect(); - mGatt = connectGatt(false); - } - else { - probeService(this); - } - } - } - - public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid()); - - if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { - mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue()); - } - - finishCurrentGattOperation(); - } - - public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid()); - - if (characteristic.getUuid().equals(reportCharacteristic)) { - // Only register controller with the native side once it has been fully configured - if (!isRegistered()) { - Log.v(TAG, "Registering Steam Controller with ID: " + getId()); - mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0); - setRegistered(); - } - } - - finishCurrentGattOperation(); - } - - public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - // Enable this for verbose logging of controller input reports - //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); - - if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { - mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); - } - } - - public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - //Log.v(TAG, "onDescriptorRead status=" + status); - } - - public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); - //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); - - if (chr.getUuid().equals(inputCharacteristic)) { - boolean hasWrittenInputDescriptor = true; - BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); - if (reportChr != null) { - Log.v(TAG, "Writing report characteristic to enter valve mode"); - reportChr.setValue(enterValveMode); - gatt.writeCharacteristic(reportChr); - } - } - - finishCurrentGattOperation(); - } - - public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { - //Log.v(TAG, "onReliableWriteCompleted status=" + status); - } - - public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { - //Log.v(TAG, "onReadRemoteRssi status=" + status); - } - - public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { - //Log.v(TAG, "onMtuChanged status=" + status); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - //////// Public API - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public int getId() { - return mDeviceId; - } - - @Override - public int getVendorId() { - // Valve Corporation - final int VALVE_USB_VID = 0x28DE; - return VALVE_USB_VID; - } - - @Override - public int getProductId() { - // We don't have an easy way to query from the Bluetooth device, but we know what it is - final int D0G_BLE2_PID = 0x1106; - return D0G_BLE2_PID; - } - - @Override - public String getSerialNumber() { - // This will be read later via feature report by Steam - return "12345"; - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public String getManufacturerName() { - return "Valve Corporation"; - } - - @Override - public String getProductName() { - return "Steam Controller"; - } - - @Override - public UsbDevice getDevice() { - return null; - } - - @Override - public boolean open() { - return true; - } - - @Override - public int sendFeatureReport(byte[] report) { - if (!isRegistered()) { - Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return -1; - } - - // We need to skip the first byte, as that doesn't go over the air - byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); - //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report)); - writeCharacteristic(reportCharacteristic, actual_report); - return report.length; - } - - @Override - public int sendOutputReport(byte[] report) { - if (!isRegistered()) { - Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return -1; - } - - //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report)); - writeCharacteristic(reportCharacteristic, report); - return report.length; - } - - @Override - public boolean getFeatureReport(byte[] report) { - if (!isRegistered()) { - Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return false; - } - - //Log.v(TAG, "getFeatureReport"); - readCharacteristic(reportCharacteristic); - return true; - } - - @Override - public void close() { - } - - @Override - public void setFrozen(boolean frozen) { - mFrozen = frozen; - } - - @Override - public void shutdown() { - close(); - - BluetoothGatt g = mGatt; - if (g != null) { - g.disconnect(); - g.close(); - mGatt = null; - } - mManager = null; - mIsRegistered = false; - mIsConnected = false; - mOperations.clear(); - } - -} - diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java deleted file mode 100644 index 041dc380f2a..00000000000 --- a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ /dev/null @@ -1,685 +0,0 @@ -package org.libsdl.app; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.PendingIntent; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.bluetooth.BluetoothProfile; -import android.os.Build; -import android.util.Log; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.hardware.usb.*; -import android.os.Handler; -import android.os.Looper; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -public class HIDDeviceManager { - private static final String TAG = "hidapi"; - private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION"; - - private static HIDDeviceManager sManager; - private static int sManagerRefCount = 0; - - public static HIDDeviceManager acquire(Context context) { - if (sManagerRefCount == 0) { - sManager = new HIDDeviceManager(context); - } - ++sManagerRefCount; - return sManager; - } - - public static void release(HIDDeviceManager manager) { - if (manager == sManager) { - --sManagerRefCount; - if (sManagerRefCount == 0) { - sManager.close(); - sManager = null; - } - } - } - - private Context mContext; - private HashMap mDevicesById = new HashMap(); - private HashMap mBluetoothDevices = new HashMap(); - private int mNextDeviceId = 0; - private SharedPreferences mSharedPreferences = null; - private boolean mIsChromebook = false; - private UsbManager mUsbManager; - private Handler mHandler; - private BluetoothManager mBluetoothManager; - private List mLastBluetoothDevices; - - private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - handleUsbDeviceAttached(usbDevice); - } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - handleUsbDeviceDetached(usbDevice); - } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); - } - } - }; - - private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - // Bluetooth device was connected. If it was a Steam Controller, handle it - if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - Log.d(TAG, "Bluetooth device connected: " + device); - - if (isSteamController(device)) { - connectBluetoothDevice(device); - } - } - - // Bluetooth device was disconnected, remove from controller manager (if any) - if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - Log.d(TAG, "Bluetooth device disconnected: " + device); - - disconnectBluetoothDevice(device); - } - } - }; - - private HIDDeviceManager(final Context context) { - mContext = context; - - // Make sure we have the HIDAPI library loaded with the native functions - try { - SDL.loadLibrary("hidapi"); - } catch (Throwable e) { - Log.w(TAG, "Couldn't load hidapi: " + e.toString()); - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setCancelable(false); - builder.setTitle("SDL HIDAPI Error"); - builder.setMessage("Please report the following error to the SDL maintainers: " + e.getMessage()); - builder.setNegativeButton("Quit", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - try { - // If our context is an activity, exit rather than crashing when we can't - // call our native functions. - Activity activity = (Activity)context; - - activity.finish(); - } - catch (ClassCastException cce) { - // Context wasn't an activity, there's nothing we can do. Give up and return. - } - } - }); - builder.show(); - - return; - } - - HIDDeviceRegisterCallback(); - - mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); - mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); - -// if (shouldClear) { -// SharedPreferences.Editor spedit = mSharedPreferences.edit(); -// spedit.clear(); -// spedit.commit(); -// } -// else - { - mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); - } - - initializeUSB(); - initializeBluetooth(); - } - - public Context getContext() { - return mContext; - } - - public int getDeviceIDForIdentifier(String identifier) { - SharedPreferences.Editor spedit = mSharedPreferences.edit(); - - int result = mSharedPreferences.getInt(identifier, 0); - if (result == 0) { - result = mNextDeviceId++; - spedit.putInt("next_device_id", mNextDeviceId); - } - - spedit.putInt(identifier, result); - spedit.commit(); - return result; - } - - private void initializeUSB() { - mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); - - /* - // Logging - for (UsbDevice device : mUsbManager.getDeviceList().values()) { - Log.i(TAG,"Path: " + device.getDeviceName()); - Log.i(TAG,"Manufacturer: " + device.getManufacturerName()); - Log.i(TAG,"Product: " + device.getProductName()); - Log.i(TAG,"ID: " + device.getDeviceId()); - Log.i(TAG,"Class: " + device.getDeviceClass()); - Log.i(TAG,"Protocol: " + device.getDeviceProtocol()); - Log.i(TAG,"Vendor ID " + device.getVendorId()); - Log.i(TAG,"Product ID: " + device.getProductId()); - Log.i(TAG,"Interface count: " + device.getInterfaceCount()); - Log.i(TAG,"---------------------------------------"); - - // Get interface details - for (int index = 0; index < device.getInterfaceCount(); index++) { - UsbInterface mUsbInterface = device.getInterface(index); - Log.i(TAG," ***** *****"); - Log.i(TAG," Interface index: " + index); - Log.i(TAG," Interface ID: " + mUsbInterface.getId()); - Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass()); - Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass()); - Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); - Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); - - // Get endpoint details - for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) - { - UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); - Log.i(TAG," ++++ ++++ ++++"); - Log.i(TAG," Endpoint index: " + epi); - Log.i(TAG," Attributes: " + mEndpoint.getAttributes()); - Log.i(TAG," Direction: " + mEndpoint.getDirection()); - Log.i(TAG," Number: " + mEndpoint.getEndpointNumber()); - Log.i(TAG," Interval: " + mEndpoint.getInterval()); - Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize()); - Log.i(TAG," Type: " + mEndpoint.getType()); - } - } - } - Log.i(TAG," No more devices connected."); - */ - - // Register for USB broadcasts and permission completions - IntentFilter filter = new IntentFilter(); - filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); - filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); - filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); - mContext.registerReceiver(mUsbBroadcast, filter); - - for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { - handleUsbDeviceAttached(usbDevice); - } - } - - UsbManager getUSBManager() { - return mUsbManager; - } - - private void shutdownUSB() { - try { - mContext.unregisterReceiver(mUsbBroadcast); - } catch (Exception e) { - // We may not have registered, that's okay - } - } - - private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) { - if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { - return true; - } - if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) { - return true; - } - return false; - } - - private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) { - final int XB360_IFACE_SUBCLASS = 93; - final int XB360_IFACE_PROTOCOL = 1; // Wired - final int XB360W_IFACE_PROTOCOL = 129; // Wireless - final int[] SUPPORTED_VENDORS = { - 0x0079, // GPD Win 2 - 0x044f, // Thrustmaster - 0x045e, // Microsoft - 0x046d, // Logitech - 0x056e, // Elecom - 0x06a3, // Saitek - 0x0738, // Mad Catz - 0x07ff, // Mad Catz - 0x0e6f, // PDP - 0x0f0d, // Hori - 0x1038, // SteelSeries - 0x11c9, // Nacon - 0x12ab, // Unknown - 0x1430, // RedOctane - 0x146b, // BigBen - 0x1532, // Razer Sabertooth - 0x15e4, // Numark - 0x162e, // Joytech - 0x1689, // Razer Onza - 0x1949, // Lab126, Inc. - 0x1bad, // Harmonix - 0x24c6, // PowerA - }; - - if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && - usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS && - (usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL || - usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) { - int vendor_id = usbDevice.getVendorId(); - for (int supportedVid : SUPPORTED_VENDORS) { - if (vendor_id == supportedVid) { - return true; - } - } - } - return false; - } - - private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) { - final int XB1_IFACE_SUBCLASS = 71; - final int XB1_IFACE_PROTOCOL = 208; - final int[] SUPPORTED_VENDORS = { - 0x045e, // Microsoft - 0x0738, // Mad Catz - 0x0e6f, // PDP - 0x0f0d, // Hori - 0x1532, // Razer Wildcat - 0x24c6, // PowerA - 0x2e24, // Hyperkin - }; - - if (usbInterface.getId() == 0 && - usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && - usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS && - usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) { - int vendor_id = usbDevice.getVendorId(); - for (int supportedVid : SUPPORTED_VENDORS) { - if (vendor_id == supportedVid) { - return true; - } - } - } - return false; - } - - private void handleUsbDeviceAttached(UsbDevice usbDevice) { - connectHIDDeviceUSB(usbDevice); - } - - private void handleUsbDeviceDetached(UsbDevice usbDevice) { - List devices = new ArrayList(); - for (HIDDevice device : mDevicesById.values()) { - if (usbDevice.equals(device.getDevice())) { - devices.add(device.getId()); - } - } - for (int id : devices) { - HIDDevice device = mDevicesById.get(id); - mDevicesById.remove(id); - device.shutdown(); - HIDDeviceDisconnected(id); - } - } - - private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) { - for (HIDDevice device : mDevicesById.values()) { - if (usbDevice.equals(device.getDevice())) { - boolean opened = false; - if (permission_granted) { - opened = device.open(); - } - HIDDeviceOpenResult(device.getId(), opened); - } - } - } - - private void connectHIDDeviceUSB(UsbDevice usbDevice) { - synchronized (this) { - int interface_mask = 0; - for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) { - UsbInterface usbInterface = usbDevice.getInterface(interface_index); - if (isHIDDeviceInterface(usbDevice, usbInterface)) { - // Check to see if we've already added this interface - // This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive - int interface_id = usbInterface.getId(); - if ((interface_mask & (1 << interface_id)) != 0) { - continue; - } - interface_mask |= (1 << interface_id); - - HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); - int id = device.getId(); - mDevicesById.put(id, device); - HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol()); - } - } - } - } - - private void initializeBluetooth() { - Log.d(TAG, "Initializing Bluetooth"); - - if (mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); - return; - } - - if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18)) { - Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE"); - return; - } - - // Find bonded bluetooth controllers and create SteamControllers for them - mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE); - if (mBluetoothManager == null) { - // This device doesn't support Bluetooth. - return; - } - - BluetoothAdapter btAdapter = mBluetoothManager.getAdapter(); - if (btAdapter == null) { - // This device has Bluetooth support in the codebase, but has no available adapters. - return; - } - - // Get our bonded devices. - for (BluetoothDevice device : btAdapter.getBondedDevices()) { - - Log.d(TAG, "Bluetooth device available: " + device); - if (isSteamController(device)) { - connectBluetoothDevice(device); - } - - } - - // NOTE: These don't work on Chromebooks, to my undying dismay. - IntentFilter filter = new IntentFilter(); - filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); - filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); - mContext.registerReceiver(mBluetoothBroadcast, filter); - - if (mIsChromebook) { - mHandler = new Handler(Looper.getMainLooper()); - mLastBluetoothDevices = new ArrayList(); - - // final HIDDeviceManager finalThis = this; - // mHandler.postDelayed(new Runnable() { - // @Override - // public void run() { - // finalThis.chromebookConnectionHandler(); - // } - // }, 5000); - } - } - - private void shutdownBluetooth() { - try { - mContext.unregisterReceiver(mBluetoothBroadcast); - } catch (Exception e) { - // We may not have registered, that's okay - } - } - - // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly. - // This function provides a sort of dummy version of that, watching for changes in the - // connected devices and attempting to add controllers as things change. - public void chromebookConnectionHandler() { - if (!mIsChromebook) { - return; - } - - ArrayList disconnected = new ArrayList(); - ArrayList connected = new ArrayList(); - - List currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); - - for (BluetoothDevice bluetoothDevice : currentConnected) { - if (!mLastBluetoothDevices.contains(bluetoothDevice)) { - connected.add(bluetoothDevice); - } - } - for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) { - if (!currentConnected.contains(bluetoothDevice)) { - disconnected.add(bluetoothDevice); - } - } - - mLastBluetoothDevices = currentConnected; - - for (BluetoothDevice bluetoothDevice : disconnected) { - disconnectBluetoothDevice(bluetoothDevice); - } - for (BluetoothDevice bluetoothDevice : connected) { - connectBluetoothDevice(bluetoothDevice); - } - - final HIDDeviceManager finalThis = this; - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - finalThis.chromebookConnectionHandler(); - } - }, 10000); - } - - public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { - Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice); - synchronized (this) { - if (mBluetoothDevices.containsKey(bluetoothDevice)) { - Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect"); - - HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); - device.reconnect(); - - return false; - } - HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice); - int id = device.getId(); - mBluetoothDevices.put(bluetoothDevice, device); - mDevicesById.put(id, device); - - // The Steam Controller will mark itself connected once initialization is complete - } - return true; - } - - public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { - synchronized (this) { - HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); - if (device == null) - return; - - int id = device.getId(); - mBluetoothDevices.remove(bluetoothDevice); - mDevicesById.remove(id); - device.shutdown(); - HIDDeviceDisconnected(id); - } - } - - public boolean isSteamController(BluetoothDevice bluetoothDevice) { - // Sanity check. If you pass in a null device, by definition it is never a Steam Controller. - if (bluetoothDevice == null) { - return false; - } - - // If the device has no local name, we really don't want to try an equality check against it. - if (bluetoothDevice.getName() == null) { - return false; - } - - return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); - } - - private void close() { - shutdownUSB(); - shutdownBluetooth(); - synchronized (this) { - for (HIDDevice device : mDevicesById.values()) { - device.shutdown(); - } - mDevicesById.clear(); - mBluetoothDevices.clear(); - HIDDeviceReleaseCallback(); - } - } - - public void setFrozen(boolean frozen) { - synchronized (this) { - for (HIDDevice device : mDevicesById.values()) { - device.setFrozen(frozen); - } - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - private HIDDevice getDevice(int id) { - synchronized (this) { - HIDDevice result = mDevicesById.get(id); - if (result == null) { - Log.v(TAG, "No device for id: " + id); - Log.v(TAG, "Available devices: " + mDevicesById.keySet()); - } - return result; - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - ////////// JNI interface functions - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - public boolean openDevice(int deviceID) { - Log.v(TAG, "openDevice deviceID=" + deviceID); - HIDDevice device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return false; - } - - // Look to see if this is a USB device and we have permission to access it - UsbDevice usbDevice = device.getDevice(); - if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) { - HIDDeviceOpenPending(deviceID); - try { - mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), 0)); - } catch (Exception e) { - Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); - HIDDeviceOpenResult(deviceID, false); - } - return false; - } - - try { - return device.open(); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return false; - } - - public int sendOutputReport(int deviceID, byte[] report) { - try { - //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return -1; - } - - return device.sendOutputReport(report); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return -1; - } - - public int sendFeatureReport(int deviceID, byte[] report) { - try { - //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return -1; - } - - return device.sendFeatureReport(report); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return -1; - } - - public boolean getFeatureReport(int deviceID, byte[] report) { - try { - //Log.v(TAG, "getFeatureReport deviceID=" + deviceID); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return false; - } - - return device.getFeatureReport(report); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return false; - } - - public void closeDevice(int deviceID) { - try { - Log.v(TAG, "closeDevice deviceID=" + deviceID); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return; - } - - device.close(); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - } - - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - /////////////// Native methods - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - private native void HIDDeviceRegisterCallback(); - private native void HIDDeviceReleaseCallback(); - - native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol); - native void HIDDeviceOpenPending(int deviceID); - native void HIDDeviceOpenResult(int deviceID, boolean opened); - native void HIDDeviceDisconnected(int deviceID); - - native void HIDDeviceInputReport(int deviceID, byte[] report); - native void HIDDeviceFeatureReport(int deviceID, byte[] report); -} diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java deleted file mode 100644 index d20fe80bc69..00000000000 --- a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java +++ /dev/null @@ -1,309 +0,0 @@ -package org.libsdl.app; - -import android.hardware.usb.*; -import android.os.Build; -import android.util.Log; -import java.util.Arrays; - -class HIDDeviceUSB implements HIDDevice { - - private static final String TAG = "hidapi"; - - protected HIDDeviceManager mManager; - protected UsbDevice mDevice; - protected int mInterfaceIndex; - protected int mInterface; - protected int mDeviceId; - protected UsbDeviceConnection mConnection; - protected UsbEndpoint mInputEndpoint; - protected UsbEndpoint mOutputEndpoint; - protected InputThread mInputThread; - protected boolean mRunning; - protected boolean mFrozen; - - public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { - mManager = manager; - mDevice = usbDevice; - mInterfaceIndex = interface_index; - mInterface = mDevice.getInterface(mInterfaceIndex).getId(); - mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); - mRunning = false; - } - - public String getIdentifier() { - return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex); - } - - @Override - public int getId() { - return mDeviceId; - } - - @Override - public int getVendorId() { - return mDevice.getVendorId(); - } - - @Override - public int getProductId() { - return mDevice.getProductId(); - } - - @Override - public String getSerialNumber() { - String result = null; - if (Build.VERSION.SDK_INT >= 21) { - try { - result = mDevice.getSerialNumber(); - } - catch (SecurityException exception) { - //Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage()); - } - } - if (result == null) { - result = ""; - } - return result; - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public String getManufacturerName() { - String result = null; - if (Build.VERSION.SDK_INT >= 21) { - result = mDevice.getManufacturerName(); - } - if (result == null) { - result = String.format("%x", getVendorId()); - } - return result; - } - - @Override - public String getProductName() { - String result = null; - if (Build.VERSION.SDK_INT >= 21) { - result = mDevice.getProductName(); - } - if (result == null) { - result = String.format("%x", getProductId()); - } - return result; - } - - @Override - public UsbDevice getDevice() { - return mDevice; - } - - public String getDeviceName() { - return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; - } - - @Override - public boolean open() { - mConnection = mManager.getUSBManager().openDevice(mDevice); - if (mConnection == null) { - Log.w(TAG, "Unable to open USB device " + getDeviceName()); - return false; - } - - // Force claim our interface - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - if (!mConnection.claimInterface(iface, true)) { - Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName()); - close(); - return false; - } - - // Find the endpoints - for (int j = 0; j < iface.getEndpointCount(); j++) { - UsbEndpoint endpt = iface.getEndpoint(j); - switch (endpt.getDirection()) { - case UsbConstants.USB_DIR_IN: - if (mInputEndpoint == null) { - mInputEndpoint = endpt; - } - break; - case UsbConstants.USB_DIR_OUT: - if (mOutputEndpoint == null) { - mOutputEndpoint = endpt; - } - break; - } - } - - // Make sure the required endpoints were present - if (mInputEndpoint == null || mOutputEndpoint == null) { - Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); - close(); - return false; - } - - // Start listening for input - mRunning = true; - mInputThread = new InputThread(); - mInputThread.start(); - - return true; - } - - @Override - public int sendFeatureReport(byte[] report) { - int res = -1; - int offset = 0; - int length = report.length; - boolean skipped_report_id = false; - byte report_number = report[0]; - - if (report_number == 0x0) { - ++offset; - --length; - skipped_report_id = true; - } - - res = mConnection.controlTransfer( - UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, - 0x09/*HID set_report*/, - (3/*HID feature*/ << 8) | report_number, - mInterface, - report, offset, length, - 1000/*timeout millis*/); - - if (res < 0) { - Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); - return -1; - } - - if (skipped_report_id) { - ++length; - } - return length; - } - - @Override - public int sendOutputReport(byte[] report) { - int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); - if (r != report.length) { - Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); - } - return r; - } - - @Override - public boolean getFeatureReport(byte[] report) { - int res = -1; - int offset = 0; - int length = report.length; - boolean skipped_report_id = false; - byte report_number = report[0]; - - if (report_number == 0x0) { - /* Offset the return buffer by 1, so that the report ID - will remain in byte 0. */ - ++offset; - --length; - skipped_report_id = true; - } - - res = mConnection.controlTransfer( - UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, - 0x01/*HID get_report*/, - (3/*HID feature*/ << 8) | report_number, - mInterface, - report, offset, length, - 1000/*timeout millis*/); - - if (res < 0) { - Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName()); - return false; - } - - if (skipped_report_id) { - ++res; - ++length; - } - - byte[] data; - if (res == length) { - data = report; - } else { - data = Arrays.copyOfRange(report, 0, res); - } - mManager.HIDDeviceFeatureReport(mDeviceId, data); - - return true; - } - - @Override - public void close() { - mRunning = false; - if (mInputThread != null) { - while (mInputThread.isAlive()) { - mInputThread.interrupt(); - try { - mInputThread.join(); - } catch (InterruptedException e) { - // Keep trying until we're done - } - } - mInputThread = null; - } - if (mConnection != null) { - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - mConnection.releaseInterface(iface); - mConnection.close(); - mConnection = null; - } - } - - @Override - public void shutdown() { - close(); - mManager = null; - } - - @Override - public void setFrozen(boolean frozen) { - mFrozen = frozen; - } - - protected class InputThread extends Thread { - @Override - public void run() { - int packetSize = mInputEndpoint.getMaxPacketSize(); - byte[] packet = new byte[packetSize]; - while (mRunning) { - int r; - try - { - r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000); - } - catch (Exception e) - { - Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e); - break; - } - if (r < 0) { - // Could be a timeout or an I/O error - } - if (r > 0) { - byte[] data; - if (r == packetSize) { - data = packet; - } else { - data = Arrays.copyOfRange(packet, 0, r); - } - - if (!mFrozen) { - mManager.HIDDeviceInputReport(mDeviceId, data); - } - } - } - } - } -} diff --git a/android-project/app/src/main/java/org/libsdl/app/SDL.java b/android-project/app/src/main/java/org/libsdl/app/SDL.java deleted file mode 100644 index dafc0cb87d5..00000000000 --- a/android-project/app/src/main/java/org/libsdl/app/SDL.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.libsdl.app; - -import android.content.Context; - -import java.lang.Class; -import java.lang.reflect.Method; - -/** - SDL library initialization -*/ -public class SDL { - - // This function should be called first and sets up the native code - // so it can call into the Java classes - public static void setupJNI() { - SDLActivity.nativeSetupJNI(); - SDLAudioManager.nativeSetupJNI(); - SDLControllerManager.nativeSetupJNI(); - } - - // This function should be called each time the activity is started - public static void initialize() { - setContext(null); - - SDLActivity.initialize(); - SDLAudioManager.initialize(); - SDLControllerManager.initialize(); - } - - // This function stores the current activity (SDL or not) - public static void setContext(Context context) { - mContext = context; - } - - public static Context getContext() { - return mContext; - } - - public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { - - if (libraryName == null) { - throw new NullPointerException("No library name provided."); - } - - try { - // Let's see if we have ReLinker available in the project. This is necessary for - // some projects that have huge numbers of local libraries bundled, and thus may - // trip a bug in Android's native library loader which ReLinker works around. (If - // loadLibrary works properly, ReLinker will simply use the normal Android method - // internally.) - // - // To use ReLinker, just add it as a dependency. For more information, see - // https://github.com/KeepSafe/ReLinker for ReLinker's repository. - // - Class relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker"); - Class relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener"); - Class contextClass = mContext.getClassLoader().loadClass("android.content.Context"); - Class stringClass = mContext.getClassLoader().loadClass("java.lang.String"); - - // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if - // they've changed during updates. - Method forceMethod = relinkClass.getDeclaredMethod("force"); - Object relinkInstance = forceMethod.invoke(null); - Class relinkInstanceClass = relinkInstance.getClass(); - - // Actually load the library! - Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass); - loadMethod.invoke(relinkInstance, mContext, libraryName, null, null); - } - catch (final Throwable e) { - // Fall back - try { - System.loadLibrary(libraryName); - } - catch (final UnsatisfiedLinkError ule) { - throw ule; - } - catch (final SecurityException se) { - throw se; - } - } - } - - protected static Context mContext; -} diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java deleted file mode 100644 index 05671635b84..00000000000 --- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java +++ /dev/null @@ -1,2323 +0,0 @@ -package org.libsdl.app; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.UiModeManager; -import android.content.ClipboardManager; -import android.content.ClipData; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.text.InputType; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.SparseArray; -import android.view.Display; -import android.view.Gravity; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.PointerIcon; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.view.inputmethod.BaseInputConnection; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import java.util.Hashtable; -import java.util.Locale; - - -/** - SDL Activity -*/ -public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { - private static final String TAG = "SDL"; - - public static boolean mIsResumedCalled, mHasFocus; - public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24); - - // Cursor types - // private static final int SDL_SYSTEM_CURSOR_NONE = -1; - private static final int SDL_SYSTEM_CURSOR_ARROW = 0; - private static final int SDL_SYSTEM_CURSOR_IBEAM = 1; - private static final int SDL_SYSTEM_CURSOR_WAIT = 2; - private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3; - private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4; - private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5; - private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6; - private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7; - private static final int SDL_SYSTEM_CURSOR_SIZENS = 8; - private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; - private static final int SDL_SYSTEM_CURSOR_NO = 10; - private static final int SDL_SYSTEM_CURSOR_HAND = 11; - - protected static final int SDL_ORIENTATION_UNKNOWN = 0; - protected static final int SDL_ORIENTATION_LANDSCAPE = 1; - protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2; - protected static final int SDL_ORIENTATION_PORTRAIT = 3; - protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; - - protected static int mCurrentOrientation; - protected static Locale mCurrentLocale; - - // Handle the state of the native layer - public enum NativeState { - INIT, RESUMED, PAUSED - } - - public static NativeState mNextNativeState; - public static NativeState mCurrentNativeState; - - /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ - public static boolean mBrokenLibraries = true; - - // Main components - protected static SDLActivity mSingleton; - protected static SDLSurface mSurface; - protected static View mTextEdit; - protected static boolean mScreenKeyboardShown; - protected static ViewGroup mLayout; - protected static SDLClipboardHandler mClipboardHandler; - protected static Hashtable mCursors; - protected static int mLastCursorID; - protected static SDLGenericMotionListener_API12 mMotionListener; - protected static HIDDeviceManager mHIDDeviceManager; - - // This is what SDL runs in. It invokes SDL_main(), eventually - protected static Thread mSDLThread; - - protected static SDLGenericMotionListener_API12 getMotionListener() { - if (mMotionListener == null) { - if (Build.VERSION.SDK_INT >= 26) { - mMotionListener = new SDLGenericMotionListener_API26(); - } else if (Build.VERSION.SDK_INT >= 24) { - mMotionListener = new SDLGenericMotionListener_API24(); - } else { - mMotionListener = new SDLGenericMotionListener_API12(); - } - } - - return mMotionListener; - } - - /** - * This method returns the name of the shared object with the application entry point - * It can be overridden by derived classes. - */ - protected String getMainSharedObject() { - String library; - String[] libraries = SDLActivity.mSingleton.getLibraries(); - if (libraries.length > 0) { - library = "lib" + libraries[libraries.length - 1] + ".so"; - } else { - library = "libmain.so"; - } - return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; - } - - /** - * This method returns the name of the application entry point - * It can be overridden by derived classes. - */ - protected String getMainFunction() { - return "SDL_main"; - } - - /** - * This method is called by SDL before loading the native shared libraries. - * It can be overridden to provide names of shared libraries to be loaded. - * The default implementation returns the defaults. It never returns null. - * An array returned by a new implementation must at least contain "SDL2". - * Also keep in mind that the order the libraries are loaded may matter. - * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). - */ - protected String[] getLibraries() { - return new String[] { - "hidapi", - "SDL2", - // "SDL2_image", - // "SDL2_mixer", - // "SDL2_net", - // "SDL2_ttf", - "main" - }; - } - - // Load the .so - public void loadLibraries() { - for (String lib : getLibraries()) { - SDL.loadLibrary(lib); - } - } - - /** - * This method is called by SDL before starting the native application thread. - * It can be overridden to provide the arguments after the application name. - * The default implementation returns an empty array. It never returns null. - * @return arguments for the native application. - */ - protected String[] getArguments() { - return new String[0]; - } - - public static void initialize() { - // The static nature of the singleton and Android quirkyness force us to initialize everything here - // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values - mSingleton = null; - mSurface = null; - mTextEdit = null; - mLayout = null; - mClipboardHandler = null; - mCursors = new Hashtable(); - mLastCursorID = 0; - mSDLThread = null; - mIsResumedCalled = false; - mHasFocus = true; - mNextNativeState = NativeState.INIT; - mCurrentNativeState = NativeState.INIT; - } - - // Setup - @Override - protected void onCreate(Bundle savedInstanceState) { - Log.v(TAG, "Device: " + Build.DEVICE); - Log.v(TAG, "Model: " + Build.MODEL); - Log.v(TAG, "onCreate()"); - super.onCreate(savedInstanceState); - - try { - Thread.currentThread().setName("SDLActivity"); - } catch (Exception e) { - Log.v(TAG, "modify thread properties failed " + e.toString()); - } - - // Load shared libraries - String errorMsgBrokenLib = ""; - try { - loadLibraries(); - mBrokenLibraries = false; /* success */ - } catch(UnsatisfiedLinkError e) { - System.err.println(e.getMessage()); - mBrokenLibraries = true; - errorMsgBrokenLib = e.getMessage(); - } catch(Exception e) { - System.err.println(e.getMessage()); - mBrokenLibraries = true; - errorMsgBrokenLib = e.getMessage(); - } - - if (mBrokenLibraries) - { - mSingleton = this; - AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); - dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." - + System.getProperty("line.separator") - + System.getProperty("line.separator") - + "Error: " + errorMsgBrokenLib); - dlgAlert.setTitle("SDL Error"); - dlgAlert.setPositiveButton("Exit", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog,int id) { - // if this button is clicked, close current activity - SDLActivity.mSingleton.finish(); - } - }); - dlgAlert.setCancelable(false); - dlgAlert.create().show(); - - return; - } - - // Set up JNI - SDL.setupJNI(); - - // Initialize state - SDL.initialize(); - - // So we can call stuff from static callbacks - mSingleton = this; - SDL.setContext(this); - - mClipboardHandler = new SDLClipboardHandler(); - - mHIDDeviceManager = HIDDeviceManager.acquire(this); - - // Set up the surface - mSurface = new SDLSurface(getApplication()); - - mLayout = new RelativeLayout(this); - mLayout.addView(mSurface); - - // Get our current screen orientation and pass it down. - mCurrentOrientation = SDLActivity.getCurrentOrientation(); - // Only record current orientation - SDLActivity.onNativeOrientationChanged(mCurrentOrientation); - - try { - if (Build.VERSION.SDK_INT < 24) { - mCurrentLocale = getContext().getResources().getConfiguration().locale; - } else { - mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); - } - } catch(Exception ignored) { - } - - setContentView(mLayout); - - setWindowStyle(false); - - getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); - - // Get filename from "Open with" of another application - Intent intent = getIntent(); - if (intent != null && intent.getData() != null) { - String filename = intent.getData().getPath(); - if (filename != null) { - Log.v(TAG, "Got filename: " + filename); - SDLActivity.onNativeDropFile(filename); - } - } - } - - protected void pauseNativeThread() { - mNextNativeState = NativeState.PAUSED; - mIsResumedCalled = false; - - if (SDLActivity.mBrokenLibraries) { - return; - } - - SDLActivity.handleNativeState(); - } - - protected void resumeNativeThread() { - mNextNativeState = NativeState.RESUMED; - mIsResumedCalled = true; - - if (SDLActivity.mBrokenLibraries) { - return; - } - - SDLActivity.handleNativeState(); - } - - // Events - @Override - protected void onPause() { - Log.v(TAG, "onPause()"); - super.onPause(); - - if (mHIDDeviceManager != null) { - mHIDDeviceManager.setFrozen(true); - } - if (!mHasMultiWindow) { - pauseNativeThread(); - } - } - - @Override - protected void onResume() { - Log.v(TAG, "onResume()"); - super.onResume(); - - if (mHIDDeviceManager != null) { - mHIDDeviceManager.setFrozen(false); - } - if (!mHasMultiWindow) { - resumeNativeThread(); - } - } - - @Override - protected void onStop() { - Log.v(TAG, "onStop()"); - super.onStop(); - if (mHasMultiWindow) { - pauseNativeThread(); - } - } - - @Override - protected void onStart() { - Log.v(TAG, "onStart()"); - super.onStart(); - if (mHasMultiWindow) { - resumeNativeThread(); - } - } - - public static int getCurrentOrientation() { - final Context context = SDLActivity.getContext(); - final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - - int result = SDL_ORIENTATION_UNKNOWN; - - switch (display.getRotation()) { - case Surface.ROTATION_0: - result = SDL_ORIENTATION_PORTRAIT; - break; - - case Surface.ROTATION_90: - result = SDL_ORIENTATION_LANDSCAPE; - break; - - case Surface.ROTATION_180: - result = SDL_ORIENTATION_PORTRAIT_FLIPPED; - break; - - case Surface.ROTATION_270: - result = SDL_ORIENTATION_LANDSCAPE_FLIPPED; - break; - } - - return result; - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); - - if (SDLActivity.mBrokenLibraries) { - return; - } - - mHasFocus = hasFocus; - if (hasFocus) { - mNextNativeState = NativeState.RESUMED; - SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded(); - - SDLActivity.handleNativeState(); - nativeFocusChanged(true); - - } else { - nativeFocusChanged(false); - if (!mHasMultiWindow) { - mNextNativeState = NativeState.PAUSED; - SDLActivity.handleNativeState(); - } - } - } - - @Override - public void onLowMemory() { - Log.v(TAG, "onLowMemory()"); - super.onLowMemory(); - - if (SDLActivity.mBrokenLibraries) { - return; - } - - SDLActivity.nativeLowMemory(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - Log.v(TAG, "onConfigurationChanged()"); - super.onConfigurationChanged(newConfig); - - if (SDLActivity.mBrokenLibraries) { - return; - } - - if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) { - mCurrentLocale = newConfig.locale; - SDLActivity.onNativeLocaleChanged(); - } - } - - @Override - protected void onDestroy() { - Log.v(TAG, "onDestroy()"); - - if (mHIDDeviceManager != null) { - HIDDeviceManager.release(mHIDDeviceManager); - mHIDDeviceManager = null; - } - - if (SDLActivity.mBrokenLibraries) { - super.onDestroy(); - return; - } - - if (SDLActivity.mSDLThread != null) { - - // Send Quit event to "SDLThread" thread - SDLActivity.nativeSendQuit(); - - // Wait for "SDLThread" thread to end - try { - SDLActivity.mSDLThread.join(); - } catch(Exception e) { - Log.v(TAG, "Problem stopping SDLThread: " + e); - } - } - - SDLActivity.nativeQuit(); - - super.onDestroy(); - } - - @Override - public void onBackPressed() { - // Check if we want to block the back button in case of mouse right click. - // - // If we do, the normal hardware back button will no longer work and people have to use home, - // but the mouse right click will work. - // - String trapBack = SDLActivity.nativeGetHint("SDL_ANDROID_TRAP_BACK_BUTTON"); - if ((trapBack != null) && trapBack.equals("1")) { - // Exit and let the mouse handler handle this button (if appropriate) - return; - } - - // Default system back button behavior. - if (!isFinishing()) { - super.onBackPressed(); - } - } - - // Called by JNI from SDL. - public static void manualBackButton() { - mSingleton.pressBackButton(); - } - - // Used to get us onto the activity's main thread - public void pressBackButton() { - runOnUiThread(new Runnable() { - @Override - public void run() { - if (!SDLActivity.this.isFinishing()) { - SDLActivity.this.superOnBackPressed(); - } - } - }); - } - - // Used to access the system back behavior. - public void superOnBackPressed() { - super.onBackPressed(); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - - if (SDLActivity.mBrokenLibraries) { - return false; - } - - int keyCode = event.getKeyCode(); - // Ignore certain special keys so they're handled by Android - if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || - keyCode == KeyEvent.KEYCODE_VOLUME_UP || - keyCode == KeyEvent.KEYCODE_CAMERA || - keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ - keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ - ) { - return false; - } - return super.dispatchKeyEvent(event); - } - - /* Transition to next state */ - public static void handleNativeState() { - - if (mNextNativeState == mCurrentNativeState) { - // Already in same state, discard. - return; - } - - // Try a transition to init state - if (mNextNativeState == NativeState.INIT) { - - mCurrentNativeState = mNextNativeState; - return; - } - - // Try a transition to paused state - if (mNextNativeState == NativeState.PAUSED) { - if (mSDLThread != null) { - nativePause(); - } - if (mSurface != null) { - mSurface.handlePause(); - } - mCurrentNativeState = mNextNativeState; - return; - } - - // Try a transition to resumed state - if (mNextNativeState == NativeState.RESUMED) { - if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) { - if (mSDLThread == null) { - // This is the entry point to the C app. - // Start up the C app thread and enable sensor input for the first time - // FIXME: Why aren't we enabling sensor input at start? - - mSDLThread = new Thread(new SDLMain(), "SDLThread"); - mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); - mSDLThread.start(); - - // No nativeResume(), don't signal Android_ResumeSem - } else { - nativeResume(); - } - mSurface.handleResume(); - - mCurrentNativeState = mNextNativeState; - } - } - } - - // Messages from the SDLMain thread - static final int COMMAND_CHANGE_TITLE = 1; - static final int COMMAND_CHANGE_WINDOW_STYLE = 2; - static final int COMMAND_TEXTEDIT_HIDE = 3; - static final int COMMAND_CHANGE_SURFACEVIEW_FORMAT = 4; - static final int COMMAND_SET_KEEP_SCREEN_ON = 5; - - protected static final int COMMAND_USER = 0x8000; - - protected static boolean mFullscreenModeActive; - - /** - * This method is called by SDL if SDL did not handle a message itself. - * This happens if a received message contains an unsupported command. - * Method can be overwritten to handle Messages in a different class. - * @param command the command of the message. - * @param param the parameter of the message. May be null. - * @return if the message was handled in overridden method. - */ - protected boolean onUnhandledMessage(int command, Object param) { - return false; - } - - /** - * A Handler class for Messages from native SDL applications. - * It uses current Activities as target (e.g. for the title). - * static to prevent implicit references to enclosing object. - */ - protected static class SDLCommandHandler extends Handler { - @Override - public void handleMessage(Message msg) { - Context context = SDL.getContext(); - if (context == null) { - Log.e(TAG, "error handling message, getContext() returned null"); - return; - } - switch (msg.arg1) { - case COMMAND_CHANGE_TITLE: - if (context instanceof Activity) { - ((Activity) context).setTitle((String)msg.obj); - } else { - Log.e(TAG, "error handling message, getContext() returned no Activity"); - } - break; - case COMMAND_CHANGE_WINDOW_STYLE: - if (Build.VERSION.SDK_INT >= 19) { - if (context instanceof Activity) { - Window window = ((Activity) context).getWindow(); - if (window != null) { - if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { - int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; - window.getDecorView().setSystemUiVisibility(flags); - window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - SDLActivity.mFullscreenModeActive = true; - } else { - int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; - window.getDecorView().setSystemUiVisibility(flags); - window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - SDLActivity.mFullscreenModeActive = false; - } - } - } else { - Log.e(TAG, "error handling message, getContext() returned no Activity"); - } - } - break; - case COMMAND_TEXTEDIT_HIDE: - if (mTextEdit != null) { - // Note: On some devices setting view to GONE creates a flicker in landscape. - // Setting the View's sizes to 0 is similar to GONE but without the flicker. - // The sizes will be set to useful values when the keyboard is shown again. - mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); - - InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); - - mScreenKeyboardShown = false; - - mSurface.requestFocus(); - } - break; - case COMMAND_SET_KEEP_SCREEN_ON: - { - if (context instanceof Activity) { - Window window = ((Activity) context).getWindow(); - if (window != null) { - if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - } - break; - } - case COMMAND_CHANGE_SURFACEVIEW_FORMAT: - { - int format = (Integer) msg.obj; - int pf; - - if (SDLActivity.mSurface == null) { - return; - } - - SurfaceHolder holder = SDLActivity.mSurface.getHolder(); - if (holder == null) { - return; - } - - if (format == 1) { - pf = PixelFormat.RGBA_8888; - } else if (format == 2) { - pf = PixelFormat.RGBX_8888; - } else { - pf = PixelFormat.RGB_565; - } - - holder.setFormat(pf); - - break; - } - default: - if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { - Log.e(TAG, "error handling message, command is " + msg.arg1); - } - } - } - } - - // Handler for the messages - Handler commandHandler = new SDLCommandHandler(); - - // Send a message from the SDLMain thread - boolean sendCommand(int command, Object data) { - Message msg = commandHandler.obtainMessage(); - msg.arg1 = command; - msg.obj = data; - boolean result = commandHandler.sendMessage(msg); - - if (Build.VERSION.SDK_INT >= 19) { - if (command == COMMAND_CHANGE_WINDOW_STYLE) { - // Ensure we don't return until the resize has actually happened, - // or 500ms have passed. - - boolean bShouldWait = false; - - if (data instanceof Integer) { - // Let's figure out if we're already laid out fullscreen or not. - Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - DisplayMetrics realMetrics = new DisplayMetrics(); - display.getRealMetrics(realMetrics); - - boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && - (realMetrics.heightPixels == mSurface.getHeight())); - - if ((Integer) data == 1) { - // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going - // to change size and should wait for surfaceChanged() before we return, so the size - // is right back in native code. If we're already laid out fullscreen, though, we're - // not going to change size even if we change decor modes, so we shouldn't wait for - // surfaceChanged() -- which may not even happen -- and should return immediately. - bShouldWait = !bFullscreenLayout; - } else { - // If we're laid out fullscreen (even if the status bar and nav bar are present), - // or are actively in fullscreen, we're going to change size and should wait for - // surfaceChanged before we return, so the size is right back in native code. - bShouldWait = bFullscreenLayout; - } - } - - if (bShouldWait && (SDLActivity.getContext() != null)) { - // We'll wait for the surfaceChanged() method, which will notify us - // when called. That way, we know our current size is really the - // size we need, instead of grabbing a size that's still got - // the navigation and/or status bars before they're hidden. - // - // We'll wait for up to half a second, because some devices - // take a surprisingly long time for the surface resize, but - // then we'll just give up and return. - // - synchronized (SDLActivity.getContext()) { - try { - SDLActivity.getContext().wait(500); - } catch (InterruptedException ie) { - ie.printStackTrace(); - } - } - } - } - } - - return result; - } - - // C functions we call - public static native int nativeSetupJNI(); - public static native int nativeRunMain(String library, String function, Object arguments); - public static native void nativeLowMemory(); - public static native void nativeSendQuit(); - public static native void nativeQuit(); - public static native void nativePause(); - public static native void nativeResume(); - public static native void nativeFocusChanged(boolean hasFocus); - public static native void onNativeDropFile(String filename); - public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, int format, float rate); - public static native void onNativeResize(); - public static native void onNativeKeyDown(int keycode); - public static native void onNativeKeyUp(int keycode); - public static native boolean onNativeSoftReturnKey(); - public static native void onNativeKeyboardFocusLost(); - public static native void onNativeMouse(int button, int action, float x, float y, boolean relative); - public static native void onNativeTouch(int touchDevId, int pointerFingerId, - int action, float x, - float y, float p); - public static native void onNativeAccel(float x, float y, float z); - public static native void onNativeClipboardChanged(); - public static native void onNativeSurfaceCreated(); - public static native void onNativeSurfaceChanged(); - public static native void onNativeSurfaceDestroyed(); - public static native String nativeGetHint(String name); - public static native void nativeSetenv(String name, String value); - public static native void onNativeOrientationChanged(int orientation); - public static native void nativeAddTouch(int touchId, String name); - public static native void nativePermissionResult(int requestCode, boolean result); - public static native void onNativeLocaleChanged(); - - /** - * This method is called by SDL using JNI. - */ - public static boolean setActivityTitle(String title) { - // Called from SDLMain() thread and can't directly affect the view - return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); - } - - /** - * This method is called by SDL using JNI. - */ - public static void setWindowStyle(boolean fullscreen) { - // Called from SDLMain() thread and can't directly affect the view - mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0); - } - - /** - * This method is called by SDL using JNI. - * This is a static method for JNI convenience, it calls a non-static method - * so that is can be overridden - */ - public static void setOrientation(int w, int h, boolean resizable, String hint) - { - if (mSingleton != null) { - mSingleton.setOrientationBis(w, h, resizable, hint); - } - } - - /** - * This can be overridden - */ - public void setOrientationBis(int w, int h, boolean resizable, String hint) - { - int orientation_landscape = -1; - int orientation_portrait = -1; - - /* If set, hint "explicitly controls which UI orientations are allowed". */ - if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; - } else if (hint.contains("LandscapeRight")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; - } else if (hint.contains("LandscapeLeft")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; - } - - if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) { - orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; - } else if (hint.contains("Portrait")) { - orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - } else if (hint.contains("PortraitUpsideDown")) { - orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; - } - - boolean is_landscape_allowed = (orientation_landscape != -1); - boolean is_portrait_allowed = (orientation_portrait != -1); - int req; /* Requested orientation */ - - /* No valid hint, nothing is explicitly allowed */ - if (!is_portrait_allowed && !is_landscape_allowed) { - if (resizable) { - /* All orientations are allowed */ - req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; - } else { - /* Fixed window and nothing specified. Get orientation from w/h of created window */ - req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); - } - } else { - /* At least one orientation is allowed */ - if (resizable) { - if (is_portrait_allowed && is_landscape_allowed) { - /* hint allows both landscape and portrait, promote to full sensor */ - req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; - } else { - /* Use the only one allowed "orientation" */ - req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); - } - } else { - /* Fixed window and both orientations are allowed. Choose one. */ - if (is_portrait_allowed && is_landscape_allowed) { - req = (w > h ? orientation_landscape : orientation_portrait); - } else { - /* Use the only one allowed "orientation" */ - req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); - } - } - } - - Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); - mSingleton.setRequestedOrientation(req); - } - - /** - * This method is called by SDL using JNI. - */ - public static void minimizeWindow() { - - if (mSingleton == null) { - return; - } - - Intent startMain = new Intent(Intent.ACTION_MAIN); - startMain.addCategory(Intent.CATEGORY_HOME); - startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mSingleton.startActivity(startMain); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean shouldMinimizeOnFocusLoss() { -/* - if (Build.VERSION.SDK_INT >= 24) { - if (mSingleton == null) { - return true; - } - - if (mSingleton.isInMultiWindowMode()) { - return false; - } - - if (mSingleton.isInPictureInPictureMode()) { - return false; - } - } - - return true; -*/ - return false; - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isScreenKeyboardShown() - { - if (mTextEdit == null) { - return false; - } - - if (!mScreenKeyboardShown) { - return false; - } - - InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - return imm.isAcceptingText(); - - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean supportsRelativeMouse() - { - // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under - // Android 7 APIs, and simply returns no data under Android 8 APIs. - // - // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and - // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result, - // we should stick to relative mode. - // - if ((Build.VERSION.SDK_INT < 27) && isDeXMode()) { - return false; - } - - return SDLActivity.getMotionListener().supportsRelativeMouse(); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean setRelativeMouseEnabled(boolean enabled) - { - if (enabled && !supportsRelativeMouse()) { - return false; - } - - return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean sendMessage(int command, int param) { - if (mSingleton == null) { - return false; - } - return mSingleton.sendCommand(command, param); - } - - /** - * This method is called by SDL using JNI. - */ - public static Context getContext() { - return SDL.getContext(); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isAndroidTV() { - UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE); - if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { - return true; - } - if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { - return true; - } - if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { - return true; - } - return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV"); - } - - public static double getDiagonal() - { - DisplayMetrics metrics = new DisplayMetrics(); - Activity activity = (Activity)getContext(); - if (activity == null) { - return 0.0; - } - activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); - - double dWidthInches = metrics.widthPixels / (double)metrics.xdpi; - double dHeightInches = metrics.heightPixels / (double)metrics.ydpi; - - return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isTablet() { - // If our diagonal size is seven inches or greater, we consider ourselves a tablet. - return (getDiagonal() >= 7.0); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isChromebook() { - if (getContext() == null) { - return false; - } - return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean isDeXMode() { - if (Build.VERSION.SDK_INT < 24) { - return false; - } - try { - final Configuration config = getContext().getResources().getConfiguration(); - final Class configClass = config.getClass(); - return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) - == configClass.getField("semDesktopModeEnabled").getInt(config); - } catch(Exception ignored) { - return false; - } - } - - /** - * This method is called by SDL using JNI. - */ - public static DisplayMetrics getDisplayDPI() { - return getContext().getResources().getDisplayMetrics(); - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean getManifestEnvironmentVariables() { - try { - if (getContext() == null) { - return false; - } - - ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); - Bundle bundle = applicationInfo.metaData; - if (bundle == null) { - return false; - } - String prefix = "SDL_ENV."; - final int trimLength = prefix.length(); - for (String key : bundle.keySet()) { - if (key.startsWith(prefix)) { - String name = key.substring(trimLength); - String value = bundle.get(key).toString(); - nativeSetenv(name, value); - } - } - /* environment variables set! */ - return true; - } catch (Exception e) { - Log.v(TAG, "exception " + e.toString()); - } - return false; - } - - // This method is called by SDLControllerManager's API 26 Generic Motion Handler. - public static View getContentView() - { - return mLayout; - } - - static class ShowTextInputTask implements Runnable { - /* - * This is used to regulate the pan&scan method to have some offset from - * the bottom edge of the input region and the top edge of an input - * method (soft keyboard) - */ - static final int HEIGHT_PADDING = 15; - - public int x, y, w, h; - - public ShowTextInputTask(int x, int y, int w, int h) { - this.x = x; - this.y = y; - this.w = w; - this.h = h; - - /* Minimum size of 1 pixel, so it takes focus. */ - if (this.w <= 0) { - this.w = 1; - } - if (this.h + HEIGHT_PADDING <= 0) { - this.h = 1 - HEIGHT_PADDING; - } - } - - @Override - public void run() { - RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); - params.leftMargin = x; - params.topMargin = y; - - if (mTextEdit == null) { - mTextEdit = new DummyEdit(SDL.getContext()); - - mLayout.addView(mTextEdit, params); - } else { - mTextEdit.setLayoutParams(params); - } - - mTextEdit.setVisibility(View.VISIBLE); - mTextEdit.requestFocus(); - - InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mTextEdit, 0); - - mScreenKeyboardShown = true; - } - } - - /** - * This method is called by SDL using JNI. - */ - public static boolean showTextInput(int x, int y, int w, int h) { - // Transfer the task to the main thread as a Runnable - return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); - } - - public static boolean isTextInputEvent(KeyEvent event) { - - // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT - if (event.isCtrlPressed()) { - return false; - } - - return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; - } - - /** - * This method is called by SDL using JNI. - */ - public static Surface getNativeSurface() { - if (SDLActivity.mSurface == null) { - return null; - } - return SDLActivity.mSurface.getNativeSurface(); - } - - /** - * This method is called by SDL using JNI. - */ - public static void setSurfaceViewFormat(int format) { - mSingleton.sendCommand(COMMAND_CHANGE_SURFACEVIEW_FORMAT, format); - } - - // Input - - /** - * This method is called by SDL using JNI. - */ - public static void initTouch() { - int[] ids = InputDevice.getDeviceIds(); - - for (int id : ids) { - InputDevice device = InputDevice.getDevice(id); - if (device != null && (device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) != 0) { - nativeAddTouch(device.getId(), device.getName()); - } - } - } - - // Messagebox - - /** Result of current messagebox. Also used for blocking the calling thread. */ - protected final int[] messageboxSelection = new int[1]; - - /** - * This method is called by SDL using JNI. - * Shows the messagebox from UI thread and block calling thread. - * buttonFlags, buttonIds and buttonTexts must have same length. - * @param buttonFlags array containing flags for every button. - * @param buttonIds array containing id for every button. - * @param buttonTexts array containing text for every button. - * @param colors null for default or array of length 5 containing colors. - * @return button id or -1. - */ - public int messageboxShowMessageBox( - final int flags, - final String title, - final String message, - final int[] buttonFlags, - final int[] buttonIds, - final String[] buttonTexts, - final int[] colors) { - - messageboxSelection[0] = -1; - - // sanity checks - - if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { - return -1; // implementation broken - } - - // collect arguments for Dialog - - final Bundle args = new Bundle(); - args.putInt("flags", flags); - args.putString("title", title); - args.putString("message", message); - args.putIntArray("buttonFlags", buttonFlags); - args.putIntArray("buttonIds", buttonIds); - args.putStringArray("buttonTexts", buttonTexts); - args.putIntArray("colors", colors); - - // trigger Dialog creation on UI thread - - runOnUiThread(new Runnable() { - @Override - public void run() { - messageboxCreateAndShow(args); - } - }); - - // block the calling thread - - synchronized (messageboxSelection) { - try { - messageboxSelection.wait(); - } catch (InterruptedException ex) { - ex.printStackTrace(); - return -1; - } - } - - // return selected value - - return messageboxSelection[0]; - } - - protected void messageboxCreateAndShow(Bundle args) { - - // TODO set values from "flags" to messagebox dialog - - // get colors - - int[] colors = args.getIntArray("colors"); - int backgroundColor; - int textColor; - int buttonBorderColor; - int buttonBackgroundColor; - int buttonSelectedColor; - if (colors != null) { - int i = -1; - backgroundColor = colors[++i]; - textColor = colors[++i]; - buttonBorderColor = colors[++i]; - buttonBackgroundColor = colors[++i]; - buttonSelectedColor = colors[++i]; - } else { - backgroundColor = Color.TRANSPARENT; - textColor = Color.TRANSPARENT; - buttonBorderColor = Color.TRANSPARENT; - buttonBackgroundColor = Color.TRANSPARENT; - buttonSelectedColor = Color.TRANSPARENT; - } - - // create dialog with title and a listener to wake up calling thread - - final AlertDialog dialog = new AlertDialog.Builder(this).create(); - dialog.setTitle(args.getString("title")); - dialog.setCancelable(false); - dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface unused) { - synchronized (messageboxSelection) { - messageboxSelection.notify(); - } - } - }); - - // create text - - TextView message = new TextView(this); - message.setGravity(Gravity.CENTER); - message.setText(args.getString("message")); - if (textColor != Color.TRANSPARENT) { - message.setTextColor(textColor); - } - - // create buttons - - int[] buttonFlags = args.getIntArray("buttonFlags"); - int[] buttonIds = args.getIntArray("buttonIds"); - String[] buttonTexts = args.getStringArray("buttonTexts"); - - final SparseArray