diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb7a9c841b..f016c985a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -349,6 +349,78 @@ jobs: release/*.AppImage release/*.tar.gz + android: + name: Build for Android + needs: meta + runs-on: macos-26 + strategy: + matrix: + arch: [arm64, x64] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + show-progress: false + + - name: Fetch submodules + run: | + git submodule update --init --depth 1 src/MaaUtils + + - name: Cache MaaDeps + id: cache-maadeps + uses: actions/cache@v5 + continue-on-error: true + with: + path: ./src/MaaUtils/MaaDeps + key: ${{ runner.os }}-${{ matrix.arch }}-android-maadeps-${{ hashFiles('tools/maadeps-download.py') }} + + - name: Bootstrap MaaDeps + if: steps.cache-maadeps.outputs.cache-hit != 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 tools/maadeps-download.py ${{ matrix.arch }}-android + + - name: Setup Android NDK + id: setup-ndk + uses: nttld/setup-ndk@v1 + with: + ndk-version: r29 + + - name: Configure, build and install + run: | + cmake -B build --preset 'android-publish-${{ matrix.arch }}' \ + -DCMAKE_TOOLCHAIN_FILE=${{ steps.setup-ndk.outputs.ndk-path }}/build/cmake/android.toolchain.cmake \ + -DMAA_HASH_VERSION='${{ needs.meta.outputs.tag }}' + cmake --build build --parallel $(sysctl -n hw.logicalcpu) + cmake --install build --prefix install + + - name: Download MaaFramework + uses: robinraju/release-downloader@v1 + with: + repository: MaaXYZ/MaaFramework + latest: true + fileName: "*android-${{ matrix.arch == 'arm64' && 'aarch64' || 'x86_64' }}*.zip" + extract: true + out-file-path: MaaFramework-temp + + - name: Copy MaaAndroidNativeControlUnit + run: | + cp MaaFramework-temp/bin/libMaaAndroidNativeControlUnit.so install/ + + - name: Tar files + run: | + cd install + tar czvf $GITHUB_WORKSPACE/MAAComponent-${{ needs.meta.outputs.tag }}-android-${{ matrix.arch }}.tar.gz . + + - name: Upload MAA to GitHub + uses: actions/upload-artifact@v6 + with: + name: MAAComponent-android-${{ matrix.arch }} + path: MAAComponent-*.tar.gz + macOS-Core: name: Build Core for macOS needs: meta @@ -574,7 +646,7 @@ jobs: release: name: Publish Release if: startsWith(github.ref, 'refs/tags/v') - needs: [meta, windows, ubuntu, macOS-Core, macOS-GUI] + needs: [meta, windows, ubuntu, android, macOS-Core, macOS-GUI] runs-on: ubuntu-latest steps: - name: Download MAA from GitHub diff --git a/.gitignore b/.gitignore index d3a9aba64e..115c63382a 100644 --- a/.gitignore +++ b/.gitignore @@ -486,3 +486,4 @@ install-* # CMake user presets CMakeUserPresets.json +.ace-tool/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 558bf1032e..37f7c9ab95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,13 @@ if(BUILD_WPF_GUI) endif() endif() +if (ANDROID) + add_library(stdc++fs INTERFACE) + add_compile_options(-Wno-unused-parameter) + add_compile_options(-ffunction-sections -fdata-sections) + add_link_options(-Wl,--gc-sections) +endif() + if(INSTALL_PYTHON) install(DIRECTORY src/Python DESTINATION .) endif() diff --git a/CMakePresets.json b/CMakePresets.json index e831802277..c15b36d7a4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -158,6 +158,39 @@ "CMAKE_OSX_ARCHITECTURES": "x86_64" } }, + { + "name": "android-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "$comment": [ + "Base for Android presets; cross-compilation via NDK toolchain", + "CMAKE_TOOLCHAIN_FILE must be passed externally pointing to NDK's android.toolchain.cmake" + ], + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Android", + "ANDROID_PLATFORM": "android-28", + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "android-arm64", + "inherits": "android-base", + "displayName": "Android arm64", + "cacheVariables": { + "ANDROID_ABI": "arm64-v8a", + "MAADEPS_TRIPLET": "maa-arm64-android" + } + }, + { + "name": "android-x64", + "inherits": "android-base", + "displayName": "Android x64", + "cacheVariables": { + "ANDROID_ABI": "x86_64", + "MAADEPS_TRIPLET": "maa-x64-android" + } + }, { "name": "publish-base", "$comment": [ @@ -239,6 +272,22 @@ ], "displayName": "macOS arm64 Publish" }, + { + "name": "android-publish-arm64", + "inherits": ["publish-base", "android-arm64"], + "displayName": "Android arm64 Publish", + "cacheVariables": { + "INSTALL_PYTHON": "OFF" + } + }, + { + "name": "android-publish-x64", + "inherits": ["publish-base", "android-x64"], + "displayName": "Android x64 Publish", + "cacheVariables": { + "INSTALL_PYTHON": "OFF" + } + }, { "name": "smoke-test", "$comment": [ @@ -400,6 +449,16 @@ "configurePreset": "macos-publish-arm64", "configuration": "RelWithDebInfo" }, + { + "name": "android-publish-arm64", + "displayName": "Build Android arm64 Publish", + "configurePreset": "android-publish-arm64" + }, + { + "name": "android-publish-x64", + "displayName": "Build Android x64 Publish", + "configurePreset": "android-publish-x64" + }, { "name": "smoke-test", "displayName": "Build macOS arm64 Smoke Test", diff --git a/src/MaaCore/Assistant.cpp b/src/MaaCore/Assistant.cpp index fff4f7a649..cd26f85ae1 100644 --- a/src/MaaCore/Assistant.cpp +++ b/src/MaaCore/Assistant.cpp @@ -32,6 +32,7 @@ #include "Task/Interface/DebugTask.h" #endif + using namespace asst; bool ::AsstExtAPI::set_static_option(StaticOptionKey key, const std::string& value) @@ -137,6 +138,12 @@ bool asst::Assistant::set_instance_option(InstanceOptionKey key, const std::stri m_ctrler->set_touch_mode(TouchMode::MaaFwAdb); return true; } +#ifdef __ANDROID__ + else if (constexpr std::string_view Android = "Android"; value == Android) { + m_ctrler->set_touch_mode(TouchMode::Android); + return true; + } +#endif break; case InstanceOptionKey::DeploymentWithPause: if (constexpr std::string_view Enable = "1"; value == Enable) { @@ -624,7 +631,7 @@ void asst::Assistant::call_proc() while (true) { std::unique_lock lock(m_call_mutex); if (m_thread_exit) { - return; + break; } if (m_call_queue.empty()) { diff --git a/src/MaaCore/CMakeLists.txt b/src/MaaCore/CMakeLists.txt index f6c6d55334..70cc546b0f 100644 --- a/src/MaaCore/CMakeLists.txt +++ b/src/MaaCore/CMakeLists.txt @@ -48,6 +48,10 @@ if(MSVC) set_target_properties(MaaCore PROPERTIES VS_DEBUGGER_VISUALIZER "${CMAKE_CURRENT_SOURCE_DIR}/meojson.natvis") endif() +if(ANDROID) + target_link_libraries(MaaCore log dl) +endif() + file(GLOB_RECURSE MaaCore_PUBLIC_HEADERS ${PROJECT_SOURCE_DIR}/include/*.h) target_sources(MaaCore PUBLIC ${MaaCore_PUBLIC_HEADERS}) set_target_properties(MaaCore PROPERTIES PUBLIC_HEADER "${MaaCore_PUBLIC_HEADERS}") @@ -120,3 +124,15 @@ if(WIN32) $ COMMAND_EXPAND_LISTS) endif() + +if(ANDROID) + if(CMAKE_STRIP) + add_custom_command( + TARGET MaaCore + POST_BUILD + COMMAND ${CMAKE_STRIP} --strip-all $ + COMMENT "Stripping MaaCore for Android") + else() + message(WARNING "CMAKE_STRIP not found, Android library will not be stripped") + endif() +endif() diff --git a/src/MaaCore/Common/AsstTypes.h b/src/MaaCore/Common/AsstTypes.h index e205b80359..30865ed2a6 100644 --- a/src/MaaCore/Common/AsstTypes.h +++ b/src/MaaCore/Common/AsstTypes.h @@ -57,6 +57,7 @@ enum class TouchMode Maatouch = 2, MacPlayTools = 3, MaaFwAdb = 4, + Android = 5, }; #ifdef _WIN32 @@ -155,7 +156,7 @@ struct Point { \ return { lhs.x Op rhs.x, lhs.y Op rhs.y }; \ } \ - friend Point& operator Op##=(Point& val, const Point& opd) noexcept \ + friend Point& operator Op## =(Point& val, const Point& opd) noexcept \ { \ val.x Op## = opd.x; \ val.y Op## = opd.y; \ @@ -260,7 +261,7 @@ struct Rect static Rect bounding_box(const std::vector& rects) { if (rects.empty()) { - return {}; + return { }; } int min_x = INT_MAX; @@ -303,11 +304,11 @@ struct AnalyzerResult { virtual ~AnalyzerResult() = default; - virtual std::string to_string() const { return {}; }; + virtual std::string to_string() const { return { }; }; explicit operator std::string() const { return to_string(); } - virtual json::object to_json() const { return {}; }; + virtual json::object to_json() const { return { }; }; explicit operator json::object() const { return to_json(); } }; @@ -418,8 +419,8 @@ struct pair_hash { size_t operator()(const std::pair& p) const noexcept { - std::size_t hash1 = std::hash {}(p.first); - std::size_t hash2 = std::hash {}(p.second); + std::size_t hash1 = std::hash { }(p.first); + std::size_t hash2 = std::hash { }(p.second); hash1 ^= hash2 + 0x9e3779b9 + (hash1 << 6) + (hash1 >> 2); hash1 ^= hash1 >> 32; diff --git a/src/MaaCore/Controller/Controller.cpp b/src/MaaCore/Controller/Controller.cpp index 40546f49c1..e39bcd910d 100644 --- a/src/MaaCore/Controller/Controller.cpp +++ b/src/MaaCore/Controller/Controller.cpp @@ -28,6 +28,10 @@ #include "Win32Controller.h" #endif +#ifdef __ANDROID__ +#include "MaaFwAndroidNativeController.h" +#endif + #include "Common/AsstTypes.h" #include "Utils/Logger.hpp" @@ -58,6 +62,11 @@ std::shared_ptr return std::make_shared(m_callback, m_inst, platform_type); case ControllerType::MaaFwAdb: return std::make_shared(m_callback, m_inst, platform_type); +#ifdef __ANDROID__ + case ControllerType::MaaFwAndroidNative: + Log.debug("Use Android"); + return std::make_shared(m_callback, m_inst); +#endif default: return nullptr; } @@ -247,11 +256,14 @@ bool asst::Controller::connect(const std::string& adb_path, const std::string& a } #endif + // Android uses lazy loading; no need to check in advance +#ifndef __ANDROID__ // try to find the fastest way if (!screencap()) { Log.error("Cannot find a proper way to screencap!"); return false; } +#endif auto proxy_callback = [&](const json::object& details) { json::value connection_info = json::object { @@ -368,6 +380,11 @@ void asst::Controller::set_touch_mode(const TouchMode& mode) noexcept case TouchMode::MaaFwAdb: m_controller_type = ControllerType::MaaFwAdb; break; +#ifdef __ANDROID__ + case TouchMode::Android: + m_controller_type = ControllerType::MaaFwAndroidNative; + break; +#endif default: m_controller_type = ControllerType::Minitouch; } @@ -409,7 +426,7 @@ cv::Mat asst::Controller::get_image(bool raw) { if (get_scale_size() == std::pair(0, 0)) { Log.error("Unknown image size"); - return {}; + return { }; } // 有些模拟器adb偶尔会莫名其妙截图失败,多试几次 @@ -433,7 +450,7 @@ cv::Mat asst::Controller::get_image(bool raw) { "uuid", m_uuid }, { "what", "ScreencapFailed" }, { "why", "ScreencapFailed" }, - { "details", json::object {} }, + { "details", json::object { } }, }; callback(AsstMsg::ConnectionInfo, info); diff --git a/src/MaaCore/Controller/Controller.h b/src/MaaCore/Controller/Controller.h index dde291b7b0..caf1815245 100644 --- a/src/MaaCore/Controller/Controller.h +++ b/src/MaaCore/Controller/Controller.h @@ -63,6 +63,8 @@ public: ControllerType get_controller_type() const noexcept; + ControllerAPI* get_underlying() const noexcept { return m_controller.get(); } + cv::Mat get_image(bool raw = false); cv::Mat get_image_cache() const; bool screencap(bool allow_reconnect = false); diff --git a/src/MaaCore/Controller/ControllerAPI.h b/src/MaaCore/Controller/ControllerAPI.h index 4f59ca6159..4c440c491d 100644 --- a/src/MaaCore/Controller/ControllerAPI.h +++ b/src/MaaCore/Controller/ControllerAPI.h @@ -19,6 +19,9 @@ enum class ControllerType Win32, #endif MaaFwAdb, +#ifdef __ANDROID__ + MaaFwAndroidNative, +#endif }; class ControllerAPI diff --git a/src/MaaCore/Controller/MaaFwAndroidNativeController.cpp b/src/MaaCore/Controller/MaaFwAndroidNativeController.cpp new file mode 100644 index 0000000000..9a7edbeaef --- /dev/null +++ b/src/MaaCore/Controller/MaaFwAndroidNativeController.cpp @@ -0,0 +1,425 @@ +#ifdef __ANDROID__ + +#include "MaaFwAndroidNativeController.h" + +#include +#include + +#include "Common/AsstMsg.h" +#include "Config/GeneralConfig.h" +#include "Controller/MaaFwControlUnitInterface.h" +#include "Controller/SwipeHelper.hpp" +#include "Utils/Logger.hpp" + +namespace asst +{ + +MaaFwAndroidNativeController::MaaFwAndroidNativeController(const AsstCallback& callback, Assistant* inst) : + InstHelper(inst), + m_callback(callback) +{ + LogTraceFunction; +} + +MaaFwAndroidNativeController::~MaaFwAndroidNativeController() +{ + LogTraceFunction; + + if (m_unit_handle && m_destroy_func) { + LogInfo << "Cleaning up MaaAndroidNativeControlUnit"; + m_destroy_func(m_unit_handle); + m_unit_handle = nullptr; + } +} + +bool MaaFwAndroidNativeController::connect( + const std::string& adb_path [[maybe_unused]], + const std::string& address [[maybe_unused]], + const std::string& config) +{ + LogTraceFunction; + + m_inited = false; + m_uuid.clear(); + auto get_info_json = [&]() -> json::object { + return json::object { + { "uuid", m_uuid }, + { "details", + json::object { + { "config", config }, + } }, + }; + }; + + if (!init_library()) { + return false; + } + + if (m_unit_handle && m_destroy_func) { + LogInfo << "Cleaning up the old connection and reconnecting"; + m_destroy_func(m_unit_handle); + m_unit_handle = nullptr; + } + + if (!config.empty()) { + if (auto config_opt = json::parse(config); config_opt.has_value()) { + auto& config_json = config_opt.value(); + if (config_json.contains("screen_resolution")) { + if (const auto& res = config_json["screen_resolution"]; + res.contains("width") && res.contains("height")) { + int width = res.get("width", 1280); + int height = res.get("height", 720); + m_screen_resolution = { width, height }; + LogInfo << "Parsed screen resolution from config:" << width << "x" << height; + } + } + } + else { + LogError << "Failed to parse config as JSON"; + return false; + } + } + + if (m_screen_resolution.first <= 0 || m_screen_resolution.second <= 0) { + LogError << "screen_resolution not provided or invalid in config, cannot connect"; + callback( + AsstMsg::ConnectionInfo, + json::object { + { "what", "ConnectFailed" }, + { "why", "screen_resolution missing in config" }, + } | get_info_json()); + return false; + } + + m_unit_handle = m_create_func(config.c_str()); + + if (!m_unit_handle) { + LogError << "Failed to create MaaAndroidNativeControlUnit"; + callback( + AsstMsg::ConnectionInfo, + json::object { + { "what", "ConnectFailed" }, + { "why", "MaaAndroidNativeControlUnit creation failed" }, + } | get_info_json()); + return false; + } + + if (!m_unit_handle->connect()) { + LogError << "MaaAndroidNativeControlUnit failed to connect"; + m_destroy_func(m_unit_handle); + m_unit_handle = nullptr; + callback( + AsstMsg::ConnectionInfo, + json::object { + { "what", "ConnectFailed" }, + { "why", "MaaAndroidNativeControlUnit failed to connect" }, + } | get_info_json()); + return false; + } + + if (!m_unit_handle->request_uuid(m_uuid)) { + LogWarn << "Failed to get UUID from MaaAndroidNativeControlUnit"; + m_destroy_func(m_unit_handle); + m_unit_handle = nullptr; + callback( + AsstMsg::ConnectionInfo, + json::object { + { "what", "ConnectFailed" }, + { "why", "MaaAndroidNativeControlUnit failed to get UUID" }, + } | get_info_json()); + return false; + } + + m_inited = true; + callback( + AsstMsg::ConnectionInfo, + json::object { + { "what", "Connected" }, + { "why", "NativeAndroid" }, + } | get_info_json()); + return true; +} + +bool MaaFwAndroidNativeController::inited() const noexcept +{ + return m_inited && m_unit_handle && m_unit_handle->connected(); +} + +const std::string& MaaFwAndroidNativeController::get_uuid() const +{ + return m_uuid; +} + +bool MaaFwAndroidNativeController::screencap(cv::Mat& image_payload, bool allow_reconnect [[maybe_unused]]) +{ + LogTraceFunction; + if (!m_unit_handle) { + LogWarn << "MaaAndroidNativeControlUnit is not initialized"; + return false; + } + + if (!m_unit_handle->screencap(image_payload)) { + LogWarn << "MaaAndroidNativeControlUnit screencap failed"; + return false; + } + + return true; +} + +bool MaaFwAndroidNativeController::start_game(const std::string& client_type) +{ + LogTraceFunction; + if (!m_unit_handle) { + LogWarn << "MaaAndroidNativeControlUnit is not initialized"; + return false; + } + + auto package_name = Config.get_package_name(client_type); + if (!package_name) { + LogWarn << "Invalid client_type" << VAR(client_type); + return false; + } + return m_unit_handle->start_app(*package_name); +} + +bool MaaFwAndroidNativeController::stop_game(const std::string& client_type) +{ + LogTraceFunction; + if (!m_unit_handle) { + LogWarn << "MaaAndroidNativeControlUnit is not initialized"; + return false; + } + + auto package_name = Config.get_package_name(client_type); + if (!package_name) { + LogWarn << "Invalid client_type" << VAR(client_type); + return false; + } + return m_unit_handle->stop_app(*package_name); +} + +bool MaaFwAndroidNativeController::click(const Point& p) +{ + LogTraceFunction; + if (!m_unit_handle) { + LogWarn << "MaaAndroidNativeControlUnit is not initialized"; + return false; + } + + if (!m_unit_handle->touch_down(0, p.x, p.y, 1)) { + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + return m_unit_handle->touch_up(0); +} + +bool MaaFwAndroidNativeController::input(const std::string& text) +{ + LogTraceFunction; + if (!m_unit_handle) { + LogWarn << "MaaAndroidNativeControlUnit is not initialized"; + return false; + } + return m_unit_handle->input_text(text); +} + +bool MaaFwAndroidNativeController::swipe( + const Point& p1, + const Point& p2, + const int duration, + const bool extra_swipe, + const double slope_in, + const double slope_out, + const bool with_pause) +{ + LogTraceFunction; + if (!m_unit_handle) { + LogWarn << "MaaAndroidNativeControlUnit is not initialized"; + return false; + } + + int x1 = p1.x, y1 = p1.y; + int x2 = p2.x, y2 = p2.y; + + // 起点不能在屏幕外,但是终点可以 + if (x1 < 0 || x1 >= m_screen_resolution.first || y1 < 0 || y1 >= m_screen_resolution.second) { + LogWarn << "swipe point1 is out of range" << x1 << y1; + x1 = std::clamp(x1, 0, m_screen_resolution.first - 1); + y1 = std::clamp(y1, 0, m_screen_resolution.second - 1); + } + + // 触摸按下起点 + if (!m_unit_handle->touch_down(0, x1, y1, 1)) { + LogError << "touch_down failed at swipe start point"; + return false; + } + + constexpr int TimeInterval = 5; // 类似 Minitoucher::DefaultSwipeDelay + + bool need_pause = with_pause; + const auto& opt = Config.get_options(); + + auto bounds_check = [this](int x, int y) { + return x >= 0 && x < m_screen_resolution.first && y >= 0 && y < m_screen_resolution.second; + }; + + auto move_func = [&](int x, int y) -> bool { + if (!m_unit_handle->touch_move(0, x, y, 1)) { + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(TimeInterval)); + return true; + }; + + auto do_swipe = [&](const int _x1, const int _y1, const int _x2, const int _y2, const int _duration) -> bool { + if (need_pause) { + auto pause_check = [&opt](const int cur_x, const int cur_y, const int start_x, const int start_y) { + return std::sqrt(std::pow(cur_x - start_x, 2) + std::pow(cur_y - start_y, 2)) > + opt.swipe_with_pause_required_distance; + }; + + return interpolate_swipe_with_pause( + _x1, + _y1, + _x2, + _y2, + _duration, + TimeInterval, + slope_in, + slope_out, + move_func, + bounds_check, + pause_check, + [&]() { + need_pause = false; + press_esc(); + }); + } + return interpolate_swipe( + _x1, + _y1, + _x2, + _y2, + _duration, + TimeInterval, + slope_in, + slope_out, + move_func, + bounds_check); + }; + + if (!do_swipe(x1, y1, x2, y2, duration ? duration : opt.minitouch_swipe_default_duration)) { + LogError << "Failed during main swipe movement"; + m_unit_handle->touch_up(0); + return false; + } + + // 额外滑动逻辑 + if (extra_swipe && opt.minitouch_extra_swipe_duration > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(opt.minitouch_swipe_extra_end_delay)); + + if (!do_swipe(x2, y2, x2, y2 - opt.minitouch_extra_swipe_dist, opt.minitouch_extra_swipe_duration)) { + LogWarn << "Failed during extra swipe movement"; + } + } + + m_unit_handle->touch_up(0); + return true; +} + +bool MaaFwAndroidNativeController::inject_input_event(const InputEvent& event) +{ + LogTraceFunction; + if (!m_unit_handle) { + LogWarn << "MaaAndroidNativeControlUnit is not initialized"; + return false; + } + + switch (event.type) { + case InputEvent::Type::TOUCH_DOWN: + return m_unit_handle->touch_down(event.pointerId, event.point.x, event.point.y, 0); + case InputEvent::Type::TOUCH_MOVE: + return m_unit_handle->touch_move(event.pointerId, event.point.x, event.point.y, 0); + case InputEvent::Type::TOUCH_UP: + return m_unit_handle->touch_up(event.pointerId); + case InputEvent::Type::TOUCH_RESET: + return true; + case InputEvent::Type::KEY_DOWN: + return m_unit_handle->key_down(event.keycode); + case InputEvent::Type::KEY_UP: + return m_unit_handle->key_up(event.keycode); + case InputEvent::Type::WAIT_MS: + std::this_thread::sleep_for(std::chrono::milliseconds(event.milisec)); + return true; + case InputEvent::Type::COMMIT: + return true; + default: + LogError << "unknown input event type" << VAR(static_cast(event.type)); + return false; + } +} + +bool MaaFwAndroidNativeController::press_esc() +{ + LogTraceFunction; + if (!m_unit_handle) { + LogWarn << "MaaAndroidNativeControlUnit is not initialized"; + return false; + } + + constexpr int KEYCODE_ESCAPE = 111; + if (!m_unit_handle->key_down(KEYCODE_ESCAPE)) { + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + return m_unit_handle->key_up(KEYCODE_ESCAPE); +} + +ControlFeat::Feat MaaFwAndroidNativeController::support_features() const noexcept +{ + // MaaFwAndroidNativeController 支持精确滑动和暂停滑动功能 + auto feat = ControlFeat::PRECISE_SWIPE; + feat |= ControlFeat::SWIPE_WITH_PAUSE; + return feat; +} + +std::pair MaaFwAndroidNativeController::get_screen_res() const noexcept +{ + return m_screen_resolution; +} + +bool MaaFwAndroidNativeController::init_library() +{ + if (m_get_version_func && m_create_func && m_destroy_func) { + LogInfo << "MaaAndroidNativeControlUnit library already loaded"; + return true; + } + if (!load_library("MaaAndroidNativeControlUnit")) { + LogError << "Failed to load MaaAndroidNativeControlUnit library"; + return false; + } + + m_get_version_func = get_function("MaaAndroidNativeControlUnitGetVersion"); + m_create_func = get_function("MaaAndroidNativeControlUnitCreate"); + m_destroy_func = get_function("MaaAndroidNativeControlUnitDestroy"); + + if (!m_get_version_func || !m_create_func || !m_destroy_func) { + LogError << "Failed to get function pointers from MaaAndroidNativeControlUnit library"; + return false; + } + + LogInfo << "MaaAndroidNativeControlUnit library version:" << m_get_version_func(); + + return true; +} + +void MaaFwAndroidNativeController::callback(const AsstMsg msg, const json::value& details) const +{ + if (m_callback) { + m_callback(msg, details, m_inst); + } +} + +} // namespace asst + +#endif // __ANDROID__ diff --git a/src/MaaCore/Controller/MaaFwAndroidNativeController.h b/src/MaaCore/Controller/MaaFwAndroidNativeController.h new file mode 100644 index 0000000000..835e711626 --- /dev/null +++ b/src/MaaCore/Controller/MaaFwAndroidNativeController.h @@ -0,0 +1,85 @@ +#pragma once + +#ifdef __ANDROID__ + +#include +#include + +#include "Common/AsstMsg.h" +#include "ControllerAPI.h" +#include "InstHelper.h" +#include "MaaFwControlUnitInterface.h" +#include "Utils/LibraryHolder.hpp" + +namespace asst +{ +class Assistant; + +class MaaFwAndroidNativeController : public ControllerAPI, private InstHelper, + public LibraryHolder +{ +public: + MaaFwAndroidNativeController(const AsstCallback& callback, Assistant* inst); + virtual ~MaaFwAndroidNativeController() override; + + MaaFwAndroidNativeController(const MaaFwAndroidNativeController&) = delete; + MaaFwAndroidNativeController& operator=(const MaaFwAndroidNativeController&) = delete; + MaaFwAndroidNativeController(MaaFwAndroidNativeController&&) = delete; + MaaFwAndroidNativeController& operator=(MaaFwAndroidNativeController&&) = delete; + +public: + virtual bool connect(const std::string& adb_path, const std::string& address, const std::string& config) override; + virtual bool inited() const noexcept override; + + virtual const std::string& get_uuid() const override; + + virtual size_t get_pipe_data_size() const noexcept override { return 0; } + + virtual size_t get_version() const noexcept override { return 1; } + + virtual bool screencap(cv::Mat& image_payload, bool allow_reconnect = false) override; + + virtual bool start_game(const std::string& client_type) override; + virtual bool stop_game(const std::string& client_type) override; + + virtual bool click(const Point& p) override; + virtual bool input(const std::string& text) override; + virtual bool swipe( + const Point& p1, + const Point& p2, + int duration = 0, + bool extra_swipe = false, + double slope_in = 1, + double slope_out = 1, + bool with_pause = false) override; + + virtual bool inject_input_event(const InputEvent& event) override; + + virtual bool press_esc() override; + virtual ControlFeat::Feat support_features() const noexcept override; + + virtual std::pair get_screen_res() const noexcept override; + +private: + bool m_inited = false; + std::string m_uuid; + std::pair m_screen_resolution = { 0, 0 }; + + MaaFwAndroidNativeControlUnitAPI* m_unit_handle = nullptr; + bool init_library(); + + AsstCallback m_callback = nullptr; + void callback(AsstMsg msg, const json::value& details) const; + + // MaaFramework/source/include/MaaControlUnit/AndroidNativeControlUnitAPI.h + using GetVersionFunc = const char*(); + using CreateFunc = MaaFwAndroidNativeControlUnitAPI*(const char*); + using DestroyFunc = void(MaaFwAndroidNativeControlUnitAPI*); + + std::function m_get_version_func; + std::function m_create_func; + std::function m_destroy_func; +}; +} // namespace asst + +#endif // __ANDROID__ diff --git a/src/MaaCore/Controller/MaaFwControlUnitInterface.h b/src/MaaCore/Controller/MaaFwControlUnitInterface.h index 1d7950850a..15ccef21b0 100644 --- a/src/MaaCore/Controller/MaaFwControlUnitInterface.h +++ b/src/MaaCore/Controller/MaaFwControlUnitInterface.h @@ -39,7 +39,10 @@ public: virtual bool key_down(int key) = 0; virtual bool key_up(int key) = 0; - virtual bool scroll(int dx, int dy) = 0; + virtual bool inactive() = 0; + + // json::object get_info() const - ABI slot occupied, not called from MAA side + virtual void* get_info() const = 0; }; class MaaFwAdbControlUnitAPI : public MaaFwControlUnitAPI @@ -54,6 +57,12 @@ public: std::chrono::milliseconds timeout = std::chrono::milliseconds(20000)) = 0; }; +class MaaFwAndroidNativeControlUnitAPI : public MaaFwControlUnitAPI +{ +public: + ~MaaFwAndroidNativeControlUnitAPI() override = default; +}; + // 与 MaaFramework 的 MaaControllerFeature 兼容的常量 namespace MaaFeature { diff --git a/src/MaaCore/Task/Miscellaneous/StartGameTaskPlugin.cpp b/src/MaaCore/Task/Miscellaneous/StartGameTaskPlugin.cpp index d52cbbac03..1677dcc617 100644 --- a/src/MaaCore/Task/Miscellaneous/StartGameTaskPlugin.cpp +++ b/src/MaaCore/Task/Miscellaneous/StartGameTaskPlugin.cpp @@ -5,8 +5,13 @@ using namespace asst; -bool StartGameTaskPlugin::start_game_with_retries(size_t pipe_data_size_limit, bool newer_android) const +bool StartGameTaskPlugin::start_game_with_retries([[maybe_unused]] size_t pipe_data_size_limit, + [[maybe_unused]] bool newer_android) const { +#ifdef __ANDROID__ + // On Android, start_game returns true only after the game is actually started + return !need_exit() && ctrler()->start_game(m_client_type); +#else int extra_runs = 0; for (int i = 0; i < 30; ++i) { if (need_exit() || !ctrler()->start_game(m_client_type)) { @@ -23,6 +28,7 @@ bool StartGameTaskPlugin::start_game_with_retries(size_t pipe_data_size_limit, b } return false; +#endif } bool StartGameTaskPlugin::_run() diff --git a/src/MaaCore/Utils/Logger.hpp b/src/MaaCore/Utils/Logger.hpp index ac996c536d..d59844d5be 100644 --- a/src/MaaCore/Utils/Logger.hpp +++ b/src/MaaCore/Utils/Logger.hpp @@ -4,7 +4,12 @@ #include #include #endif +#include +#include #include +#include +#include +#include #include #include #include @@ -15,6 +20,7 @@ #include #include #include +#include #include #include "Common/AsstTypes.h" @@ -31,6 +37,14 @@ #include #endif +#ifdef __ANDROID__ +#include +#include +#include + +#include "Demangle.hpp" +#endif + namespace asst { template @@ -251,7 +265,7 @@ public: } private: - std::vector m_state {}; + std::vector m_state { }; }; } // namespace detail @@ -320,7 +334,7 @@ public: requires has_stream_insertion_operator ostreams& operator<<(T&& x) { - streams_put(m_ofss, x, std::index_sequence_for {}); + streams_put(m_ofss, x, std::index_sequence_for { }); return *this; } @@ -332,7 +346,7 @@ public: ostreams& operator<<(std::ostream& (*pf)(std::ostream&)) { - streams_put(m_ofss, pf, std::index_sequence_for {}); + streams_put(m_ofss, pf, std::index_sequence_for { }); return *this; } @@ -476,7 +490,7 @@ public: #else int pid = ::getpid(); #endif - auto tid = static_cast(std::hash {}(std::this_thread::get_id())); + auto tid = static_cast(std::hash { }(std::this_thread::get_id())); s << std::format("[{}][{}][Px{}][Tx{}]", MAA_NS::format_now(), v.str, pid, tid); } @@ -491,7 +505,7 @@ public: } else if constexpr (std::ranges::input_range) { s << "["; - std::string_view comma_space {}; + std::string_view comma_space { }; for (const auto& elem : std::forward(v)) { s << comma_space; stream_put(s, elem); @@ -842,7 +856,23 @@ private: } } - inline static std::atomic g_last_signal_reason { nullptr }; + inline static std::atomic g_last_signal { 0 }; + + static const char* format_signal_reason(int sig) noexcept + { + switch (sig) { + case SIGSEGV: + return "SIGSEGV (Segmentation Fault)"; + case SIGABRT: + return "SIGABRT (Abort)"; + case SIGFPE: + return "SIGFPE (Floating Point Error)"; + case SIGILL: + return "SIGILL (Illegal Instruction)"; + default: + return "Unknown Signal"; + } + } static void write_crash_file(const char* reason, const char* detail = nullptr) noexcept { @@ -900,6 +930,103 @@ private: } #endif +#ifdef __ANDROID__ + static constexpr const char* AndroidCrashLogTag = "MaaCoreCrash"; + + struct AndroidBacktraceState + { + void** current = nullptr; + void** end = nullptr; + }; + + static _Unwind_Reason_Code android_unwind_callback(_Unwind_Context* context, void* arg) noexcept + { + auto* state = static_cast(arg); + if (state == nullptr || state->current == state->end) { + return _URC_END_OF_STACK; + } + + const auto pc = reinterpret_cast(_Unwind_GetIP(context)); + if (pc == nullptr) { + return _URC_NO_REASON; + } + + *state->current++ = pc; + return _URC_NO_REASON; + } + + static std::size_t capture_android_backtrace(void** frames, std::size_t max_frames) noexcept + { + AndroidBacktraceState state { + .current = frames, + .end = frames + max_frames, + }; + + _Unwind_Backtrace(android_unwind_callback, &state); + return static_cast(state.current - frames); + } + + static void log_android_crash_signal(const char* signal_reason) noexcept + { + __android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, "=== FATAL ERROR ==="); + if (signal_reason != nullptr) { + __android_log_print(ANDROID_LOG_FATAL, AndroidCrashLogTag, "Fatal Signal: %s", signal_reason); + } + __android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, "==================="); + } + + static void log_android_crash_terminate(const char* exception_info) noexcept + { + __android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, "=== FATAL ERROR ==="); + if (exception_info != nullptr) { + __android_log_print(ANDROID_LOG_FATAL, AndroidCrashLogTag, "Unhandled exception: %s", exception_info); + } + __android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, "==================="); + } + + static void dump_android_stacktrace(Logger& logger) noexcept + { + std::array frames { }; + const auto frame_count = capture_android_backtrace(frames.data(), frames.size()); + + std::size_t frame_start = 0; + while (frame_start < frame_count && frames[frame_start] == nullptr) { + ++frame_start; + } + if (frame_start < frame_count) { + ++frame_start; // Skip the helper frame itself. + } + + const auto dump_count = frame_count > frame_start ? frame_count - frame_start : 0; + __android_log_print(ANDROID_LOG_FATAL, AndroidCrashLogTag, "Native backtrace (%zu frames):", dump_count); + logger.error("Native backtrace", dump_count, "frames"); + + for (std::size_t i = frame_start; i < frame_count; ++i) { + Dl_info info { }; + std::string frame_message; + if (dladdr(frames[i], &info) != 0 && info.dli_sname != nullptr) { + const auto symbol_name = utils::demangle(info.dli_sname); + const auto base = reinterpret_cast(info.dli_saddr); + const auto pc = reinterpret_cast(frames[i]); + const auto offset = pc >= base ? pc - base : 0; + frame_message = std::format( + "#{:02} pc {:p} {} ({}+0x{:x})", + i - frame_start, + frames[i], + info.dli_fname != nullptr ? info.dli_fname : "", + symbol_name, + offset); + } + else { + frame_message = std::format("#{:02} pc {:p}", i - frame_start, frames[i]); + } + + __android_log_write(ANDROID_LOG_FATAL, AndroidCrashLogTag, frame_message.c_str()); + logger.error(frame_message); + } + } +#endif + static void custom_terminate_handler() noexcept { static bool in_handler = false; @@ -908,18 +1035,10 @@ private: } in_handler = true; + const int last_signal = g_last_signal.exchange(0); + const char* signal_info = last_signal != 0 ? format_signal_reason(last_signal) : nullptr; + try { - auto& logger = Logger::get_instance(); - - // 先写信号信息 - if (auto sig_reason = g_last_signal_reason.load()) { - logger.error("=== FATAL ERROR ==="); - logger.error("Signal caught:", sig_reason); - logger.flush(); - write_crash_file("Fatal Signal", sig_reason); - } - - // 再处理 C++ 异常 std::string exception_info = "Unknown exception"; if (auto eptr = std::current_exception()) { try { @@ -933,11 +1052,29 @@ private: } } +#ifdef __ANDROID__ + if (last_signal == 0) { + log_android_crash_terminate(exception_info.c_str()); + } +#endif + + auto& logger = Logger::get_instance(); + + if (signal_info != nullptr) { + logger.error("=== FATAL ERROR ==="); + logger.error("Signal caught:", signal_info); + logger.flush(); + write_crash_file("Fatal Signal", signal_info); + } + logger.error("=== FATAL ERROR ==="); logger.error("Version", MAA_VERSION); logger.error("Built at", __DATE__, __TIME__); logger.error("User Dir", UserDir.get()); logger.error("Unhandled exception caught:", exception_info); +#ifdef __ANDROID__ + dump_android_stacktrace(logger); +#endif logger.error("Program terminating..."); logger.error("==================="); logger.flush(); @@ -953,40 +1090,36 @@ private: static void signal_handler(int sig) { - std::string sig_name; - switch (sig) { - case SIGSEGV: - sig_name = "SIGSEGV (Segmentation Fault)"; - break; - case SIGABRT: - sig_name = "SIGABRT (Abort)"; - break; - case SIGFPE: - sig_name = "SIGFPE (Floating Point Error)"; - break; - case SIGILL: - sig_name = "SIGILL (Illegal Instruction)"; - break; - default: - sig_name = "Signal " + std::to_string(sig); - break; - } - g_last_signal_reason.store(sig_name.c_str()); +#ifdef __ANDROID__ + log_android_crash_signal(format_signal_reason(sig)); +#endif + g_last_signal.store(sig); custom_terminate_handler(); std::_Exit(EXIT_FAILURE); } +#ifdef __ANDROID__ + [[noreturn]] static void android_terminate_handler() noexcept + { + custom_terminate_handler(); + std::_Exit(EXIT_FAILURE); + } +#endif + static void initialize_exception_handlers() { #ifdef _WIN32 // Windows: 设置未处理异常过滤器 SetUnhandledExceptionFilter(unhandled_exception_filter); #endif - +#ifdef __ANDROID__ + std::set_terminate(android_terminate_handler); +#endif std::signal(SIGSEGV, signal_handler); std::signal(SIGABRT, signal_handler); std::signal(SIGFPE, signal_handler); std::signal(SIGILL, signal_handler); + #ifdef ASST_DEBUG const auto& path = UserDir.get() / "debug" / "crash.log"; if (std::filesystem::exists(path)) { @@ -1047,7 +1180,7 @@ private: std::ostream m_of; std::size_t m_file_size = 0; - static inline utils::NullStreambuf null_buf {}; + static inline utils::NullStreambuf null_buf { }; static inline std::ostream null_stream { &null_buf }; const std::size_t MaxLogSize = 64LL * 1024 * 1024; };