Files
and-bak/app/src/main/java/com/hikoncont/manager/ScreenCaptureManager.kt

3251 lines
134 KiB
Kotlin
Raw Normal View History

2026-02-11 16:59:49 +08:00
package com.hikoncont.manager
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import com.hikoncont.MediaProjectionHolder
import com.hikoncont.service.AccessibilityRemoteService
import kotlinx.coroutines.*
import java.io.ByteArrayOutputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
/**
* 屏幕捕获管理器 - 使用无障碍服务截图API
*
* 负责屏幕录制和截图功能
*/
class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
companion object {
private const val TAG = "ScreenCaptureManager"
private const val CAPTURE_FPS = 15 // 进一步提升到15FPS让视频更加流畅从10提升到15
private const val CAPTURE_QUALITY = 55 // 优化:提升压缩质量,在数据量和画质间找到最佳平衡(30->55)
2026-02-11 16:59:49 +08:00
private const val MAX_WIDTH = 480 // 更小宽度480px测试单消息大小理论
private const val MAX_HEIGHT = 854 // 更小高度854px保持16:9比例
private const val MIN_CAPTURE_INTERVAL = 3000L // 调整为3000ms无障碍服务截图间隔3秒
2026-02-11 16:59:49 +08:00
// 持久化暂停状态相关常量
2026-02-11 16:59:49 +08:00
private const val PAUSE_STATE_PREF = "screen_capture_pause_state"
private const val KEY_IS_PAUSED = "is_paused"
}
private var isCapturing = false
private var screenWidth = 0
private var screenHeight = 0
// 后台线程处理
private val handlerThread = HandlerThread("ScreenCapture")
private val backgroundHandler: Handler
private val captureScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// MediaProjection相关资源复用
private var virtualDisplay: android.hardware.display.VirtualDisplay? = null
private var imageReader: android.media.ImageReader? = null
private var mediaProjection: android.media.projection.MediaProjection? = null
// 图像缓存机制 - @Volatile确保多协程可见性
@Volatile private var lastValidBitmap: Bitmap? = null
@Volatile private var lastCaptureTime = 0L
private var lastScreenshotTime = 0L // 新增:记录上次截图时间,防止截图间隔太短
2026-02-11 16:59:49 +08:00
// 状态跟踪
private var isPaused = false
private var lastSuccessfulSendTime: Long? = null
// 添加Android 15图像传输验证相关变量
@Volatile private var android15ImageTransmissionVerified = false
private val android15ImageVerificationDelay = 3000L // 3秒后验证
// Android 15 VirtualDisplay重新创建状态跟踪
2026-02-11 16:59:49 +08:00
@Volatile
private var isVirtualDisplayRecreating = false
private val virtualDisplayRecreationLock = ReentrantLock()
// Android 15 Session使用状态跟踪
2026-02-11 16:59:49 +08:00
@Volatile
private var android15SessionUsed = false
// VirtualDisplay重新初始化智能控制
2026-02-11 16:59:49 +08:00
private var lastVirtualDisplayRecreationTime = 0L
private val minRecreationInterval = 60000L // 最小重新创建间隔60秒
private var consecutiveRecreationCount = 0
private val maxConsecutiveRecreations = 3 // 最多连续重新创建3次
private var virtualDisplayRecreationCooldown = false
// 图像获取失败计数
2026-02-11 16:59:49 +08:00
private var consecutiveImageFailures = 0
private val maxImageFailuresBeforeRecreation = 10 // 连续10次失败才考虑重新创建
// VirtualDisplay重建次数限制防止无限重建循环
private var virtualDisplayRebuildCount = 0
private val MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS = 3
// 新增AccessibilityService截图模式开关
2026-02-11 16:59:49 +08:00
private var useAccessibilityScreenshot = false
// 自适应画质运行时可调参数覆盖companion object中的常量
2026-02-11 16:59:49 +08:00
@Volatile private var dynamicFps: Int = CAPTURE_FPS
@Volatile private var dynamicQuality: Int = CAPTURE_QUALITY
@Volatile private var dynamicMaxWidth: Int = MAX_WIDTH
@Volatile private var dynamicMaxHeight: Int = MAX_HEIGHT
// Android 11+专用:防止闪烁的状态管理
2026-02-11 16:59:49 +08:00
private var android11ConsecutiveFailures = 0
private var android11LastSuccessTime = 0L
private var android11InTestMode = false
private val android11FailureThreshold = 3 // Android 11+设备连续失败3次后进入测试模式
private val android11TestModeStabilityTime = 5000L // 测试模式保持5秒以确保稳定性
// 新增屏幕数据发送队列管理避免数据积压导致OOM
2026-02-11 16:59:49 +08:00
private val screenDataQueue = java.util.concurrent.LinkedBlockingQueue<ByteArray>(15) // 增加到15帧容量避免频繁丢帧
private var droppedFrameCount = 0L
private var totalFrameCount = 0L
// 新增:内存监控和自动清理
2026-02-11 16:59:49 +08:00
private var lastMemoryCheckTime = 0L
private val memoryCheckInterval = 10000L // 10秒检查一次内存
private val maxMemoryUsagePercent = 0.8f // 超过80%内存使用率时触发清理
// 新增:资源追踪
2026-02-11 16:59:49 +08:00
private val activeBitmaps = mutableSetOf<WeakReference<Bitmap>>()
private val activeImages = mutableSetOf<WeakReference<android.media.Image>>()
// 新增:单一队列处理协程,避免协程泄漏
2026-02-11 16:59:49 +08:00
private var queueProcessorJob: Job? = null
private val queueProcessingStarted = AtomicBoolean(false)
// 专用截图执行器,避免每次创建线程并避免占用主线程
// 使用可重建的方式管理防止shutdownNow后无法恢复导致闪退
@Volatile
private var screenshotExecutor: java.util.concurrent.ExecutorService =
2026-02-11 16:59:49 +08:00
java.util.concurrent.Executors.newSingleThreadExecutor()
init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
// 获取屏幕尺寸 - 使用完整屏幕尺寸(包含状态栏和导航栏)
// 这样可以确保与MediaProjection实际捕获的内容尺寸一致
val metrics = service.resources.displayMetrics
screenWidth = metrics.widthPixels
screenHeight = metrics.heightPixels
Log.i(TAG, "屏幕尺寸: ${screenWidth}x${screenHeight}")
}
/**
* 获取可用的截图执行器如果已关闭则重建
* 防止shutdownNow后再次使用导致RejectedExecutionException闪退
*/
private fun getOrCreateScreenshotExecutor(): java.util.concurrent.ExecutorService {
val executor = screenshotExecutor
if (executor.isShutdown) {
Log.i(TAG, "截图执行器已关闭,重新创建")
val newExecutor = java.util.concurrent.Executors.newSingleThreadExecutor()
screenshotExecutor = newExecutor
return newExecutor
}
return executor
}
/**
* 安全回收lastValidBitmap防止多线程并发导致的native crash (SIGSEGV)
*
* 使用synchronized确保原子性先置null再recycle
* 防止两个协程同时拿到同一个引用并double-recycle
*/
@Synchronized
private fun safeRecycleLastValidBitmap() {
val bitmapToRecycle = lastValidBitmap
lastValidBitmap = null // 先置null防止其他线程访问
try {
if (bitmapToRecycle != null && !bitmapToRecycle.isRecycled) {
bitmapToRecycle.recycle()
}
} catch (e: Exception) {
// 捕获所有异常包括可能的native异常包装
Log.w(TAG, "回收缓存Bitmap异常可能已被其他线程回收: ${e.message}")
}
}
/**
* 安全更新lastValidBitmap缓存
* synchronized确保与safeRecycleLastValidBitmap互斥
*/
@Synchronized
private fun updateLastValidBitmap(newBitmap: Bitmap) {
val old = lastValidBitmap
lastValidBitmap = newBitmap
trackBitmap(newBitmap)
lastCaptureTime = System.currentTimeMillis()
// 回收旧的缓存
try {
if (old != null && !old.isRecycled) {
old.recycle()
}
} catch (e: Exception) {
Log.w(TAG, "回收旧缓存Bitmap异常: ${e.message}")
}
}
/**
* 安全压缩缓存帧并返回JPEG数据
* synchronized确保读取期间lastValidBitmap不被回收
* @param maxAge 缓存最大有效期(ms)
* @return JPEG数据缓存无效时返回null
*/
@Synchronized
private fun compressCachedFrame(maxAge: Long): ByteArray? {
val cached = lastValidBitmap ?: return null
if (cached.isRecycled) return null
val age = System.currentTimeMillis() - lastCaptureTime
if (age > maxAge) return null
return try {
compressBitmap(cached)
} catch (e: Exception) {
Log.w(TAG, "压缩缓存帧失败: ${e.message}")
null
}
}
/**
* 安全复制lastValidBitmap
* synchronized确保复制期间不被其他线程回收
* @return Bitmap副本缓存无效时返回null
*/
@Synchronized
private fun safeCopyLastValidBitmap(): Bitmap? {
val cached = lastValidBitmap ?: return null
if (cached.isRecycled) return null
return try {
cached.copy(cached.config ?: Bitmap.Config.ARGB_8888, false)
} catch (e: Exception) {
Log.w(TAG, "复制缓存Bitmap失败: ${e.message}")
null
}
}
/**
* 安全回收任意Bitmap防止native crash
*/
private fun safeRecycleBitmap(bitmap: Bitmap?) {
try {
if (bitmap != null && !bitmap.isRecycled) {
bitmap.recycle()
}
} catch (e: Exception) {
Log.w(TAG, "回收Bitmap异常: ${e.message}")
}
}
/**
* 清空ImageReader中所有已acquired的Image释放缓冲区槽位
* 当acquireLatestImage抛出maxImages异常时调用
*/
private fun drainImageReader() {
try {
val reader = imageReader ?: return
var drained = 0
while (true) {
val img = try {
reader.acquireNextImage()
} catch (e: IllegalStateException) {
// 缓冲区仍然满说明有Image未被正确close
Log.w(TAG, "drainImageReader: acquireNextImage也失败重建ImageReader")
break
} ?: break
try { img.close() } catch (_: Exception) {}
drained++
}
if (drained > 0) {
Log.i(TAG, "已清空ImageReader缓冲区: 释放${drained}个Image")
}
} catch (e: Exception) {
Log.e(TAG, "drainImageReader失败: ${e.message}")
}
}
2026-02-11 16:59:49 +08:00
/**
* 启用AccessibilityService截图模式
2026-02-11 16:59:49 +08:00
* 用于绕过黑屏遮罩让Web端能够正常显示画面
*/
fun enableAccessibilityScreenshotMode() {
Log.i(TAG, "启用AccessibilityService截图模式 - 绕过黑屏遮罩")
2026-02-11 16:59:49 +08:00
useAccessibilityScreenshot = true
}
/**
* 禁用AccessibilityService截图模式
2026-02-11 16:59:49 +08:00
* 恢复使用MediaProjection截图
*/
fun disableAccessibilityScreenshotMode() {
Log.i(TAG, "禁用AccessibilityService截图模式 - 恢复MediaProjection")
2026-02-11 16:59:49 +08:00
useAccessibilityScreenshot = false
}
/**
* 自适应画质动态调整采集参数
2026-02-11 16:59:49 +08:00
* 由服务端根据Web端反馈下发调整指令
*/
fun adjustQuality(fps: Int, quality: Int, maxWidth: Int, maxHeight: Int) {
Log.i(TAG, "收到画质调整: fps=$fps, quality=$quality, resolution=${maxWidth}x${maxHeight}")
2026-02-11 16:59:49 +08:00
if (fps in 1..30) {
dynamicFps = fps
Log.i(TAG, "帧率调整为: ${fps}fps (间隔${1000 / fps}ms)")
2026-02-11 16:59:49 +08:00
}
if (quality in 20..90) {
dynamicQuality = quality
Log.i(TAG, "JPEG质量调整为: $quality")
2026-02-11 16:59:49 +08:00
}
if (maxWidth in 240..1920) {
dynamicMaxWidth = maxWidth
Log.i(TAG, "最大宽度调整为: $maxWidth")
2026-02-11 16:59:49 +08:00
}
if (maxHeight in 320..2560) {
dynamicMaxHeight = maxHeight
Log.i(TAG, "最大高度调整为: $maxHeight")
2026-02-11 16:59:49 +08:00
}
}
/**
* 切换到无障碍截图模式由服务端指令触发
* 已禁用不再允许服务端黑帧检测触发模式切换避免误判导致权限回退
2026-02-11 16:59:49 +08:00
*/
fun switchToAccessibilityMode() {
Log.i(TAG, "收到切换无障碍截图模式指令,已忽略(禁止服务端触发模式切换)")
2026-02-11 16:59:49 +08:00
}
/**
* 切换到 MediaProjection 模式由服务端指令触发
2026-02-11 16:59:49 +08:00
*/
fun switchToMediaProjectionMode() {
if (!useAccessibilityScreenshot) {
Log.d(TAG, "已经在MediaProjection模式跳过切换")
2026-02-11 16:59:49 +08:00
return
}
Log.i(TAG, "切换到MediaProjection模式")
2026-02-11 16:59:49 +08:00
stopCapture()
disableAccessibilityScreenshotMode()
startCapture()
}
/**
* 初始化屏幕捕获
*/
fun initialize() {
try {
Log.i(TAG, "初始化屏幕捕获管理器 - 使用无障碍服务截图API")
// 恢复持久化的暂停状态
2026-02-11 16:59:49 +08:00
restorePauseState()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Log.i(TAG, "Android 9+支持无障碍服务截图API")
} else {
Log.w(TAG, "Android版本过低将使用测试图像")
}
} catch (e: Exception) {
Log.e(TAG, "初始化屏幕捕获失败", e)
}
}
/**
* 保存暂停状态到SharedPreferences
2026-02-11 16:59:49 +08:00
*/
private fun savePauseState() {
try {
val sp = service.getSharedPreferences(PAUSE_STATE_PREF, android.content.Context.MODE_PRIVATE)
sp.edit().putBoolean(KEY_IS_PAUSED, isPaused).apply()
Log.d(TAG, "已保存暂停状态: isPaused=$isPaused")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "保存暂停状态失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 从SharedPreferences恢复暂停状态
2026-02-11 16:59:49 +08:00
*/
private fun restorePauseState() {
try {
val sp = service.getSharedPreferences(PAUSE_STATE_PREF, android.content.Context.MODE_PRIVATE)
val savedIsPaused = sp.getBoolean(KEY_IS_PAUSED, false)
if (savedIsPaused) {
Log.i(TAG, "检测到持久化的暂停状态,恢复暂停状态")
2026-02-11 16:59:49 +08:00
isPaused = true
isCapturing = false
} else {
Log.d(TAG, "未检测到暂停状态,使用默认状态")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "恢复暂停状态失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 开始屏幕捕获
*/
fun startCapture() {
if (isCapturing) {
Log.w(TAG, "屏幕捕获已在运行")
return
}
// 检查持久化的暂停状态,如果暂停则不启动
2026-02-11 16:59:49 +08:00
if (isPaused) {
Log.i(TAG, "屏幕捕获处于暂停状态,跳过启动")
2026-02-11 16:59:49 +08:00
return
}
try {
Log.i(TAG, "开始屏幕捕获")
// Android 11+ (API 30+):优先尝试 MediaProjection帧率更高
2026-02-11 16:59:49 +08:00
// 如果 MediaProjection 不可用则回退到无障碍截图
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 检查是否已被显式设置为无障碍截图模式
if (useAccessibilityScreenshot) {
Log.i(TAG, "Android 11+:已设置为无障碍截图模式")
2026-02-11 16:59:49 +08:00
startAccessibilityScreenCapture()
} else if (ensureMediaProjection()) {
// MediaProjection 可用,使用 VirtualDisplay 连续流式捕获
Log.i(TAG, "Android 11+MediaProjection 可用,使用 VirtualDisplay 连续流式捕获")
2026-02-11 16:59:49 +08:00
startMediaProjectionCapture()
} else {
// MediaProjection 不可用,回退到无障碍截图
Log.i(TAG, "Android 11+MediaProjection 不可用,回退到无障碍截图模式")
fallbackToAccessibilityCapture()
2026-02-11 16:59:49 +08:00
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Log.i(TAG, "Android 5-10使用 MediaProjection VirtualDisplay 连续流式捕获")
2026-02-11 16:59:49 +08:00
startMediaProjectionCapture()
} else {
Log.w(TAG, "Android版本过低使用测试图像")
startFallbackCapture()
}
} catch (e: Exception) {
Log.e(TAG, "启动屏幕捕获失败", e)
startFallbackCapture()
}
}
/**
* 使用MediaProjection VirtualDisplay连续流式捕获核心采集方法
* 通过 ImageReader.OnImageAvailableListener 回调驱动无系统级间隔限制
*/
private fun startMediaProjectionCapture() {
isCapturing = true
Log.i(TAG, "启动 MediaProjection VirtualDisplay 连续流式捕获")
2026-02-11 16:59:49 +08:00
captureScope.launch {
// 先确保 MediaProjection 可用
if (!ensureMediaProjection()) {
Log.e(TAG, "MediaProjection 不可用,回退到无障碍截图")
fallbackToAccessibilityCapture()
2026-02-11 16:59:49 +08:00
return@launch
}
// 初始化 ImageReader + VirtualDisplay
if (imageReader == null || virtualDisplay == null) {
setupMediaProjectionResources()
}
if (virtualDisplay == null) {
Log.e(TAG, "VirtualDisplay 创建失败,回退到无障碍截图")
fallbackToAccessibilityCapture()
2026-02-11 16:59:49 +08:00
return@launch
}
Log.i(TAG, "MediaProjection VirtualDisplay 就绪,开始连续帧采集循环")
2026-02-11 16:59:49 +08:00
var consecutiveFailures = 0
virtualDisplayRebuildCount = 0
2026-02-11 16:59:49 +08:00
// 最小有效帧大小阈值正常480x854 JPEG即使最低质量也>5KB
2026-02-11 16:59:49 +08:00
// 低于此值的帧几乎肯定是黑屏/空白帧VirtualDisplay未刷新
val MIN_VALID_FRAME_SIZE = 5 * 1024 // 5KB
// 连续黑屏帧计数 - 不再用于触发回退,仅用于日志统计
2026-02-11 16:59:49 +08:00
var consecutiveBlackFrames = 0
while (isCapturing) {
try {
// MediaProjection对象有效性检测
// 如果本地引用为null尝试从Holder重新获取
if (mediaProjection == null) {
Log.w(TAG, "MediaProjection本地引用为null尝试从Holder重新获取")
mediaProjection = MediaProjectionHolder.getMediaProjection()
if (mediaProjection == null) {
Log.w(TAG, "Holder中也无有效MediaProjection等待权限恢复发送缓存帧")
val cachedJpeg = compressCachedFrame(30000)
if (cachedJpeg != null) {
sendFrameToServer(cachedJpeg)
}
delay(3000)
continue
}
Log.i(TAG, "从Holder重新获取到MediaProjection继续采集")
}
// Surface有效性检测BufferQueue被系统abandoned后尝试重建
val currentSurface = imageReader?.surface
if (currentSurface == null || !currentSurface.isValid) {
Log.w(TAG, "ImageReader Surface已失效, 尝试重建VirtualDisplay")
cleanupVirtualDisplayOnly()
delay(500)
if (ensureMediaProjection()) {
setupMediaProjectionResources()
}
val rebuiltSurface = imageReader?.surface
if (rebuiltSurface == null || !rebuiltSurface.isValid) {
Log.w(TAG, "Surface重建失败等待下次循环重试")
}
delay(1000)
continue
}
// 安全获取Image防止maxImages溢出
val image = try {
imageReader?.acquireLatestImage()
} catch (e: IllegalStateException) {
Log.w(TAG, "流式采集ImageReader缓冲区已满清空: ${e.message}")
drainImageReader()
null
}
2026-02-11 16:59:49 +08:00
if (image != null) {
try {
val bitmap = convertImageToBitmap(image)
if (bitmap != null) {
trackBitmap(bitmap)
val jpegData = compressBitmap(bitmap)
if (jpegData.size >= MIN_VALID_FRAME_SIZE) {
// 有效帧:发送并更新缓存
2026-02-11 16:59:49 +08:00
sendFrameToServer(jpegData)
// 安全更新缓存帧
val copy = bitmap.copy(Bitmap.Config.ARGB_8888, false)
if (copy != null) {
updateLastValidBitmap(copy)
}
2026-02-11 16:59:49 +08:00
consecutiveFailures = 0
consecutiveBlackFrames = 0
Log.d(TAG, "MediaProjection 有效帧: ${jpegData.size} bytes")
2026-02-11 16:59:49 +08:00
} else {
// 疑似黑屏帧,仅记录日志,不触发回退
// MediaProjection仍在工作黑屏帧可能是短暂的屏幕状态变化
2026-02-11 16:59:49 +08:00
consecutiveBlackFrames++
if (consecutiveBlackFrames % 50 == 1) {
Log.w(TAG, "黑屏帧(${jpegData.size}B < ${MIN_VALID_FRAME_SIZE}B), 连续${consecutiveBlackFrames}次, 发送缓存帧")
2026-02-11 16:59:49 +08:00
}
// 发送缓存的有效帧保持画面连续性
val cachedJpeg = compressCachedFrame(10000)
if (cachedJpeg != null && cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg)
2026-02-11 16:59:49 +08:00
}
}
safeRecycleBitmap(bitmap)
2026-02-11 16:59:49 +08:00
}
} finally {
image.close()
}
} else {
consecutiveFailures++
// Send cached frame when no new frame available
val cachedJpeg2 = compressCachedFrame(10000)
if (cachedJpeg2 != null && cachedJpeg2.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg2)
Log.d(TAG, "No new frame, sent cached frame")
2026-02-11 16:59:49 +08:00
}
// 连续失败过多,尝试重建 VirtualDisplay
if (consecutiveFailures >= maxImageFailuresBeforeRecreation) {
virtualDisplayRebuildCount++
Log.w(TAG, "连续 ${consecutiveFailures} 次无法获取有效帧,重建 VirtualDisplay (${virtualDisplayRebuildCount}/${MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS})")
2026-02-11 16:59:49 +08:00
cleanupVirtualDisplayOnly()
delay(500)
// 重新检查MediaProjection是否可用
if (!ensureMediaProjection()) {
Log.w(TAG, "MediaProjection不可用等待恢复继续发送缓存帧")
consecutiveFailures = 0
continue
}
2026-02-11 16:59:49 +08:00
setupMediaProjectionResources()
consecutiveFailures = 0
// 重建后验证VirtualDisplay和Surface有效性
val rebuiltSurface = imageReader?.surface
if (virtualDisplay == null || rebuiltSurface == null || !rebuiltSurface.isValid) {
Log.w(TAG, "VirtualDisplay 重建后无效,等待下次重试")
// 不回退,继续循环,下次会再尝试重建
2026-02-11 16:59:49 +08:00
}
}
}
// 按动态帧率控制采集间隔
delay(1000L / dynamicFps)
} catch (e: CancellationException) {
throw e
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.w(TAG, "MediaProjection 帧采集异常: ${e.message}")
2026-02-11 16:59:49 +08:00
consecutiveFailures++
delay(100)
}
}
}
}
/**
* 统一回退到无障碍截图的方法
* Android 11+ 使用无障碍截图低版本使用测试图像兜底
*/
private fun fallbackToAccessibilityCapture() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
}
2026-02-11 16:59:49 +08:00
/**
* 确保 MediaProjection 可用
*
* 核心修复禁止重复调用 getMediaProjection(resultCode, resultData)
* 每次调用都会创建新实例系统会自动 stop 旧实例触发 onStop 回调
* 形成"权限丢失->恢复->再丢失"的死循环这是权限频繁掉落的根因
*
* 只从 MediaProjectionHolder 获取已有对象不重新创建
2026-02-11 16:59:49 +08:00
*/
private fun ensureMediaProjection(): Boolean {
if (mediaProjection != null) return true
// 从全局 Holder 获取已有的 MediaProjection 对象
2026-02-11 16:59:49 +08:00
mediaProjection = MediaProjectionHolder.getMediaProjection()
if (mediaProjection != null) {
Log.i(TAG, "从 MediaProjectionHolder 获取到已有 MediaProjection")
return true
2026-02-11 16:59:49 +08:00
}
// 不再重复调用 getMediaProjection() 创建新实例
// 如果 Holder 中没有有效对象,说明权限确实未授予或已过期
// 应由 MainActivity 的权限申请流程统一创建
Log.w(TAG, "MediaProjectionHolder 中无有效 MediaProjection等待权限授予")
2026-02-11 16:59:49 +08:00
return false
}
/**
* 使用MediaProjection进行屏幕捕获旧方法保留作为无障碍截图兜底
*/
private fun startAccessibilityScreenCapture() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
Log.w(TAG, "屏幕捕获需要Android 9+")
startFallbackCapture()
return
}
isCapturing = true
Log.i(TAG, "启动屏幕捕获,尝试获取真实屏幕")
captureScope.launch {
var consecutiveFailures = 0
val maxConsecutiveFailures = 10 // 最多连续10次失败后显示权限恢复提示
while (isCapturing) {
try {
// 尝试获取真实屏幕截图
val screenshot = captureRealScreen()
if (screenshot != null) {
// 成功获取真实屏幕
2026-02-11 16:59:49 +08:00
Log.d(TAG, "成功获取真实屏幕截图: ${screenshot.width}x${screenshot.height}")
val jpegData = compressBitmap(screenshot)
sendFrameToServer(jpegData)
// 缓存成功的截图,用于防止闪烁
2026-02-11 16:59:49 +08:00
try {
val copy = screenshot.copy(screenshot.config ?: Bitmap.Config.ARGB_8888, false)
if (copy != null) {
updateLastValidBitmap(copy)
Log.d(TAG, "已缓存有效截图用于防闪烁: ${copy.width}x${copy.height}")
}
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.w(TAG, "缓存有效截图失败", e)
}
screenshot.recycle()
consecutiveFailures = 0 // 重置失败计数
// Android 11+特殊处理:成功获取截图时的状态管理
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
android11ConsecutiveFailures = 0
android11LastSuccessTime = System.currentTimeMillis()
android11InTestMode = false
Log.d(TAG, "Android 11+设备:真实截图成功,退出测试模式")
2026-02-11 16:59:49 +08:00
}
} else {
// 真实截图失败,不发送任何数据,等待下一次截图
2026-02-11 16:59:49 +08:00
// 之前发送测试图像会被服务端过滤且占用发送队列,得不偿失
consecutiveFailures++
if (consecutiveFailures % 50 == 1) {
Log.d(TAG, "无障碍截图失败(${consecutiveFailures}次),跳过本帧等待下次")
}
2026-02-11 16:59:49 +08:00
}
// 无障碍截图统一使用固定3秒间隔不区分成功/失败
// 系统对无障碍截图有约3秒的最小间隔限制低于此值会报错
val elapsed = System.currentTimeMillis() - lastScreenshotTime
val remaining = MIN_CAPTURE_INTERVAL - elapsed
if (remaining > 0) {
delay(remaining)
2026-02-11 16:59:49 +08:00
} else {
delay(MIN_CAPTURE_INTERVAL)
2026-02-11 16:59:49 +08:00
}
} catch (e: CancellationException) {
throw e
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.w(TAG, "屏幕捕获失败: ${e.message}")
2026-02-11 16:59:49 +08:00
consecutiveFailures++
delay(MIN_CAPTURE_INTERVAL) // 异常时也等待3秒再重试
2026-02-11 16:59:49 +08:00
}
}
}
}
/**
* Android 11+专用处理截图失败的智能逻辑
2026-02-11 16:59:49 +08:00
*/
private fun handleAndroid11ScreenshotFailure(consecutiveFailures: Int, maxConsecutiveFailures: Int): Bitmap? {
android11ConsecutiveFailures++
val currentTime = System.currentTimeMillis()
Log.d(TAG, "Android 11+设备:截图失败 (连续${android11ConsecutiveFailures}次)")
2026-02-11 16:59:49 +08:00
// 安全读取缓存帧synchronized保护
val cachedCopy = safeCopyLastValidBitmap()
if (cachedCopy != null) {
2026-02-11 16:59:49 +08:00
val cacheAge = currentTime - lastCaptureTime
if (cacheAge < 10000) {
Log.d(TAG, "Android 11+: 返回缓存截图(${cacheAge}ms前)")
return cachedCopy
2026-02-11 16:59:49 +08:00
}
if (cacheAge < 60000 && android11ConsecutiveFailures < 20) {
Log.d(TAG, "Android 11+: 返回较旧缓存截图(${cacheAge}ms前)")
return cachedCopy
2026-02-11 16:59:49 +08:00
}
// 缓存过旧回收copy
safeRecycleBitmap(cachedCopy)
2026-02-11 16:59:49 +08:00
}
// 无可用缓存或缓存过旧,智能判断是否应该进入测试模式
2026-02-11 16:59:49 +08:00
val shouldEnterTestMode = when {
android11ConsecutiveFailures >= android11FailureThreshold -> true
android11InTestMode && (currentTime - android11LastSuccessTime) < android11TestModeStabilityTime -> true
else -> false
}
return if (shouldEnterTestMode) {
if (!android11InTestMode) {
Log.i(TAG, "Android 11+设备:连续失败${android11ConsecutiveFailures}次,进入稳定测试模式")
2026-02-11 16:59:49 +08:00
android11InTestMode = true
}
// 在测试模式下,根据总失败次数决定显示什么
if (consecutiveFailures > maxConsecutiveFailures) {
Log.w(TAG, "Android 11+设备:长期失败,显示权限恢复提示")
2026-02-11 16:59:49 +08:00
generatePermissionRecoveryTestImage()
} else {
Log.d(TAG, "Android 11+设备:显示专用测试画面")
2026-02-11 16:59:49 +08:00
generateAndroid11TestImage()
}
} else {
// 还未达到测试模式阈值,使用简单测试图像
Log.d(TAG, "Android 11+设备:短暂失败,显示简单测试图像")
2026-02-11 16:59:49 +08:00
generateTestImage()
}
}
/**
* 捕获真实屏幕用于单次截图场景
* 优先使用 MediaProjection不可用时回退到无障碍截图
* useAccessibilityScreenshot=true 直接使用无障碍截图跳过 MediaProjection
*/
private fun captureRealScreen(): Bitmap? {
return try {
// 如果已切换到无障碍截图模式,直接使用无障碍截图,不再尝试 MediaProjection
2026-02-11 16:59:49 +08:00
if (useAccessibilityScreenshot) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return captureWithAccessibilityService()
}
return null
}
// 优先尝试 MediaProjection
val mpResult = captureWithMediaProjection()
if (mpResult != null) return mpResult
// MediaProjection 不可用回退到无障碍截图Android 11+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Log.d(TAG, "MediaProjection 无帧,回退到无障碍截图")
captureWithAccessibilityService()
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "捕获真实屏幕失败", e)
null
}
}
/**
* 使用无障碍服务截图 (Android 11+) - 优化版防止截图间隔太短
*/
private fun captureWithAccessibilityService(): Bitmap? {
return try {
// 修复:检查截图间隔,防止截图间隔太短
2026-02-11 16:59:49 +08:00
val currentTime = System.currentTimeMillis()
val timeSinceLastScreenshot = currentTime - lastScreenshotTime
if (timeSinceLastScreenshot < MIN_CAPTURE_INTERVAL) {
2026-02-11 16:59:49 +08:00
return null
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Log.d(TAG, "使用无障碍服务截图API")
// Android 11+特殊处理:增强错误检查和重试机制
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.d(TAG, "Android 11+设备:使用增强的无障碍截图逻辑")
2026-02-11 16:59:49 +08:00
}
// 使用同步方式获取截图
var resultBitmap: Bitmap? = null
var errorCode: Int? = null
val latch = java.util.concurrent.CountDownLatch(1)
// 后台线程直接调用截图API回调在专用执行器上执行避免占用主线程
try {
val safeExecutor = getOrCreateScreenshotExecutor()
2026-02-11 16:59:49 +08:00
service.takeScreenshot(
android.view.Display.DEFAULT_DISPLAY,
safeExecutor,
2026-02-11 16:59:49 +08:00
object : android.accessibilityservice.AccessibilityService.TakeScreenshotCallback {
override fun onSuccess(screenshotResult: android.accessibilityservice.AccessibilityService.ScreenshotResult) {
try {
Log.d(TAG, "无障碍服务截图成功")
// 修复:更新截图时间戳,防止截图间隔太短
2026-02-11 16:59:49 +08:00
lastScreenshotTime = System.currentTimeMillis()
// 关键修复正确提取ScreenshotResult中的Bitmap
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val hardwareBuffer = screenshotResult.hardwareBuffer
val colorSpace = screenshotResult.colorSpace
if (hardwareBuffer != null) {
// 从HardwareBuffer创建Bitmap
resultBitmap = android.graphics.Bitmap.wrapHardwareBuffer(
hardwareBuffer,
colorSpace
)
// 跟踪新创建的Bitmap
2026-02-11 16:59:49 +08:00
resultBitmap?.let { trackBitmap(it) }
Log.i(TAG, "成功从ScreenshotResult提取Bitmap: ${resultBitmap?.width}x${resultBitmap?.height}")
2026-02-11 16:59:49 +08:00
// Android 11+特殊处理额外验证Bitmap有效性
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
if (resultBitmap != null && !resultBitmap!!.isRecycled) {
Log.i(TAG, "Android 11+设备Bitmap验证通过尺寸=${resultBitmap!!.width}x${resultBitmap!!.height}")
2026-02-11 16:59:49 +08:00
} else {
Log.w(TAG, "Android 11+设备Bitmap验证失败")
2026-02-11 16:59:49 +08:00
resultBitmap = null
}
}
} else {
Log.w(TAG, "ScreenshotResult中的HardwareBuffer为null")
// Android 11+特殊处理:记录详细错误信息
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.e(TAG, "Android 11+设备HardwareBuffer为null可能是权限问题")
2026-02-11 16:59:49 +08:00
}
}
} else {
Log.w(TAG, "Android版本不支持ScreenshotResult API")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "处理截图结果失败", e)
// Android 11+特殊处理:记录详细异常信息
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.e(TAG, "Android 11+设备:处理截图结果时发生异常", e)
2026-02-11 16:59:49 +08:00
}
} finally {
latch.countDown()
}
}
override fun onFailure(failureErrorCode: Int) {
Log.e(TAG, "无障碍服务截图失败,错误码: $failureErrorCode")
2026-02-11 16:59:49 +08:00
errorCode = failureErrorCode
// Android 11+特殊处理:详细分析错误码
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
val errorMessage = when (failureErrorCode) {
android.accessibilityservice.AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR -> "内部错误"
android.accessibilityservice.AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT -> "截图间隔太短"
2026-02-11 16:59:49 +08:00
android.accessibilityservice.AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY -> "无效显示"
android.accessibilityservice.AccessibilityService.ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS -> "无障碍权限不足"
else -> "未知错误($failureErrorCode)"
}
Log.e(TAG, "Android 11+设备:截图失败详情 - $errorMessage")
2026-02-11 16:59:49 +08:00
}
latch.countDown()
}
}
)
} catch (e: Exception) {
Log.e(TAG, "调用takeScreenshot失败", e)
// Android 11+特殊处理:记录调用失败详情
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.e(TAG, "Android 11+设备调用takeScreenshot时发生异常", e)
2026-02-11 16:59:49 +08:00
}
latch.countDown()
}
// Android 11+特殊处理:缩短等待时间,避免阻塞采集循环
2026-02-11 16:59:49 +08:00
val timeout = if (Build.VERSION.SDK_INT >= 30) 500 else 500
val success = latch.await(timeout.toLong(), java.util.concurrent.TimeUnit.MILLISECONDS)
if (!success) {
Log.w(TAG, "无障碍服务截图超时")
// Android 11+特殊处理返回null让主循环处理失败逻辑
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.w(TAG, "Android 11+设备无障碍截图超时返回null让主循环处理")
2026-02-11 16:59:49 +08:00
return null
}
return captureWithMediaProjection() // 其他版本超时时回退到MediaProjection
}
if (resultBitmap != null) {
Log.i(TAG, "无障碍服务截图成功获取,绕过黑屏遮罩")
2026-02-11 16:59:49 +08:00
resultBitmap
} else {
// Android 11+特殊处理返回null让主循环处理失败逻辑
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.w(TAG, "Android 11+设备无障碍截图失败返回null让主循环处理")
2026-02-11 16:59:49 +08:00
return null
}
// 如果AccessibilityService截图失败回退到MediaProjection
Log.w(TAG, "无障碍服务截图失败回退到MediaProjection")
2026-02-11 16:59:49 +08:00
captureWithMediaProjection()
}
} else {
Log.w(TAG, "Android版本不支持无障碍服务截图API使用MediaProjection")
// Android 11+特殊处理返回null让主循环处理
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.w(TAG, "Android 11+设备不支持无障碍截图API返回null让主循环处理")
2026-02-11 16:59:49 +08:00
return null
}
captureWithMediaProjection()
}
} catch (e: Exception) {
Log.e(TAG, "无障碍服务截图异常", e)
// Android 11+特殊处理异常时返回null让主循环处理
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.e(TAG, "Android 11+设备无障碍截图异常返回null让主循环处理", e)
2026-02-11 16:59:49 +08:00
return null
}
// 出现异常时回退到MediaProjection
captureWithMediaProjection()
}
}
/**
* 使用MediaProjection截图 - 优化版本复用资源
*/
private fun captureWithMediaProjection(): Bitmap? {
return try {
// 获取或复用MediaProjection增加真实可用性验证
if (mediaProjection == null) {
mediaProjection = MediaProjectionHolder.getMediaProjection()
if (mediaProjection == null) {
Log.w(TAG, "MediaProjection未初始化Holder中无有效对象")
2026-02-11 16:59:49 +08:00
// 核心修复:不再重复调用 getMediaProjection() 创建新实例
// 重复创建会导致系统 stop 旧实例,触发 onStop 回调死循环
// 权限应由 MainActivity 统一申请
2026-02-11 16:59:49 +08:00
// Android 15优化直接报告权限不可用
if (android.os.Build.VERSION.SDK_INT >= 35) {
Log.w(TAG, "Android 15 MediaProjection为null等待权限重新授予")
triggerPermissionRecovery()
return null
} else {
Log.e(TAG, "MediaProjection权限丢失触发自动权限恢复")
triggerPermissionRecovery()
return null
2026-02-11 16:59:49 +08:00
}
}
}
// 保守策略:只在实际使用失败时才进行权限检测,避免过度检测
2026-02-11 16:59:49 +08:00
// 移除主动的可用性测试,让权限在实际使用中自然暴露问题
// 初始化ImageReader和VirtualDisplay仅第一次
if (imageReader == null || virtualDisplay == null) {
setupMediaProjectionResources()
}
// 尝试获取最新图像,带缓存机制
val currentTime = System.currentTimeMillis()
var newBitmap: Bitmap? = null
// 尝试获取新图像用安全方式防止maxImages溢出
2026-02-11 16:59:49 +08:00
var retryCount = 0
val maxRetries = if (Build.VERSION.SDK_INT >= 35) 5 else 2
2026-02-11 16:59:49 +08:00
while (retryCount < maxRetries) {
var image: android.media.Image? = null
try {
image = imageReader?.acquireLatestImage()
} catch (e: IllegalStateException) {
// maxImages已满先清空所有已acquired的Image再重试
Log.w(TAG, "ImageReader缓冲区已满执行紧急清空: ${e.message}")
drainImageReader()
retryCount++
continue
}
2026-02-11 16:59:49 +08:00
if (image != null) {
try {
newBitmap = convertImageToBitmap(image)
2026-02-11 16:59:49 +08:00
if (newBitmap != null) {
trackBitmap(newBitmap)
Log.d(TAG, "MediaProjection获取新图像: ${newBitmap.width}x${newBitmap.height}")
if (consecutiveImageFailures > 0) {
Log.d(TAG, "图像获取成功,重置失败计数(之前${consecutiveImageFailures}次)")
2026-02-11 16:59:49 +08:00
consecutiveImageFailures = 0
}
val copy = newBitmap.copy(Bitmap.Config.ARGB_8888, false)
if (copy != null) {
updateLastValidBitmap(copy)
}
2026-02-11 16:59:49 +08:00
return newBitmap
}
} catch (e: Exception) {
Log.e(TAG, "转换图像失败", e)
} finally {
// 确保Image被close释放ImageReader缓冲区槽位
try { image.close() } catch (_: Exception) {}
2026-02-11 16:59:49 +08:00
}
}
val waitTime = if (Build.VERSION.SDK_INT >= 35) {
if (retryCount == 0) 50L else (retryCount * 30L)
2026-02-11 16:59:49 +08:00
} else {
20L
}
Thread.sleep(waitTime)
retryCount++
}
// If no new image available, check if cache can be used
val cachedCopy3 = safeCopyLastValidBitmap()
if (cachedCopy3 != null) {
2026-02-11 16:59:49 +08:00
val timeSinceLastCapture = currentTime - lastCaptureTime
if (timeSinceLastCapture < 30000) {
Log.d(TAG, "Using cached image (${timeSinceLastCapture}ms ago) - static page")
return cachedCopy3
2026-02-11 16:59:49 +08:00
} else {
Log.w(TAG, "Cached image expired (${timeSinceLastCapture}ms ago), cleaning")
safeRecycleBitmap(cachedCopy3)
safeRecycleLastValidBitmap()
2026-02-11 16:59:49 +08:00
}
}
// 智能重新初始化逻辑:增加失败计数和多层检查
2026-02-11 16:59:49 +08:00
consecutiveImageFailures++
Log.w(TAG, "MediaProjection无新图像且无有效缓存 (连续失败${consecutiveImageFailures}次)")
// Android 15特殊处理智能重新初始化策略
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 35) {
// 首先尝试强制刷新(轻量级恢复)
if (consecutiveImageFailures <= 5) {
Log.i(TAG, "Android 15尝试轻量级强制刷新 (失败${consecutiveImageFailures}/5次)")
2026-02-11 16:59:49 +08:00
val refreshResult = forceRefreshAndroid15Images()
if (refreshResult != null) {
Log.i(TAG, "Android 15轻量级刷新成功重置失败计数")
2026-02-11 16:59:49 +08:00
consecutiveImageFailures = 0 // 重置失败计数
return refreshResult
}
}
// 检查是否应该进行重新初始化(重量级恢复)
val shouldRecreate = shouldRecreateVirtualDisplay()
if (shouldRecreate) {
Log.i(TAG, "Android 15条件满足进行VirtualDisplay重新初始化")
2026-02-11 16:59:49 +08:00
val recreateSuccess = reinitializeVirtualDisplayForAndroid15()
if (recreateSuccess) {
// 重新初始化成功,重置失败计数
consecutiveImageFailures = 0
// 重新初始化后再次尝试获取图像
Thread.sleep(500)
val retryImage = imageReader?.acquireLatestImage()
if (retryImage != null) {
try {
val retryBitmap = convertImageToBitmap(retryImage)
if (retryBitmap != null) {
Log.i(TAG, "Android 15重新初始化后成功获取图像")
val retryCopy = retryBitmap.copy(Bitmap.Config.ARGB_8888, false)
if (retryCopy != null) {
updateLastValidBitmap(retryCopy)
}
2026-02-11 16:59:49 +08:00
return retryBitmap
}
} catch (e: Exception) {
Log.e(TAG, "Android 15重新初始化后图像转换失败", e)
} finally {
retryImage.close()
}
}
}
} else {
Log.i(TAG, "Android 15不满足重新初始化条件跳过VirtualDisplay重建")
2026-02-11 16:59:49 +08:00
}
} else {
// 非Android 15设备的处理
if (consecutiveImageFailures >= maxImageFailuresBeforeRecreation) {
cleanupVirtualDisplayOnly() // 只清理VirtualDisplay保留MediaProjection权限
consecutiveImageFailures = 0
}
}
return null
} catch (e: Exception) {
Log.e(TAG, "MediaProjection截图失败", e)
// 出错时只重置VirtualDisplay保留MediaProjection权限
cleanupVirtualDisplayOnly()
null
}
}
/**
* 测试MediaProjection的可用性
*/
private fun testMediaProjectionUsability(mediaProjection: android.media.projection.MediaProjection): Boolean {
return try {
Log.v(TAG, "测试MediaProjection可用性...")
2026-02-11 16:59:49 +08:00
// 尝试创建VirtualDisplay进行真实可用性测试
val displayMetrics = service.resources.displayMetrics
val virtualDisplay = mediaProjection.createVirtualDisplay(
"usability-test-display",
displayMetrics.widthPixels,
displayMetrics.heightPixels,
displayMetrics.densityDpi,
android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
null,
null,
null
)
// 立即释放测试用的VirtualDisplay
virtualDisplay?.release()
Log.v(TAG, "MediaProjection可用性测试通过")
2026-02-11 16:59:49 +08:00
true
} catch (e: SecurityException) {
Log.w(TAG, "MediaProjection可用性测试失败安全异常 - ${e.message}")
2026-02-11 16:59:49 +08:00
// 这是权限失效的明确信号
false
} catch (e: Exception) {
Log.w(TAG, "MediaProjection可用性测试失败其他异常 - ${e.message}")
2026-02-11 16:59:49 +08:00
false
}
}
/**
* 设置MediaProjection相关资源 - Android 15适配单次令牌限制
*/
private fun setupMediaProjectionResources() {
try {
Log.i(TAG, "初始化MediaProjection资源 - 屏幕尺寸: ${screenWidth}x${screenHeight}")
// Android 15检查如果MediaProjection已经创建过VirtualDisplay需要重新获取
if (Build.VERSION.SDK_INT >= 35 && mediaProjection != null) {
Log.i(TAG, "Android 15检查验证MediaProjection令牌可用性")
2026-02-11 16:59:49 +08:00
if (isMediaProjectionTokenUsed()) {
Log.w(TAG, "Android 15令牌已使用需要重新获取MediaProjection")
2026-02-11 16:59:49 +08:00
if (!regenerateMediaProjectionForAndroid15()) {
Log.e(TAG, "Android 15重新生成MediaProjection失败")
2026-02-11 16:59:49 +08:00
return
}
}
}
// 只清理VirtualDisplay和ImageReader保留MediaProjection权限
cleanupVirtualDisplayOnly()
// 创建ImageReaderbufferCount至少4个
// acquireLatestImage()内部需要先acquireNextImage再遍历找最新帧
// bufferCount=2时如果有1个未close的Image就会触发maxImages异常
2026-02-11 16:59:49 +08:00
val bufferCount = if (Build.VERSION.SDK_INT >= 35) {
5 // Android 15需要更多缓存确保图像连续性
2026-02-11 16:59:49 +08:00
} else {
4 // 其他版本也需要足够缓存避免acquireLatestImage的maxImages异常
2026-02-11 16:59:49 +08:00
}
imageReader = android.media.ImageReader.newInstance(
screenWidth, screenHeight,
android.graphics.PixelFormat.RGBA_8888, bufferCount
)
// Android 15添加监听器来监控图像可用性
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 35) {
imageReader?.setOnImageAvailableListener({ reader ->
Log.v(TAG, "Android 15: ImageReader有新图像可用")
2026-02-11 16:59:49 +08:00
}, backgroundHandler)
}
Log.i(TAG, "ImageReader创建完成")
// 创建VirtualDisplay
2026-02-11 16:59:49 +08:00
virtualDisplay = mediaProjection?.createVirtualDisplay(
"RemoteControlCapture",
screenWidth, screenHeight,
service.resources.displayMetrics.densityDpi,
android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
imageReader?.surface, null, null
2026-02-11 16:59:49 +08:00
)
if (virtualDisplay != null) {
Log.i(TAG, "VirtualDisplay创建成功")
// 验证Surface有效性防止BufferQueue已被abandoned
val createdSurface = imageReader?.surface
if (createdSurface == null || !createdSurface.isValid) {
Log.e(TAG, "VirtualDisplay创建后Surface无效(null=${createdSurface == null}, isValid=${createdSurface?.isValid}), 清理资源")
// 释放顺序先VirtualDisplay再ImageReader
val failVd = virtualDisplay
val failIr = imageReader
virtualDisplay = null
imageReader = null
try { failVd?.release() } catch (_: Exception) {}
try { failIr?.close() } catch (_: Exception) {}
return
}
// Android 15VirtualDisplay创建成功后触发权限申请处理
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "Android 15VirtualDisplay创建成功触发权限申请处理")
2026-02-11 16:59:49 +08:00
triggerAndroid15PermissionRequest("首次初始化VirtualDisplay")
}
// Android 15标记session为已使用
2026-02-11 16:59:49 +08:00
markAndroid15SessionUsed()
} else {
Log.e(TAG, "VirtualDisplay创建失败")
return
}
Log.i(TAG, "MediaProjection资源初始化完成等待画面稳定...")
// Android 15需要更长的稳定时间
2026-02-11 16:59:49 +08:00
val stabilizeTime = if (Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "Android 15设备等待更长时间确保VirtualDisplay稳定")
2026-02-11 16:59:49 +08:00
2000L // Android 15需要2秒稳定时间从1.2秒增加到2秒
} else {
500L // 其他版本500ms
}
Thread.sleep(stabilizeTime)
// Android 15额外处理强制刷新Surface确保连接
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 35) {
try {
Log.i(TAG, "Android 15强制刷新Surface连接")
2026-02-11 16:59:49 +08:00
// 通过重新设置Surface来确保连接
imageReader?.surface?.let { surface ->
// 触发Surface刷新
surface.toString() // 这会触发Surface的内部状态检查
}
Thread.sleep(300) // 给Surface刷新时间
} catch (e: Exception) {
Log.w(TAG, "Android 15 Surface刷新失败${e.message}")
2026-02-11 16:59:49 +08:00
}
}
// Android 15验证尝试预先获取一帧图像确保VirtualDisplay工作正常
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "Android 15验证预先测试图像获取...")
2026-02-11 16:59:49 +08:00
try {
val testImage = imageReader?.acquireLatestImage()
if (testImage != null) {
Log.i(TAG, "Android 15 VirtualDisplay预验证成功")
2026-02-11 16:59:49 +08:00
testImage.close()
} else {
Log.w(TAG, "Android 15 VirtualDisplay预验证暂无图像但这可能是正常的")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.w(TAG, "Android 15 VirtualDisplay预验证异常${e.message}")
2026-02-11 16:59:49 +08:00
}
}
} catch (e: SecurityException) {
Log.e(TAG, "初始化MediaProjection资源失败权限问题", e)
// Android 15特殊处理检查是否是session重复使用错误
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 35 && e.message?.contains("non-current") == true) {
Log.w(TAG, "Android 15错误MediaProjection session已失效或被重复使用")
Log.w(TAG, "需要重新申请用户权限因为Android 15每个session只能使用一次")
2026-02-11 16:59:49 +08:00
// 标记session为已使用
markAndroid15SessionUsed()
// 清理资源并触发重新申请
cleanupVirtualDisplayOnly()
// 通知权限丢失,需要重新申请
Log.i(TAG, "通知权限丢失,触发重新申请流程")
2026-02-11 16:59:49 +08:00
onMediaProjectionLost()
} else {
Log.w(TAG, "检测到其他权限问题,但不立即重新申请权限")
// 保守处理:只清理资源,不触发权限重新申请
2026-02-11 16:59:49 +08:00
cleanupVirtualDisplayOnly()
// 记录权限问题,但不采取激进措施
Log.i(TAG, "权限问题已记录,保持保守策略")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "初始化MediaProjection资源失败其他异常", e)
cleanupVirtualDisplayOnly()
}
}
/**
* 将Image转换为Bitmap
*/
private fun convertImageToBitmap(image: android.media.Image): Bitmap? {
return try {
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * screenWidth
val bitmap = Bitmap.createBitmap(
screenWidth + rowPadding / pixelStride,
screenHeight, Bitmap.Config.ARGB_8888
)
bitmap.copyPixelsFromBuffer(buffer)
// If there is padding, crop and recycle the original
2026-02-11 16:59:49 +08:00
if (rowPadding != 0) {
val cropped = Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight)
if (cropped !== bitmap) {
bitmap.recycle()
}
cropped
2026-02-11 16:59:49 +08:00
} else {
bitmap
}
} catch (e: Exception) {
Log.e(TAG, "convertImageToBitmap failed", e)
2026-02-11 16:59:49 +08:00
null
}
}
/**
* 生成实时测试图像增强版
*/
private fun generateRealtimeTestImage(): Bitmap? {
return try {
val testBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(testBitmap)
// 绘制一个模拟真实应用的测试图案
val paint = android.graphics.Paint().apply {
isAntiAlias = true
}
// 背景渐变
val time = System.currentTimeMillis()
val hue = (time / 50) % 360
canvas.drawColor(android.graphics.Color.HSVToColor(floatArrayOf(hue.toFloat(), 0.3f, 0.9f)))
// 标题
paint.apply {
textSize = 60f
color = android.graphics.Color.WHITE
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
canvas.drawText("实时屏幕捕获", 50f, 100f, paint)
// 设备信息
paint.apply {
textSize = 40f
color = android.graphics.Color.BLACK
typeface = android.graphics.Typeface.DEFAULT
}
canvas.drawText("分辨率: ${screenWidth}x${screenHeight}", 50f, 180f, paint)
canvas.drawText("帧率: ${CAPTURE_FPS} FPS", 50f, 240f, paint)
canvas.drawText("时间: ${java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())}", 50f, 300f, paint)
// 动态元素
val frameCount = (time / 100) % 1000
canvas.drawText("帧数: $frameCount", 50f, 360f, paint)
// 移动的几何图形
paint.color = android.graphics.Color.RED
val x1 = ((time / 20) % screenWidth).toFloat()
canvas.drawCircle(x1, 450f, 30f, paint)
paint.color = android.graphics.Color.GREEN
val x2 = ((time / 30) % screenWidth).toFloat()
canvas.drawRect(x2, 520f, x2 + 60f, 580f, paint)
paint.color = android.graphics.Color.BLUE
val x3 = ((time / 40) % screenWidth).toFloat()
val y3 = 650f + 50f * kotlin.math.sin(time / 200.0).toFloat()
canvas.drawCircle(x3, y3, 25f, paint)
// 模拟状态栏
paint.color = android.graphics.Color.BLACK
paint.alpha = 200
canvas.drawRect(0f, 0f, screenWidth.toFloat(), 80f, paint)
paint.apply {
color = android.graphics.Color.WHITE
alpha = 255
textSize = 30f
}
canvas.drawText("Android ${Build.VERSION.RELEASE}", 20f, 50f, paint)
canvas.drawText("100%", screenWidth - 150f, 50f, paint)
2026-02-11 16:59:49 +08:00
Log.d(TAG, "生成实时测试图像: ${screenWidth}x${screenHeight}")
testBitmap
} catch (e: Exception) {
Log.e(TAG, "生成实时测试图像失败", e)
generateTestImage() // 回退到简单测试图像
}
}
/**
* Android 15静默恢复已废弃因为session只能使用一次
*/
private fun attemptAndroid15SilentRecovery(): Boolean {
Log.w(TAG, "Android 15无法静默恢复每个MediaProjection session只能使用一次")
Log.w(TAG, "需要重新申请用户权限才能继续使用屏幕捕获")
2026-02-11 16:59:49 +08:00
return false
}
/**
* Android 15新增标记session为已使用
*/
private fun markAndroid15SessionUsed() {
try {
if (Build.VERSION.SDK_INT >= 35) {
// 如果有Android 15专用管理器调用其方法
val accessibilityService = com.hikoncont.service.AccessibilityRemoteService.getInstance()
val android15Manager = accessibilityService?.getAndroid15MediaProjectionManager()
android15Manager?.markSessionUsed()
Log.i(TAG, "已标记Android 15 session为已使用")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "标记Android 15 session失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* Android 15新增权限丢失通知
*/
private fun onMediaProjectionLost() {
try {
Log.w(TAG, "MediaProjection权限丢失通知AccessibilityService")
2026-02-11 16:59:49 +08:00
val accessibilityService = com.hikoncont.service.AccessibilityRemoteService.getInstance()
if (accessibilityService != null) {
// 通过广播通知权限丢失
val intent = Intent("android.mycustrecev.MEDIA_PROJECTION_LOST")
intent.putExtra("reason", "android15_session_used")
intent.putExtra("requireNewPermission", true)
service.sendBroadcast(intent)
Log.i(TAG, "已发送MediaProjection权限丢失广播")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "通知MediaProjection权限丢失失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 暂停屏幕捕获但保留权限用于重连时避免发送失败
*/
fun pauseCapture() {
try {
Log.i(TAG, "暂停屏幕捕获(保留权限)")
isCapturing = false
isPaused = true
// 持久化暂停状态
2026-02-11 16:59:49 +08:00
savePauseState()
// 只清理VirtualDisplay保留MediaProjection权限
cleanupVirtualDisplayOnly()
Log.i(TAG, "屏幕捕获已暂停,权限保留,状态已持久化")
} catch (e: Exception) {
Log.e(TAG, "暂停屏幕捕获失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 恢复屏幕捕获
*/
fun resumeCapture() {
try {
if (!isPaused) {
Log.d(TAG, "屏幕捕获未暂停,无需恢复")
return
}
Log.i(TAG, "恢复屏幕捕获")
isPaused = false
// 持久化恢复状态
2026-02-11 16:59:49 +08:00
savePauseState()
startCapture()
} catch (e: Exception) {
Log.e(TAG, "恢复屏幕捕获失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 检查是否暂停
*/
fun isPaused(): Boolean {
return isPaused
}
/**
* 获取最后成功发送时间
*/
fun getLastSuccessfulSendTime(): Long? {
return lastSuccessfulSendTime
}
/**
* 停止屏幕捕获
*/
fun stopCapture() {
try {
Log.i(TAG, "停止屏幕捕获")
isCapturing = false
isPaused = false // 清理暂停状态
// 清理持久化的暂停状态
2026-02-11 16:59:49 +08:00
try {
val sp = service.getSharedPreferences(PAUSE_STATE_PREF, android.content.Context.MODE_PRIVATE)
sp.edit().putBoolean(KEY_IS_PAUSED, false).apply()
Log.d(TAG, "已清理持久化的暂停状态")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.w(TAG, "清理持久化暂停状态失败", e)
2026-02-11 16:59:49 +08:00
}
// 停止队列处理协程
2026-02-11 16:59:49 +08:00
stopQueueProcessor()
// 修复只清理VirtualDisplay保留MediaProjection权限防止Android 15权限丢失
2026-02-11 16:59:49 +08:00
// 正常停止时不应该销毁权限,只有用户主动停止或应用退出时才销毁
cleanupVirtualDisplayOnly() // 只清理显示资源,保留权限
Log.i(TAG, "屏幕捕获已停止(权限保留)")
// 不关闭截图执行器因为stopCapture后可能还会startCapture
// 执行器只在release/forceRelease时关闭
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "停止屏幕捕获失败", e)
}
}
/**
* 强制停止屏幕捕获并清理权限仅在用户主动停止时使用
*/
fun forceStopCapture() {
try {
Log.i(TAG, "强制停止屏幕捕获")
isCapturing = false
isPaused = false // 清理暂停状态
// 清理持久化的暂停状态
2026-02-11 16:59:49 +08:00
try {
val sp = service.getSharedPreferences(PAUSE_STATE_PREF, android.content.Context.MODE_PRIVATE)
sp.edit().putBoolean(KEY_IS_PAUSED, false).apply()
Log.d(TAG, "已清理持久化的暂停状态")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.w(TAG, "清理持久化暂停状态失败", e)
2026-02-11 16:59:49 +08:00
}
// 停止队列处理协程
2026-02-11 16:59:49 +08:00
stopQueueProcessor()
// 强制清理MediaProjection权限
forceCleanupMediaProjectionResources()
Log.i(TAG, "屏幕捕获已强制停止")
// 不关闭截图执行器由release/forceRelease统一管理
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "强制停止屏幕捕获失败", e)
}
}
/**
* 设置MediaProjection
*/
fun setMediaProjection(projection: android.media.projection.MediaProjection) {
Log.i(TAG, "MediaProjection已设置到ScreenCaptureManager")
2026-02-11 16:59:49 +08:00
this.mediaProjection = projection
MediaProjectionHolder.setMediaProjection(projection)
}
/**
* 智能压缩Bitmap为JPEG - 动态调整质量以平衡画质和传输效率
2026-02-11 16:59:49 +08:00
*/
private fun compressBitmap(bitmap: Bitmap): ByteArray {
val outputStream = ByteArrayOutputStream()
try {
// 计算缩放比例,确保不超过最大尺寸
val scaledBitmap = scaleDownBitmap(bitmap)
// 智能压缩:根据数据大小动态调整质量
2026-02-11 16:59:49 +08:00
var quality = dynamicQuality
var compressedData: ByteArray
var attempts = 0
val maxAttempts = 3
val targetSize = 150 * 1024 // 目标150KB平衡质量和传输效率
do {
outputStream.reset()
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
compressedData = outputStream.toByteArray()
attempts++
if (compressedData.size > targetSize && attempts < maxAttempts) {
// 数据过大,降低质量重新压缩
quality = maxOf(25, quality - 15) // 最低不低于25
Log.v(TAG, "数据过大(${compressedData.size} bytes),降低质量到$quality 重新压缩")
2026-02-11 16:59:49 +08:00
} else {
break
}
} while (attempts < maxAttempts)
// 如果缩放了释放缩放后的bitmap
if (scaledBitmap != bitmap) {
scaledBitmap.recycle()
}
Log.d(TAG, "智能压缩完成: 原始${bitmap.width}x${bitmap.height} -> 输出${compressedData.size} bytes (质量$quality, 尝试${attempts}次)")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "智能压缩失败,使用基础压缩", e)
2026-02-11 16:59:49 +08:00
// 如果智能压缩失败,使用基础压缩
outputStream.reset()
bitmap.compress(Bitmap.CompressFormat.JPEG, dynamicQuality, outputStream)
}
return outputStream.toByteArray()
}
/**
* 缩放图片以控制数据大小
*/
private fun scaleDownBitmap(bitmap: Bitmap): Bitmap {
val originalWidth = bitmap.width
val originalHeight = bitmap.height
// 如果已经在限制范围内,直接返回
if (originalWidth <= dynamicMaxWidth && originalHeight <= dynamicMaxHeight) {
return bitmap
}
// 计算缩放比例,保持宽高比
val widthRatio = dynamicMaxWidth.toFloat() / originalWidth
val heightRatio = dynamicMaxHeight.toFloat() / originalHeight
val scaleFactor = minOf(widthRatio, heightRatio)
val newWidth = (originalWidth * scaleFactor).toInt()
val newHeight = (originalHeight * scaleFactor).toInt()
Log.d(TAG, "缩放图片: ${originalWidth}x${originalHeight} -> ${newWidth}x${newHeight} (scale=${scaleFactor})")
return try {
Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
} catch (e: Exception) {
Log.e(TAG, "缩放图片失败,使用原图", e)
bitmap
}
}
/**
* 发送帧数据到服务器
*/
private fun sendFrameToServer(frameData: ByteArray) {
try {
totalFrameCount++
// 启动队列处理器(只启动一次)
2026-02-11 16:59:49 +08:00
startQueueProcessor()
// 优化队列清理策略,减少画面跳跃,提供更平滑的体验
2026-02-11 16:59:49 +08:00
when {
screenDataQueue.size >= 13 -> {
// 队列接近满载适度清理2帧保持流畅避免5帧跳跃
Log.w(TAG, "队列接近满载(${screenDataQueue.size}/15),轻度清理旧数据保持流畅")
2026-02-11 16:59:49 +08:00
repeat(2) {
screenDataQueue.poll()?.let {
droppedFrameCount++
}
}
}
screenDataQueue.size >= 11 -> {
// 中等负载清理1帧防止积压
Log.v(TAG, "队列中等负载(${screenDataQueue.size}/15)清理1帧防止积压")
2026-02-11 16:59:49 +08:00
screenDataQueue.poll()?.let {
droppedFrameCount++
}
}
}
// 尝试添加新帧,如果满了就丢弃旧帧
if (!screenDataQueue.offer(frameData)) {
// 队列已满,丢弃最旧的帧并添加新帧
screenDataQueue.poll()?.let {
droppedFrameCount++
}
if (!screenDataQueue.offer(frameData)) {
Log.e(TAG, "队列清理后仍无法添加新帧,跳过此帧")
2026-02-11 16:59:49 +08:00
droppedFrameCount++
return
}
}
// 定期内存检查和清理
2026-02-11 16:59:49 +08:00
checkAndCleanMemory()
} catch (e: Exception) {
Log.e(TAG, "发送帧数据失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 启动单一的队列处理协程避免协程泄漏
2026-02-11 16:59:49 +08:00
*/
private fun startQueueProcessor() {
if (queueProcessingStarted.compareAndSet(false, true)) {
Log.d(TAG, "启动屏幕数据队列处理协程")
2026-02-11 16:59:49 +08:00
queueProcessorJob = serviceScope.launch {
try {
while (isCapturing && isActive) {
try {
val frameData = screenDataQueue.poll()
if (frameData != null) {
processFrameData(frameData)
} else {
delay(10)
}
} catch (e: CancellationException) {
// 协程取消是正常流程,直接传播退出循环
throw e
} catch (e: Exception) {
Log.e(TAG, "队列处理协程异常", e)
delay(100)
2026-02-11 16:59:49 +08:00
}
}
} catch (e: CancellationException) {
Log.d(TAG, "队列处理协程被取消(正常停止)")
} finally {
Log.d(TAG, "屏幕数据队列处理协程结束")
queueProcessingStarted.set(false)
2026-02-11 16:59:49 +08:00
}
}
}
}
/**
* 停止队列处理协程
2026-02-11 16:59:49 +08:00
*/
private fun stopQueueProcessor() {
try {
Log.d(TAG, "停止屏幕数据队列处理协程")
2026-02-11 16:59:49 +08:00
queueProcessorJob?.cancel()
queueProcessorJob = null
queueProcessingStarted.set(false)
// 清空队列中剩余的数据
val remainingFrames = screenDataQueue.size
screenDataQueue.clear()
if (remainingFrames > 0) {
Log.d(TAG, "清空队列中剩余的${remainingFrames}帧数据")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "停止队列处理协程失败", e)
2026-02-11 16:59:49 +08:00
}
}
// Socket unavailable log throttle counter
private var socketUnavailableLogCount = 0
2026-02-11 16:59:49 +08:00
/**
* Process single frame data.
* Throttles "socket unavailable" log to avoid flooding logcat
* when connection is down but capture is still running.
2026-02-11 16:59:49 +08:00
*/
private suspend fun processFrameData(frameData: ByteArray) {
try {
var success = false
2026-02-11 16:59:49 +08:00
val socketIOManager = service.getSocketIOManager()
if (socketIOManager != null && socketIOManager.isConnected()) {
socketIOManager.sendScreenData(frameData)
Log.v(TAG, "Socket.IO sent frame: ${frameData.size} bytes")
2026-02-11 16:59:49 +08:00
success = true
// Reset throttle counter on success
if (socketUnavailableLogCount > 0) {
Log.i(TAG, "Socket.IO connection restored, " +
"skipped $socketUnavailableLogCount frames while disconnected")
socketUnavailableLogCount = 0
}
2026-02-11 16:59:49 +08:00
} else {
socketUnavailableLogCount++
// Log first occurrence, then every 50th to avoid flooding
if (socketUnavailableLogCount == 1 || socketUnavailableLogCount % 50 == 0) {
Log.w(TAG, "Socket.IO unavailable, cannot send screen data " +
"(count=$socketUnavailableLogCount)")
}
2026-02-11 16:59:49 +08:00
}
2026-02-11 16:59:49 +08:00
if (success) {
lastSuccessfulSendTime = System.currentTimeMillis()
}
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "Process frame data failed", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 内存检查和清理机制
2026-02-11 16:59:49 +08:00
*/
private fun checkAndCleanMemory() {
val currentTime = System.currentTimeMillis()
if (currentTime - lastMemoryCheckTime < memoryCheckInterval) {
return
}
lastMemoryCheckTime = currentTime
try {
// 获取内存使用情况
val runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory()
val totalMemory = runtime.totalMemory()
val freeMemory = runtime.freeMemory()
val usedMemory = totalMemory - freeMemory
val memoryUsagePercent = usedMemory.toFloat() / maxMemory.toFloat()
Log.d(TAG, "内存使用: ${(memoryUsagePercent * 100).toInt()}% (${usedMemory / 1024 / 1024}MB / ${maxMemory / 1024 / 1024}MB)")
2026-02-11 16:59:49 +08:00
// 内存使用率过高时触发清理
2026-02-11 16:59:49 +08:00
if (memoryUsagePercent > maxMemoryUsagePercent) {
Log.w(TAG, "内存使用率过高(${(memoryUsagePercent * 100).toInt()}%),触发紧急清理")
2026-02-11 16:59:49 +08:00
performEmergencyCleanup()
}
// 定期清理无效的WeakReference
2026-02-11 16:59:49 +08:00
cleanupWeakReferences()
// 每分钟记录一次统计信息
2026-02-11 16:59:49 +08:00
if (totalFrameCount % 150 == 0L) { // 5fps150帧约30秒
val dropRate = if (totalFrameCount > 0) droppedFrameCount.toFloat() / totalFrameCount.toFloat() else 0f
Log.i(TAG, "帧统计: 总帧数=$totalFrameCount, 丢弃帧数=$droppedFrameCount, 丢帧率=${(dropRate * 100).toInt()}%")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "内存检查失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 紧急内存清理
2026-02-11 16:59:49 +08:00
*/
private fun performEmergencyCleanup() {
try {
Log.w(TAG, "执行紧急内存清理")
2026-02-11 16:59:49 +08:00
// 1. 清空屏幕数据队列
val queueSize = screenDataQueue.size
screenDataQueue.clear()
Log.w(TAG, "清空屏幕数据队列,释放${queueSize}帧数据")
2026-02-11 16:59:49 +08:00
// 2. 清理缓存的图像
safeRecycleLastValidBitmap()
Log.w(TAG, "清理缓存图像")
2026-02-11 16:59:49 +08:00
// 3. 强制回收弱引用中的资源
cleanupWeakReferences(true)
// 4. 降低临时图像质量(如果需要的话)
Log.w(TAG, "临时降低图像质量以减少内存压力")
2026-02-11 16:59:49 +08:00
// 5. 建议系统进行垃圾回收
System.gc()
Log.w(TAG, "紧急内存清理完成")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "紧急内存清理失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 清理WeakReference
2026-02-11 16:59:49 +08:00
*/
private fun cleanupWeakReferences(force: Boolean = false) {
try {
// 清理已回收的Bitmap引用
val bitmapIterator = activeBitmaps.iterator()
var cleanedBitmaps = 0
while (bitmapIterator.hasNext()) {
val ref = bitmapIterator.next()
val bitmap = ref.get()
if (bitmap == null || bitmap.isRecycled || (force && !bitmap.isRecycled)) {
if (force && bitmap != null && !bitmap.isRecycled) {
bitmap.recycle()
}
bitmapIterator.remove()
cleanedBitmaps++
}
}
// 清理已回收的Image引用
val imageIterator = activeImages.iterator()
var cleanedImages = 0
while (imageIterator.hasNext()) {
val ref = imageIterator.next()
val image = ref.get()
if (image == null || (force && image != null)) {
if (force && image != null) {
try {
image.close()
} catch (e: Exception) {
// Image可能已经关闭
}
}
imageIterator.remove()
cleanedImages++
}
}
if (cleanedBitmaps > 0 || cleanedImages > 0) {
Log.d(TAG, "清理WeakReference: Bitmap=${cleanedBitmaps}, Image=${cleanedImages}")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "清理WeakReference失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 跟踪Bitmap资源
2026-02-11 16:59:49 +08:00
*/
private fun trackBitmap(bitmap: Bitmap): Bitmap {
activeBitmaps.add(WeakReference(bitmap))
return bitmap
}
/**
* 跟踪Image资源
2026-02-11 16:59:49 +08:00
*/
private fun trackImage(image: android.media.Image): android.media.Image {
activeImages.add(WeakReference(image))
return image
}
/**
* 降级捕获方案 - 测试图像
*/
private fun startFallbackCapture() {
Log.i(TAG, "启动降级捕获方案")
captureScope.launch {
isCapturing = true
while (isCapturing) {
try {
// 使用测试图像
val screenshot = generateTestImage()
if (screenshot != null) {
val jpegData = compressBitmap(screenshot)
sendFrameToServer(jpegData)
screenshot.recycle()
}
// 控制帧率
delay(1000 / CAPTURE_FPS.toLong())
} catch (e: Exception) {
Log.e(TAG, "降级捕获失败", e)
delay(1000) // 出错时延长间隔
}
}
}
}
/**
* 生成权限恢复提示测试图像
2026-02-11 16:59:49 +08:00
*/
private fun generatePermissionRecoveryTestImage(): Bitmap? {
return try {
val testBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(testBitmap)
// 绘制权限恢复提示界面
val paint = android.graphics.Paint().apply {
isAntiAlias = true
}
// 橙色警告背景
canvas.drawColor(android.graphics.Color.rgb(255, 193, 7))
// 标题
paint.apply {
textSize = 70f
color = android.graphics.Color.WHITE
typeface = android.graphics.Typeface.DEFAULT_BOLD
textAlign = android.graphics.Paint.Align.CENTER
}
canvas.drawText("权限恢复中", screenWidth / 2f, 150f, paint)
2026-02-11 16:59:49 +08:00
// 状态信息
paint.apply {
textSize = 45f
color = android.graphics.Color.BLACK
typeface = android.graphics.Typeface.DEFAULT
}
canvas.drawText("检测到屏幕录制权限丢失", screenWidth / 2f, 250f, paint)
canvas.drawText("正在自动重新申请权限...", screenWidth / 2f, 320f, paint)
// 动态进度指示
val time = System.currentTimeMillis()
val progress = ((time / 500) % 4).toInt()
val progressText = "请稍候" + ".".repeat(progress + 1)
paint.apply {
textSize = 50f
color = android.graphics.Color.rgb(220, 53, 69)
}
canvas.drawText(progressText, screenWidth / 2f, 420f, paint)
// 说明文字
paint.apply {
textSize = 35f
color = android.graphics.Color.BLACK
textAlign = android.graphics.Paint.Align.CENTER
}
canvas.drawText("• 连接状态正常,控制功能可用", screenWidth / 2f, 520f, paint)
canvas.drawText("• 屏幕显示将在权限恢复后正常", screenWidth / 2f, 570f, paint)
canvas.drawText("• 如有权限弹窗请点击允许", screenWidth / 2f, 620f, paint)
// 旋转的恢复图标
val centerX = screenWidth / 2f
val centerY = 750f
val radius = 40f
val rotation = (time / 50f) % 360f
canvas.save()
canvas.rotate(rotation, centerX, centerY)
paint.apply {
color = android.graphics.Color.WHITE
strokeWidth = 8f
style = android.graphics.Paint.Style.STROKE
}
canvas.drawCircle(centerX, centerY, radius, paint)
// 箭头
paint.style = android.graphics.Paint.Style.FILL
val arrowPath = android.graphics.Path().apply {
moveTo(centerX + radius - 5, centerY - 15)
lineTo(centerX + radius + 10, centerY)
lineTo(centerX + radius - 5, centerY + 15)
close()
}
canvas.drawPath(arrowPath, paint)
canvas.restore()
// 时间戳
paint.apply {
textSize = 25f
color = android.graphics.Color.GRAY
textAlign = android.graphics.Paint.Align.CENTER
}
canvas.drawText("${java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}", screenWidth / 2f, screenHeight - 50f, paint)
Log.d(TAG, "生成权限恢复提示图像: ${screenWidth}x${screenHeight}")
testBitmap
} catch (e: Exception) {
Log.e(TAG, "生成权限恢复提示图像失败", e)
generateRealtimeTestImage() // 回退到普通测试图像
}
}
/**
* 生成测试图像
*/
private fun generateTestImage(): Bitmap? {
return try {
val testBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(testBitmap)
// 绘制一个简单的测试图案
val paint = android.graphics.Paint().apply {
textSize = 50f
color = android.graphics.Color.WHITE
}
canvas.drawColor(android.graphics.Color.BLACK)
canvas.drawText("无障碍服务截图API测试", 50f, 100f, paint)
canvas.drawText("Screen: ${screenWidth}x${screenHeight}", 50f, 200f, paint)
canvas.drawText("Time: ${java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}", 50f, 300f, paint)
canvas.drawText("Frame: ${System.currentTimeMillis() % 10000}", 50f, 400f, paint)
// 绘制一个移动的圆点来显示动画效果
val time = System.currentTimeMillis()
val x = ((time / 10) % screenWidth).toFloat()
val y = 500f
paint.color = android.graphics.Color.RED
canvas.drawCircle(x, y, 20f, paint)
Log.d(TAG, "生成测试屏幕图像: ${screenWidth}x${screenHeight}")
testBitmap
} catch (e: Exception) {
Log.e(TAG, "生成测试图像失败", e)
null
}
}
/**
* 获取当前屏幕截图 (兼容性方法)
*/
fun captureScreenshot(): Bitmap? {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 这里可以实现同步截图现在返回null
null
} else {
generateTestImage()
}
} catch (e: Exception) {
Log.e(TAG, "截图失败", e)
null
}
}
/**
* 触发智能权限恢复机制
*
* 核心修复优先从 Holder 获取已有对象
* 避免通过 SmartManager 重复创建新实例导致旧实例被 stop
2026-02-11 16:59:49 +08:00
*/
private fun triggerPermissionRecovery() {
try {
Log.i(TAG, "尝试权限恢复")
// 第一步:从 Holder 获取已有对象
val holderProjection = com.hikoncont.MediaProjectionHolder.getMediaProjection()
if (holderProjection != null) {
Log.i(TAG, "Holder中已有有效MediaProjection直接复用")
setMediaProjection(holderProjection)
return
}
2026-02-11 16:59:49 +08:00
// 第二步:从智能管理器获取
2026-02-11 16:59:49 +08:00
val smartManager = com.hikoncont.manager.SmartMediaProjectionManager.getInstance(service)
val currentProjection = smartManager.getCurrentMediaProjection()
if (currentProjection != null) {
Log.i(TAG, "智能管理器找到有效的MediaProjection直接使用")
2026-02-11 16:59:49 +08:00
setMediaProjection(currentProjection)
return
}
// 第三步:都没有有效对象,回退到传统恢复(重新申请权限)
Log.w(TAG, "无有效MediaProjection对象回退到传统恢复机制")
2026-02-11 16:59:49 +08:00
triggerTraditionalPermissionRecovery()
} catch (e: Exception) {
Log.e(TAG, "权限恢复失败,回退到传统方式", e)
2026-02-11 16:59:49 +08:00
triggerTraditionalPermissionRecovery()
}
}
/**
* 传统权限恢复机制作为智能恢复的备用方案
*/
private fun triggerTraditionalPermissionRecovery() {
try {
Log.i(TAG, "开始传统MediaProjection权限恢复流程")
2026-02-11 16:59:49 +08:00
// 检查权限申请状态避免在权限申请达到上限时继续启动MainActivity
2026-02-11 16:59:49 +08:00
val accessibilityService = com.hikoncont.service.AccessibilityRemoteService.getInstance()
if (accessibilityService != null) {
if (accessibilityService.areAllPermissionsCompleted() || accessibilityService.isPermissionRequestInProgress()) {
Log.i(TAG, "权限申请已完成或正在进行中,跳过权限恢复流程")
2026-02-11 16:59:49 +08:00
return
}
}
// 使用协程避免阻塞主线程
captureScope.launch {
try {
// 修复权限恢复时不要清理MediaProjection权限
2026-02-11 16:59:49 +08:00
// 只清理VirtualDisplay等显示资源保留权限以防Android 15权限丢失
cleanupVirtualDisplayOnly() // 只清理显示资源,保留权限
Log.i(TAG, "启动MainActivity重新申请MediaProjection权限保留现有权限防止丢失")
2026-02-11 16:59:49 +08:00
// 启动MainActivity重新申请权限
val intent = android.content.Intent(service, com.hikoncont.MainActivity::class.java).apply {
addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra("AUTO_REQUEST_PERMISSION", true) // 标记为自动权限申请
putExtra("PERMISSION_LOST_RECOVERY", true) // 标记为权限丢失恢复
putExtra("TRADITIONAL_RECOVERY", true) // 标记为传统恢复模式
}
service.startActivity(intent)
Log.i(TAG, "传统MediaProjection权限恢复流程已启动")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "传统MediaProjection权限恢复失败", e)
2026-02-11 16:59:49 +08:00
}
}
} catch (e: Exception) {
Log.e(TAG, "触发传统权限恢复失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 只清理VirtualDisplay和ImageReader保留MediaProjection权限
*
* 释放顺序至关重要必须先释放VirtualDisplay消费者再关闭ImageReader生产者
* 如果先关闭ImageReader其内部Surface的BufferQueue会被abandon
* 但VirtualDisplay仍持有该Surface引用并继续dequeueBuffer
* 导致大量"BufferQueue has been abandoned"错误日志洪泛最终进程崩溃
2026-02-11 16:59:49 +08:00
*/
private fun cleanupVirtualDisplayOnly() {
try {
// 第一步保存引用并立即置null防止其他线程继续使用
val vd = virtualDisplay
val ir = imageReader
2026-02-11 16:59:49 +08:00
virtualDisplay = null
imageReader = null
2026-02-11 16:59:49 +08:00
// 第二步先释放VirtualDisplay消费者断开与Surface的连接
try {
vd?.release()
} catch (e: Exception) {
Log.w(TAG, "释放VirtualDisplay异常: ${e.message}")
}
// 第三步再关闭ImageReader生产者此时Surface不再被引用
try {
ir?.close()
} catch (e: Exception) {
Log.w(TAG, "关闭ImageReader异常: ${e.message}")
}
// 第四步安全清理缓存的Bitmap
safeRecycleLastValidBitmap()
2026-02-11 16:59:49 +08:00
lastCaptureTime = 0L
Log.i(TAG, "VirtualDisplay和ImageReader已清理MediaProjection权限保留")
} catch (e: Exception) {
Log.e(TAG, "清理VirtualDisplay资源失败", e)
}
}
/**
* 清理MediaProjection相关资源完全清理包括权限- 修复Android 15权限丢失问题
*/
private fun cleanupMediaProjectionResources() {
try {
// 先清理VirtualDisplay和ImageReader
cleanupVirtualDisplayOnly()
// 修复不要随意停止MediaProjection权限特别是Android 15设备
2026-02-11 16:59:49 +08:00
// 这会导致权限永久失效,只有在用户主动停止或应用完全退出时才清理权限
// mediaProjection?.stop() // 删除这行,防止权限被意外停止
mediaProjection = null
Log.i(TAG, "MediaProjection资源已清理权限保留防止Android 15权限丢失")
} catch (e: Exception) {
Log.e(TAG, "清理MediaProjection资源失败", e)
}
}
/**
* 强制清理MediaProjection权限仅在用户主动停止或应用退出时使用
*/
private fun forceCleanupMediaProjectionResources() {
try {
// 先清理VirtualDisplay和ImageReader
cleanupVirtualDisplayOnly()
// 强制停止MediaProjection权限
mediaProjection?.stop()
mediaProjection = null
Log.i(TAG, "MediaProjection权限已强制清理")
} catch (e: Exception) {
Log.e(TAG, "强制清理MediaProjection资源失败", e)
}
}
/**
* 清理资源
*/
fun release() {
try {
Log.i(TAG, "清理屏幕捕获资源")
isCapturing = false
// 应用完全退出时清理显示资源,保留权限
cleanupVirtualDisplayOnly()
2026-02-11 16:59:49 +08:00
// 取消所有协程
captureScope.cancel()
// 关闭截图执行器
shutdownScreenshotExecutor()
2026-02-11 16:59:49 +08:00
// 停止后台线程
handlerThread.quitSafely()
Log.i(TAG, "屏幕捕获资源清理完成(权限保留)")
} catch (e: Exception) {
Log.e(TAG, "清理屏幕捕获资源失败", e)
}
}
/**
* 强制释放所有资源仅在应用完全退出时使用
*/
fun forceRelease() {
try {
Log.i(TAG, "强制清理屏幕捕获资源")
isCapturing = false
// 强制清理MediaProjection权限
forceCleanupMediaProjectionResources()
// 取消所有协程
captureScope.cancel()
// 关闭截图执行器
shutdownScreenshotExecutor()
2026-02-11 16:59:49 +08:00
// 停止后台线程
handlerThread.quitSafely()
Log.i(TAG, "屏幕捕获资源强制清理完成")
} catch (e: Exception) {
Log.e(TAG, "强制清理屏幕捕获资源失败", e)
}
}
/**
* 安全关闭截图执行器
*/
private fun shutdownScreenshotExecutor() {
try {
val executor = screenshotExecutor
if (!executor.isShutdown) {
executor.shutdownNow()
}
} catch (e: Exception) {
Log.w(TAG, "关闭截图执行器异常: ${e.message}")
}
}
2026-02-11 16:59:49 +08:00
/**
* Android 15检查MediaProjection令牌是否已被使用
*/
private fun isMediaProjectionTokenUsed(): Boolean {
return try {
if (Build.VERSION.SDK_INT >= 35) {
// 通过Android 15专用管理器检查
val accessibilityService = com.hikoncont.service.AccessibilityRemoteService.getInstance()
val android15Manager = accessibilityService?.getAndroid15MediaProjectionManager()
return android15Manager?.isSessionUsed() ?: false
}
false
} catch (e: Exception) {
Log.e(TAG, "检查Android 15令牌状态失败", e)
false
}
}
/**
* Android 15重新生成MediaProjection以解决单次令牌限制
*
* 核心修复只从 Holder 获取已有对象禁止重复创建
2026-02-11 16:59:49 +08:00
*/
private fun regenerateMediaProjectionForAndroid15(): Boolean {
return try {
if (Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "Android 15尝试获取可用的MediaProjection")
2026-02-11 16:59:49 +08:00
// 只从 Holder 获取已有对象
val existingProjection = MediaProjectionHolder.getMediaProjection()
if (existingProjection != null) {
Log.i(TAG, "Holder中已有有效MediaProjection直接复用")
mediaProjection = existingProjection
return true
}
// 不再重新创建!避免死循环
Log.w(TAG, "Holder中无有效MediaProjection等待权限重新授予")
2026-02-11 16:59:49 +08:00
}
false
} catch (e: Exception) {
Log.e(TAG, "Android 15获取MediaProjection异常", e)
2026-02-11 16:59:49 +08:00
false
}
}
/**
* Android 15专用重新初始化VirtualDisplay以获取图像
*/
private fun reinitializeVirtualDisplayForAndroid15(): Boolean {
virtualDisplayRecreationLock.lock()
try {
if (Build.VERSION.SDK_INT >= 35 && mediaProjection != null) {
Log.i(TAG, "Android 15重新初始化VirtualDisplay以获取图像")
2026-02-11 16:59:49 +08:00
// 更新重新创建统计
2026-02-11 16:59:49 +08:00
val currentTime = System.currentTimeMillis()
lastVirtualDisplayRecreationTime = currentTime
consecutiveRecreationCount++
// 设置重新创建状态,抑制权限保活检查
2026-02-11 16:59:49 +08:00
isVirtualDisplayRecreating = true
Log.i(TAG, "[VirtualDisplay重建] 开始重新创建(第${consecutiveRecreationCount}次),抑制权限保活检查")
2026-02-11 16:59:49 +08:00
// 清理当前的VirtualDisplay和ImageReader
// 释放顺序先VirtualDisplay消费者再ImageReader生产者
val oldVd = virtualDisplay
val oldIr = imageReader
2026-02-11 16:59:49 +08:00
virtualDisplay = null
imageReader = null
try { oldVd?.release() } catch (e: Exception) {
Log.w(TAG, "重建时释放VirtualDisplay异常: ${e.message}")
}
try { oldIr?.close() } catch (e: Exception) {
Log.w(TAG, "重建时关闭ImageReader异常: ${e.message}")
}
2026-02-11 16:59:49 +08:00
// 等待系统完全清理
Thread.sleep(500)
2026-02-11 16:59:49 +08:00
Log.i(TAG, "Android 15完全重新创建ImageReader和VirtualDisplay")
2026-02-11 16:59:49 +08:00
// 重新创建ImageReader
imageReader = android.media.ImageReader.newInstance(
screenWidth, screenHeight,
android.graphics.PixelFormat.RGBA_8888, 4 // 增加到4个缓存
)
// 添加监听器
imageReader?.setOnImageAvailableListener({ reader ->
Log.v(TAG, "Android 15重新初始化后: ImageReader有新图像可用")
2026-02-11 16:59:49 +08:00
}, backgroundHandler)
Log.i(TAG, "Android 15ImageReader重新创建完成")
2026-02-11 16:59:49 +08:00
// 重新创建VirtualDisplay
2026-02-11 16:59:49 +08:00
virtualDisplay = mediaProjection?.createVirtualDisplay(
"RemoteControlCaptureRetry_${System.currentTimeMillis()}",
screenWidth, screenHeight,
service.resources.displayMetrics.densityDpi,
android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
imageReader?.surface, null, null
2026-02-11 16:59:49 +08:00
)
if (virtualDisplay != null) {
Log.i(TAG, "Android 15 VirtualDisplay完全重新创建成功")
2026-02-11 16:59:49 +08:00
// 验证Surface有效性
val rebuiltSurface = imageReader?.surface
if (rebuiltSurface == null || !rebuiltSurface.isValid) {
Log.e(TAG, "Android 15 VirtualDisplay重建后Surface无效, 放弃")
val failVd = virtualDisplay
val failIr = imageReader
virtualDisplay = null
imageReader = null
try { failVd?.release() } catch (_: Exception) {}
try { failIr?.close() } catch (_: Exception) {}
isVirtualDisplayRecreating = false
return false
}
// Android 15VirtualDisplay重新创建成功后触发权限申请处理
Log.i(TAG, "Android 15VirtualDisplay重新创建成功触发权限申请处理")
2026-02-11 16:59:49 +08:00
triggerAndroid15PermissionRequest("重新初始化VirtualDisplay")
// 等待更长时间让新的VirtualDisplay完全稳定
Thread.sleep(1500)
// 进行二次验证
Log.i(TAG, "Android 15进行重新初始化后的验证")
2026-02-11 16:59:49 +08:00
val testImage = imageReader?.acquireLatestImage()
if (testImage != null) {
Log.i(TAG, "Android 15重新初始化后验证成功图像可用")
2026-02-11 16:59:49 +08:00
testImage.close()
} else {
Log.w(TAG, "Android 15重新初始化后验证仍无图像")
2026-02-11 16:59:49 +08:00
}
// 延迟重置状态,给系统时间完成权限检查
2026-02-11 16:59:49 +08:00
Thread.sleep(2000) // 等待2秒让系统完成所有权限检查
isVirtualDisplayRecreating = false
Log.i(TAG, "[VirtualDisplay重建] 重新创建完成,恢复权限保活检查")
2026-02-11 16:59:49 +08:00
return true
} else {
Log.e(TAG, "Android 15 VirtualDisplay重新创建失败")
2026-02-11 16:59:49 +08:00
isVirtualDisplayRecreating = false
return false
}
}
} catch (e: Exception) {
Log.e(TAG, "Android 15重新初始化VirtualDisplay失败", e)
2026-02-11 16:59:49 +08:00
// 确保异常情况下也重置状态
isVirtualDisplayRecreating = false
return false
} finally {
virtualDisplayRecreationLock.unlock()
}
return false
}
/**
* Android 15专用强制刷新图像获取
*/
private fun forceRefreshAndroid15Images(): Bitmap? {
try {
if (Build.VERSION.SDK_INT >= 35 && imageReader != null && virtualDisplay != null) {
Log.i(TAG, "Android 15尝试强制刷新图像获取")
2026-02-11 16:59:49 +08:00
// 方法1触发VirtualDisplay刷新
virtualDisplay?.surface?.let { surface ->
try {
// 强制触发surface渲染
Log.v(TAG, "Android 15触发Surface刷新")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.w(TAG, "Surface刷新失败${e.message}")
}
}
// 方法2强制等待更长时间
Thread.sleep(1000)
// 方法3尝试获取所有可用图像
var latestImage: android.media.Image? = null
var attempts = 0
while (attempts < 3) {
val image = imageReader?.acquireLatestImage()
if (image != null) {
latestImage?.close() // 关闭之前的图像
latestImage = image
Log.v(TAG, "Android 15强制刷新获得图像(尝试${attempts + 1})")
2026-02-11 16:59:49 +08:00
break
}
Thread.sleep(200)
attempts++
}
// 转换图像
if (latestImage != null) {
try {
val bitmap = convertImageToBitmap(latestImage!!)
if (bitmap != null) {
Log.i(TAG, "Android 15强制刷新成功${bitmap.width}x${bitmap.height}")
2026-02-11 16:59:49 +08:00
// 安全更新缓存
val copy = bitmap.copy(Bitmap.Config.ARGB_8888, false)
if (copy != null) {
updateLastValidBitmap(copy)
}
2026-02-11 16:59:49 +08:00
return bitmap
}
} catch (e: Exception) {
Log.e(TAG, "Android 15强制刷新图像转换失败", e)
} finally {
latestImage?.close()
}
}
Log.w(TAG, "Android 15强制刷新未获得有效图像")
2026-02-11 16:59:49 +08:00
return null
} else {
return null
}
} catch (e: Exception) {
Log.e(TAG, "Android 15强制刷新失败", e)
2026-02-11 16:59:49 +08:00
return null
}
}
/**
* Android 15设备注册成功后图像传输验证
*/
fun verifyAndroid15ImageTransmission() {
if (Build.VERSION.SDK_INT >= 35 && !android15ImageTransmissionVerified) {
Log.i(TAG, "Android 15设备注册成功开始图像传输验证...")
2026-02-11 16:59:49 +08:00
serviceScope.launch {
delay(android15ImageVerificationDelay)
try {
// 检查当前图像传输状态
val hasValidImage = checkImageTransmissionStatus()
if (!hasValidImage) {
Log.w(TAG, "Android 15图像传输验证失败尝试修复...")
2026-02-11 16:59:49 +08:00
repairAndroid15ImageTransmission()
} else {
Log.i(TAG, "Android 15图像传输验证成功")
2026-02-11 16:59:49 +08:00
android15ImageTransmissionVerified = true
}
} catch (e: Exception) {
Log.e(TAG, "Android 15图像传输验证异常", e)
2026-02-11 16:59:49 +08:00
}
}
}
}
/**
* 检查图像传输状态
*/
private fun checkImageTransmissionStatus(): Boolean {
try {
// 检查VirtualDisplay是否存在
if (!isVirtualDisplayCreated()) {
Log.w(TAG, "VirtualDisplay未创建")
2026-02-11 16:59:49 +08:00
return false
}
// 尝试获取最新图像
val latestImage = getLatestImage()
if (latestImage == null) {
Log.w(TAG, "无法获取最新图像")
2026-02-11 16:59:49 +08:00
return false
}
// 检查图像是否为有效图像(非测试图像)
val bitmap = convertImageToBitmap(latestImage)
if (bitmap == null) {
Log.w(TAG, "图像转换为Bitmap失败")
2026-02-11 16:59:49 +08:00
return false
}
// 检查图像内容是否有效(避免全黑或全白图像)
val pixels = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
val uniqueColors = pixels.distinct().size
if (uniqueColors < 10) {
Log.w(TAG, "图像内容可能无效,颜色种类过少: $uniqueColors")
2026-02-11 16:59:49 +08:00
return false
}
Log.i(TAG, "图像传输状态检查通过")
2026-02-11 16:59:49 +08:00
return true
} catch (e: Exception) {
Log.e(TAG, "检查图像传输状态失败", e)
2026-02-11 16:59:49 +08:00
return false
}
}
/**
* 修复Android 15图像传输问题
*/
private suspend fun repairAndroid15ImageTransmission() {
try {
Log.i(TAG, "开始修复Android 15图像传输...")
2026-02-11 16:59:49 +08:00
// 步骤1强制刷新Surface
Log.i(TAG, "步骤1强制刷新Surface")
2026-02-11 16:59:49 +08:00
refreshSurfaceForAndroid15()
delay(2000)
if (checkImageTransmissionStatus()) {
Log.i(TAG, "Surface刷新修复成功")
2026-02-11 16:59:49 +08:00
android15ImageTransmissionVerified = true
return
}
// 步骤2重新初始化ImageReader
Log.i(TAG, "步骤2重新初始化ImageReader")
2026-02-11 16:59:49 +08:00
reinitializeImageReaderForAndroid15()
delay(3000)
if (checkImageTransmissionStatus()) {
Log.i(TAG, "ImageReader重新初始化修复成功")
2026-02-11 16:59:49 +08:00
android15ImageTransmissionVerified = true
return
}
// 步骤3完整重建VirtualDisplay
Log.i(TAG, "步骤3完整重建VirtualDisplay")
2026-02-11 16:59:49 +08:00
recreateVirtualDisplayForAndroid15()
delay(3000)
if (checkImageTransmissionStatus()) {
Log.i(TAG, "VirtualDisplay重建修复成功")
2026-02-11 16:59:49 +08:00
android15ImageTransmissionVerified = true
return
}
Log.w(TAG, "Android 15图像传输修复失败将依赖后续自动恢复机制")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "Android 15图像传输修复异常", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 为Android 15强制刷新Surface
* 通过完整重建ImageReader实现不直接release Surface会导致悬空引用
2026-02-11 16:59:49 +08:00
*/
private suspend fun refreshSurfaceForAndroid15() {
try {
// 不能直接release ImageReader的Surface这会导致ImageReader持有悬空引用
// 正确做法先释放VirtualDisplay再重建ImageReader最后重建VirtualDisplay
val oldVd = virtualDisplay
val oldIr = imageReader
virtualDisplay = null
imageReader = null
try { oldVd?.release() } catch (_: Exception) {}
try { oldIr?.close() } catch (_: Exception) {}
delay(500)
// 重新创建ImageReader
createImageReader()
// 重新创建VirtualDisplay关联新的Surface
if (imageReader != null && mediaProjection != null) {
virtualDisplay = mediaProjection?.createVirtualDisplay(
"RemoteControlCaptureRefresh_${System.currentTimeMillis()}",
screenWidth, screenHeight,
service.resources.displayMetrics.densityDpi,
android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
imageReader?.surface, null, null
)
if (virtualDisplay != null) {
Log.i(TAG, "Android 15 Surface刷新完成VirtualDisplay已重建")
} else {
Log.w(TAG, "Android 15 Surface刷新后VirtualDisplay创建失败")
}
}
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "Android 15 Surface刷新失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 为Android 15重新初始化ImageReader
* 必须先释放VirtualDisplay再重建ImageReader防止Surface悬空引用
2026-02-11 16:59:49 +08:00
*/
private suspend fun reinitializeImageReaderForAndroid15() {
try {
Log.d(TAG, "Android 15重新初始化ImageReader")
2026-02-11 16:59:49 +08:00
// 先释放VirtualDisplay消费者再释放ImageReader生产者
val oldVd = virtualDisplay
virtualDisplay = null
try { oldVd?.release() } catch (_: Exception) {}
releaseImageReader()
delay(1000)
2026-02-11 16:59:49 +08:00
// 重新创建ImageReader
2026-02-11 16:59:49 +08:00
createImageReader()
delay(1000)
2026-02-11 16:59:49 +08:00
// 重新创建VirtualDisplay关联新的ImageReader Surface
if (imageReader != null && mediaProjection != null) {
virtualDisplay = mediaProjection?.createVirtualDisplay(
"RemoteControlCaptureReinit_${System.currentTimeMillis()}",
screenWidth, screenHeight,
service.resources.displayMetrics.densityDpi,
android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
imageReader?.surface, null, null
)
if (virtualDisplay != null) {
Log.d(TAG, "Android 15已重建VirtualDisplay关联新ImageReader")
} else {
Log.w(TAG, "Android 15重建VirtualDisplay失败")
2026-02-11 16:59:49 +08:00
}
}
} catch (e: Exception) {
Log.e(TAG, "Android 15 ImageReader重新初始化失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 为Android 15完整重建VirtualDisplay
*/
private suspend fun recreateVirtualDisplayForAndroid15() {
try {
Log.d(TAG, "Android 15完整重建VirtualDisplay")
2026-02-11 16:59:49 +08:00
// 停止当前屏幕捕获
stopScreenCapture()
delay(2000)
// 重新启动屏幕捕获
if (startScreenCapture()) {
Log.i(TAG, "Android 15 VirtualDisplay重建成功")
2026-02-11 16:59:49 +08:00
} else {
Log.w(TAG, "Android 15 VirtualDisplay重建失败")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "Android 15 VirtualDisplay重建异常", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 检查VirtualDisplay是否已创建
*/
private fun isVirtualDisplayCreated(): Boolean {
return virtualDisplay != null
}
/**
* 获取最新图像
*/
private fun getLatestImage(): android.media.Image? {
return try {
imageReader?.acquireLatestImage()
} catch (e: Exception) {
Log.e(TAG, "获取最新图像失败", e)
null
}
}
/**
* 创建ImageReader
*/
private fun createImageReader() {
try {
val width = screenWidth.coerceAtMost(MAX_WIDTH)
val height = screenHeight.coerceAtMost(MAX_HEIGHT)
imageReader = android.media.ImageReader.newInstance(
width, height,
android.graphics.PixelFormat.RGBA_8888,
if (Build.VERSION.SDK_INT >= 35) 4 else 2 // Android 15增加缓冲区
)
Log.d(TAG, "创建ImageReader: ${width}x${height}")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "创建ImageReader失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 释放ImageReader
*/
private fun releaseImageReader() {
try {
imageReader?.close()
imageReader = null
Log.d(TAG, "释放ImageReader")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.e(TAG, "释放ImageReader失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 开始屏幕捕获别名方法
*/
fun startScreenCapture(): Boolean {
return try {
startCapture()
true
} catch (e: Exception) {
Log.e(TAG, "启动屏幕捕获失败", e)
2026-02-11 16:59:49 +08:00
false
}
}
/**
* 停止屏幕捕获别名方法
*/
fun stopScreenCapture() {
try {
stopCapture()
} catch (e: Exception) {
Log.e(TAG, "停止屏幕捕获失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* 获取当前屏幕截图
*/
fun getCurrentScreenshot(): Bitmap? {
return try {
if (mediaProjection == null) {
Log.w(TAG, "MediaProjection未初始化")
return null
}
captureWithMediaProjection()
} catch (e: Exception) {
Log.e(TAG, "获取屏幕截图失败", e)
null
}
}
/**
* Android 15检查是否正在重新创建VirtualDisplay
2026-02-11 16:59:49 +08:00
*/
fun isVirtualDisplayRecreating(): Boolean {
return isVirtualDisplayRecreating
}
/**
* 智能判断是否应该重新创建VirtualDisplay
2026-02-11 16:59:49 +08:00
*/
private fun shouldRecreateVirtualDisplay(): Boolean {
val currentTime = System.currentTimeMillis()
// 检查冷却期
if (virtualDisplayRecreationCooldown) {
Log.i(TAG, "VirtualDisplay重新创建处于冷却期跳过")
2026-02-11 16:59:49 +08:00
return false
}
// 检查最小间隔
if (currentTime - lastVirtualDisplayRecreationTime < minRecreationInterval) {
val remainingTime = minRecreationInterval - (currentTime - lastVirtualDisplayRecreationTime)
Log.i(TAG, "VirtualDisplay重新创建间隔不足还需等待${remainingTime}ms")
2026-02-11 16:59:49 +08:00
return false
}
// 检查连续重新创建次数
if (consecutiveRecreationCount >= maxConsecutiveRecreations) {
Log.w(TAG, "VirtualDisplay连续重新创建次数过多(${consecutiveRecreationCount}/${maxConsecutiveRecreations}),进入冷却期")
2026-02-11 16:59:49 +08:00
virtualDisplayRecreationCooldown = true
// 5分钟后重置冷却期
serviceScope.launch {
delay(300000) // 5分钟
virtualDisplayRecreationCooldown = false
consecutiveRecreationCount = 0
Log.i(TAG, "VirtualDisplay重新创建冷却期结束重置计数")
2026-02-11 16:59:49 +08:00
}
return false
}
// 检查失败次数是否达到阈值
if (consecutiveImageFailures < maxImageFailuresBeforeRecreation) {
Log.i(TAG, "图像失败次数未达到阈值(${consecutiveImageFailures}/${maxImageFailuresBeforeRecreation}),暂不重新创建")
2026-02-11 16:59:49 +08:00
return false
}
// 检查VirtualDisplay和ImageReader是否存在
if (virtualDisplay == null || imageReader == null) {
Log.w(TAG, "VirtualDisplay或ImageReader为null需要重新创建")
2026-02-11 16:59:49 +08:00
return true
}
// 所有条件都满足,可以重新创建
Log.i(TAG, "满足VirtualDisplay重新创建条件失败${consecutiveImageFailures}次,上次重建${currentTime - lastVirtualDisplayRecreationTime}ms前")
2026-02-11 16:59:49 +08:00
return true
}
/**
* Android 15触发权限申请流程
2026-02-11 16:59:49 +08:00
*/
private fun triggerAndroid15PermissionRequest(reason: String) {
try {
Log.i(TAG, "[权限申请] Android 15触发权限申请$reason")
2026-02-11 16:59:49 +08:00
// Android 11+特殊处理统一使用无障碍截图API跳过权限申请触发
2026-02-11 16:59:49 +08:00
if (Build.VERSION.SDK_INT >= 30) {
Log.i(TAG, "Android 11+设备使用无障碍截图API跳过权限申请触发")
2026-02-11 16:59:49 +08:00
return
}
// 获取PermissionGranter实例
val permissionGranter = try {
(service as? AccessibilityRemoteService)?.getPermissionGranter()
} catch (e: Exception) {
Log.e(TAG, "无法获取PermissionGranter实例", e)
2026-02-11 16:59:49 +08:00
null
}
if (permissionGranter != null) {
// 强制设置权限申请状态,绕过现有权限检查
Log.i(TAG, "[权限申请] 强制设置MediaProjection申请状态为true准备处理权限弹窗")
2026-02-11 16:59:49 +08:00
// 使用反射或直接设置绕过setMediaProjectionRequesting中的权限检查
try {
// 直接设置内部状态,绕过权限检查
val field = permissionGranter.javaClass.getDeclaredField("isRequestingMediaProjection")
field.isAccessible = true
field.setBoolean(permissionGranter, true)
Log.i(TAG, "[权限申请] 已直接设置权限申请状态,绕过权限检查")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.w(TAG, "无法直接设置权限状态,使用常规方法", e)
2026-02-11 16:59:49 +08:00
permissionGranter.setMediaProjectionRequesting(true)
}
Log.i(TAG, "[权限申请] Android 15权限申请流程已触发等待自动处理权限弹窗")
2026-02-11 16:59:49 +08:00
// 启动异步监控,在权限处理完成后重置状态
serviceScope.launch {
delay(15000) // 15秒后自动重置状态给权限处理更多时间
try {
permissionGranter.setMediaProjectionRequesting(false)
Log.i(TAG, "[权限申请] 15秒后自动重置权限申请状态")
2026-02-11 16:59:49 +08:00
} catch (e: Exception) {
Log.w(TAG, "重置权限申请状态失败", e)
}
}
} else {
Log.w(TAG, "[权限申请] 无法获取PermissionGranter跳过权限申请触发")
2026-02-11 16:59:49 +08:00
}
} catch (e: Exception) {
Log.e(TAG, "[权限申请] 触发Android 15权限申请失败", e)
2026-02-11 16:59:49 +08:00
}
}
/**
* Android 11+专用生成测试图像
2026-02-11 16:59:49 +08:00
*/
private fun generateAndroid11TestImage(): Bitmap? {
return try {
val testBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(testBitmap)
// 绘制Android 11+专用测试界面
val paint = android.graphics.Paint().apply {
isAntiAlias = true
}
// 深蓝色背景显示这是Android 11+特殊模式
canvas.drawColor(android.graphics.Color.rgb(13, 71, 161))
// 主标题
paint.apply {
textSize = 65f
color = android.graphics.Color.WHITE
typeface = android.graphics.Typeface.DEFAULT_BOLD
textAlign = android.graphics.Paint.Align.CENTER
}
canvas.drawText("Android 11+ 设备", screenWidth / 2f, 120f, paint)
2026-02-11 16:59:49 +08:00
// 副标题
paint.apply {
textSize = 50f
color = android.graphics.Color.rgb(100, 181, 246)
typeface = android.graphics.Typeface.DEFAULT
}
canvas.drawText("无障碍截图模式", screenWidth / 2f, 190f, paint)
// 状态信息
paint.apply {
textSize = 40f
color = android.graphics.Color.WHITE
textAlign = android.graphics.Paint.Align.CENTER
}
canvas.drawText("• 已跳过MediaProjection权限", screenWidth / 2f, 280f, paint)
canvas.drawText("• 正在尝试无障碍截图API", screenWidth / 2f, 340f, paint)
canvas.drawText("• 连接功能正常可用", screenWidth / 2f, 400f, paint)
// 详细技术信息
paint.apply {
textSize = 35f
color = android.graphics.Color.rgb(187, 222, 251)
textAlign = android.graphics.Paint.Align.LEFT
}
val leftMargin = 50f
canvas.drawText("分辨率: ${screenWidth} × ${screenHeight}", leftMargin, 500f, paint)
canvas.drawText("API版本: Android ${Build.VERSION.SDK_INT}", leftMargin, 550f, paint)
canvas.drawText("截图方式: AccessibilityService", leftMargin, 600f, paint)
canvas.drawText("权限状态: 已绕过MediaProjection", leftMargin, 650f, paint)
// 动态状态指示器
val time = System.currentTimeMillis()
val frameCount = (time / 200) % 1000
paint.apply {
textSize = 30f
color = android.graphics.Color.rgb(255, 193, 7)
textAlign = android.graphics.Paint.Align.CENTER
}
canvas.drawText("尝试获取真实屏幕中...", screenWidth / 2f, 730f, paint)
// 动态进度条
val progressWidth = screenWidth - 100f
val progressHeight = 20f
val progressX = 50f
val progressY = 760f
// 进度条背景
paint.apply {
color = android.graphics.Color.rgb(33, 37, 41)
style = android.graphics.Paint.Style.FILL
}
canvas.drawRect(progressX, progressY, progressX + progressWidth, progressY + progressHeight, paint)
// 动态进度
val progress = ((time / 50) % progressWidth).toFloat()
paint.apply {
color = android.graphics.Color.rgb(0, 200, 83)
}
canvas.drawRect(progressX, progressY, progressX + progress, progressY + progressHeight, paint)
// 帧数计数器
paint.apply {
textSize = 25f
color = android.graphics.Color.WHITE
textAlign = android.graphics.Paint.Align.CENTER
}
canvas.drawText("帧: $frameCount", screenWidth / 2f, 820f, paint)
// 时间戳
paint.apply {
textSize = 25f
color = android.graphics.Color.GRAY
textAlign = android.graphics.Paint.Align.CENTER
}
val currentTime = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
canvas.drawText(currentTime, screenWidth / 2f, 860f, paint)
// 旋转图标 - 表示正在处理
val centerX = screenWidth / 2f
val centerY = 950f
val iconRadius = 30f
val rotation = (time / 50f) % 360f
canvas.save()
canvas.rotate(rotation, centerX, centerY)
paint.apply {
color = android.graphics.Color.rgb(255, 193, 7)
strokeWidth = 6f
style = android.graphics.Paint.Style.STROKE
}
canvas.drawCircle(centerX, centerY, iconRadius, paint)
// 绘制旋转的点
paint.style = android.graphics.Paint.Style.FILL
for (i in 0 until 8) {
val angle = (i * 45f + rotation) * Math.PI / 180
val dotX = centerX + (iconRadius + 15) * Math.cos(angle).toFloat()
val dotY = centerY + (iconRadius + 15) * Math.sin(angle).toFloat()
val size = if (i % 2 == 0) 8f else 4f
canvas.drawCircle(dotX, dotY, size, paint)
}
canvas.restore()
// 说明文字
paint.apply {
textSize = 28f
color = android.graphics.Color.rgb(158, 158, 158)
textAlign = android.graphics.Paint.Align.CENTER
}
canvas.drawText("如果看到此画面,说明无障碍截图权限", screenWidth / 2f, screenHeight - 120f, paint)
canvas.drawText("可能需要额外配置或系统重启", screenWidth / 2f, screenHeight - 80f, paint)
Log.d(TAG, "生成Android 11+专用测试图像: ${screenWidth}x${screenHeight}")
testBitmap
} catch (e: Exception) {
Log.e(TAG, "生成Android 11+测试图像失败", e)
// 回退到简单测试图像
generateTestImage()
}
}
}