Compare commits
11 Commits
0c516f7307
...
c0a7109816
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0a7109816 | ||
|
|
56648697bb | ||
|
|
92cfc10150 | ||
|
|
c63fbbd90f | ||
|
|
4ac6078ca3 | ||
|
|
5b2ddd7730 | ||
|
|
aa516590c8 | ||
|
|
466453a903 | ||
|
|
1501067287 | ||
|
|
cdc4606574 | ||
|
|
410219f382 |
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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-recycle:Bitmap已被其他线程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
@@ -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, "权限健康监控服务已清理")
|
||||
}
|
||||
}
|
||||
@@ -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. 等待一下确保剪贴板操作完成
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user