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

@@ -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");
}