Feat: TV remote control via D-pad focus navigation using AccessibilityService

This commit is contained in:
yuanyuanxiang
2026-06-22 17:57:51 +02:00
parent 45553ec5b6
commit 218ee4f43d
22 changed files with 631 additions and 67 deletions

View File

@@ -3,6 +3,9 @@ plugins {
id 'org.jetbrains.kotlin.android'
}
// 模拟器调试时传 -PincludeEmulatorAbis=truebuild_apk.sh 发布时强制传 false
def INCLUDE_EMULATOR_ABIS = project.findProperty('includeEmulatorAbis')?.toBoolean() ?: false
android {
namespace 'com.yama.client'
compileSdk 35
@@ -16,8 +19,12 @@ android {
versionName "1.0"
ndk {
if (INCLUDE_EMULATOR_ABIS) {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
} else {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
externalNativeBuild {
cmake {

View File

@@ -24,6 +24,7 @@ set(CORE_SOURCES
${YAMA_ROOT}/client/Buffer.cpp
${YAMA_ROOT}/client/IOCPClient.cpp
${YAMA_ROOT}/client/sign_shim_unix.cpp
${YAMA_ROOT}/common/logger.cpp
${YAMA_ROOT}/common/ikcp.c
)
@@ -34,12 +35,11 @@ set(ANDROID_SOURCES
add_library(yama SHARED ${CORE_SOURCES} ${ANDROID_SOURCES})
# ---- 预编译静态库(由 SimplePlugins/sign_lib/build_android.sh 产出)----
# ---- 预编译静态库(由 android/build_android_libs.ps1 产出)----
# 支持 arm64-v8a / armeabi-v7a真机和 x86 / x86_64Android 模拟器)
target_link_libraries(yama PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/lib/${ANDROID_ABI}/libsign.a"
)
# ---- zstd由 android/build_zstd_android.sh 产出)----
target_link_libraries(yama PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/lib/${ANDROID_ABI}/libzstd.a"
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,7 @@
#include <jni.h>
#include <android/log.h>
#include <thread>
#include <chrono>
#include <atomic>
#include <mutex>
#include <set>
@@ -9,7 +10,9 @@
#include <cinttypes>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
@@ -22,13 +25,18 @@
#include "ScreenHandler.h"
#define XXH_INLINE_ALL
#include "common/xxhash.h"
#include "common/logger.h"
#define LOG_TAG "YAMA"
#undef Mprintf
#define Mprintf(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// 1 = 状态提示显示到电视屏幕0 = 只写 logcat不显示到屏幕
#define SCREEN_STATUS_ENABLED 0
extern "C" int signMessage_c(const char* pk, int pkLen, const unsigned char* msg,
int msgLen, char* buf, int bufSize);
// ---- 设备信息 helper登录时填充仅调用一次----
static int GetCpuCores() {
@@ -109,23 +117,31 @@ static std::string JsonGetStr(const std::string& json, const char* key) {
// 查询 ip-api.com一次请求同时获取公网 IP 和地理位置(与 Windows 客户端同源)
// 在连接线程中同步调用,超时 3 秒
static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
pubIp.clear(); location.clear();
struct GeoResult {
std::string pubIp, location;
std::atomic<bool> ready{false};
};
static void FetchGeoInfoImpl(std::shared_ptr<GeoResult> out) {
struct addrinfo hints = {}, *res = nullptr;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo("ip-api.com", "80", &hints, &res) != 0 || !res) return false;
if (getaddrinfo("ip-api.com", "80", &hints, &res) != 0 || !res) {
out->ready.store(true, std::memory_order_release); return;
}
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { freeaddrinfo(res); return false; }
if (fd < 0) { freeaddrinfo(res); out->ready.store(true, std::memory_order_release); return; }
struct timeval tv = {3, 0};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
connect(fd, res->ai_addr, res->ai_addrlen);
fd_set wfds; FD_ZERO(&wfds); FD_SET(fd, &wfds);
bool connected = (select(fd + 1, nullptr, &wfds, nullptr, &tv) == 1);
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
bool ok = false;
if (connect(fd, res->ai_addr, res->ai_addrlen) == 0) {
if (connected) {
const char* req = "GET /json/?fields=status,query,country,city HTTP/1.0\r\n"
"Host: ip-api.com\r\n"
"Connection: close\r\n\r\n";
@@ -140,19 +156,31 @@ static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
if (sep != std::string::npos) {
const std::string body = resp.substr(sep + 4);
if (JsonGetStr(body, "status") == "success") {
pubIp = JsonGetStr(body, "query");
out->pubIp = JsonGetStr(body, "query");
std::string city = JsonGetStr(body, "city");
std::string country = JsonGetStr(body, "country");
if (!city.empty() && !country.empty()) location = city + ", " + country;
else if (!country.empty()) location = country;
else if (!city.empty()) location = city;
ok = true;
if (!city.empty() && !country.empty()) out->location = city + ", " + country;
else if (!country.empty()) out->location = country;
else if (!city.empty()) out->location = city;
}
}
}
close(fd);
freeaddrinfo(res);
return ok;
out->ready.store(true, std::memory_order_release);
}
// getaddrinfo 在某些设备上会永久阻塞,用独立线程 + 5 秒轮询超时保护 ConnectionThread
static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
pubIp.clear(); location.clear();
auto result = std::make_shared<GeoResult>();
std::thread(FetchGeoInfoImpl, result).detach();
for (int i = 0; i < 50 && !result->ready.load(std::memory_order_acquire); i++)
usleep(100000); // 100ms × 50 = 5 秒
if (!result->ready.load(std::memory_order_acquire)) return false;
pubIp = result->pubIp;
location = result->location;
return !pubIp.empty();
}
// ---- 全局状态 ----
@@ -173,6 +201,7 @@ static jmethodID g_getActiveWindowMid = nullptr;
static jclass g_captureClass = nullptr;
static jmethodID g_requestIdrMid = nullptr;
static jmethodID g_forceFirstFrameMid = nullptr;
static jmethodID g_statusMid = nullptr; // CaptureService.onNativeStatus(String)
// 心跳日志限流:每 60 秒最多打一次
static std::atomic<uint64_t> g_lastHbLogMs{0};
@@ -277,6 +306,29 @@ static void RequestIdrFromJava() { CallCaptureStaticVoid(g_requestIdrMid);
// 强制 VirtualDisplay 推一帧(静止屏幕绕过 SurfaceFlinger 空闲优化)
static void ForceFirstFrameFromJava() { CallCaptureStaticVoid(g_forceFirstFrameMid); }
// 向 CaptureService.onNativeStatus() 发送状态 Toast连接线程诊断用
static void PostStatus(const char* msg) {
LOGI("STATUS: %s", msg);
#if SCREEN_STATUS_ENABLED
if (!g_jvm || !g_captureClass || !g_statusMid) return;
JNIEnv* jenv = nullptr;
bool attached = false;
if (g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6) == JNI_EDETACHED) {
g_jvm->AttachCurrentThread(&jenv, nullptr);
attached = true;
}
if (jenv) {
jstring js = jenv->NewStringUTF(msg);
if (js) {
jenv->CallStaticVoidMethod(g_captureClass, g_statusMid, js);
jenv->DeleteLocalRef(js);
}
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
}
if (attached) g_jvm->DetachCurrentThread();
#endif
}
static void ScreenSpyThread()
{
// 等待 Java 侧 startCapture() 调用 nativeSetScreenSize 设置真实分辨率。
@@ -299,32 +351,36 @@ static void ScreenSpyThread()
std::unique_ptr<IOCPClient> client;
std::unique_ptr<AndroidScreenHandler> handler;
try {
LOGI("SST[%d]: creating IOCPClient", attempt);
client = std::make_unique<IOCPClient>(g_bExit, true);
client->EnableSubConnAuth(true, g_myClientID);
LOGI("SST[%d]: connecting %s:%d", attempt, g_serverIp.c_str(), g_serverPort);
if (!client->ConnectServer(g_serverIp.c_str(), g_serverPort)) {
LOGI("ScreenSpyThread: connect failed (attempt %d/20), retry 2s", attempt);
Sleep(2000);
continue;
}
LOGI("SST[%d]: connected, creating handler w=%d h=%d", attempt, w, h);
handler = std::make_unique<AndroidScreenHandler>(client.get(), w, h);
LOGI("SST[%d]: handler created, inserting to set", attempt);
{
std::lock_guard<std::mutex> lk(g_shMutex);
g_screenHandlers.insert(handler.get());
}
LOGI("SST[%d]: setManagerCallBack", attempt);
client->setManagerCallBack(handler.get(),
IOCPManager::DataProcess,
IOCPManager::ReconnectProcess);
LOGI("SST[%d]: SendBitmapInfo", attempt);
handler->SendBitmapInfo();
// 子连接刚建立,浏览器 initDecoder 完成后需要关键帧才能开始显示。
// ForceFirstFrameFromJava短暂 resize VirtualDisplay 强制 SurfaceFlinger
// 推一帧给编码器——静止屏幕时 SurfaceFlinger 有空闲优化不主动推帧,
// 导致 REQUEST_SYNC_FRAME 排队等待,造成 10-60 秒黑屏。
// RequestIdrFromJava 确保此帧被编码为 IDR。
LOGI("SST[%d]: ForceFirstFrame", attempt);
ForceFirstFrameFromJava();
LOGI("SST[%d]: RequestIdr", attempt);
RequestIdrFromJava();
LOGI("SST[%d]: entering wait loop", attempt);
while (client->IsRunning() && client->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(200);
@@ -354,24 +410,28 @@ static void ScreenSpyThread()
int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
{
if (!szBuffer || !ulLength) return TRUE;
if (!ClientAuth::IsCommandAllowed(szBuffer[0]))
int allowed = (int)ClientAuth::IsCommandAllowed(szBuffer[0]);
if (!allowed) {
LOGI("DataProcess cmd=%d len=%lu allowed=%d",
(int)(unsigned char)szBuffer[0], (unsigned long)ulLength, allowed);
return TRUE;
}
switch (szBuffer[0]) {
case COMMAND_BYE:
LOGI("COMMAND_BYE");
PostStatus("BYE from server");
g_bExit = S_CLIENT_EXIT;
break;
case CMD_HEARTBEAT_ACK:
if (ulLength >= 1 + (ULONG)sizeof(HeartbeatACK)) {
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
HeartbeatACK ack;
memcpy(&ack, szBuffer + 1, sizeof(HeartbeatACK));
uint64_t now = GetUnixMs();
g_lastHeartbeatAckMs.store(now, std::memory_order_relaxed);
int64_t rtt = (int64_t)now - (int64_t)ack->Time;
if (ack->ProcessingMs > 0 && (int64_t)ack->ProcessingMs < rtt)
rtt -= ack->ProcessingMs;
int64_t rtt = (int64_t)now - (int64_t)ack.Time;
if (ack.ProcessingMs > 0 && (int64_t)ack.ProcessingMs < rtt)
rtt -= ack.ProcessingMs;
g_rttEstimator.update_from_sample((double)rtt);
if (now - g_lastHbLogMs.load(std::memory_order_relaxed) >= HB_LOG_INTERVAL_MS) {
g_lastHbLogMs.store(now, std::memory_order_relaxed);
@@ -386,6 +446,9 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
if (settings.ReportInterval > 0)
g_heartbeatInterval = std::max(settings.ReportInterval, 30);
LOGI("MasterSettings OK interval=%ds (server=%d)", g_heartbeatInterval, settings.ReportInterval);
PostStatus("masterSettings: OK");
} else {
PostStatus("masterSettings: FAIL");
}
break;
}
@@ -394,7 +457,8 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
// 每个 COMMAND_SCREEN_SPY 对应一个独立子连接,支持多人同时观看/控制
size_t active;
{ std::lock_guard<std::mutex> lk(g_shMutex); active = g_screenHandlers.size(); }
LOGI("COMMAND_SCREEN_SPY → ScreenSpyThread (active=%zu)", active);
LOGI("cmd: COMMAND_SCREEN_SPY len=%lu active=%zu w=%d h=%d",
(unsigned long)ulLength, active, g_screenWidth.load(), g_screenHeight.load());
std::thread(ScreenSpyThread).detach();
break;
}
@@ -421,7 +485,7 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
break;
default:
LOGI("Unhandled cmd=%d", (int)szBuffer[0]);
LOGI("cmd: unhandled cmd=%d len=%lu", (int)szBuffer[0], (unsigned long)ulLength);
break;
}
return TRUE;
@@ -432,11 +496,10 @@ static void ConnectionThread()
{
LOGI("ConnectionThread → %s:%d", g_serverIp.c_str(), g_serverPort);
PostStatus("geo: fetching...");
std::string g_pubIp, g_location;
FetchGeoInfo(g_pubIp, g_location);
// 失败时留空串——服务端检测到空串会回落到自己的 IP 地理查询;
// 发 "?" 会被服务端当作有效值缓存,导致永远显示问号
LOGI("GeoInfo: ip=%s loc=%s", g_pubIp.c_str(), g_location.c_str());
PostStatus(("geo: " + (g_pubIp.empty() ? "failed" : g_pubIp)).c_str());
LOGIN_INFOR logInfo;
strncpy(logInfo.szPCName, g_deviceModel.c_str(), sizeof(logInfo.szPCName) - 1);
@@ -474,14 +537,21 @@ static void ConnectionThread()
std::unique_ptr<IOCPClient> client(new IOCPClient(g_bExit, false));
client->setManagerCallBack(nullptr, DataProcess, nullptr);
int g_connAttempt = 0;
while (S_CLIENT_NORMAL == g_bExit && g_running.load()) {
char connMsg[64];
snprintf(connMsg, sizeof(connMsg), "connect #%d → %s:%d",
++g_connAttempt, g_serverIp.c_str(), g_serverPort);
PostStatus(connMsg);
clock_t c = clock();
if (!client->ConnectServer(g_serverIp.c_str(), g_serverPort)) {
LOGI("Connect failed, retry 5s");
snprintf(connMsg, sizeof(connMsg), "connect #%d failed errno=%d", g_connAttempt, errno);
PostStatus(connMsg);
Sleep(5000);
continue;
}
PostStatus("connected! sending login...");
ClientAuth::OnNewConnection();
client->SendLoginInfo(logInfo.Speed(clock() - c));
g_lastHeartbeatAckMs.store(GetUnixMs(), std::memory_order_relaxed);
@@ -493,16 +563,22 @@ static void ConnectionThread()
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
for (int i = 0; i < interval; ++i) {
if (!client->IsRunning() || !client->IsConnected()
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) break;
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) {
char dbg[96];
snprintf(dbg, sizeof(dbg), "drop@%ds run=%d conn=%d exit=%d run2=%d",
i, (int)client->IsRunning(), (int)client->IsConnected(),
(int)(g_bExit == S_CLIENT_NORMAL), (int)g_running.load());
PostStatus(dbg);
break;
}
Sleep(1000);
}
if (!client->IsRunning() || !client->IsConnected()
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) break;
if (ClientAuth::IsTimedOut()) {
LOGI("MasterSettings timeout → reconnect");
client->Disconnect();
break;
PostStatus("timeout: masterSettings");
continue;
}
{
@@ -510,9 +586,8 @@ static void ConnectionThread()
uint64_t last = g_lastHeartbeatAckMs.load(std::memory_order_relaxed);
uint64_t now = GetUnixMs();
if (last > 0 && now > last && now - last > (uint64_t)ackTO * 1000ULL) {
LOGI("ACK timeout → reconnect");
client->Disconnect();
break;
PostStatus("timeout: ACK");
continue;
}
}
@@ -534,7 +609,10 @@ static void ConnectionThread()
}
}
}
LOGI("Disconnected");
PostStatus("disconnected, retry...");
// Give server 2 s to remove old context from HostList; without this
// the immediate reconnect hits "already exists" and server skips SendMasterSettings.
std::this_thread::sleep_for(std::chrono::seconds(2));
}
g_running.store(false);
@@ -603,6 +681,10 @@ Java_com_yama_client_YamaBridge_nativeInit(
g_forceFirstFrameMid = env->GetStaticMethodID(g_captureClass, "forceFirstFrame", "()V");
if (env->ExceptionCheck()) { env->ExceptionClear(); g_forceFirstFrameMid = nullptr; }
if (!g_forceFirstFrameMid) LOGE("CaptureService.forceFirstFrame not found");
g_statusMid = env->GetStaticMethodID(g_captureClass, "onNativeStatus", "(Ljava/lang/String;)V");
if (env->ExceptionCheck()) { env->ExceptionClear(); g_statusMid = nullptr; }
if (!g_statusMid) LOGE("CaptureService.onNativeStatus not found");
UseAndroidLog(PostStatus);
} else {
LOGE("CaptureService class not found");
}

View File

@@ -21,6 +21,7 @@ import android.provider.Settings
import android.util.DisplayMetrics
import android.util.Log
import android.view.WindowManager
import android.widget.Toast
class CaptureService : Service() {
@@ -39,6 +40,14 @@ class CaptureService : Service() {
// CS-01 fix: 通过 idrHandler.post 序列化到主线程,避免 TOCTOU 竞争
@Volatile var instance: CaptureService? = null
@JvmStatic
fun onNativeStatus(msg: String) {
val svc = instance ?: return
svc.idrHandler.post {
Toast.makeText(svc, msg, Toast.LENGTH_LONG).show()
}
}
@JvmStatic
fun requestIdr() {
val svc = instance ?: return
@@ -100,9 +109,16 @@ class CaptureService : Service() {
super.onCreate()
instance = this
createNotificationChannel()
// CS-06: start with DATA_SYNC type only; upgrade to MEDIA_PROJECTION in
// onStartCommand when the token is actually available. On Android 14+
// (targetSdk 34+) calling startForeground(MEDIA_PROJECTION) without
// immediately pairing it with getMediaProjection() causes the system to
// kill the service after a ~5-second grace period — the root cause of the
// 3-9 s disconnect seen on Google TV when no screen-capture permission
// was granted.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIF_ID, buildNotification())
}
@@ -124,12 +140,23 @@ class CaptureService : Service() {
Log.i(TAG, "CaptureService: server=$ip:$port device=$model res=$res")
// YB-01 fix: so 库加载失败是 Error不被 catch(Exception) 捕获,需单独处理
// ExceptionInInitializerError 包裹 UnsatisfiedLinkError首次访问 object 时触发)
val initRet: Int
try {
YamaBridge.nativeInit(ip, port, androidId, model, osVer, res, user, packageCodePath ?: "")
initRet = YamaBridge.nativeInit(ip, port, androidId, model, osVer, res, user, packageCodePath ?: "")
} catch (e: UnsatisfiedLinkError) {
Log.e(TAG, "Native library load failed: $e")
stopSelf()
return START_NOT_STICKY
} catch (e: ExceptionInInitializerError) {
Log.e(TAG, "Native library init failed: ${e.cause}")
stopSelf()
return START_NOT_STICKY
}
if (initRet != 0) {
Log.e(TAG, "nativeInit failed: ret=$initRet")
stopSelf()
return START_NOT_STICKY
}
val projResult = intent.getIntExtra(EXTRA_PROJECTION_RESULT, -1)
@@ -139,12 +166,22 @@ class CaptureService : Service() {
@Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_PROJECTION_DATA)
if (projResult == android.app.Activity.RESULT_OK && projData != null) {
// Android 14+: upgrade foreground type to MEDIA_PROJECTION in the same
// start command that calls getMediaProjection(), as required by API 34+.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
}
val pm = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val mp = pm.getMediaProjection(projResult, projData)
mediaProjection = mp
// CS-05 fix: 分辨率为 0 时不启动采集,避免 MediaCodec 配置崩溃
if (sw > 0 && sh > 0) {
try {
startCapture(mp, sw, sh)
} catch (e: Exception) {
Log.e(TAG, "Screen capture failed: $e — running without capture")
}
} else {
Log.e(TAG, "Invalid screen resolution ${sw}x${sh}, skipping capture")
}
@@ -223,9 +260,14 @@ class CaptureService : Service() {
// Android 14+ requires callback registered before createVirtualDisplay()
mp.registerCallback(object : MediaProjection.Callback() {
override fun onStop() {
Log.i(TAG, "MediaProjection stopped")
Log.i(TAG, "MediaProjection stopped — switching to dataSync foreground type")
stopCapture()
stopSelf()
// Android 14+: system auto-kills a foreground service whose mediaProjection
// is revoked. Re-declare as dataSync-only to survive without screen capture.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
}
}
}, null)

View File

@@ -14,7 +14,9 @@ import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
/**
* AccessibilityService that injects touch gestures and global key actions
@@ -88,6 +90,22 @@ class ControlService : AccessibilityService() {
// ── Main-thread handler ───────────────────────────────────────────────
private val handler = Handler(Looper.getMainLooper())
// ── TV (Google TV / Android TV) D-pad mode ───────────────────────────
// Activated when FEATURE_LEANBACK is present. Mouse events are translated
// to accessibility focus navigation — no touchscreen or root required.
private var isTV = false
private var tvLastEncX = -1f
private var tvLastEncY = -1f
private var tvAccumDX = 0f
private var tvAccumDY = 0f
private val TV_STEP = 40f // encoding-space pixels per D-pad step
// Touch-drag disambiguation: fire click on LBUTTONUP only if finger barely moved
private var tvTouchDown = false
private var tvTouchDownX = 0f
private var tvTouchDownY = 0f
private var tvTouchMoved = false
private val TV_TAP_THRESHOLD = 15f // encoding pixels — below this = tap, above = drag
// ── Left-button drag accumulator ─────────────────────────────────────
private var lbuttonDown = false
private var gestureStart = 0L
@@ -100,6 +118,8 @@ class ControlService : AccessibilityService() {
override fun onServiceConnected() {
super.onServiceConnected()
instance = this
isTV = packageManager.hasSystemFeature("android.software.leanback")
if (isTV) Log.i(TAG, "TV mode: D-pad navigation enabled")
val filter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_USER_PRESENT)
@@ -127,6 +147,7 @@ class ControlService : AccessibilityService() {
// Event routing (runs on main thread)
private fun handleEvent(message: Int, wParam: Long, ptX: Int, ptY: Int) {
if (isTV) { handleEventTV(message, wParam, ptX, ptY); return }
// Scale encoding-space coords to physical screen
val x = if (encW > 0) ptX.toFloat() * physW / encW else ptX.toFloat()
val y = if (encH > 0) ptY.toFloat() * physH / encH else ptY.toFloat()
@@ -265,6 +286,119 @@ class ControlService : AccessibilityService() {
if (!ok) Log.e(TAG, "dispatchGesture returned false (service not ready?)")
}
// ─────────────────────────────────────────────────────────────────────
// TV D-pad mode — translates mouse events to accessibility focus actions
private fun handleEventTV(message: Int, wParam: Long, ptX: Int, ptY: Int) {
when (message) {
WM_MOUSEMOVE -> {
if (tvLastEncX >= 0) {
tvAccumDX += ptX - tvLastEncX
tvAccumDY += ptY - tvLastEncY
while (tvAccumDX > TV_STEP) { tvMoveFocus(View.FOCUS_RIGHT); tvAccumDX -= TV_STEP }
while (tvAccumDX < -TV_STEP) { tvMoveFocus(View.FOCUS_LEFT); tvAccumDX += TV_STEP }
while (tvAccumDY > TV_STEP) { tvMoveFocus(View.FOCUS_DOWN); tvAccumDY -= TV_STEP }
while (tvAccumDY < -TV_STEP) { tvMoveFocus(View.FOCUS_UP); tvAccumDY += TV_STEP }
}
if (tvTouchDown) {
val dx = ptX - tvTouchDownX
val dy = ptY - tvTouchDownY
if (dx * dx + dy * dy > TV_TAP_THRESHOLD * TV_TAP_THRESHOLD) tvTouchMoved = true
}
tvLastEncX = ptX.toFloat()
tvLastEncY = ptY.toFloat()
}
WM_LBUTTONDOWN -> {
tvTouchDown = true
tvTouchDownX = ptX.toFloat()
tvTouchDownY = ptY.toFloat()
tvTouchMoved = false
tvAccumDX = 0f
tvAccumDY = 0f
tvLastEncX = ptX.toFloat()
tvLastEncY = ptY.toFloat()
}
WM_LBUTTONUP -> {
if (tvTouchDown && !tvTouchMoved) tvClick()
tvTouchDown = false
}
WM_LBUTTONDBLCLK -> tvClick()
WM_RBUTTONDOWN -> performGlobalAction(GLOBAL_ACTION_BACK)
WM_MOUSEWHEEL -> {
val delta = (wParam.toInt() ushr 16).toShort().toInt()
tvScroll(delta < 0)
}
WM_KEYDOWN, WM_SYSKEYDOWN -> {
val vk = wParam.toInt() and 0xFFFF
when (vk) {
0x25 -> tvMoveFocus(View.FOCUS_LEFT) // VK_LEFT
0x26 -> tvMoveFocus(View.FOCUS_UP) // VK_UP
0x27 -> tvMoveFocus(View.FOCUS_RIGHT) // VK_RIGHT
0x28 -> tvMoveFocus(View.FOCUS_DOWN) // VK_DOWN
0x0D -> tvClick() // VK_RETURN
else -> handleKeyDown(vk)
}
}
}
}
@Suppress("DEPRECATION")
private fun tvMoveFocus(direction: Int) {
val root = rootInActiveWindow ?: return
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
if (focused == null) {
val first = findFirstFocusable(root)
first?.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
first?.recycle()
} else {
val next = focused.focusSearch(direction)
next?.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
focused.recycle()
next?.recycle()
}
root.recycle()
}
@Suppress("DEPRECATION")
private fun tvClick() {
val root = rootInActiveWindow ?: return
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
if (focused != null) {
focused.performAction(AccessibilityNodeInfo.ACTION_CLICK)
focused.recycle()
} else {
Log.w(TAG, "tvClick: no focused node")
}
root.recycle()
}
@Suppress("DEPRECATION")
private fun tvScroll(forward: Boolean) {
val root = rootInActiveWindow ?: return
val action = if (forward) AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
else AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
if (focused == null || !focused.performAction(action)) root.performAction(action)
focused?.recycle()
root.recycle()
}
// Walk accessibility tree depth-first, return first visible focusable node (caller must recycle)
@Suppress("DEPRECATION")
private fun findFirstFocusable(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (node.isFocusable && node.isVisibleToUser) return AccessibilityNodeInfo.obtain(node)
for (i in 0 until node.childCount) {
val child = node.getChild(i) ?: continue
val hit = findFirstFocusable(child)
child.recycle()
if (hit != null) return hit
}
return null
}
private fun handleKeyDown(vk: Int) {
when (vk) {
0x08, 0x1B -> performGlobalAction(GLOBAL_ACTION_BACK) // Backspace / Esc

View File

@@ -3,5 +3,6 @@
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:canPerformGestures="true"
android:canRetrieveWindowContent="true"
android:notificationTimeout="0"
android:settingsActivity="com.yama.client.MainActivity" />

View File

@@ -0,0 +1,197 @@
<#
.SYNOPSIS
Build libzstd.a and libsign.a for Android (4 ABIs)
.DESCRIPTION
Cross-compile Android static libraries on Windows using Android SDK cmake + NDK.
Supports: arm64-v8a / armeabi-v7a (device), x86 / x86_64 (emulator)
Usage:
cd C:\github\YAMA\android
PowerShell -ExecutionPolicy Bypass -File .\build_android_libs.ps1
Options:
-Force Rebuild even if .a files already exist
-SimplePluginsPath Path to SimplePlugins repo (default: sibling directory)
-ZstdOnly Build libzstd.a only
-SignOnly Build libsign.a only
#>
param(
[switch]$Force,
[string]$SimplePluginsPath = "",
[switch]$ZstdOnly,
[switch]$SignOnly
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$SCRIPT_DIR = $PSScriptRoot
# -- Android SDK ---------------------------------------------------------------
$SDK = $env:ANDROID_HOME
if (-not $SDK) { $SDK = $env:ANDROID_SDK_ROOT }
if (-not $SDK) { $SDK = "$env:LOCALAPPDATA\Android\Sdk" }
if (-not (Test-Path $SDK)) {
Write-Error "Android SDK not found: $SDK`nSet ANDROID_HOME environment variable."
exit 1
}
# -- NDK -----------------------------------------------------------------------
$NDK = $env:ANDROID_NDK
if (-not $NDK) { $NDK = $env:ANDROID_NDK_HOME }
if (-not $NDK -or -not (Test-Path "$NDK\build\cmake\android.toolchain.cmake")) {
$ndkCandidate = Get-ChildItem "$SDK\ndk" -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($ndkCandidate) { $NDK = $ndkCandidate.FullName }
}
if (-not (Test-Path "$NDK\build\cmake\android.toolchain.cmake")) {
Write-Error "NDK not found. Install via Android Studio > SDK Manager > NDK, or set ANDROID_NDK."
exit 1
}
Write-Host "NDK: $NDK"
# -- Android SDK cmake + ninja -------------------------------------------------
$CMAKE = $null
$sdkCmakeDirs = Get-ChildItem "$SDK\cmake" -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending
foreach ($dir in $sdkCmakeDirs) {
$cmakeBin = "$($dir.FullName)\bin\cmake.exe"
$ninjaBin = "$($dir.FullName)\bin\ninja.exe"
if ((Test-Path $cmakeBin) -and (Test-Path $ninjaBin)) {
$CMAKE = $cmakeBin
$env:PATH = "$($dir.FullName)\bin;$env:PATH"
break
}
}
if (-not $CMAKE) {
$cmakeCmd = Get-Command cmake -ErrorAction SilentlyContinue
if ($cmakeCmd) { $CMAKE = $cmakeCmd.Source }
}
if (-not $CMAKE) {
Write-Error "cmake not found. Install via Android Studio > SDK Manager > cmake."
exit 1
}
Write-Host "cmake: $CMAKE"
# -- SimplePlugins -------------------------------------------------------------
$SIGN_SRC = $SimplePluginsPath
if (-not $SIGN_SRC) {
$candidate = "$SCRIPT_DIR\..\..\SimplePlugins"
if (Test-Path $candidate) {
$SIGN_SRC = (Resolve-Path $candidate).Path
}
}
if ((-not $ZstdOnly) -and (-not (Test-Path "$SIGN_SRC\license_unix.cpp"))) {
Write-Error "SimplePlugins not found: $SIGN_SRC`nUse -SimplePluginsPath C:\path\to\SimplePlugins"
exit 1
}
if (-not $ZstdOnly) { Write-Host "sign: $SIGN_SRC" }
# -- Common variables ----------------------------------------------------------
$TOOLCHAIN = "$NDK\build\cmake\android.toolchain.cmake"
$ABIS = @("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
$LIB_BASE = "$SCRIPT_DIR\app\src\main\cpp\lib"
# ==============================================================================
# Part 1 - libzstd.a
# ==============================================================================
if (-not $SignOnly) {
$ZSTD_VER = "1.5.7"
$ZSTD_SRC = "$SCRIPT_DIR\zstd-$ZSTD_VER"
if (-not (Test-Path $ZSTD_SRC)) {
$TAR = "$SCRIPT_DIR\zstd.tar.gz"
Write-Host "`nDownloading zstd $ZSTD_VER..."
Invoke-WebRequest `
"https://github.com/facebook/zstd/releases/download/v$ZSTD_VER/zstd-$ZSTD_VER.tar.gz" `
-OutFile $TAR
Write-Host "Extracting..."
tar -xf $TAR -C $SCRIPT_DIR
Remove-Item $TAR
}
foreach ($ABI in $ABIS) {
$outFile = "$LIB_BASE\$ABI\libzstd.a"
if ((Test-Path $outFile) -and (-not $Force)) {
Write-Host "`n[skip] zstd $ABI -- already exists (use -Force to rebuild)"
continue
}
Write-Host "`n===== zstd $ABI ====="
$BUILD = "$SCRIPT_DIR\zstd_build\$ABI"
Remove-Item -Recurse -Force $BUILD -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force "$LIB_BASE\$ABI" | Out-Null
$configArgs = @(
"-B", $BUILD,
"-S", "$ZSTD_SRC\build\cmake",
"-G", "Ninja",
"-DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN",
"-DANDROID_ABI=$ABI",
"-DANDROID_PLATFORM=android-21",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_C_FLAGS=-O2 -g0",
"-DZSTD_BUILD_STATIC=ON",
"-DZSTD_BUILD_SHARED=OFF",
"-DZSTD_BUILD_PROGRAMS=OFF",
"-DZSTD_BUILD_TESTS=OFF",
"-DZSTD_LEGACY_SUPPORT=0"
)
& $CMAKE @configArgs
if ($LASTEXITCODE -ne 0) { Write-Error "cmake configure failed"; exit 1 }
& $CMAKE --build $BUILD --config Release
if ($LASTEXITCODE -ne 0) { Write-Error "cmake build failed"; exit 1 }
Copy-Item "$BUILD\lib\libzstd.a" $outFile -Force
$sz = [math]::Round((Get-Item $outFile).Length / 1KB)
Write-Host " --> $outFile (${sz} KB)"
}
Remove-Item -Recurse -Force "$SCRIPT_DIR\zstd_build" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force $ZSTD_SRC -ErrorAction SilentlyContinue
}
# ==============================================================================
# Part 2 - libsign.a
# ==============================================================================
if (-not $ZstdOnly) {
foreach ($ABI in $ABIS) {
$outFile = "$LIB_BASE\$ABI\libsign.a"
if ((Test-Path $outFile) -and (-not $Force)) {
Write-Host "`n[skip] sign $ABI -- already exists (use -Force to rebuild)"
continue
}
Write-Host "`n===== sign $ABI ====="
$BUILD = "$SCRIPT_DIR\sign_build\$ABI"
Remove-Item -Recurse -Force $BUILD -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force "$LIB_BASE\$ABI" | Out-Null
$configArgs = @(
"-B", $BUILD,
"-S", "$SIGN_SRC\sign_lib",
"-G", "Ninja",
"-DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN",
"-DANDROID_ABI=$ABI",
"-DANDROID_PLATFORM=android-21",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_C_FLAGS=-Os -g0"
)
& $CMAKE @configArgs
if ($LASTEXITCODE -ne 0) { Write-Error "cmake configure failed"; exit 1 }
& $CMAKE --build $BUILD --config Release
if ($LASTEXITCODE -ne 0) { Write-Error "cmake build failed"; exit 1 }
Copy-Item "$BUILD\libsign.a" $outFile -Force
$sz = [math]::Round((Get-Item $outFile).Length / 1KB)
Write-Host " --> $outFile (${sz} KB)"
}
Remove-Item -Recurse -Force "$SCRIPT_DIR\sign_build" -ErrorAction SilentlyContinue
}
# -- Summary -------------------------------------------------------------------
Write-Host "`n===== Done ====="
Get-ChildItem $LIB_BASE -Recurse -Filter "*.a" | Sort-Object FullName |
ForEach-Object {
$rel = $_.FullName.Substring($LIB_BASE.Length + 1)
$sz = [math]::Round($_.Length / 1KB)
Write-Host (" {0,-40} {1,6} KB" -f $rel, $sz)
}

View File

@@ -289,7 +289,7 @@ fi
echo ""
echo "Building $BUILD_TYPE APK..."
chmod +x gradlew
./gradlew "$TASK"
./gradlew "$TASK" -PincludeEmulatorAbis=false
# ── Output ────────────────────────────────────────────────────────────────────
APK=$(find "app/build/outputs/apk/${BUILD_TYPE}" -name "*.apk" 2>/dev/null | head -1 || true)

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# build_zstd_android.sh - 为 Android 交叉编译 libzstd.a
# 与 linux/lib/libzstd.a 版本对齐1.5.6
# 版本与 compress/zstd/zstd.h 头文件对齐1.5.7),确保服务端压缩帧可正确解压
#
# 用法:
# ./build_zstd_android.sh # 自动查找 NDK
@@ -13,7 +13,7 @@
set -euo pipefail
cd "$(dirname "$0")"
ZSTD_VERSION="1.5.6"
ZSTD_VERSION="1.5.7"
ZSTD_TAR="zstd-${ZSTD_VERSION}.tar.gz"
ZSTD_URL="https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/${ZSTD_TAR}"
ZSTD_DIR="zstd-${ZSTD_VERSION}"
@@ -33,6 +33,13 @@ fi
TOOLCHAIN="$ANDROID_NDK/build/cmake/android.toolchain.cmake"
echo "NDK: $ANDROID_NDK"
# --- 自动检测宿主平台(用于选择 strip 工具路径)---
case "$(uname -s 2>/dev/null || echo Windows)" in
Linux*) HOST_TAG="linux-x86_64" ;;
Darwin*) HOST_TAG="darwin-x86_64" ;;
*) HOST_TAG="windows-x86_64" ;;
esac
# --- 下载 zstd 源码 ---
if [[ ! -d "$ZSTD_DIR" ]]; then
echo "Downloading zstd $ZSTD_VERSION..."
@@ -42,7 +49,8 @@ if [[ ! -d "$ZSTD_DIR" ]]; then
fi
OUT_BASE="app/src/main/cpp/lib"
ABIS=("arm64-v8a" "armeabi-v7a")
# arm64-v8a / armeabi-v7a: 真机x86 / x86_64: Android 模拟器(避免 libndk_translation 解压 bug
ABIS=("arm64-v8a" "armeabi-v7a" "x86" "x86_64")
for ABI in "${ABIS[@]}"; do
BUILD_DIR="zstd_build/$ABI"
OUT_DIR="$OUT_BASE/$ABI"
@@ -55,17 +63,17 @@ for ABI in "${ABIS[@]}"; do
-DANDROID_ABI="$ABI" \
-DANDROID_PLATFORM=android-21 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_FLAGS="-Os -g0" \
-DCMAKE_C_FLAGS="-O2 -g0" \
-DZSTD_BUILD_STATIC=ON \
-DZSTD_BUILD_SHARED=OFF \
-DZSTD_BUILD_PROGRAMS=OFF \
-DZSTD_BUILD_TESTS=OFF \
-DZSTD_LEGACY_SUPPORT=0
cmake --build "$BUILD_DIR" --config Release -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu)"
-DZSTD_LEGACY_SUPPORT=0 \
-DZSTD_DISABLE_ASM=ON
cmake --build "$BUILD_DIR" --config Release -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 4)"
mkdir -p "$OUT_DIR"
cp "$BUILD_DIR/lib/libzstd.a" "$OUT_DIR/libzstd.a"
# strip residual debug symbols from the archive
STRIP="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip"
STRIP="$ANDROID_NDK/toolchains/llvm/prebuilt/${HOST_TAG}/bin/llvm-strip"
[[ -f "$STRIP" ]] && "$STRIP" --strip-debug "$OUT_DIR/libzstd.a"
ls -lh "$OUT_DIR/libzstd.a"
done

View File

@@ -1,3 +1,5 @@
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048m
# 模拟器x86/x86_64支持build_apk.sh 会强制覆盖为 false此行只影响 Android Studio
includeEmulatorAbis=true

View File

@@ -186,8 +186,7 @@ bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len)
{
std::lock_guard<std::mutex> lk(m_authMtx);
if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生)
const ConnAuthAck* ack = (const ConnAuthAck*)buf;
m_authStatus = ack->status;
m_authStatus = (int)buf[1]; // ConnAuthAck::status at byte offset 1; avoids misaligned uint64_t cast
m_authPending = false;
}
m_authCv.notify_all();
@@ -479,6 +478,13 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
if (ret == 0) {
m_bWorkThread = S_RUN;
m_bIsRunning = TRUE;
// Store pthread_t so subsequent ConnectServer calls (reconnects) find
// m_hWorkThread != NULL and reuse this thread instead of spawning a new
// one. Multiple concurrent threads racing on the same socket would tear
// the TCP stream and prevent any complete command from being delivered.
// SAFE_CLOSE_HANDLE and CloseHandle are no-ops on Android, so the value
// is never dereferenced as a kernel handle.
m_hWorkThread = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(id));
}
#endif
}

View File

@@ -39,11 +39,18 @@ BOOL IOCPUDPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
// 创建工作线程(如果需要)
if (m_hWorkThread == NULL) {
#ifdef _WIN32
m_bIsRunning = TRUE;
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, WorkThreadProc, (LPVOID)this, 0, NULL);
m_bWorkThread = m_hWorkThread ? S_RUN : S_STOP;
m_bIsRunning = m_hWorkThread ? TRUE : FALSE;
#else
pthread_t id = 0;
m_hWorkThread = (HANDLE)pthread_create(&id, nullptr, (void* (*)(void*))IOCPClient::WorkThreadProc, this);
int ret = (HANDLE)pthread_create(&id, nullptr, (void* (*)(void*))IOCPClient::WorkThreadProc, this);
if (ret == 0) {
m_bWorkThread = S_RUN;
m_bIsRunning = TRUE;
m_hWorkThread = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(id));
}
#endif
}

17
common/logger.cpp Normal file
View File

@@ -0,0 +1,17 @@
#include "logger.h"
#if defined(__ANDROID__)
static void (*androidLog)(const char*) = nullptr;
void UseAndroidLog(void (*cb)(const char*)) {
androidLog = cb;
}
__attribute__((format(printf, 1, 2)))
void android_log(const char* fmt, ...) {
char buf[256];
va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap);
__android_log_print(ANDROID_LOG_DEBUG, "YAMA_NET", "%s", buf);
if (androidLog) androidLog(buf);
}
#endif

View File

@@ -336,6 +336,15 @@ inline const char* getFileName(const char* path)
#else
#define Mprintf(format, ...) printf(format, ##__VA_ARGS__)
#endif
#elif defined(__ANDROID__)
#include <android/log.h>
#include <cstdarg>
#ifdef Mprintf
#undef Mprintf
#endif
void UseAndroidLog(void (*cb)(const char*));
void android_log(const char* fmt, ...);
#define Mprintf android_log
#else
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
#ifdef Mprintf

View File

@@ -47,4 +47,4 @@ struct RttEstimator {
// 进程级全局:所有翻译单元共享同一份估算器与心跳间隔
inline RttEstimator g_rttEstimator;
inline int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
inline int g_heartbeatInterval = 30; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新

50
linux/build_zstd_linux.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# build_zstd_linux.sh - 用官方源码编译 libzstd.a (x86_64 Linux)
# 产出linux/lib/libzstd.a (覆盖旧版本)
#
# 用法(在 WSL 或 Linux 下):
# cd linux && bash build_zstd_linux.sh
set -euo pipefail
cd "$(dirname "$0")"
ZSTD_VERSION="1.5.6"
ZSTD_TAR="zstd-${ZSTD_VERSION}.tar.gz"
ZSTD_URL="https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/${ZSTD_TAR}"
ZSTD_DIR="zstd-${ZSTD_VERSION}"
BUILD_DIR="zstd_build"
OUT_DIR="lib"
# --- 下载源码 ---
if [[ ! -d "$ZSTD_DIR" ]]; then
echo "Downloading zstd $ZSTD_VERSION..."
wget -q "$ZSTD_URL" -O "$ZSTD_TAR" || curl -sSL "$ZSTD_URL" -o "$ZSTD_TAR"
tar xf "$ZSTD_TAR"
rm "$ZSTD_TAR"
fi
# --- CMake 构建 ---
rm -rf "$BUILD_DIR"
cmake \
-B "$BUILD_DIR" -S "$ZSTD_DIR/build/cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_FLAGS="-O2 -g0" \
-DZSTD_BUILD_STATIC=ON \
-DZSTD_BUILD_SHARED=OFF \
-DZSTD_BUILD_PROGRAMS=OFF \
-DZSTD_BUILD_TESTS=OFF \
-DZSTD_LEGACY_SUPPORT=0
cmake --build "$BUILD_DIR" --config Release -j"$(nproc)"
# --- 拷贝产物 ---
mkdir -p "$OUT_DIR"
cp "$BUILD_DIR/lib/libzstd.a" "$OUT_DIR/libzstd.a"
strip --strip-debug "$OUT_DIR/libzstd.a" 2>/dev/null || true
ls -lh "$OUT_DIR/libzstd.a"
rm -rf "$BUILD_DIR" "$ZSTD_DIR"
echo
echo "===== Done ====="
echo "libzstd.a 已写入 linux/lib/libzstd.a"

View File

@@ -1582,7 +1582,8 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
LeaveCriticalSection(&m_cs);
Mprintf("主机[%s]上线: %s[%s][%s]\n", v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(),
std::to_string(id).c_str(), loc, groupName.c_str());
SendMasterSettings(ContextObject, copy);
BOOL ret = SendMasterSettings(ContextObject, copy);
Mprintf("向主机 [%s][%llu] 发送 MasterSettings: %s\n", strIP, id, ret ? "成功" : "失败");
if (m_needNotify && (GetTickCount() - g_StartTick > 30*1000))
PostMessageA(WM_SHOWNOTIFY, WPARAM(title), LPARAM(text));
else {
@@ -6832,13 +6833,13 @@ context* CMy2015RemoteDlg::FindHost(uint64_t id)
return FindHostNoLock(id);
}
void CMy2015RemoteDlg::SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m)
BOOL CMy2015RemoteDlg::SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m)
{
BYTE buf[sizeof(MasterSettings) + 1] = { CMD_MASTERSETTING };
memcpy(buf+1, &m, sizeof(MasterSettings));
if (ctx) {
ctx->Send2Client(buf, sizeof(buf));
return ctx->Send2Client(buf, sizeof(buf));
} else {
EnterCriticalSection(&m_cs);
for (auto i = m_HostList.begin(); i != m_HostList.end(); ++i) {
@@ -6848,6 +6849,7 @@ void CMy2015RemoteDlg::SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSetti
ContextObject->Send2Client(buf, sizeof(buf));
}
LeaveCriticalSection(&m_cs);
return TRUE;
}
}

View File

@@ -229,7 +229,7 @@ public:
std::tuple<bool, bool, bool, bool> VerifyClientAuth(context* host, const std::string& sn,
const std::string& passcode, uint64_t hmac, const std::string& hmacV2, const std::string& ip,
const char* source = "AUTH");
void SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m);
BOOL SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m);
void SendFilesToClientV2(context* mainCtx, const std::vector<std::string>& files, const std::string& targetDir = "");
void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files,
uint64_t resumeTransferID, const std::map<uint32_t, uint64_t>& startOffsets, const std::string& targetDir = "");