feat: support native android (#16179)

* feat: add android controller support

* feat: add android compile options and add core static options

* fix: add __ANDROID__ macro for Android platform

* fix: move switch break into macro conditional block

* ci: add Android CMake presets (arm64, x64)

* fix: add missing Android TouchMode handling

* fix: remove unnecessary pre-check in android

* fix: restore unnecessary modifications

* fix: unified use __ANDROID__

* refactor: use interpolate_swipe_with_pause in AndroidController

* feat: add Android crash logcat output and native backtrace

* feat: only copy once from external lib screencap

* feat: add Android JNI AttachThread/DetachThread to working_proc

* fix: break control in switch

* fix: update click interval & swipe interval

* ci: restore Android build job with MaaFramework download

* fix: remove thread attach

* ci: temporarily remove MaaAndroidNativeControlUnit.so

* feat(android-ctrl): SWIPE_WITH_PAUSE 由 InstanceOption DeploymentWithPause 控制

* feat: SWIPE_WITH_PAUSE 默认启用

* feat: add android compile options and add core static options

* feat: use maafw controll unit

* fix: minor adjustments

* refactor: adapt maafw android lib

* fix: remove screencap when connect

* chore: change maafw lib use

* feat: adapt maafw control unit click & click_key

* fix: address critical issues in Android native controller

- Restore InstanceOptionKey::ClientType case in set_instance_option,
  which was accidentally removed and broke client type setting on all platforms
- Enable libMaaAndroidNativeControlUnit.so copy step in CI (was commented out),
  so Android artifacts actually include the required control unit library
- Fail connect() early when screen_resolution is missing or invalid in config,
  preventing silent {0,0} resolution that would break swipe and screencap
- Check touch_down() return value in swipe() and abort on failure

* fix: correct bounds_check off-by-one and extract KEYCODE_ESCAPE constant

- bounds_check used <= which allowed coordinates equal to screen width/height
  (off-screen); use < to match the valid range [0, dimension-1]
- Extract magic number 111 into KEYCODE_ESCAPE constexpr in press_esc()

* fix: avoid heap allocation in noexcept terminate/signal handlers

format_signal_reason was returning std::string and using std::format in
its default branch. Called from noexcept custom_terminate_handler before
the outer try block, any allocation failure would throw std::bad_alloc,
triggering recursive std::terminate and aborting crash reporting.

- Change return type to const char* noexcept, returning string literals
- Replace std::format("Signal {}", sig) with "Unknown Signal" (only the
  four registered signals are ever passed, so this branch is unreachable)
- Change signal_info in custom_terminate_handler from std::string to
  const char*, eliminating all allocations on the pre-try path

* fix: suppress unused-variable warning for signal_reason on non-Android
This commit is contained in:
Aliothmoon
2026-05-12 22:15:15 +08:00
committed by GitHub
parent 13fa72510a
commit c6f930a60d
15 changed files with 893 additions and 50 deletions

View File

@@ -349,6 +349,78 @@ jobs:
release/*.AppImage release/*.AppImage
release/*.tar.gz 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: macOS-Core:
name: Build Core for macOS name: Build Core for macOS
needs: meta needs: meta
@@ -574,7 +646,7 @@ jobs:
release: release:
name: Publish Release name: Publish Release
if: startsWith(github.ref, 'refs/tags/v') 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 runs-on: ubuntu-latest
steps: steps:
- name: Download MAA from GitHub - name: Download MAA from GitHub

1
.gitignore vendored
View File

@@ -486,3 +486,4 @@ install-*
# CMake user presets # CMake user presets
CMakeUserPresets.json CMakeUserPresets.json
.ace-tool/

View File

@@ -49,6 +49,13 @@ if(BUILD_WPF_GUI)
endif() endif()
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) if(INSTALL_PYTHON)
install(DIRECTORY src/Python DESTINATION .) install(DIRECTORY src/Python DESTINATION .)
endif() endif()

View File

@@ -158,6 +158,39 @@
"CMAKE_OSX_ARCHITECTURES": "x86_64" "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", "name": "publish-base",
"$comment": [ "$comment": [
@@ -239,6 +272,22 @@
], ],
"displayName": "macOS arm64 Publish" "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", "name": "smoke-test",
"$comment": [ "$comment": [
@@ -400,6 +449,16 @@
"configurePreset": "macos-publish-arm64", "configurePreset": "macos-publish-arm64",
"configuration": "RelWithDebInfo" "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", "name": "smoke-test",
"displayName": "Build macOS arm64 Smoke Test", "displayName": "Build macOS arm64 Smoke Test",

View File

@@ -32,6 +32,7 @@
#include "Task/Interface/DebugTask.h" #include "Task/Interface/DebugTask.h"
#endif #endif
using namespace asst; using namespace asst;
bool ::AsstExtAPI::set_static_option(StaticOptionKey key, const std::string& value) 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); m_ctrler->set_touch_mode(TouchMode::MaaFwAdb);
return true; 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; break;
case InstanceOptionKey::DeploymentWithPause: case InstanceOptionKey::DeploymentWithPause:
if (constexpr std::string_view Enable = "1"; value == Enable) { if (constexpr std::string_view Enable = "1"; value == Enable) {
@@ -624,7 +631,7 @@ void asst::Assistant::call_proc()
while (true) { while (true) {
std::unique_lock<std::mutex> lock(m_call_mutex); std::unique_lock<std::mutex> lock(m_call_mutex);
if (m_thread_exit) { if (m_thread_exit) {
return; break;
} }
if (m_call_queue.empty()) { if (m_call_queue.empty()) {

View File

@@ -48,6 +48,10 @@ if(MSVC)
set_target_properties(MaaCore PROPERTIES VS_DEBUGGER_VISUALIZER "${CMAKE_CURRENT_SOURCE_DIR}/meojson.natvis") set_target_properties(MaaCore PROPERTIES VS_DEBUGGER_VISUALIZER "${CMAKE_CURRENT_SOURCE_DIR}/meojson.natvis")
endif() endif()
if(ANDROID)
target_link_libraries(MaaCore log dl)
endif()
file(GLOB_RECURSE MaaCore_PUBLIC_HEADERS ${PROJECT_SOURCE_DIR}/include/*.h) file(GLOB_RECURSE MaaCore_PUBLIC_HEADERS ${PROJECT_SOURCE_DIR}/include/*.h)
target_sources(MaaCore PUBLIC ${MaaCore_PUBLIC_HEADERS}) target_sources(MaaCore PUBLIC ${MaaCore_PUBLIC_HEADERS})
set_target_properties(MaaCore PROPERTIES PUBLIC_HEADER "${MaaCore_PUBLIC_HEADERS}") set_target_properties(MaaCore PROPERTIES PUBLIC_HEADER "${MaaCore_PUBLIC_HEADERS}")
@@ -120,3 +124,15 @@ if(WIN32)
$<TARGET_FILE_DIR:MaaCore> $<TARGET_FILE_DIR:MaaCore>
COMMAND_EXPAND_LISTS) COMMAND_EXPAND_LISTS)
endif() endif()
if(ANDROID)
if(CMAKE_STRIP)
add_custom_command(
TARGET MaaCore
POST_BUILD
COMMAND ${CMAKE_STRIP} --strip-all $<TARGET_FILE:MaaCore>
COMMENT "Stripping MaaCore for Android")
else()
message(WARNING "CMAKE_STRIP not found, Android library will not be stripped")
endif()
endif()

View File

@@ -57,6 +57,7 @@ enum class TouchMode
Maatouch = 2, Maatouch = 2,
MacPlayTools = 3, MacPlayTools = 3,
MaaFwAdb = 4, MaaFwAdb = 4,
Android = 5,
}; };
#ifdef _WIN32 #ifdef _WIN32
@@ -155,7 +156,7 @@ struct Point
{ \ { \
return { lhs.x Op rhs.x, lhs.y Op rhs.y }; \ 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.x Op## = opd.x; \
val.y Op## = opd.y; \ val.y Op## = opd.y; \
@@ -260,7 +261,7 @@ struct Rect
static Rect bounding_box(const std::vector<Rect>& rects) static Rect bounding_box(const std::vector<Rect>& rects)
{ {
if (rects.empty()) { if (rects.empty()) {
return {}; return { };
} }
int min_x = INT_MAX; int min_x = INT_MAX;
@@ -303,11 +304,11 @@ struct AnalyzerResult
{ {
virtual ~AnalyzerResult() = default; 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(); } 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(); } explicit operator json::object() const { return to_json(); }
}; };
@@ -418,8 +419,8 @@ struct pair_hash
{ {
size_t operator()(const std::pair<T1, T2>& p) const noexcept size_t operator()(const std::pair<T1, T2>& p) const noexcept
{ {
std::size_t hash1 = std::hash<T1> {}(p.first); std::size_t hash1 = std::hash<T1> { }(p.first);
std::size_t hash2 = std::hash<T2> {}(p.second); std::size_t hash2 = std::hash<T2> { }(p.second);
hash1 ^= hash2 + 0x9e3779b9 + (hash1 << 6) + (hash1 >> 2); hash1 ^= hash2 + 0x9e3779b9 + (hash1 << 6) + (hash1 >> 2);
hash1 ^= hash1 >> 32; hash1 ^= hash1 >> 32;

View File

@@ -28,6 +28,10 @@
#include "Win32Controller.h" #include "Win32Controller.h"
#endif #endif
#ifdef __ANDROID__
#include "MaaFwAndroidNativeController.h"
#endif
#include "Common/AsstTypes.h" #include "Common/AsstTypes.h"
#include "Utils/Logger.hpp" #include "Utils/Logger.hpp"
@@ -58,6 +62,11 @@ std::shared_ptr<asst::ControllerAPI>
return std::make_shared<PlayToolsController>(m_callback, m_inst, platform_type); return std::make_shared<PlayToolsController>(m_callback, m_inst, platform_type);
case ControllerType::MaaFwAdb: case ControllerType::MaaFwAdb:
return std::make_shared<MaaFwAdbController>(m_callback, m_inst, platform_type); return std::make_shared<MaaFwAdbController>(m_callback, m_inst, platform_type);
#ifdef __ANDROID__
case ControllerType::MaaFwAndroidNative:
Log.debug("Use Android");
return std::make_shared<MaaFwAndroidNativeController>(m_callback, m_inst);
#endif
default: default:
return nullptr; return nullptr;
} }
@@ -247,11 +256,14 @@ bool asst::Controller::connect(const std::string& adb_path, const std::string& a
} }
#endif #endif
// Android uses lazy loading; no need to check in advance
#ifndef __ANDROID__
// try to find the fastest way // try to find the fastest way
if (!screencap()) { if (!screencap()) {
Log.error("Cannot find a proper way to screencap!"); Log.error("Cannot find a proper way to screencap!");
return false; return false;
} }
#endif
auto proxy_callback = [&](const json::object& details) { auto proxy_callback = [&](const json::object& details) {
json::value connection_info = json::object { json::value connection_info = json::object {
@@ -368,6 +380,11 @@ void asst::Controller::set_touch_mode(const TouchMode& mode) noexcept
case TouchMode::MaaFwAdb: case TouchMode::MaaFwAdb:
m_controller_type = ControllerType::MaaFwAdb; m_controller_type = ControllerType::MaaFwAdb;
break; break;
#ifdef __ANDROID__
case TouchMode::Android:
m_controller_type = ControllerType::MaaFwAndroidNative;
break;
#endif
default: default:
m_controller_type = ControllerType::Minitouch; 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)) { if (get_scale_size() == std::pair(0, 0)) {
Log.error("Unknown image size"); Log.error("Unknown image size");
return {}; return { };
} }
// 有些模拟器adb偶尔会莫名其妙截图失败多试几次 // 有些模拟器adb偶尔会莫名其妙截图失败多试几次
@@ -433,7 +450,7 @@ cv::Mat asst::Controller::get_image(bool raw)
{ "uuid", m_uuid }, { "uuid", m_uuid },
{ "what", "ScreencapFailed" }, { "what", "ScreencapFailed" },
{ "why", "ScreencapFailed" }, { "why", "ScreencapFailed" },
{ "details", json::object {} }, { "details", json::object { } },
}; };
callback(AsstMsg::ConnectionInfo, info); callback(AsstMsg::ConnectionInfo, info);

View File

@@ -63,6 +63,8 @@ public:
ControllerType get_controller_type() const noexcept; 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(bool raw = false);
cv::Mat get_image_cache() const; cv::Mat get_image_cache() const;
bool screencap(bool allow_reconnect = false); bool screencap(bool allow_reconnect = false);

View File

@@ -19,6 +19,9 @@ enum class ControllerType
Win32, Win32,
#endif #endif
MaaFwAdb, MaaFwAdb,
#ifdef __ANDROID__
MaaFwAndroidNative,
#endif
}; };
class ControllerAPI class ControllerAPI

View File

@@ -0,0 +1,425 @@
#ifdef __ANDROID__
#include "MaaFwAndroidNativeController.h"
#include <cmath>
#include <thread>
#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<int>(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<int, int> 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<GetVersionFunc>("MaaAndroidNativeControlUnitGetVersion");
m_create_func = get_function<CreateFunc>("MaaAndroidNativeControlUnitCreate");
m_destroy_func = get_function<DestroyFunc>("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__

View File

@@ -0,0 +1,85 @@
#pragma once
#ifdef __ANDROID__
#include <string>
#include <utility>
#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<MaaFwAndroidNativeController>
{
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<int, int> get_screen_res() const noexcept override;
private:
bool m_inited = false;
std::string m_uuid;
std::pair<int, int> 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<GetVersionFunc> m_get_version_func;
std::function<CreateFunc> m_create_func;
std::function<DestroyFunc> m_destroy_func;
};
} // namespace asst
#endif // __ANDROID__

View File

@@ -39,7 +39,10 @@ public:
virtual bool key_down(int key) = 0; virtual bool key_down(int key) = 0;
virtual bool key_up(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 class MaaFwAdbControlUnitAPI : public MaaFwControlUnitAPI
@@ -54,6 +57,12 @@ public:
std::chrono::milliseconds timeout = std::chrono::milliseconds(20000)) = 0; std::chrono::milliseconds timeout = std::chrono::milliseconds(20000)) = 0;
}; };
class MaaFwAndroidNativeControlUnitAPI : public MaaFwControlUnitAPI
{
public:
~MaaFwAndroidNativeControlUnitAPI() override = default;
};
// 与 MaaFramework 的 MaaControllerFeature 兼容的常量 // 与 MaaFramework 的 MaaControllerFeature 兼容的常量
namespace MaaFeature namespace MaaFeature
{ {

View File

@@ -5,8 +5,13 @@
using namespace asst; 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; int extra_runs = 0;
for (int i = 0; i < 30; ++i) { for (int i = 0; i < 30; ++i) {
if (need_exit() || !ctrler()->start_game(m_client_type)) { 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; return false;
#endif
} }
bool StartGameTaskPlugin::_run() bool StartGameTaskPlugin::_run()

View File

@@ -4,7 +4,12 @@
#include <fcntl.h> #include <fcntl.h>
#include <io.h> #include <io.h>
#endif #endif
#include <atomic>
#include <array>
#include <csignal> #include <csignal>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <filesystem> #include <filesystem>
#include <format> #include <format>
#include <fstream> #include <fstream>
@@ -15,6 +20,7 @@
#include <streambuf> #include <streambuf>
#include <thread> #include <thread>
#include <type_traits> #include <type_traits>
#include <typeinfo>
#include <utility> #include <utility>
#include "Common/AsstTypes.h" #include "Common/AsstTypes.h"
@@ -31,6 +37,14 @@
#include <unistd.h> #include <unistd.h>
#endif #endif
#ifdef __ANDROID__
#include <android/log.h>
#include <dlfcn.h>
#include <unwind.h>
#include "Demangle.hpp"
#endif
namespace asst namespace asst
{ {
template <typename Stream, typename T> template <typename Stream, typename T>
@@ -251,7 +265,7 @@ public:
} }
private: private:
std::vector<id> m_state {}; std::vector<id> m_state { };
}; };
} // namespace detail } // namespace detail
@@ -320,7 +334,7 @@ public:
requires has_stream_insertion_operator<std::ostream, T> requires has_stream_insertion_operator<std::ostream, T>
ostreams& operator<<(T&& x) ostreams& operator<<(T&& x)
{ {
streams_put(m_ofss, x, std::index_sequence_for<Args...> {}); streams_put(m_ofss, x, std::index_sequence_for<Args...> { });
return *this; return *this;
} }
@@ -332,7 +346,7 @@ public:
ostreams& operator<<(std::ostream& (*pf)(std::ostream&)) ostreams& operator<<(std::ostream& (*pf)(std::ostream&))
{ {
streams_put(m_ofss, pf, std::index_sequence_for<Args...> {}); streams_put(m_ofss, pf, std::index_sequence_for<Args...> { });
return *this; return *this;
} }
@@ -476,7 +490,7 @@ public:
#else #else
int pid = ::getpid(); int pid = ::getpid();
#endif #endif
auto tid = static_cast<uint16_t>(std::hash<std::thread::id> {}(std::this_thread::get_id())); auto tid = static_cast<uint16_t>(std::hash<std::thread::id> { }(std::this_thread::get_id()));
s << std::format("[{}][{}][Px{}][Tx{}]", MAA_NS::format_now(), v.str, pid, tid); 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<T>) { else if constexpr (std::ranges::input_range<T>) {
s << "["; s << "[";
std::string_view comma_space {}; std::string_view comma_space { };
for (const auto& elem : std::forward<T>(v)) { for (const auto& elem : std::forward<T>(v)) {
s << comma_space; s << comma_space;
stream_put(s, elem); stream_put(s, elem);
@@ -842,7 +856,23 @@ private:
} }
} }
inline static std::atomic<const char*> g_last_signal_reason { nullptr }; inline static std::atomic<int> 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 static void write_crash_file(const char* reason, const char* detail = nullptr) noexcept
{ {
@@ -900,6 +930,103 @@ private:
} }
#endif #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<AndroidBacktraceState*>(arg);
if (state == nullptr || state->current == state->end) {
return _URC_END_OF_STACK;
}
const auto pc = reinterpret_cast<void*>(_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<std::size_t>(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<void*, 32> 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<std::uintptr_t>(info.dli_saddr);
const auto pc = reinterpret_cast<std::uintptr_t>(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 : "<unknown>",
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 void custom_terminate_handler() noexcept
{ {
static bool in_handler = false; static bool in_handler = false;
@@ -908,18 +1035,10 @@ private:
} }
in_handler = true; 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 { 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"; std::string exception_info = "Unknown exception";
if (auto eptr = std::current_exception()) { if (auto eptr = std::current_exception()) {
try { 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("=== FATAL ERROR ===");
logger.error("Version", MAA_VERSION); logger.error("Version", MAA_VERSION);
logger.error("Built at", __DATE__, __TIME__); logger.error("Built at", __DATE__, __TIME__);
logger.error("User Dir", UserDir.get()); logger.error("User Dir", UserDir.get());
logger.error("Unhandled exception caught:", exception_info); logger.error("Unhandled exception caught:", exception_info);
#ifdef __ANDROID__
dump_android_stacktrace(logger);
#endif
logger.error("Program terminating..."); logger.error("Program terminating...");
logger.error("==================="); logger.error("===================");
logger.flush(); logger.flush();
@@ -953,40 +1090,36 @@ private:
static void signal_handler(int sig) static void signal_handler(int sig)
{ {
std::string sig_name; #ifdef __ANDROID__
switch (sig) { log_android_crash_signal(format_signal_reason(sig));
case SIGSEGV: #endif
sig_name = "SIGSEGV (Segmentation Fault)"; g_last_signal.store(sig);
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());
custom_terminate_handler(); custom_terminate_handler();
std::_Exit(EXIT_FAILURE); 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() static void initialize_exception_handlers()
{ {
#ifdef _WIN32 #ifdef _WIN32
// Windows: 设置未处理异常过滤器 // Windows: 设置未处理异常过滤器
SetUnhandledExceptionFilter(unhandled_exception_filter); SetUnhandledExceptionFilter(unhandled_exception_filter);
#endif #endif
#ifdef __ANDROID__
std::set_terminate(android_terminate_handler);
#endif
std::signal(SIGSEGV, signal_handler); std::signal(SIGSEGV, signal_handler);
std::signal(SIGABRT, signal_handler); std::signal(SIGABRT, signal_handler);
std::signal(SIGFPE, signal_handler); std::signal(SIGFPE, signal_handler);
std::signal(SIGILL, signal_handler); std::signal(SIGILL, signal_handler);
#ifdef ASST_DEBUG #ifdef ASST_DEBUG
const auto& path = UserDir.get() / "debug" / "crash.log"; const auto& path = UserDir.get() / "debug" / "crash.log";
if (std::filesystem::exists(path)) { if (std::filesystem::exists(path)) {
@@ -1047,7 +1180,7 @@ private:
std::ostream m_of; std::ostream m_of;
std::size_t m_file_size = 0; 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 }; static inline std::ostream null_stream { &null_buf };
const std::size_t MaxLogSize = 64LL * 1024 * 1024; const std::size_t MaxLogSize = 64LL * 1024 * 1024;
}; };