mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-01 01:10:34 +08:00
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:
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -486,3 +486,4 @@ install-*
|
||||
|
||||
# CMake user presets
|
||||
CMakeUserPresets.json
|
||||
.ace-tool/
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<std::mutex> lock(m_call_mutex);
|
||||
if (m_thread_exit) {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_call_queue.empty()) {
|
||||
|
||||
@@ -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)
|
||||
$<TARGET_FILE_DIR:MaaCore>
|
||||
COMMAND_EXPAND_LISTS)
|
||||
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()
|
||||
|
||||
@@ -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<Rect>& 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<T1, T2>& p) const noexcept
|
||||
{
|
||||
std::size_t hash1 = std::hash<T1> {}(p.first);
|
||||
std::size_t hash2 = std::hash<T2> {}(p.second);
|
||||
std::size_t hash1 = std::hash<T1> { }(p.first);
|
||||
std::size_t hash2 = std::hash<T2> { }(p.second);
|
||||
hash1 ^= hash2 + 0x9e3779b9 + (hash1 << 6) + (hash1 >> 2);
|
||||
|
||||
hash1 ^= hash1 >> 32;
|
||||
|
||||
@@ -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<asst::ControllerAPI>
|
||||
return std::make_shared<PlayToolsController>(m_callback, m_inst, platform_type);
|
||||
case ControllerType::MaaFwAdb:
|
||||
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:
|
||||
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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,6 +19,9 @@ enum class ControllerType
|
||||
Win32,
|
||||
#endif
|
||||
MaaFwAdb,
|
||||
#ifdef __ANDROID__
|
||||
MaaFwAndroidNative,
|
||||
#endif
|
||||
};
|
||||
|
||||
class ControllerAPI
|
||||
|
||||
425
src/MaaCore/Controller/MaaFwAndroidNativeController.cpp
Normal file
425
src/MaaCore/Controller/MaaFwAndroidNativeController.cpp
Normal 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__
|
||||
85
src/MaaCore/Controller/MaaFwAndroidNativeController.h
Normal file
85
src/MaaCore/Controller/MaaFwAndroidNativeController.h
Normal 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__
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#endif
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
@@ -15,6 +20,7 @@
|
||||
#include <streambuf>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <typeinfo>
|
||||
#include <utility>
|
||||
|
||||
#include "Common/AsstTypes.h"
|
||||
@@ -31,6 +37,14 @@
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#include <dlfcn.h>
|
||||
#include <unwind.h>
|
||||
|
||||
#include "Demangle.hpp"
|
||||
#endif
|
||||
|
||||
namespace asst
|
||||
{
|
||||
template <typename Stream, typename T>
|
||||
@@ -251,7 +265,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<id> m_state {};
|
||||
std::vector<id> m_state { };
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
@@ -320,7 +334,7 @@ public:
|
||||
requires has_stream_insertion_operator<std::ostream, T>
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -332,7 +346,7 @@ public:
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -476,7 +490,7 @@ public:
|
||||
#else
|
||||
int pid = ::getpid();
|
||||
#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);
|
||||
}
|
||||
@@ -491,7 +505,7 @@ public:
|
||||
}
|
||||
else if constexpr (std::ranges::input_range<T>) {
|
||||
s << "[";
|
||||
std::string_view comma_space {};
|
||||
std::string_view comma_space { };
|
||||
for (const auto& elem : std::forward<T>(v)) {
|
||||
s << comma_space;
|
||||
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
|
||||
{
|
||||
@@ -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<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 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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user