Compare commits

...

11 Commits

Author SHA1 Message Date
wdvipa
c0a7109816 perf: Android端帧采集和发送性能优化
- compressBitmap改为单次压缩,去掉多次重试循环节省CPU
- 帧队列清理策略简化:仅在队列满时丢弃1帧,减少画面跳跃
- SocketIO发送间隔从66ms降到50ms,提升帧率上限到20fps
2026-02-15 20:51:10 +08:00
wdvipa
56648697bb fix: 修复MediaProjection误判回退到无障碍截图问题
- 移除采集循环中所有自动回退到无障碍截图的逻辑
- 黑屏帧不再触发模式切换,改为发送缓存帧保持画面连续
- Surface失效时尝试重建VirtualDisplay而非直接回退
- MediaProjection为null时从Holder重新获取而非回退
- 禁止服务端captureMode指令触发switchToAccessibilityMode
- 无障碍截图模式统一固定3秒间隔(成功/失败/异常均等3秒)
2026-02-15 19:21:04 +08:00
wdvipa
92cfc10150 fix: 修复投屏权限频繁丢失的问题
- MainActivity.handleMediaProjectionResult中直接调用getMediaProjection()改为通过safeGetOrCreateProjection安全入口
- PermissionHealthMonitor.preparePermissionRefresh中禁止重新创建MediaProjection实例,改为仅刷新权限数据时间戳
- 根因:重复调用getMediaProjection()创建新实例会导致系统自动stop旧实例,触发onStop回调形成权限掉落死循环
- 清理PermissionHealthMonitor中的emoji符号
2026-02-15 19:08:23 +08:00
wdvipa
c63fbbd90f fix: MediaProjection权限丢失后无法回退无障碍截图和Web画面闪烁
- 采集循环每帧检测mediaProjection对象有效性,null时立即回退无障碍截图
- 统一fallbackToAccessibilityCapture方法,消除7处重复回退代码
- 无障碍截图失败后等待MIN_CAPTURE_INTERVAL剩余时间,避免截图间隔太短错误
- Web端canvas尺寸锁定策略:首帧锁定尺寸,后续帧统一缩放绘制,消除闪烁
2026-02-15 18:41:38 +08:00
wdvipa
4ac6078ca3 fix: NetworkManager连接超时机制重写
- 移除旧的30秒刚性超时(60次x500ms轮询),改为两阶段连接策略
- Phase 1: 10秒快速轮询(20次x500ms)检测即时连接
- Phase 2: 后台监控器观察Socket.IO内部重连最长2分钟
- 后台监控超时后触发forceReconnect而非宣告连接失败
- 重连机制增加冷却期(30秒)和指数退避
- 配置读取方法从assets读取server_config.json
- 所有日志移除emoji符号
- 完整重写消除之前的重复方法和缺失闭合括号问题
2026-02-15 18:18:23 +08:00
wdvipa
5b2ddd7730 fix: Socket.IO连接超时和日志刷屏修复
- NetworkManager重写为两阶段连接策略:Phase1快速10秒轮询 + Phase2后台监控(最长2分钟)
- Phase2与Socket.IO内置重连机制协作而非对抗,超时后触发forceReconnect
- 所有魔法数字提取为companion object常量
- ScreenCaptureManager: Socket.IO不可用日志降频(首次+每50次打印)
- 连接恢复时打印跳过帧数统计
- MainActivity: 清理残留emoji符号
2026-02-15 18:17:13 +08:00
wdvipa
aa516590c8 fix: Android Socket.IO ws协议导致设备永远离线
- server_config.json: ws://改为http://,符合Socket.IO v4规范
- SocketIOManager: 新增convertToSocketIoProtocol方法,自动将ws/wss转换为http/https
- connect方法: 连接前自动转换协议,config一致性检查也做协议归一化
- forceReconnect: IO.socket调用前也做协议转换
- handleServerUrlChange: URL验证扩展支持http/https/ws/wss四种协议
- 清理全文件emoji符号,替换为英文日志
2026-02-15 18:08:33 +08:00
wdvipa
466453a903 fix: 修复设备注册延迟导致中继报离线问题
- EVENT_CONNECT回调去掉0-2秒随机延迟,连接成功后立即注册
- 注册防重复间隔从3秒降到1秒,最大重试次数从10增加到30
- 注册超时从15秒降到10秒,更快重试
- 清理SocketIOManager日志中的emoji符号
2026-02-15 17:42:52 +08:00
wdvipa
1501067287 fix: 修复启动日志报错问题
- SocketIOManager: 连接错误日志添加降频机制(前3次每次打印,之后每10次打印一次),避免服务器不可达时日志刷屏
- SocketIOManager: 连接成功时重置connectionFailureCount计数器,确保降频逻辑正确
- MainActivity: MediaProjectionManager延迟初始化日志从ERROR降级为INFO,这是正常行为不应报错
2026-02-15 15:42:08 +08:00
wdvipa
cdc4606574 fix: 修复启动时三个应用层报错问题
- MediaProjectionManager初始化提前到onCreate开头,避免多个return路径跳过初始化导致后续null
- InputController剪贴板访问添加SecurityException防护,非前台时安全降级而非崩溃
- HuaweiAuthorizationHandler剪贴板访问添加异常捕获,防止ClipboardService拒绝访问
- SocketIO连接错误区分瞬态错误(xhr poll/timeout)和持久错误,瞬态错误降级为WARN
- SocketIO connect方法添加URL格式验证和空值检查
2026-02-15 15:40:55 +08:00
wdvipa
410219f382 fix: 修复Bitmap并发回收闪退和缓存帧竞态问题
- lastValidBitmap/lastCaptureTime添加@Volatile确保多协程可见性
- safeRecycleLastValidBitmap()添加@Synchronized防止并发double-recycle
- 新增updateLastValidBitmap()原子替换缓存帧(synchronized)
- 新增compressCachedFrame()安全压缩缓存帧(synchronized)
- 新增safeCopyLastValidBitmap()安全复制缓存帧(synchronized)
- 替换所有直接访问lastValidBitmap的代码为synchronized方法调用
- 涉及方法: startMediaProjectionCapture/startAccessibilityScreenCapture/handleAndroid11ScreenshotFailure/captureWithMediaProjection/forceRefreshAndroid15Images
- 清理MainActivity和SocketIOManager中日志的emoji符号
2026-02-15 14:57:38 +08:00
8 changed files with 1820 additions and 1833 deletions

View File

@@ -1,5 +1,5 @@
{
"serverUrl": "ws://192.168.0.105:3001",
"serverUrl": "ws://192.168.0.103:3001",
"webUrl": "https://yhdm.one",
"buildTime": "2025-09-09T11:45:57.889Z",
"version": "1.0.1.6",

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,23 @@ class InputController(private val service: AccessibilityRemoteService) {
private val context: Context = service
private val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
/**
* Safe clipboard write - checks if the service can access clipboard
* Android 10+ restricts clipboard access to foreground apps only
*/
private fun safeSetClipboard(clipData: ClipData): Boolean {
return try {
clipboardManager.setPrimaryClip(clipData)
true
} catch (e: SecurityException) {
Log.w(TAG, "Clipboard access denied (app not in foreground): ${e.message}")
false
} catch (e: Exception) {
Log.w(TAG, "Clipboard operation failed: ${e.message}")
false
}
}
/**
* 输入文本 - 智能输入策略
*/
@@ -118,11 +135,14 @@ class InputController(private val service: AccessibilityRemoteService) {
*/
private fun tryClipboardPaste(text: String): Boolean {
return try {
// 1. 将文本复制到剪贴板
// 1. Safe clipboard write
val clipData = ClipData.newPlainText("remote_input", text)
clipboardManager.setPrimaryClip(clipData)
if (!safeSetClipboard(clipData)) {
Log.w(TAG, "Clipboard paste skipped: clipboard access denied")
return false
}
// 2. 模拟粘贴操作
// 2. Perform paste action
val focusedNode = findFocusedInputNode()
focusedNode?.let { node ->
val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
@@ -131,7 +151,7 @@ class InputController(private val service: AccessibilityRemoteService) {
} ?: false
} catch (e: Exception) {
Log.w(TAG, "剪贴板粘贴失败", e)
Log.w(TAG, "Clipboard paste failed", e)
false
}
}
@@ -189,29 +209,30 @@ class InputController(private val service: AccessibilityRemoteService) {
return try {
val focusedNode = findFocusedInputNode()
focusedNode?.let { node ->
// 获取当前文本
val currentText = node.text?.toString() ?: ""
// 追加新字符
val newText = currentText + char
// 将新文本复制到剪贴板
// Safe clipboard write
val clipData = ClipData.newPlainText("remote_append", newText)
clipboardManager.setPrimaryClip(clipData)
if (!safeSetClipboard(clipData)) {
Log.w(TAG, "Clipboard append skipped: clipboard access denied")
node.recycle()
return false
}
// 先清空再粘贴
// Clear then paste
val clearBundle = Bundle().apply {
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "")
}
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, clearBundle)
// 粘贴新内容
val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
node.recycle()
result
} ?: false
} catch (e: Exception) {
Log.w(TAG, "剪贴板追加失败", e)
Log.w(TAG, "Clipboard append failed", e)
false
}
}

View File

@@ -52,9 +52,9 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
private var imageReader: android.media.ImageReader? = null
private var mediaProjection: android.media.projection.MediaProjection? = null
// 图像缓存机制
private var lastValidBitmap: Bitmap? = null
private var lastCaptureTime = 0L
// 图像缓存机制 - @Volatile确保多协程可见性
@Volatile private var lastValidBitmap: Bitmap? = null
@Volatile private var lastCaptureTime = 0L
private var lastScreenshotTime = 0L // 新增:记录上次截图时间,防止截图间隔太短
// 状态跟踪
@@ -159,12 +159,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
/**
* 安全回收lastValidBitmap防止多线程并发导致的native crash (SIGSEGV)
*
* 问题根因多个协程startMediaProjectionCapture、startAccessibilityScreenCapture、
* captureWithMediaProjection等并发访问lastValidBitmap可能导致
* 1. double-recycleBitmap已被其他线程recycle但引用未置null
* 2. Hardware Bitmap的底层HardwareBuffer已被释放
* 两种情况都会在native层BitmapWrapper::freePixels()触发SIGSEGV
* 使用synchronized确保原子性先置null再recycle
* 防止两个协程同时拿到同一个引用并double-recycle
*/
@Synchronized
private fun safeRecycleLastValidBitmap() {
val bitmapToRecycle = lastValidBitmap
lastValidBitmap = null // 先置null防止其他线程访问
@@ -178,6 +176,63 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
}
}
/**
* 安全更新lastValidBitmap缓存
* synchronized确保与safeRecycleLastValidBitmap互斥
*/
@Synchronized
private fun updateLastValidBitmap(newBitmap: Bitmap) {
val old = lastValidBitmap
lastValidBitmap = newBitmap
trackBitmap(newBitmap)
lastCaptureTime = System.currentTimeMillis()
// 回收旧的缓存
try {
if (old != null && !old.isRecycled) {
old.recycle()
}
} catch (e: Exception) {
Log.w(TAG, "回收旧缓存Bitmap异常: ${e.message}")
}
}
/**
* 安全压缩缓存帧并返回JPEG数据
* synchronized确保读取期间lastValidBitmap不被回收
* @param maxAge 缓存最大有效期(ms)
* @return JPEG数据缓存无效时返回null
*/
@Synchronized
private fun compressCachedFrame(maxAge: Long): ByteArray? {
val cached = lastValidBitmap ?: return null
if (cached.isRecycled) return null
val age = System.currentTimeMillis() - lastCaptureTime
if (age > maxAge) return null
return try {
compressBitmap(cached)
} catch (e: Exception) {
Log.w(TAG, "压缩缓存帧失败: ${e.message}")
null
}
}
/**
* 安全复制lastValidBitmap
* synchronized确保复制期间不被其他线程回收
* @return Bitmap副本缓存无效时返回null
*/
@Synchronized
private fun safeCopyLastValidBitmap(): Bitmap? {
val cached = lastValidBitmap ?: return null
if (cached.isRecycled) return null
return try {
cached.copy(cached.config ?: Bitmap.Config.ARGB_8888, false)
} catch (e: Exception) {
Log.w(TAG, "复制缓存Bitmap失败: ${e.message}")
null
}
}
/**
* 安全回收任意Bitmap防止native crash
*/
@@ -262,21 +317,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
/**
* 切换到无障碍截图模式(由服务端指令触发)
* 停止当前采集,切换到 AccessibilityService.takeScreenshot
* 已禁用:不再允许服务端黑帧检测触发模式切换,避免误判导致权限回退
*/
fun switchToAccessibilityMode() {
if (useAccessibilityScreenshot) {
Log.d(TAG, "已经在无障碍截图模式,跳过切换")
return
}
Log.i(TAG, "切换到无障碍截图模式")
// 停止当前采集
stopCapture()
// 清理 MediaProjection 资源
cleanupVirtualDisplayOnly()
// 启用无障碍截图并重新开始
enableAccessibilityScreenshotMode()
startCapture()
Log.i(TAG, "收到切换无障碍截图模式指令,已忽略(禁止服务端触发模式切换)")
}
/**
@@ -380,8 +424,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
} else {
// MediaProjection 不可用,回退到无障碍截图
Log.i(TAG, "Android 11+MediaProjection 不可用,回退到无障碍截图模式")
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
fallbackToAccessibilityCapture()
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Log.i(TAG, "Android 5-10使用 MediaProjection VirtualDisplay 连续流式捕获")
@@ -409,13 +452,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
// 先确保 MediaProjection 可用
if (!ensureMediaProjection()) {
Log.e(TAG, "MediaProjection 不可用,回退到无障碍截图")
// 回退到无障碍截图作为兜底
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
fallbackToAccessibilityCapture()
return@launch
}
@@ -426,12 +463,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
if (virtualDisplay == null) {
Log.e(TAG, "VirtualDisplay 创建失败,回退到无障碍截图")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
fallbackToAccessibilityCapture()
return@launch
}
@@ -440,29 +472,47 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
var consecutiveFailures = 0
virtualDisplayRebuildCount = 0
// 最小有效帧大小阈值正常480×854 JPEG即使最低质量也>5KB
// 最小有效帧大小阈值正常480x854 JPEG即使最低质量也>5KB
// 低于此值的帧几乎肯定是黑屏/空白帧VirtualDisplay未刷新
val MIN_VALID_FRAME_SIZE = 5 * 1024 // 5KB
// 连续黑屏帧计数,用于判断是否应该切换到无障碍截图
// 连续黑屏帧计数 - 不再用于触发回退,仅用于日志统计
var consecutiveBlackFrames = 0
val MAX_BLACK_FRAMES_BEFORE_FALLBACK = 30 // 连续30个黑屏帧后切换到无障碍截图
while (isCapturing) {
try {
// Surface有效性检测BufferQueue被系统abandoned后立即回退
// 防止acquireLatestImage()持续返回null触发无限重建循环
// MediaProjection对象有效性检测
// 如果本地引用为null尝试从Holder重新获取
if (mediaProjection == null) {
Log.w(TAG, "MediaProjection本地引用为null尝试从Holder重新获取")
mediaProjection = MediaProjectionHolder.getMediaProjection()
if (mediaProjection == null) {
Log.w(TAG, "Holder中也无有效MediaProjection等待权限恢复发送缓存帧")
val cachedJpeg = compressCachedFrame(30000)
if (cachedJpeg != null) {
sendFrameToServer(cachedJpeg)
}
delay(3000)
continue
}
Log.i(TAG, "从Holder重新获取到MediaProjection继续采集")
}
// Surface有效性检测BufferQueue被系统abandoned后尝试重建
val currentSurface = imageReader?.surface
if (currentSurface == null || !currentSurface.isValid) {
Log.w(TAG, "ImageReader Surface已失效(null=${currentSurface == null}, isValid=${currentSurface?.isValid}), 停止采集并回退到无障碍截图")
Log.w(TAG, "ImageReader Surface已失效, 尝试重建VirtualDisplay")
cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
delay(500)
if (ensureMediaProjection()) {
setupMediaProjectionResources()
}
return@launch
val rebuiltSurface = imageReader?.surface
if (rebuiltSurface == null || !rebuiltSurface.isValid) {
Log.w(TAG, "Surface重建失败等待下次循环重试")
}
delay(1000)
continue
}
// 安全获取Image防止maxImages溢出
@@ -484,49 +534,27 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
// 有效帧:发送并更新缓存
sendFrameToServer(jpegData)
safeRecycleLastValidBitmap()
lastValidBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
lastValidBitmap?.let { trackBitmap(it) }
lastCaptureTime = System.currentTimeMillis()
// 安全更新缓存帧
val copy = bitmap.copy(Bitmap.Config.ARGB_8888, false)
if (copy != null) {
updateLastValidBitmap(copy)
}
consecutiveFailures = 0
consecutiveBlackFrames = 0
Log.d(TAG, "MediaProjection 有效帧: ${jpegData.size} bytes")
} else {
// 疑似黑屏帧
// 疑似黑屏帧,仅记录日志,不触发回退
// MediaProjection仍在工作黑屏帧可能是短暂的屏幕状态变化
consecutiveBlackFrames++
consecutiveFailures++
Log.w(TAG, "黑屏帧(${jpegData.size}B < ${MIN_VALID_FRAME_SIZE}B)连续${consecutiveBlackFrames}")
// 连续黑屏帧过多MediaProjection在此设备上不可靠切换到无障碍截图
if (consecutiveBlackFrames >= MAX_BLACK_FRAMES_BEFORE_FALLBACK) {
Log.w(TAG, "MediaProjection连续${consecutiveBlackFrames}个黑屏帧,切换到无障碍截图模式")
safeRecycleBitmap(bitmap)
cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch
if (consecutiveBlackFrames % 50 == 1) {
Log.w(TAG, "黑屏帧(${jpegData.size}B < ${MIN_VALID_FRAME_SIZE}B), 连续${consecutiveBlackFrames}, 发送缓存帧")
}
// Send cached valid frame to maintain continuity (if available)
val cachedBmp = lastValidBitmap
if (cachedBmp != null && !cachedBmp.isRecycled) {
val cacheAge = System.currentTimeMillis() - lastCaptureTime
if (cacheAge < 10000) {
try {
val cachedJpeg = compressBitmap(cachedBmp)
if (cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg)
Log.d(TAG, "Used cached frame instead of black frame (${cacheAge}ms ago)")
}
} catch (e: Exception) {
Log.w(TAG, "Failed to compress cached frame: ${e.message}")
}
}
// 发送缓存的有效帧保持画面连续性
val cachedJpeg = compressCachedFrame(10000)
if (cachedJpeg != null && cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg)
}
}
@@ -539,20 +567,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
consecutiveFailures++
// Send cached frame when no new frame available
val cachedBmp2 = lastValidBitmap
if (cachedBmp2 != null && !cachedBmp2.isRecycled) {
val cacheAge = System.currentTimeMillis() - lastCaptureTime
if (cacheAge < 10000) {
try {
val cachedJpeg = compressBitmap(cachedBmp2)
if (cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg)
Log.d(TAG, "No new frame, sent cached frame (${cacheAge}ms ago)")
}
} catch (e: Exception) {
Log.w(TAG, "Failed to compress cached frame: ${e.message}")
}
}
val cachedJpeg2 = compressCachedFrame(10000)
if (cachedJpeg2 != null && cachedJpeg2.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg2)
Log.d(TAG, "No new frame, sent cached frame")
}
// 连续失败过多,尝试重建 VirtualDisplay
@@ -560,34 +578,24 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
virtualDisplayRebuildCount++
Log.w(TAG, "连续 ${consecutiveFailures} 次无法获取有效帧,重建 VirtualDisplay (${virtualDisplayRebuildCount}/${MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS})")
// 重建次数超限,回退到无障碍截图
if (virtualDisplayRebuildCount > MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS) {
Log.w(TAG, "重建次数已达上限,回退到无障碍截图")
cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch
}
cleanupVirtualDisplayOnly()
delay(500)
// 重新检查MediaProjection是否可用
if (!ensureMediaProjection()) {
Log.w(TAG, "MediaProjection不可用等待恢复继续发送缓存帧")
consecutiveFailures = 0
continue
}
setupMediaProjectionResources()
consecutiveFailures = 0
// 重建后验证VirtualDisplay和Surface有效性
val rebuiltSurface = imageReader?.surface
if (virtualDisplay == null || rebuiltSurface == null || !rebuiltSurface.isValid) {
Log.w(TAG, "VirtualDisplay 重建后无效(vd=${virtualDisplay != null}, surface=${rebuiltSurface != null}, valid=${rebuiltSurface?.isValid}), 回退到无障碍截图")
cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
}
return@launch
Log.w(TAG, "VirtualDisplay 重建后无效,等待下次重试")
// 不回退,继续循环,下次会再尝试重建
}
}
}
@@ -606,12 +614,25 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
}
}
/**
* 统一回退到无障碍截图的方法
* Android 11+ 使用无障碍截图,低版本使用测试图像兜底
*/
private fun fallbackToAccessibilityCapture() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
}
/**
* 确保 MediaProjection 可用
*
* 核心修复:禁止重复调用 getMediaProjection(resultCode, resultData)
* 每次调用都会创建新实例,系统会自动 stop 旧实例,触发 onStop 回调,
* 形成"权限丢失→恢复→再丢失"的死循环,这是权限频繁掉落的根因。
* 形成"权限丢失->恢复->再丢失"的死循环,这是权限频繁掉落的根因。
*
* 只从 MediaProjectionHolder 获取已有对象,不重新创建。
*/
@@ -662,12 +683,11 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
// 缓存成功的截图,用于防止闪烁
try {
// 清理旧的缓存
safeRecycleLastValidBitmap()
// 保存当前成功的截图副本
lastValidBitmap = screenshot.copy(screenshot.config ?: Bitmap.Config.ARGB_8888, false)
lastCaptureTime = System.currentTimeMillis()
Log.d(TAG, "已缓存有效截图用于防闪烁: ${lastValidBitmap?.width}x${lastValidBitmap?.height}")
val copy = screenshot.copy(screenshot.config ?: Bitmap.Config.ARGB_8888, false)
if (copy != null) {
updateLastValidBitmap(copy)
Log.d(TAG, "已缓存有效截图用于防闪烁: ${copy.width}x${copy.height}")
}
} catch (e: Exception) {
Log.w(TAG, "缓存有效截图失败", e)
}
@@ -692,11 +712,14 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
}
}
// 截图成功时按帧率延迟,失败时短延迟后立即重试
if (consecutiveFailures == 0) {
delay(1000 / dynamicFps.toLong())
// 无障碍截图统一使用固定3秒间隔不区分成功/失败
// 系统对无障碍截图有约3秒的最小间隔限制低于此值会报错
val elapsed = System.currentTimeMillis() - lastScreenshotTime
val remaining = MIN_CAPTURE_INTERVAL - elapsed
if (remaining > 0) {
delay(remaining)
} else {
delay(50) // 失败时50ms后重试让系统截图间隔限制自己控制节奏
delay(MIN_CAPTURE_INTERVAL)
}
} catch (e: CancellationException) {
@@ -704,7 +727,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
} catch (e: Exception) {
Log.w(TAG, "屏幕捕获失败: ${e.message}")
consecutiveFailures++
delay(100) // 出错时进一步缩短间隔保持流畅度从200ms改为100ms
delay(MIN_CAPTURE_INTERVAL) // 异常时也等待3秒再重试
}
}
}
@@ -719,32 +742,20 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
Log.d(TAG, "Android 11+设备:截图失败 (连续${android11ConsecutiveFailures}次)")
// Prefer cached last frame to avoid flicker
val cachedBmp = lastValidBitmap
if (cachedBmp != null && !cachedBmp.isRecycled) {
// 安全读取缓存帧synchronized保护
val cachedCopy = safeCopyLastValidBitmap()
if (cachedCopy != null) {
val cacheAge = currentTime - lastCaptureTime
// Cache is fresh (within 10s), use it directly
if (cacheAge < 10000) {
Log.d(TAG, "Android 11+ device: returning cached screenshot (${cacheAge}ms ago)")
return try {
cachedBmp.copy(cachedBmp.config ?: Bitmap.Config.ARGB_8888, false)
} catch (e: Exception) {
Log.w(TAG, "Failed to copy cached screenshot", e)
null
}
Log.d(TAG, "Android 11+: 返回缓存截图(${cacheAge}ms)")
return cachedCopy
}
// Cache is older but still usable (within 60s), use during short failures
if (cacheAge < 60000 && android11ConsecutiveFailures < 20) {
Log.d(TAG, "Android 11+ device: returning older cached screenshot (${cacheAge}ms ago)")
return try {
cachedBmp.copy(cachedBmp.config ?: Bitmap.Config.ARGB_8888, false)
} catch (e: Exception) {
Log.w(TAG, "Failed to copy cached screenshot", e)
null
}
Log.d(TAG, "Android 11+: 返回较旧缓存截图(${cacheAge}ms)")
return cachedCopy
}
// 缓存过旧回收copy
safeRecycleBitmap(cachedCopy)
}
// 无可用缓存或缓存过旧,智能判断是否应该进入测试模式
@@ -1038,10 +1049,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
consecutiveImageFailures = 0
}
safeRecycleLastValidBitmap()
lastValidBitmap = newBitmap.copy(Bitmap.Config.ARGB_8888, false)
lastValidBitmap?.let { trackBitmap(it) }
lastCaptureTime = currentTime
val copy = newBitmap.copy(Bitmap.Config.ARGB_8888, false)
if (copy != null) {
updateLastValidBitmap(copy)
}
return newBitmap
}
@@ -1063,19 +1074,15 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
}
// If no new image available, check if cache can be used
val cachedBmp3 = lastValidBitmap
if (cachedBmp3 != null && !cachedBmp3.isRecycled) {
val cachedCopy3 = safeCopyLastValidBitmap()
if (cachedCopy3 != null) {
val timeSinceLastCapture = currentTime - lastCaptureTime
if (timeSinceLastCapture < 30000) {
Log.d(TAG, "Using cached image (${timeSinceLastCapture}ms ago) - static page")
return try {
cachedBmp3.copy(Bitmap.Config.ARGB_8888, false)
} catch (e: Exception) {
Log.w(TAG, "Failed to copy cached bitmap: ${e.message}")
null
}
return cachedCopy3
} else {
Log.w(TAG, "Cached image expired (${timeSinceLastCapture}ms ago), cleaning")
safeRecycleBitmap(cachedCopy3)
safeRecycleLastValidBitmap()
}
}
@@ -1115,9 +1122,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
val retryBitmap = convertImageToBitmap(retryImage)
if (retryBitmap != null) {
Log.i(TAG, "Android 15重新初始化后成功获取图像")
safeRecycleLastValidBitmap()
lastValidBitmap = retryBitmap.copy(Bitmap.Config.ARGB_8888, false)
lastCaptureTime = System.currentTimeMillis()
val retryCopy = retryBitmap.copy(Bitmap.Config.ARGB_8888, false)
if (retryCopy != null) {
updateLastValidBitmap(retryCopy)
}
return retryBitmap
}
} catch (e: Exception) {
@@ -1643,7 +1651,7 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
}
/**
* 智能压缩Bitmap为JPEG - 动态调整质量以平衡画质和传输效
* 压缩Bitmap为JPEG - 单次压缩避免多次重试浪费CPU降低帧
*/
private fun compressBitmap(bitmap: Bitmap): ByteArray {
val outputStream = ByteArrayOutputStream()
@@ -1652,38 +1660,17 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
// 计算缩放比例,确保不超过最大尺寸
val scaledBitmap = scaleDownBitmap(bitmap)
// 智能压缩:根据数据大小动态调整质量
var quality = dynamicQuality
var compressedData: ByteArray
var attempts = 0
val maxAttempts = 3
val targetSize = 150 * 1024 // 目标150KB平衡质量和传输效率
do {
outputStream.reset()
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
compressedData = outputStream.toByteArray()
attempts++
if (compressedData.size > targetSize && attempts < maxAttempts) {
// 数据过大,降低质量重新压缩
quality = maxOf(25, quality - 15) // 最低不低于25
Log.v(TAG, "数据过大(${compressedData.size} bytes),降低质量到$quality 重新压缩")
} else {
break
}
} while (attempts < maxAttempts)
// 单次压缩:直接使用动态质量参数,不做多次重试
// 多次压缩会显著降低帧率每次重试都是完整的JPEG编码
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, dynamicQuality, outputStream)
// 如果缩放了释放缩放后的bitmap
if (scaledBitmap != bitmap) {
scaledBitmap.recycle()
}
Log.d(TAG, "智能压缩完成: 原始${bitmap.width}x${bitmap.height} -> 输出${compressedData.size} bytes (质量$quality, 尝试${attempts}次)")
} catch (e: Exception) {
Log.e(TAG, "智能压缩失败,使用基础压缩", e)
// 如果智能压缩失败,使用基础压缩
Log.e(TAG, "压缩失败,使用基础压缩", e)
outputStream.reset()
bitmap.compress(Bitmap.CompressFormat.JPEG, dynamicQuality, outputStream)
}
@@ -1731,24 +1718,12 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
// 启动队列处理器(只启动一次)
startQueueProcessor()
// 优化队列清理策略,减少画面跳跃,提供更平滑的体验
when {
screenDataQueue.size >= 13 -> {
// 队列接近满载适度清理2帧保持流畅避免5帧跳跃
Log.w(TAG, "队列接近满载(${screenDataQueue.size}/15),轻度清理旧数据保持流畅")
repeat(2) {
screenDataQueue.poll()?.let {
droppedFrameCount++
}
}
}
screenDataQueue.size >= 11 -> {
// 中等负载清理1帧防止积压
Log.v(TAG, "队列中等负载(${screenDataQueue.size}/15)清理1帧防止积压")
screenDataQueue.poll()?.let {
droppedFrameCount++
}
// 优化队列清理策略只在队列满时丢弃最旧1帧,减少画面跳跃
if (screenDataQueue.remainingCapacity() == 0) {
screenDataQueue.poll()?.let {
droppedFrameCount++
}
Log.v(TAG, "队列已满(15/15)丢弃最旧1帧保持流畅")
}
// 尝试添加新帧,如果满了就丢弃旧帧
@@ -1827,30 +1802,44 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
}
}
// Socket unavailable log throttle counter
private var socketUnavailableLogCount = 0
/**
* 处理单帧数据
* Process single frame data.
* Throttles "socket unavailable" log to avoid flooding logcat
* when connection is down but capture is still running.
*/
private suspend fun processFrameData(frameData: ByteArray) {
try {
var success = false
// 优先使用Socket.IO v4官方客户端发送屏幕数据
val socketIOManager = service.getSocketIOManager()
if (socketIOManager != null && socketIOManager.isConnected()) {
socketIOManager.sendScreenData(frameData)
Log.v(TAG, "Socket.IO v4发送帧数据: ${frameData.size} bytes")
Log.v(TAG, "Socket.IO sent frame: ${frameData.size} bytes")
success = true
// Reset throttle counter on success
if (socketUnavailableLogCount > 0) {
Log.i(TAG, "Socket.IO connection restored, " +
"skipped $socketUnavailableLogCount frames while disconnected")
socketUnavailableLogCount = 0
}
} else {
Log.w(TAG, "Socket.IO连接不可用无法发送屏幕数据")
socketUnavailableLogCount++
// Log first occurrence, then every 50th to avoid flooding
if (socketUnavailableLogCount == 1 || socketUnavailableLogCount % 50 == 0) {
Log.w(TAG, "Socket.IO unavailable, cannot send screen data " +
"(count=$socketUnavailableLogCount)")
}
}
// 记录成功发送时间
if (success) {
lastSuccessfulSendTime = System.currentTimeMillis()
}
} catch (e: Exception) {
Log.e(TAG, "处理帧数据失败", e)
Log.e(TAG, "Process frame data failed", e)
}
}
@@ -2615,10 +2604,11 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
if (bitmap != null) {
Log.i(TAG, "Android 15强制刷新成功${bitmap.width}x${bitmap.height}")
// 更新缓存
safeRecycleLastValidBitmap()
lastValidBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
lastCaptureTime = System.currentTimeMillis()
// 安全更新缓存
val copy = bitmap.copy(Bitmap.Config.ARGB_8888, false)
if (copy != null) {
updateLastValidBitmap(copy)
}
return bitmap
}

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,7 @@ class PermissionHealthMonitor(private val context: Context) {
return
}
Log.i(TAG, "🏥 启动权限健康监控服务")
Log.i(TAG, "启动权限健康监控服务")
isMonitoring.set(true)
val checkInterval = if (Build.VERSION.SDK_INT >= 35) {
@@ -67,13 +67,13 @@ class PermissionHealthMonitor(private val context: Context) {
performHealthCheck()
delay(checkInterval)
} catch (e: Exception) {
Log.e(TAG, "健康检查异常", e)
Log.e(TAG, "健康检查异常", e)
delay(checkInterval)
}
}
}
Log.i(TAG, "权限健康监控已启动,检查间隔: ${checkInterval / 1000}")
Log.i(TAG, "权限健康监控已启动,检查间隔: ${checkInterval / 1000}")
}
/**
@@ -85,7 +85,7 @@ class PermissionHealthMonitor(private val context: Context) {
return
}
Log.i(TAG, "🛑 停止权限健康监控服务")
Log.i(TAG, "停止权限健康监控服务")
isMonitoring.set(false)
healthCheckJob?.cancel()
@@ -100,9 +100,9 @@ class PermissionHealthMonitor(private val context: Context) {
try {
totalChecks++
Log.v(TAG, "🔍 执行第${totalChecks}次权限健康检查")
Log.v(TAG, "执行第${totalChecks}次权限健康检查")
// 🔧 优化权限检查逻辑,避免误报
// 优化权限检查逻辑,避免误报
val smartManager = SmartMediaProjectionManager.getInstance(context)
val hasMediaProjection = smartManager.getCurrentMediaProjection() != null
val hasPermissionData = MediaProjectionHolder.getPermissionData() != null
@@ -111,7 +111,7 @@ class PermissionHealthMonitor(private val context: Context) {
// 获取权限统计信息
val stats = MediaProjectionHolder.getPermissionStats()
// 🔧 更智能的健康判断Android 15设备更宽松的判断标准
// 更智能的健康判断Android 15设备更宽松的判断标准
val isHealthy = if (android.os.Build.VERSION.SDK_INT >= 35) {
// Android 15只要有权限数据且数据有效就认为是健康的
hasPermissionData && permissionDataValid
@@ -120,20 +120,20 @@ class PermissionHealthMonitor(private val context: Context) {
hasMediaProjection || (hasPermissionData && permissionDataValid)
}
Log.v(TAG, "🔍 权限健康检查: MediaProjection=$hasMediaProjection, 权限数据=$hasPermissionData, 数据有效=$permissionDataValid, 总体健康=$isHealthy")
Log.v(TAG, "权限健康检查: MediaProjection=$hasMediaProjection, 权限数据=$hasPermissionData, 数据有效=$permissionDataValid, 总体健康=$isHealthy")
if (isHealthy) {
// 权限状态正常
healthyChecks++
handleHealthyState(stats)
} else {
// 🚨 只有在确实没有任何权限时才认为是问题
Log.w(TAG, "⚠️ 确认权限问题 - MediaProjection=$hasMediaProjection, 权限数据=$hasPermissionData, 数据有效=$permissionDataValid")
// 只有在确实没有任何权限时才认为是问题
Log.w(TAG, "确认权限问题 - MediaProjection=$hasMediaProjection, 权限数据=$hasPermissionData, 数据有效=$permissionDataValid")
handleUnhealthyState(hasMediaProjection, permissionDataValid, stats)
}
} catch (e: Exception) {
Log.e(TAG, "健康检查执行失败", e)
Log.e(TAG, "健康检查执行失败", e)
}
}
@@ -141,7 +141,7 @@ class PermissionHealthMonitor(private val context: Context) {
* 处理健康状态
*/
private fun handleHealthyState(stats: Map<String, Any>) {
Log.v(TAG, "权限状态健康")
Log.v(TAG, "权限状态健康")
// Android 15设备进行额外的稳定性检查
if (Build.VERSION.SDK_INT >= 35) {
@@ -149,7 +149,7 @@ class PermissionHealthMonitor(private val context: Context) {
val maxAge = 2 * 60 * 60 * 1000L // 2小时
if (permissionAge > maxAge * 0.8) { // 权限年龄超过80%
Log.w(TAG, "⚠️ Android 15权限即将过期提前准备更新")
Log.w(TAG, "Android 15权限即将过期提前准备更新")
preparePermissionRefresh()
}
}
@@ -163,14 +163,14 @@ class PermissionHealthMonitor(private val context: Context) {
permissionDataValid: Boolean,
stats: Map<String, Any>
) {
Log.w(TAG, "⚠️ 检测到权限问题 - 有效权限: $hasValidPermission, 数据有效: $permissionDataValid")
Log.w(TAG, "检测到权限问题 - 有效权限: $hasValidPermission, 数据有效: $permissionDataValid")
// 添加保护性检查:确认权限确实丢失
// 添加保护性检查:确认权限确实丢失
val permissionData = MediaProjectionHolder.getPermissionData()
val hasMediaProjection = MediaProjectionHolder.getMediaProjection() != null
if (permissionData != null) {
Log.i(TAG, "🛡️ 权限数据仍然存在,可能是误报,不计入权限丢失事件")
Log.i(TAG, "权限数据仍然存在,可能是误报,不计入权限丢失事件")
return
}
@@ -179,17 +179,17 @@ class PermissionHealthMonitor(private val context: Context) {
// 尝试智能恢复
if (attemptSmartRecovery()) {
successfulRecoveries++
Log.i(TAG, "智能恢复成功")
Log.i(TAG, "智能恢复成功")
} else {
failedRecoveries++
Log.w(TAG, "智能恢复失败")
Log.w(TAG, "智能恢复失败")
// 只有在恢复失败次数真的很多时才发送权限异常通知
// 只有在恢复失败次数真的很多时才发送权限异常通知
if (failedRecoveries >= 5) { // 提高阈值从 3 到 5
Log.w(TAG, "⚠️ 连续恢复失败次数过多($failedRecoveries),发送权限异常通知")
Log.w(TAG, "连续恢复失败次数过多($failedRecoveries),发送权限异常通知")
notifyPermissionIssue()
} else {
Log.i(TAG, "🔄 恢复失败次数尚可接受($failedRecoveries),继续监控")
Log.i(TAG, "恢复失败次数尚可接受($failedRecoveries),继续监控")
}
}
}
@@ -199,13 +199,13 @@ class PermissionHealthMonitor(private val context: Context) {
*/
private suspend fun attemptSmartRecovery(): Boolean {
return try {
Log.i(TAG, "🔧 尝试智能权限恢复")
Log.i(TAG, "尝试智能权限恢复")
// 使用MediaProjectionHolder的智能恢复
val recovered = MediaProjectionHolder.attemptSmartRecovery()
if (recovered != null) {
Log.i(TAG, "智能恢复成功")
Log.i(TAG, "智能恢复成功")
// 通知AccessibilityService权限已恢复
val intent = Intent("android.mycustrecev.PERMISSION_HEALTH_RECOVERED")
@@ -213,40 +213,41 @@ class PermissionHealthMonitor(private val context: Context) {
true
} else {
Log.w(TAG, "智能恢复失败")
Log.w(TAG, "智能恢复失败")
false
}
} catch (e: Exception) {
Log.e(TAG, "智能恢复异常", e)
Log.e(TAG, "智能恢复异常", e)
false
}
}
/**
* 准备权限刷新Android 15预防性措施
*
* 禁止调用 setMediaProjection() 重新创建实例!
* 重新创建会导致系统 stop 旧实例,触发 onStop 回调形成权限掉落死循环。
* 只刷新权限数据的时间戳,延长有效期。
*/
private fun preparePermissionRefresh() {
monitorScope.launch {
try {
Log.i(TAG, "🔄 Android 15权限预防性刷新")
val smartManager = SmartMediaProjectionManager.getInstance(context)
val permissionData = MediaProjectionHolder.getPermissionData()
if (permissionData != null) {
val (resultCode, resultData) = permissionData
if (resultData != null) {
// 重新设置权限,刷新时间戳
if (smartManager.setMediaProjection(resultCode, resultData)) {
Log.i(TAG, "✅ 权限预防性刷新成功")
}
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ 权限预防性刷新失败", e)
try {
Log.i(TAG, "Android 15权限预防性刷新仅刷新时间戳")
val permissionData = MediaProjectionHolder.getPermissionData()
val hasMediaProjection = MediaProjectionHolder.getMediaProjection() != null
if (permissionData != null && hasMediaProjection) {
// 只刷新权限数据时间戳,不重新创建 MediaProjection 实例
val (resultCode, resultData) = permissionData
MediaProjectionHolder.setPermissionData(resultCode, resultData)
Log.i(TAG, "权限时间戳已刷新,避免过期")
} else {
Log.w(TAG, "权限数据或对象不存在,跳过刷新")
}
} catch (e: Exception) {
Log.e(TAG, "权限预防性刷新失败", e)
}
}
@@ -265,10 +266,10 @@ class PermissionHealthMonitor(private val context: Context) {
}
context.sendBroadcast(intent)
Log.w(TAG, "📡 已发送权限健康问题通知")
Log.w(TAG, "已发送权限健康问题通知")
} catch (e: Exception) {
Log.e(TAG, "发送权限问题通知失败", e)
Log.e(TAG, "发送权限问题通知失败", e)
}
}
@@ -309,7 +310,7 @@ class PermissionHealthMonitor(private val context: Context) {
val stats = getHealthStatistics()
Log.i(TAG, """
📊 权限健康监控统计报告:
权限健康监控统计报告:
========================================
• 监控状态: ${if (stats["isMonitoring"] as Boolean) "运行中" else "已停止"}
• 总检查次数: ${stats["totalChecks"]}
@@ -330,11 +331,11 @@ class PermissionHealthMonitor(private val context: Context) {
*/
fun manualHealthCheck() {
if (!isMonitoring.get()) {
Log.w(TAG, "⚠️ 监控服务未运行,无法执行手动检查")
Log.w(TAG, "监控服务未运行,无法执行手动检查")
return
}
Log.i(TAG, "🔍 执行手动健康检查")
Log.i(TAG, "执行手动健康检查")
monitorScope.launch {
performHealthCheck()
}
@@ -349,7 +350,7 @@ class PermissionHealthMonitor(private val context: Context) {
permissionLossEvents = 0
successfulRecoveries = 0
failedRecoveries = 0
Log.i(TAG, "🔄 健康统计信息已重置")
Log.i(TAG, "健康统计信息已重置")
}
/**
@@ -359,6 +360,6 @@ class PermissionHealthMonitor(private val context: Context) {
stopMonitoring()
monitorScope.cancel()
instance = null
Log.i(TAG, "🧹 权限健康监控服务已清理")
Log.i(TAG, "权限健康监控服务已清理")
}
}

View File

@@ -958,14 +958,22 @@ class HuaweiAuthorizationHandler(
* 尝试输入搜索文本(使用剪贴板粘贴)
*/
private fun tryInputSearchText(searchText: String = "悬浮窗"): Boolean {
Log.d(TAG, "🔍 尝试通过剪贴板粘贴搜索文本")
Log.d(TAG, "尝试通过剪贴板粘贴搜索文本")
try {
// 1. 将"悬浮窗"复制到剪贴板
// 1. Safe clipboard write - may fail if app is not in foreground
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
val clipData = android.content.ClipData.newPlainText("search_text", searchText)
clipboardManager.setPrimaryClip(clipData)
Log.d(TAG, "✅ 已将'${searchText}'复制到剪贴板")
try {
clipboardManager.setPrimaryClip(clipData)
Log.d(TAG, "已将'${searchText}'复制到剪贴板")
} catch (e: SecurityException) {
Log.w(TAG, "剪贴板访问被拒绝(非前台应用): ${e.message}")
return false
} catch (e: Exception) {
Log.w(TAG, "剪贴板操作失败: ${e.message}")
return false
}
// 2. 等待一下确保剪贴板操作完成

View File

@@ -1,23 +1,21 @@
package com.hikoncont.service.modules
import android.content.Context
import android.os.Build
import android.util.Log
import com.hikoncont.network.SocketIOManager
import com.hikoncont.service.AccessibilityRemoteService
import kotlinx.coroutines.*
import java.io.InputStreamReader
import org.json.JSONObject
/**
* 网络管理器
* 负责网络通信管理
*
* 主要职责:
* - SocketIO连接管理
* - 连接状态监控
* - 自动重连机制
* - 服务器配置读取
* - 网络异常处理
* Network manager - manages Socket.IO connection lifecycle.
*
* Socket.IO client has its own internal reconnect mechanism.
* NetworkManager cooperates with it rather than fighting it:
* 1. Phase 1 (fast poll): Quick 10s check for immediate connections
* 2. Phase 2 (background monitor): If Phase 1 times out, monitor
* in background while Socket.IO keeps retrying internally
* 3. Only trigger full reconnect if background monitor times out (2min)
*/
class NetworkManager(
private val service: AccessibilityRemoteService,
@@ -25,483 +23,422 @@ class NetworkManager(
) {
companion object {
private const val TAG = "NetworkManager"
private const val INITIAL_CHECK_INTERVAL_MS = 500L
private const val INITIAL_CHECK_MAX_ATTEMPTS = 20 // 10s
private const val BG_MONITOR_INTERVAL_MS = 3000L
private const val BG_MONITOR_MAX_DURATION_MS = 120_000L // 2min
private const val RECONNECT_INTERVAL_MS = 5000L
private const val MAX_RECONNECT_ATTEMPTS = 10
private const val RECONNECT_COOLDOWN_MS = 30_000L
private const val CONFIG_FILE_SERVER = "server_config.json"
}
// 网络连接管理器
private lateinit var socketIOManager: SocketIOManager
// 协程作用域
private val networkScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 连接状态
@Volatile private var isConnected = false
@Volatile private var isConnecting = false
@Volatile private var lastConnectionAttempt = 0L
// 重连配置
private val connectionTimeout = 30000L // 30秒连接超时
private val reconnectInterval = 5000L // 5秒重连间隔
private val maxReconnectAttempts = 10 // 最大重连次数
private var backgroundMonitorJob: Job? = null
private var reconnectAttempts = 0
private var lastReconnectTime = 0L
/**
* 初始化网络管理器
* Initialize NetworkManager and create SocketIOManager instance.
*/
fun initialize() {
try {
Log.i(TAG, "🌐 初始化网络管理器 (单一Socket.IO连接)")
Log.i(TAG, "Initializing NetworkManager (Socket.IO)")
socketIOManager = SocketIOManager(service)
Log.i(TAG, "✅ 网络管理器初始化完成 (Socket.IO)")
Log.i(TAG, "NetworkManager initialized")
} catch (e: Exception) {
Log.e(TAG, "❌ 网络管理器初始化失败", e)
Log.e(TAG, "NetworkManager init failed", e)
}
}
/**
* 连接到服务器
* Connect to server with two-phase approach:
* Phase 1: Fast poll for 10s
* Phase 2: Background monitor for up to 2min
*/
suspend fun connectToServer() {
if (isConnecting) {
Log.d(TAG, "⚠️ 连接正在进行中,跳过重复连接")
Log.d(TAG, "Connection in progress, skip duplicate")
return
}
// ✅ 检查是否已经连接,避免重复连接
if (isConnected()) {
Log.d(TAG, "✅ 服务器已连接,跳过重复连接")
Log.d(TAG, "Already connected, skip")
return
}
try {
isConnecting = true
lastConnectionAttempt = System.currentTimeMillis()
val serverUrl = getServerUrl()
isConnected = false
Log.i(TAG, "🚀 开始连接到服务器: $serverUrl")
Log.d(TAG, "🔍 服务器URL调试: 长度=${serverUrl.length}, 内容='$serverUrl'")
// 统一使用初始化延迟确保Android 11+设备稳定性
Log.i(TAG, "🔧 初始化延迟优化")
delay(2000)
// 使用Socket.IO连接
if (trySocketIOConnection(serverUrl)) {
isConnected = true
reconnectAttempts = 0
Log.i(TAG, "✅ Socket.IO连接成功")
if (serverUrl.isNullOrBlank()) {
Log.e(TAG, "Server URL is empty, cannot connect")
return
}
Log.w(TAG, "⚠️ Socket.IO连接失败")
isConnected = false
Log.i(TAG, "Connecting to server: $serverUrl")
// Brief delay for SocketIOManager setup
delay(1000)
try {
socketIOManager.connect(serverUrl)
} catch (e: Exception) {
Log.e(TAG, "Socket.IO connect call failed: ${e.message}")
}
// Phase 1: Fast poll (10s)
if (pollForConnection(INITIAL_CHECK_MAX_ATTEMPTS, INITIAL_CHECK_INTERVAL_MS)) {
isConnected = true
reconnectAttempts = 0
Log.i(TAG, "Socket.IO connected (Phase 1, fast poll)")
return
}
// Phase 2: Background monitor
Log.i(TAG, "Phase 1 (10s) did not connect, starting background monitor")
startBackgroundMonitor()
} catch (e: Exception) {
Log.e(TAG, "❌ 连接到服务器失败", e)
Log.e(TAG, "Connect to server failed: ${e.message}", e)
isConnected = false
} finally {
isConnecting = false
}
}
/**
* 尝试Socket.IO连接
* Poll Socket.IO connection status at fixed intervals.
* @return true if connected within the polling window
*/
private suspend fun trySocketIOConnection(serverUrl: String): Boolean {
return try {
Log.i(TAG, "🚀 尝试使用Socket.IO v4官方客户端连接")
socketIOManager.connect(serverUrl)
val maxAttempts = 60 // 统一使用60次尝试确保Android 11+设备连接稳定性
val checkInterval = 500L
Log.i(TAG, "🔧 SocketIO连接检测配置: ${maxAttempts}次尝试, ${checkInterval}ms间隔")
repeat(maxAttempts) { attempt ->
delay(checkInterval)
if (socketIOManager.isConnected()) {
Log.i(TAG, "✅ Socket.IO v4连接成功: $serverUrl (${(attempt + 1) * checkInterval}ms)")
return true
private suspend fun pollForConnection(maxAttempts: Int, intervalMs: Long): Boolean {
repeat(maxAttempts) { attempt ->
delay(intervalMs)
if (isSocketIOReady()) {
val elapsedMs = (attempt + 1) * intervalMs
Log.i(TAG, "Socket.IO connected after ${elapsedMs}ms")
return true
}
}
return false
}
/**
* Background monitor: watches Socket.IO internal reconnect for up to 2min.
* If connected during this window, updates state.
* If timeout, triggers force reconnect.
*/
private fun startBackgroundMonitor() {
backgroundMonitorJob?.cancel()
backgroundMonitorJob = networkScope.launch {
val startTime = System.currentTimeMillis()
var checkCount = 0
while (isActive) {
delay(BG_MONITOR_INTERVAL_MS)
checkCount++
try {
if (isSocketIOReady()) {
isConnected = true
reconnectAttempts = 0
val sec = (System.currentTimeMillis() - startTime) / 1000
Log.i(TAG, "Background monitor: connected after ${sec}s")
return@launch
}
val elapsed = System.currentTimeMillis() - startTime
if (elapsed > BG_MONITOR_MAX_DURATION_MS) {
Log.w(TAG, "Background monitor timeout (${elapsed / 1000}s), force reconnect")
triggerForceReconnect()
return@launch
}
if (checkCount % 10 == 0) {
Log.d(TAG, "Background monitor: waiting (${elapsed / 1000}s)")
}
} catch (e: Exception) {
Log.e(TAG, "Background monitor check failed: ${e.message}")
}
}
val totalTime = maxAttempts * checkInterval / 1000
Log.w(TAG, "⚠️ Socket.IO v4连接超时(${totalTime}秒)")
false
} catch (e: Exception) {
Log.w(TAG, "⚠️ Socket.IO v4连接失败: ${e.message}")
false
}
}
/**
* 重新连接到服务器
* Reconnect to server with cooldown and attempt limits.
*/
fun reconnectToServer() {
val now = System.currentTimeMillis()
if (now - lastReconnectTime < RECONNECT_COOLDOWN_MS) {
Log.d(TAG, "Reconnect cooldown active, skip")
return
}
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
Log.w(TAG, "Max reconnect attempts ($MAX_RECONNECT_ATTEMPTS) reached, reset counter")
reconnectAttempts = 0
}
lastReconnectTime = now
reconnectAttempts++
Log.i(TAG, "Reconnecting to server (attempt $reconnectAttempts/$MAX_RECONNECT_ATTEMPTS)")
networkScope.launch {
try {
Log.i(TAG, "🔄 开始重新连接到服务器")
// ✅ 修复:检查是否已经连接,避免重复重连
if (isConnected()) {
Log.d(TAG, "✅ 服务器已连接,跳过重连")
reconnectAttempts = 0 // 重置计数器
return@launch
}
if (reconnectAttempts >= maxReconnectAttempts) {
Log.w(TAG, "⚠️ 已达到最大重连次数($maxReconnectAttempts),停止重连")
return@launch
}
val timeSinceLastAttempt = System.currentTimeMillis() - lastConnectionAttempt
if (timeSinceLastAttempt < reconnectInterval) {
val waitTime = reconnectInterval - timeSinceLastAttempt
Log.d(TAG, "⏰ 等待重连间隔: ${waitTime}ms")
delay(waitTime)
}
reconnectAttempts++
Log.i(TAG, "🔄 重连尝试 $reconnectAttempts/$maxReconnectAttempts")
backgroundMonitorJob?.cancel()
isConnected = false
isConnecting = false
connectToServer()
// ✅ 修复:等待连接结果,避免立即检查状态
delay(2000) // 等待2秒让连接稳定
if (isConnected()) {
Log.i(TAG, "✅ 重连成功")
reconnectAttempts = 0 // 重置计数器
} else {
Log.w(TAG, "⚠️ 重连失败,等待下次尝试")
scheduleNextReconnect()
}
} catch (e: Exception) {
Log.e(TAG, "❌ 重新连接失败", e)
scheduleNextReconnect()
Log.e(TAG, "Reconnect failed: ${e.message}", e)
}
}
}
/**
* 计划下次重连
* Schedule next reconnect with exponential backoff.
*/
private fun scheduleNextReconnect() {
if (reconnectAttempts < maxReconnectAttempts) {
networkScope.launch {
delay(reconnectInterval)
// ✅ 修复:避免无限循环,只在需要时重连
if (!isConnected()) {
reconnectToServer()
} else {
Log.d(TAG, "✅ 连接已恢复,取消计划重连")
reconnectAttempts = 0
}
val delayMs = RECONNECT_INTERVAL_MS * (1L shl minOf(reconnectAttempts, 5))
Log.i(TAG, "Scheduling reconnect in ${delayMs}ms (attempt $reconnectAttempts)")
networkScope.launch {
delay(delayMs)
if (!isConnected()) {
reconnectToServer()
}
} else {
Log.w(TAG, "⚠️ 已达到最大重连次数,停止自动重连")
}
}
/**
* 检查并启动连接
* Check connection and start if not connected.
* Called externally to verify and recover connection.
*/
fun checkAndStartConnection() {
networkScope.launch {
try {
Log.d(TAG, "🔍 智能检查连接状态")
val socketIOConnected = if (::socketIOManager.isInitialized) {
socketIOManager.isConnected()
} else false
Log.d(TAG, "连接状态检查: Socket.IO=$socketIOConnected")
if (socketIOConnected) {
Log.d(TAG, "✅ 至少有一个连接正常,无需重连")
isConnected = true
reconnectAttempts = 0
if (isConnected()) {
Log.d(TAG, "Connection check: already connected")
return@launch
}
if (!::socketIOManager.isInitialized) {
Log.i(TAG, "🔄 连接管理器未初始化,创建新连接")
connectToServer()
} else {
Log.d(TAG, "⚠️ 检测到所有连接均断开,启动重连机制")
isConnected = false
reconnectToServer()
}
Log.i(TAG, "Connection check: not connected, starting connection")
connectToServer()
} catch (e: Exception) {
Log.e(TAG, "❌ 检查连接状态失败", e)
Log.e(TAG, "checkAndStartConnection failed: ${e.message}", e)
}
}
}
/**
* 启动服务连接
* Start connection (alias for checkAndStartConnection).
* Used by AccessibilityRemoteService.
*/
fun startConnection() {
networkScope.launch {
connectToServer()
}
checkAndStartConnection()
}
/**
* 断开连接
* Disconnect from server and cancel background jobs.
*/
fun disconnect() {
try {
Log.i(TAG, "🔌 断开网络连接")
Log.i(TAG, "Disconnecting from server")
backgroundMonitorJob?.cancel()
backgroundMonitorJob = null
isConnected = false
isConnecting = false
if (::socketIOManager.isInitialized) {
socketIOManager.disconnect()
}
isConnected = false
reconnectAttempts = 0
Log.i(TAG, "✅ 网络连接已断开")
Log.i(TAG, "Disconnected from server")
} catch (e: Exception) {
Log.e(TAG, "❌ 断开连接失败", e)
Log.e(TAG, "Disconnect failed: ${e.message}", e)
}
}
/**
* 清理资源
* Cleanup all resources. Call on service destroy.
*/
fun cleanup() {
try {
Log.i(TAG, "🧹 清理网络管理器资源")
Log.i(TAG, "Cleaning up NetworkManager resources")
disconnect()
networkScope.cancel()
Log.i(TAG, "✅ 网络管理器资源清理完成")
Log.i(TAG, "NetworkManager cleanup complete")
} catch (e: Exception) {
Log.e(TAG, "❌ 清理网络管理器资源失败", e)
Log.e(TAG, "Cleanup failed: ${e.message}", e)
}
}
/**
* 获取SocketIOManager实例
* Get SocketIOManager instance.
* @return SocketIOManager or null if not initialized
*/
fun getSocketIOManager(): SocketIOManager? {
return if (::socketIOManager.isInitialized) socketIOManager else null
}
/**
* 检查是否已连接
*/
fun isConnected(): Boolean {
// ✅ 修复递归调用问题:检查实际的连接状态,而不是调用自己
val actuallyConnected = (
(::socketIOManager.isInitialized && socketIOManager.isConnected())
)
// 更新内部状态
if (actuallyConnected != this.isConnected) {
this.isConnected = actuallyConnected
Log.d(TAG, "🔄 连接状态更新: $actuallyConnected")
}
return actuallyConnected
}
/**
* 检查是否正在连接
*/
fun isConnecting(): Boolean = isConnecting
/**
* 获取连接状态信息
*/
fun getConnectionStatus(): ConnectionStatus {
val socketIOConnected = if (::socketIOManager.isInitialized) {
socketIOManager.isConnected()
} else false
val actuallyConnected = socketIOConnected
return ConnectionStatus(
isConnected = actuallyConnected,
isConnecting = isConnecting,
socketIOConnected = socketIOConnected,
reconnectAttempts = reconnectAttempts,
lastConnectionAttempt = lastConnectionAttempt
)
}
/**
* 获取服务器URL
*/
private fun getServerUrl(): String {
// 1. 首先尝试从SharedPreferences获取用户设置的地址
val sharedPrefs = context.getSharedPreferences("remote_control_settings", Context.MODE_PRIVATE)
val userSetUrl = sharedPrefs.getString("server_url", null)
if (!userSetUrl.isNullOrEmpty()) {
Log.i(TAG, "使用用户设置的服务器地址: $userSetUrl")
return userSetUrl
}
// 2. 尝试从构建时的配置文件读取
val buildConfigUrl = readServerConfigFromAssets()
if (!buildConfigUrl.isNullOrEmpty()) {
Log.i(TAG, "使用构建时配置的服务器地址: $buildConfigUrl")
return buildConfigUrl
}
// 3. 尝试从strings.xml读取默认值
val defaultUrl = try {
service.getString(com.hikoncont.R.string.default_server_url)
} catch (e: Exception) {
Log.w(TAG, "无法从strings.xml读取默认服务器地址: ${e.message}")
return if (::socketIOManager.isInitialized) {
socketIOManager
} else {
Log.w(TAG, "SocketIOManager not initialized")
null
}
if (!defaultUrl.isNullOrEmpty()) {
Log.i(TAG, "使用strings.xml中的默认服务器地址: $defaultUrl")
return defaultUrl
}
// 4. 最后的兜底地址
val fallbackUrl = "ws://59.153.148.205:3001"
Log.i(TAG, "使用兜底服务器地址: $fallbackUrl")
return fallbackUrl
}
/**
* 获取服务器URL
* Check if currently connected to server.
*/
private fun getWebUrl(): String {
// 1. 首先尝试从SharedPreferences获取用户设置的地址
val sharedPrefs = context.getSharedPreferences("remote_control_settings", Context.MODE_PRIVATE)
val userSetUrl = sharedPrefs.getString("web_url", null)
if (!userSetUrl.isNullOrEmpty()) {
Log.i(TAG, "使用用户设置的服务器地址: $userSetUrl")
return userSetUrl
fun isConnected(): Boolean {
// Sync local state with actual Socket.IO state
if (::socketIOManager.isInitialized) {
val actualConnected = socketIOManager.isConnected()
if (isConnected != actualConnected) {
isConnected = actualConnected
}
return actualConnected
}
// 2. 尝试从构建时的配置文件读取
val buildConfigUrl = readWebConfigFromAssets()
if (!buildConfigUrl.isNullOrEmpty()) {
Log.i(TAG, "使用构建时配置的服务器地址: $buildConfigUrl")
return buildConfigUrl
}
// 4. 最后的兜底地址
val fallbackUrl = "https://m.baidu.com"
return fallbackUrl
return false
}
/**
* Check if connection is in progress.
*/
fun isConnecting(): Boolean = isConnecting
/**
* 从assets目录读取构建时的服务器配置
* Get current connection status summary.
*/
fun getConnectionStatus(): ConnectionStatus {
return ConnectionStatus(
connected = isConnected(),
connecting = isConnecting,
reconnectAttempts = reconnectAttempts,
lastAttemptTime = lastConnectionAttempt
)
}
// -- Config reading methods --
/**
* Read server URL from assets config file.
* @return server URL string or null on failure
*/
fun getServerUrl(): String? {
return readServerConfigFromAssets()
}
/**
* Read web URL from assets config file.
* @return web URL string or null on failure
*/
fun getWebUrl(): String? {
return readWebConfigFromAssets()
}
/**
* Parse server_config.json and extract serverUrl.
*/
private fun readServerConfigFromAssets(): String? {
return try {
val inputStream = context.assets.open("server_config.json")
val jsonString = inputStream.bufferedReader().use { it.readText() }
inputStream.close()
// 简单的JSON解析避免引入额外依赖
val serverUrlPattern = "\"serverUrl\"\\s*:\\s*\"([^\"]+)\"".toRegex()
val matchResult = serverUrlPattern.find(jsonString)
val serverUrl = matchResult?.groupValues?.get(1)
Log.i(TAG, "从assets读取到服务器配置: $serverUrl")
serverUrl
val jsonStr = context.assets.open(CONFIG_FILE_SERVER)
.bufferedReader().use { it.readText() }
val json = JSONObject(jsonStr)
val url = json.optString("serverUrl", "")
if (url.isBlank()) {
Log.e(TAG, "serverUrl is empty in $CONFIG_FILE_SERVER")
null
} else {
url
}
} catch (e: Exception) {
Log.w(TAG, "读取assets中的服务器配置失败: ${e.message}")
Log.e(TAG, "Failed to read $CONFIG_FILE_SERVER: ${e.message}")
null
}
}
/**
* Parse server_config.json and extract webUrl.
*/
private fun readWebConfigFromAssets(): String? {
return try {
val inputStream = context.assets.open("server_config.json")
val jsonString = inputStream.bufferedReader().use { it.readText() }
inputStream.close()
// 简单的JSON解析避免引入额外依赖
val serverUrlPattern = "\"webUrl\"\\s*:\\s*\"([^\"]+)\"".toRegex()
val matchResult = serverUrlPattern.find(jsonString)
val serverUrl = matchResult?.groupValues?.get(1)
Log.i(TAG, "从assets读取到服务器配置: $serverUrl")
serverUrl
val jsonStr = context.assets.open(CONFIG_FILE_SERVER)
.bufferedReader().use { it.readText() }
val json = JSONObject(jsonStr)
val url = json.optString("webUrl", "")
if (url.isBlank()) {
Log.e(TAG, "webUrl is empty in $CONFIG_FILE_SERVER")
null
} else {
url
}
} catch (e: Exception) {
Log.w(TAG, "读取assets中的服务器配置失败: ${e.message}")
Log.e(TAG, "Failed to read webUrl from $CONFIG_FILE_SERVER: ${e.message}")
null
}
}
// -- State reset methods --
/**
* 重置重连计数器
* Reset reconnect counter. Called after successful connection.
*/
fun resetReconnectCounter() {
reconnectAttempts = 0
lastConnectionAttempt = 0L
isConnected = false
isConnecting = false
Log.d(TAG, "🔄 重连计数器已重置")
lastReconnectTime = 0L
Log.d(TAG, "Reconnect counter reset")
}
/**
* 重置网络管理器状态(程序重启时调用)
* Reset all network state for fresh start (e.g. service restart).
*/
fun resetNetworkState() {
try {
Log.i(TAG, "🔄 重置网络管理器状态")
resetReconnectCounter()
// 断开现有连接
if (::socketIOManager.isInitialized) {
socketIOManager.disconnect()
}
Log.i(TAG, "✅ 网络管理器状态重置完成")
} catch (e: Exception) {
Log.e(TAG, "❌ 重置网络管理器状态失败", e)
}
Log.i(TAG, "Resetting network state")
backgroundMonitorJob?.cancel()
backgroundMonitorJob = null
isConnected = false
isConnecting = false
reconnectAttempts = 0
lastReconnectTime = 0L
lastConnectionAttempt = 0L
}
/**
* 强制重连
* Force reconnect by disconnecting and reconnecting.
*/
fun forceReconnect() {
Log.i(TAG, "Force reconnect requested")
networkScope.launch {
Log.i(TAG, "🔄 强制重连")
disconnect()
delay(1000)
resetReconnectCounter()
connectToServer()
try {
disconnect()
delay(1000)
connectToServer()
} catch (e: Exception) {
Log.e(TAG, "Force reconnect failed: ${e.message}", e)
}
}
}
// -- Private helpers --
/**
* 连接状态数据类
* Check if SocketIOManager is initialized and connected.
*/
private fun isSocketIOReady(): Boolean {
return ::socketIOManager.isInitialized && socketIOManager.isConnected()
}
/**
* Trigger force reconnect via SocketIOManager.
*/
private fun triggerForceReconnect() {
try {
if (::socketIOManager.isInitialized) {
socketIOManager.forceReconnect()
}
} catch (e: Exception) {
Log.e(TAG, "triggerForceReconnect failed: ${e.message}")
}
}
/**
* Connection status data class for external queries.
*/
data class ConnectionStatus(
val isConnected: Boolean,
val isConnecting: Boolean,
val socketIOConnected: Boolean,
val connected: Boolean,
val connecting: Boolean,
val reconnectAttempts: Int,
val lastConnectionAttempt: Long
) {
override fun toString(): String {
return "ConnectionStatus(connected=$isConnected, connecting=$isConnecting, " +
"socketIO=$socketIOConnected, attempts=$reconnectAttempts, lastAttempt=$lastConnectionAttempt)"
}
}
}
val lastAttemptTime: Long
)
}