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

3102 lines
134 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.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)
private const val MAX_WIDTH = 480 // 更小宽度480px测试单消息大小理论
private const val MAX_HEIGHT = 854 // 更小高度854px保持16:9比例
private const val MIN_CAPTURE_INTERVAL = 66L // ✅ 调整为66ms适配15FPS1000ms/15fps≈66ms
// ✅ 持久化暂停状态相关常量
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
// 图像缓存机制
private var lastValidBitmap: Bitmap? = null
private var lastCaptureTime = 0L
private var lastScreenshotTime = 0L // ✅ 新增:记录上次截图时间,防止截图间隔太短
// 状态跟踪
private var isPaused = false
private var lastSuccessfulSendTime: Long? = null
// 添加Android 15图像传输验证相关变量
@Volatile private var android15ImageTransmissionVerified = false
private val android15ImageVerificationDelay = 3000L // 3秒后验证
// ✅ Android 15 VirtualDisplay重新创建状态跟踪
@Volatile
private var isVirtualDisplayRecreating = false
private val virtualDisplayRecreationLock = ReentrantLock()
// ✅ Android 15 Session使用状态跟踪
@Volatile
private var android15SessionUsed = false
// ✅ VirtualDisplay重新初始化智能控制
private var lastVirtualDisplayRecreationTime = 0L
private val minRecreationInterval = 60000L // 最小重新创建间隔60秒
private var consecutiveRecreationCount = 0
private val maxConsecutiveRecreations = 3 // 最多连续重新创建3次
private var virtualDisplayRecreationCooldown = false
// ✅ 图像获取失败计数
private var consecutiveImageFailures = 0
private val maxImageFailuresBeforeRecreation = 10 // 连续10次失败才考虑重新创建
// 🔑 新增AccessibilityService截图模式开关
private var useAccessibilityScreenshot = false
// 🔑 无障碍截图间隔自适应系统takeScreenshot有最小间隔限制通常≥1秒
// 当收到ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT时动态增加间隔
@Volatile private var accessibilityMinInterval = 1100L // 初始1100msAndroid系统takeScreenshot最小间隔约1秒
private val ACCESSIBILITY_INTERVAL_STEP = 200L // 每次错误码3增加200ms
private val ACCESSIBILITY_INTERVAL_MAX = 3000L // 最大间隔3秒
private val ACCESSIBILITY_INTERVAL_MIN = 800L // 最小间隔800ms成功时逐步回落的下限
// 📊 自适应画质运行时可调参数覆盖companion object中的常量
@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+专用:防止闪烁的状态管理
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
private val screenDataQueue = java.util.concurrent.LinkedBlockingQueue<ByteArray>(15) // 增加到15帧容量避免频繁丢帧
private var droppedFrameCount = 0L
private var totalFrameCount = 0L
// 🔧 新增:内存监控和自动清理
private var lastMemoryCheckTime = 0L
private val memoryCheckInterval = 10000L // 10秒检查一次内存
private val maxMemoryUsagePercent = 0.8f // 超过80%内存使用率时触发清理
// 🔧 新增:资源追踪
private val activeBitmaps = mutableSetOf<WeakReference<Bitmap>>()
private val activeImages = mutableSetOf<WeakReference<android.media.Image>>()
// 🔧 新增:单一队列处理协程,避免协程泄漏
private var queueProcessorJob: Job? = null
private val queueProcessingStarted = AtomicBoolean(false)
// ✅ 专用截图执行器,避免每次创建线程并避免占用主线程
private val screenshotExecutor: java.util.concurrent.ExecutorService =
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}")
}
/**
* 🔑 启用AccessibilityService截图模式
* 用于绕过黑屏遮罩让Web端能够正常显示画面
*/
fun enableAccessibilityScreenshotMode() {
Log.i(TAG, "🔑 启用AccessibilityService截图模式 - 绕过黑屏遮罩")
useAccessibilityScreenshot = true
}
/**
* 🔑 禁用AccessibilityService截图模式
* 恢复使用MediaProjection截图
*/
fun disableAccessibilityScreenshotMode() {
Log.i(TAG, "🔑 禁用AccessibilityService截图模式 - 恢复MediaProjection")
useAccessibilityScreenshot = false
}
/**
* 📊 自适应画质:动态调整采集参数
* 由服务端根据Web端反馈下发调整指令
*/
fun adjustQuality(fps: Int, quality: Int, maxWidth: Int, maxHeight: Int) {
Log.i(TAG, "📊 收到画质调整: fps=$fps, quality=$quality, resolution=${maxWidth}x${maxHeight}")
if (fps in 1..30) {
dynamicFps = fps
Log.i(TAG, "📊 帧率调整为: ${fps}fps (间隔${1000 / fps}ms)")
}
if (quality in 20..90) {
dynamicQuality = quality
Log.i(TAG, "📊 JPEG质量调整为: $quality")
}
if (maxWidth in 240..1920) {
dynamicMaxWidth = maxWidth
Log.i(TAG, "📊 最大宽度调整为: $maxWidth")
}
if (maxHeight in 320..2560) {
dynamicMaxHeight = maxHeight
Log.i(TAG, "📊 最大高度调整为: $maxHeight")
}
}
/**
* 🔄 切换到无障碍截图模式(由服务端指令触发)
* 停止当前采集,切换到 AccessibilityService.takeScreenshot
*/
fun switchToAccessibilityMode() {
if (useAccessibilityScreenshot) {
Log.d(TAG, "🔄 已经在无障碍截图模式,跳过切换")
return
}
Log.i(TAG, "🔄 切换到无障碍截图模式")
// 停止当前采集
stopCapture()
// 清理 MediaProjection 资源
cleanupVirtualDisplayOnly()
// 启用无障碍截图并重新开始
enableAccessibilityScreenshotMode()
startCapture()
}
/**
* 🔄 切换到 MediaProjection 模式(由服务端指令触发)
*/
fun switchToMediaProjectionMode() {
if (!useAccessibilityScreenshot) {
Log.d(TAG, "🔄 已经在MediaProjection模式跳过切换")
return
}
Log.i(TAG, "🔄 切换到MediaProjection模式")
stopCapture()
disableAccessibilityScreenshotMode()
startCapture()
}
/**
* 初始化屏幕捕获
*/
fun initialize() {
try {
Log.i(TAG, "初始化屏幕捕获管理器 - 使用无障碍服务截图API")
// ✅ 恢复持久化的暂停状态
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
*/
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")
} catch (e: Exception) {
Log.e(TAG, "❌ 保存暂停状态失败", e)
}
}
/**
* ✅ 从SharedPreferences恢复暂停状态
*/
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, "📋 检测到持久化的暂停状态,恢复暂停状态")
isPaused = true
isCapturing = false
} else {
Log.d(TAG, "📋 未检测到暂停状态,使用默认状态")
}
} catch (e: Exception) {
Log.e(TAG, "❌ 恢复暂停状态失败", e)
}
}
/**
* 开始屏幕捕获
*/
fun startCapture() {
if (isCapturing) {
Log.w(TAG, "屏幕捕获已在运行")
return
}
// ✅ 检查持久化的暂停状态,如果暂停则不启动
if (isPaused) {
Log.i(TAG, "⏸️ 屏幕捕获处于暂停状态,跳过启动")
return
}
try {
Log.i(TAG, "开始屏幕捕获")
// ✅ Android 11+ (API 30+):优先尝试 MediaProjection帧率更高
// 如果 MediaProjection 不可用则回退到无障碍截图
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 检查是否已被显式设置为无障碍截图模式
if (useAccessibilityScreenshot) {
Log.i(TAG, "📱 Android 11+:已设置为无障碍截图模式")
startAccessibilityScreenCapture()
} else if (ensureMediaProjection()) {
// MediaProjection 可用,使用 VirtualDisplay 连续流式捕获
Log.i(TAG, "📱 Android 11+MediaProjection 可用,使用 VirtualDisplay 连续流式捕获")
startMediaProjectionCapture()
} else {
// MediaProjection 不可用,回退到无障碍截图
Log.i(TAG, "📱 Android 11+MediaProjection 不可用,回退到无障碍截图模式")
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Log.i(TAG, "📱 Android 5-10使用 MediaProjection VirtualDisplay 连续流式捕获")
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 连续流式捕获")
captureScope.launch {
// 先确保 MediaProjection 可用
if (!ensureMediaProjection()) {
Log.e(TAG, "❌ MediaProjection 不可用,回退到无障碍截图")
// 回退到无障碍截图作为兜底
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch
}
// 初始化 ImageReader + VirtualDisplay
if (imageReader == null || virtualDisplay == null) {
setupMediaProjectionResources()
}
if (virtualDisplay == null) {
Log.e(TAG, "❌ VirtualDisplay 创建失败,回退到无障碍截图")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch
}
Log.i(TAG, "✅ MediaProjection VirtualDisplay 就绪,开始连续帧采集循环")
var consecutiveFailures = 0
// ✅ 最小有效帧大小阈值正常480×854 JPEG即使最低质量也>5KB
// 低于此值的帧几乎肯定是黑屏/空白帧VirtualDisplay未刷新
val MIN_VALID_FRAME_SIZE = 5 * 1024 // 5KB
// ✅ 连续黑屏帧计数,用于判断是否应该切换到无障碍截图
var consecutiveBlackFrames = 0
val MAX_BLACK_FRAMES_BEFORE_FALLBACK = 30 // 连续30个黑屏帧后切换到无障碍截图
while (isCapturing) {
try {
val image = imageReader?.acquireLatestImage()
if (image != null) {
try {
val bitmap = convertImageToBitmap(image)
if (bitmap != null) {
trackBitmap(bitmap)
val jpegData = compressBitmap(bitmap)
if (jpegData.size >= MIN_VALID_FRAME_SIZE) {
// ✅ 有效帧:发送并更新缓存
sendFrameToServer(jpegData)
lastValidBitmap?.recycle()
lastValidBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
lastValidBitmap?.let { trackBitmap(it) }
lastCaptureTime = System.currentTimeMillis()
consecutiveFailures = 0
consecutiveBlackFrames = 0
Log.d(TAG, "📸 MediaProjection 有效帧: ${jpegData.size} bytes")
} else {
// ⚠️ 疑似黑屏帧
consecutiveBlackFrames++
consecutiveFailures++
Log.w(TAG, "⚠️ 黑屏帧(${jpegData.size}B < ${MIN_VALID_FRAME_SIZE}B),连续${consecutiveBlackFrames}")
// 🔄 连续黑屏帧过多MediaProjection在此设备上不可靠切换到无障碍截图
if (consecutiveBlackFrames >= MAX_BLACK_FRAMES_BEFORE_FALLBACK) {
Log.w(TAG, "🔄 MediaProjection连续${consecutiveBlackFrames}个黑屏帧,切换到无障碍截图模式")
bitmap.recycle()
cleanupVirtualDisplayOnly()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
} else {
startFallbackCapture()
}
return@launch
}
// 发送缓存的有效帧保持画面连续(如果有的话)
val cachedBitmap = lastValidBitmap
if (cachedBitmap != null && !cachedBitmap.isRecycled) {
val cacheAge = System.currentTimeMillis() - lastCaptureTime
if (cacheAge < 10000) {
try {
val cachedJpeg = compressBitmap(cachedBitmap)
if (cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg)
Log.d(TAG, "📸 使用缓存帧替代黑屏帧 (${cacheAge}ms前)")
}
} catch (e: Exception) {
Log.w(TAG, "⚠️ 压缩缓存帧失败(可能已被回收)", e)
}
}
}
}
bitmap.recycle()
}
} finally {
image.close()
}
} else {
consecutiveFailures++
// ✅ 无新帧时发送缓存帧,保持画面连续
val cachedBitmap2 = lastValidBitmap
if (cachedBitmap2 != null && !cachedBitmap2.isRecycled) {
val cacheAge = System.currentTimeMillis() - lastCaptureTime
if (cacheAge < 10000) {
try {
val cachedJpeg = compressBitmap(cachedBitmap2)
if (cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
sendFrameToServer(cachedJpeg)
Log.d(TAG, "📸 无新帧,发送缓存帧 (${cacheAge}ms前)")
}
} catch (e: Exception) {
Log.w(TAG, "⚠️ 压缩缓存帧失败(可能已被回收)", e)
}
}
}
// 连续失败过多,尝试重建 VirtualDisplay
if (consecutiveFailures >= maxImageFailuresBeforeRecreation) {
Log.w(TAG, "⚠️ 连续 ${consecutiveFailures} 次无法获取有效帧,尝试重建 VirtualDisplay")
cleanupVirtualDisplayOnly()
delay(500)
setupMediaProjectionResources()
consecutiveFailures = 0
if (virtualDisplay == null) {
Log.e(TAG, "❌ VirtualDisplay 重建失败,回退到无障碍截图")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enableAccessibilityScreenshotMode()
startAccessibilityScreenCapture()
}
return@launch
}
}
}
// 按动态帧率控制采集间隔
delay(1000L / dynamicFps)
} catch (e: Exception) {
Log.e(TAG, "MediaProjection 帧采集异常", e)
consecutiveFailures++
delay(100)
}
}
}
}
/**
* 确保 MediaProjection 可用,尝试获取或恢复
*/
private fun ensureMediaProjection(): Boolean {
if (mediaProjection != null) return true
mediaProjection = MediaProjectionHolder.getMediaProjection()
if (mediaProjection != null) return true
// 尝试从权限数据重新创建
val permissionData = MediaProjectionHolder.getPermissionData()
if (permissionData != null) {
val (resultCode, resultData) = permissionData
if (resultData != null) {
try {
val mediaProjectionManager = service.getSystemService(
android.content.Context.MEDIA_PROJECTION_SERVICE
) as android.media.projection.MediaProjectionManager
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, resultData)
if (mediaProjection != null) {
Log.i(TAG, "✅ 重新创建 MediaProjection 成功")
MediaProjectionHolder.setMediaProjection(mediaProjection)
return true
}
} catch (e: Exception) {
Log.e(TAG, "❌ 重新创建 MediaProjection 失败", e)
}
}
}
Log.e(TAG, "❌ 无法获取 MediaProjection权限可能未授予")
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) {
// ✅ 成功获取真实屏幕
Log.d(TAG, "成功获取真实屏幕截图: ${screenshot.width}x${screenshot.height}")
val jpegData = compressBitmap(screenshot)
sendFrameToServer(jpegData)
// 🔑 缓存成功的截图,用于防止闪烁
try {
// 清理旧的缓存
lastValidBitmap?.recycle()
// 保存当前成功的截图副本
lastValidBitmap = screenshot.copy(screenshot.config ?: Bitmap.Config.ARGB_8888, false)
lastCaptureTime = System.currentTimeMillis()
Log.d(TAG, "✅ 已缓存有效截图用于防闪烁: ${lastValidBitmap?.width}x${lastValidBitmap?.height}")
} catch (e: Exception) {
Log.w(TAG, "缓存有效截图失败", e)
}
screenshot.recycle()
consecutiveFailures = 0 // 重置失败计数
// 🚨 Android 11+特殊处理:成功获取截图时的状态管理
if (Build.VERSION.SDK_INT >= 30) {
android11ConsecutiveFailures = 0
android11LastSuccessTime = System.currentTimeMillis()
android11InTestMode = false
Log.d(TAG, "📱 Android 11+设备:真实截图成功,退出测试模式")
}
} else {
// ❌ 真实截图失败,不发送任何数据,等待下一次截图
// 之前发送测试图像会被服务端过滤且占用发送队列,得不偿失
consecutiveFailures++
Log.d(TAG, "📱 无障碍截图失败(${consecutiveFailures}次),跳过本帧等待下次")
}
// ✅ 截图成功时按帧率延迟,失败时按自适应间隔延迟
// 无障碍截图模式下,成功延迟也不能低于系统最小间隔
if (consecutiveFailures == 0) {
val fpsDelay = 1000 / dynamicFps.toLong()
delay(maxOf(fpsDelay, accessibilityMinInterval))
} else {
// 失败时等待自适应间隔,避免疯狂触发系统截图间隔限制
delay(accessibilityMinInterval)
}
} catch (e: Exception) {
Log.e(TAG, "屏幕捕获失败", e)
consecutiveFailures++
delay(accessibilityMinInterval) // 异常时也按自适应间隔等待
}
}
}
}
/**
* 🚨 Android 11+专用:处理截图失败的智能逻辑
*/
private fun handleAndroid11ScreenshotFailure(consecutiveFailures: Int, maxConsecutiveFailures: Int): Bitmap? {
android11ConsecutiveFailures++
val currentTime = System.currentTimeMillis()
Log.d(TAG, "📱 Android 11+设备:截图失败 (连续${android11ConsecutiveFailures}次)")
// 🔑 优先策略:返回缓存的上一帧,避免闪烁
if (lastValidBitmap != null && !lastValidBitmap!!.isRecycled) {
val cacheAge = currentTime - lastCaptureTime
// 如果缓存还比较新10秒内直接使用缓存
if (cacheAge < 10000) {
Log.d(TAG, "📱 Android 11+设备:返回缓存截图 (${cacheAge}ms前),避免闪烁")
return try {
lastValidBitmap!!.copy(lastValidBitmap!!.config ?: Bitmap.Config.ARGB_8888, false)
} catch (e: Exception) {
Log.w(TAG, "复制缓存截图失败", e)
null
}
}
// 缓存较旧但仍可用60秒内短期失败时继续使用
if (cacheAge < 60000 && android11ConsecutiveFailures < 20) {
Log.d(TAG, "📱 Android 11+设备:返回稍旧的缓存截图 (${cacheAge}ms前),避免闪烁")
return try {
lastValidBitmap!!.copy(lastValidBitmap!!.config ?: Bitmap.Config.ARGB_8888, false)
} catch (e: Exception) {
Log.w(TAG, "复制缓存截图失败", e)
null
}
}
}
// 🚨 无可用缓存或缓存过旧,智能判断是否应该进入测试模式
val shouldEnterTestMode = when {
android11ConsecutiveFailures >= android11FailureThreshold -> true
android11InTestMode && (currentTime - android11LastSuccessTime) < android11TestModeStabilityTime -> true
else -> false
}
return if (shouldEnterTestMode) {
if (!android11InTestMode) {
Log.i(TAG, "📱 Android 11+设备:连续失败${android11ConsecutiveFailures}次,进入稳定测试模式")
android11InTestMode = true
}
// 在测试模式下,根据总失败次数决定显示什么
if (consecutiveFailures > maxConsecutiveFailures) {
Log.w(TAG, "📱 Android 11+设备:长期失败,显示权限恢复提示")
generatePermissionRecoveryTestImage()
} else {
Log.d(TAG, "📱 Android 11+设备:显示专用测试画面")
generateAndroid11TestImage()
}
} else {
// 还未达到测试模式阈值,使用简单测试图像
Log.d(TAG, "📱 Android 11+设备:短暂失败,显示简单测试图像")
generateTestImage()
}
}
/**
* 捕获真实屏幕(用于单次截图场景)
* 优先使用 MediaProjection不可用时回退到无障碍截图
* 当 useAccessibilityScreenshot=true 时,直接使用无障碍截图(跳过 MediaProjection
*/
private fun captureRealScreen(): Bitmap? {
return try {
// 🔑 如果已切换到无障碍截图模式,直接使用无障碍截图,不再尝试 MediaProjection
if (useAccessibilityScreenshot) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Log.d(TAG, "🔑 无障碍截图模式,直接使用 AccessibilityService 截图")
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 {
// ✅ 修复无障碍截图始终使用自适应间隔系统takeScreenshot有最小间隔限制通常≥1秒
val currentTime = System.currentTimeMillis()
val timeSinceLastScreenshot = currentTime - lastScreenshotTime
if (timeSinceLastScreenshot < accessibilityMinInterval) {
Log.d(TAG, "📱 截图间隔太短,跳过本次截图: ${timeSinceLastScreenshot}ms < ${accessibilityMinInterval}ms")
return null
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Log.d(TAG, "使用无障碍服务截图API")
// 🚨 Android 11+特殊处理:增强错误检查和重试机制
if (Build.VERSION.SDK_INT >= 30) {
Log.d(TAG, "📱 Android 11+设备:使用增强的无障碍截图逻辑")
}
// 使用同步方式获取截图
var resultBitmap: Bitmap? = null
var errorCode: Int? = null
val latch = java.util.concurrent.CountDownLatch(1)
// 后台线程直接调用截图API回调在专用执行器上执行避免占用主线程
try {
// ✅ 关键修复在调用takeScreenshot之前就更新时间戳
// 防止异步回调延迟导致间隔检查失效,避免连续触发系统限制
lastScreenshotTime = System.currentTimeMillis()
service.takeScreenshot(
android.view.Display.DEFAULT_DISPLAY,
screenshotExecutor,
object : android.accessibilityservice.AccessibilityService.TakeScreenshotCallback {
override fun onSuccess(screenshotResult: android.accessibilityservice.AccessibilityService.ScreenshotResult) {
try {
Log.d(TAG, "无障碍服务截图成功")
// ✅ 修复:更新截图时间戳,防止截图间隔太短
lastScreenshotTime = System.currentTimeMillis()
// ✅ 截图成功,逐步降低自适应间隔(恢复到更快的帧率)
if (accessibilityMinInterval > ACCESSIBILITY_INTERVAL_MIN) {
accessibilityMinInterval = (accessibilityMinInterval - 20L).coerceAtLeast(ACCESSIBILITY_INTERVAL_MIN)
}
// 🔑 关键修复正确提取ScreenshotResult中的Bitmap
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
resultBitmap?.let { trackBitmap(it) }
Log.i(TAG, "✅ 成功从ScreenshotResult提取Bitmap: ${resultBitmap?.width}x${resultBitmap?.height}")
// 🚨 Android 11+特殊处理额外验证Bitmap有效性
if (Build.VERSION.SDK_INT >= 30) {
if (resultBitmap != null && !resultBitmap!!.isRecycled) {
Log.i(TAG, "📱 Android 11+设备Bitmap验证通过尺寸=${resultBitmap!!.width}x${resultBitmap!!.height}")
} else {
Log.w(TAG, "📱 Android 11+设备Bitmap验证失败")
resultBitmap = null
}
}
} else {
Log.w(TAG, "⚠️ ScreenshotResult中的HardwareBuffer为null")
// 🚨 Android 11+特殊处理:记录详细错误信息
if (Build.VERSION.SDK_INT >= 30) {
Log.e(TAG, "📱 Android 11+设备HardwareBuffer为null可能是权限问题")
}
}
} else {
Log.w(TAG, "⚠️ Android版本不支持ScreenshotResult API")
}
} catch (e: Exception) {
Log.e(TAG, "处理截图结果失败", e)
// 🚨 Android 11+特殊处理:记录详细异常信息
if (Build.VERSION.SDK_INT >= 30) {
Log.e(TAG, "📱 Android 11+设备:处理截图结果时发生异常", e)
}
} finally {
latch.countDown()
}
}
override fun onFailure(failureErrorCode: Int) {
errorCode = failureErrorCode
// ✅ 关键修复:失败时也更新时间戳,确保下次间隔检查基于实际失败时间
// 防止系统内部计时与我们的计时不同步导致连续触发错误码3
lastScreenshotTime = System.currentTimeMillis()
// 🚨 Android 11+特殊处理:详细分析错误码
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 -> {
// ✅ 错误码3截图间隔太短动态增加间隔
val newInterval = (accessibilityMinInterval + ACCESSIBILITY_INTERVAL_STEP).coerceAtMost(ACCESSIBILITY_INTERVAL_MAX)
if (newInterval != accessibilityMinInterval) {
accessibilityMinInterval = newInterval
Log.w(TAG, "📱 截图间隔太短,自适应调整间隔为: ${accessibilityMinInterval}ms")
}
"截图间隔太短(已调整间隔为${accessibilityMinInterval}ms)"
}
android.accessibilityservice.AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY -> "无效显示"
android.accessibilityservice.AccessibilityService.ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS -> "无障碍权限不足"
else -> "未知错误($failureErrorCode)"
}
// 错误码3是预期的限流行为用WARN级别其他错误用ERROR级别
if (failureErrorCode == android.accessibilityservice.AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT) {
Log.w(TAG, "📱 Android 11+设备:截图失败详情 - $errorMessage")
} else {
Log.e(TAG, "无障碍服务截图失败,错误码: $failureErrorCode")
Log.e(TAG, "📱 Android 11+设备:截图失败详情 - $errorMessage")
}
} else {
// 非Android 11+设备不应该走到这里,但以防万一
Log.w(TAG, "无障碍服务截图失败,错误码: $failureErrorCode")
}
latch.countDown()
}
}
)
} catch (e: Exception) {
Log.e(TAG, "调用takeScreenshot失败", e)
// 🚨 Android 11+特殊处理:记录调用失败详情
if (Build.VERSION.SDK_INT >= 30) {
Log.e(TAG, "📱 Android 11+设备调用takeScreenshot时发生异常", e)
}
latch.countDown()
}
// 🚨 Android 11+特殊处理:缩短等待时间,避免阻塞采集循环
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, "无障碍服务截图超时")
// ✅ 超时时也更新时间戳,确保下次间隔检查正确
lastScreenshotTime = System.currentTimeMillis()
// 🚨 Android 11+特殊处理返回null让主循环处理失败逻辑
if (Build.VERSION.SDK_INT >= 30) {
Log.w(TAG, "📱 Android 11+设备无障碍截图超时返回null让主循环处理")
return null
}
return captureWithMediaProjection() // 其他版本超时时回退到MediaProjection
}
if (resultBitmap != null) {
Log.i(TAG, "✅ 无障碍服务截图成功获取,绕过黑屏遮罩")
resultBitmap
} else {
// 🚨 Android 11+特殊处理返回null让主循环处理失败逻辑
if (Build.VERSION.SDK_INT >= 30) {
Log.w(TAG, "📱 Android 11+设备无障碍截图失败返回null让主循环处理")
return null
}
// 如果AccessibilityService截图失败回退到MediaProjection
Log.w(TAG, "⚠️ 无障碍服务截图失败回退到MediaProjection")
captureWithMediaProjection()
}
} else {
Log.w(TAG, "Android版本不支持无障碍服务截图API使用MediaProjection")
// 🚨 Android 11+特殊处理返回null让主循环处理
if (Build.VERSION.SDK_INT >= 30) {
Log.w(TAG, "📱 Android 11+设备不支持无障碍截图API返回null让主循环处理")
return null
}
captureWithMediaProjection()
}
} catch (e: Exception) {
Log.e(TAG, "无障碍服务截图异常", e)
// 🚨 Android 11+特殊处理异常时返回null让主循环处理
if (Build.VERSION.SDK_INT >= 30) {
Log.e(TAG, "📱 Android 11+设备无障碍截图异常返回null让主循环处理", e)
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未初始化检查权限数据")
val permissionData = MediaProjectionHolder.getPermissionData()
Log.w(TAG, "权限数据状态: ${if (permissionData != null) "存在" else "不存在"}")
// 尝试重新创建MediaProjection
if (permissionData != null) {
val (resultCode, resultData) = permissionData
if (resultData != null) {
val mediaProjectionManager = service.getSystemService(android.content.Context.MEDIA_PROJECTION_SERVICE) as android.media.projection.MediaProjectionManager
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, resultData)
if (mediaProjection != null) {
Log.i(TAG, "重新创建MediaProjection成功")
MediaProjectionHolder.setMediaProjection(mediaProjection)
} else {
Log.w(TAG, "重新创建MediaProjection失败")
}
}
}
if (mediaProjection == null) {
// ✅ Android 15优化先尝试静默恢复再考虑权限申请
if (android.os.Build.VERSION.SDK_INT >= 35) {
Log.w(TAG, "⚠️ Android 15 MediaProjection为null尝试静默恢复")
if (attemptAndroid15SilentRecovery()) {
// 静默恢复成功,继续执行
Log.i(TAG, "✅ Android 15静默恢复成功继续屏幕捕获")
} else {
// 静默恢复失败,可能确实需要重新申请权限
Log.e(TAG, "❌ Android 15静默恢复失败MediaProjection权限确实丢失")
triggerPermissionRecovery()
return null
}
} else {
// 其他版本的处理逻辑
Log.e(TAG, "❌ MediaProjection权限丢失触发自动权限恢复")
triggerPermissionRecovery()
return null
}
}
}
}
// 🛡️ 保守策略:只在实际使用失败时才进行权限检测,避免过度检测
// 移除主动的可用性测试,让权限在实际使用中自然暴露问题
// 初始化ImageReader和VirtualDisplay仅第一次
if (imageReader == null || virtualDisplay == null) {
setupMediaProjectionResources()
}
// 尝试获取最新图像,带缓存机制
val currentTime = System.currentTimeMillis()
var newBitmap: Bitmap? = null
// ✅ Android 15优化首先尝试获取新图像增加重试次数和等待时间
var retryCount = 0
val maxRetries = if (Build.VERSION.SDK_INT >= 35) 5 else 2 // Android 15需要更多重试
while (retryCount < maxRetries) {
val image = imageReader?.acquireLatestImage()
if (image != null) {
// 🔧 跟踪Image资源
val trackedImage = trackImage(image)
try {
newBitmap = convertImageToBitmap(trackedImage)
if (newBitmap != null) {
// 🔧 跟踪新创建的Bitmap
trackBitmap(newBitmap)
Log.d(TAG, "MediaProjection获取新图像: ${newBitmap.width}x${newBitmap.height}")
// ✅ 成功获取图像,重置失败计数
if (consecutiveImageFailures > 0) {
Log.d(TAG, "✅ 图像获取成功,重置失败计数(之前${consecutiveImageFailures}次)")
consecutiveImageFailures = 0
}
// 更新缓存
lastValidBitmap?.recycle()
lastValidBitmap = newBitmap.copy(Bitmap.Config.ARGB_8888, false)
// 🔧 跟踪缓存的Bitmap副本
lastValidBitmap?.let { trackBitmap(it) }
lastCaptureTime = currentTime
return newBitmap
}
} catch (e: Exception) {
Log.e(TAG, "转换图像失败", e)
} finally {
trackedImage.close()
}
}
// ✅ Android 15增加等待时间因为图像生成可能需要更长时间
val waitTime = if (Build.VERSION.SDK_INT >= 35) {
if (retryCount == 0) 50L else (retryCount * 30L) // 首次50ms后续递增
} else {
20L
}
Thread.sleep(waitTime)
retryCount++
// Android 15特殊处理在重试期间记录更多调试信息
if (Build.VERSION.SDK_INT >= 35 && retryCount > 1) {
Log.d(TAG, "Android 15图像获取重试 $retryCount/$maxRetries,等待${waitTime}ms")
}
}
// 如果获取不到新图像,检查是否可以使用缓存
if (lastValidBitmap != null) {
val timeSinceLastCapture = currentTime - lastCaptureTime
if (timeSinceLastCapture < 30000) { // 延长到30秒内的缓存有效
Log.d(TAG, "使用缓存图像 (${timeSinceLastCapture}ms前) - 静止页面")
return lastValidBitmap!!.copy(Bitmap.Config.ARGB_8888, false)
} else {
Log.w(TAG, "缓存图像过期 (${timeSinceLastCapture}ms前),清理缓存")
lastValidBitmap?.recycle()
lastValidBitmap = null
}
}
// ✅ 智能重新初始化逻辑:增加失败计数和多层检查
consecutiveImageFailures++
Log.w(TAG, "MediaProjection无新图像且无有效缓存 (连续失败${consecutiveImageFailures}次)")
// ✅ Android 15特殊处理智能重新初始化策略
if (Build.VERSION.SDK_INT >= 35) {
// 首先尝试强制刷新(轻量级恢复)
if (consecutiveImageFailures <= 5) {
Log.i(TAG, "🔧 Android 15尝试轻量级强制刷新 (失败${consecutiveImageFailures}/5次)")
val refreshResult = forceRefreshAndroid15Images()
if (refreshResult != null) {
Log.i(TAG, "✅ Android 15轻量级刷新成功重置失败计数")
consecutiveImageFailures = 0 // 重置失败计数
return refreshResult
}
}
// 检查是否应该进行重新初始化(重量级恢复)
val shouldRecreate = shouldRecreateVirtualDisplay()
if (shouldRecreate) {
Log.i(TAG, "🔧 Android 15条件满足进行VirtualDisplay重新初始化")
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重新初始化后成功获取图像")
lastValidBitmap?.recycle()
lastValidBitmap = retryBitmap.copy(Bitmap.Config.ARGB_8888, false)
lastCaptureTime = System.currentTimeMillis()
return retryBitmap
}
} catch (e: Exception) {
Log.e(TAG, "Android 15重新初始化后图像转换失败", e)
} finally {
retryImage.close()
}
}
}
} else {
Log.i(TAG, "🛡️ Android 15不满足重新初始化条件跳过VirtualDisplay重建")
}
} 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可用性...")
// 尝试创建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可用性测试通过")
true
} catch (e: SecurityException) {
Log.w(TAG, "❌ MediaProjection可用性测试失败安全异常 - ${e.message}")
// 这是权限失效的明确信号
false
} catch (e: Exception) {
Log.w(TAG, "❌ MediaProjection可用性测试失败其他异常 - ${e.message}")
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令牌可用性")
if (isMediaProjectionTokenUsed()) {
Log.w(TAG, "⚠️ Android 15令牌已使用需要重新获取MediaProjection")
if (!regenerateMediaProjectionForAndroid15()) {
Log.e(TAG, "❌ Android 15重新生成MediaProjection失败")
return
}
}
}
// 只清理VirtualDisplay和ImageReader保留MediaProjection权限
cleanupVirtualDisplayOnly()
// ✅ Android 15优化创建ImageReader调整缓存和格式
val bufferCount = if (Build.VERSION.SDK_INT >= 35) {
3 // Android 15需要更多缓存确保图像连续性
} else {
2 // 其他版本减少缓存避免内存问题
}
imageReader = android.media.ImageReader.newInstance(
screenWidth, screenHeight,
android.graphics.PixelFormat.RGBA_8888, bufferCount
)
// ✅ Android 15添加监听器来监控图像可用性
if (Build.VERSION.SDK_INT >= 35) {
imageReader?.setOnImageAvailableListener({ reader ->
Log.v(TAG, "🔧 Android 15: ImageReader有新图像可用")
}, backgroundHandler)
}
Log.i(TAG, "ImageReader创建完成")
// 创建VirtualDisplay
virtualDisplay = mediaProjection?.createVirtualDisplay(
"RemoteControlCapture",
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, "VirtualDisplay创建成功")
// ✅ Android 15VirtualDisplay创建成功后触发权限申请处理
if (Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "🔧 Android 15VirtualDisplay创建成功触发权限申请处理")
triggerAndroid15PermissionRequest("首次初始化VirtualDisplay")
}
// ✅ Android 15标记session为已使用
markAndroid15SessionUsed()
} else {
Log.e(TAG, "VirtualDisplay创建失败")
return
}
Log.i(TAG, "MediaProjection资源初始化完成等待画面稳定...")
// ✅ Android 15需要更长的稳定时间
val stabilizeTime = if (Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "🔧 Android 15设备等待更长时间确保VirtualDisplay稳定")
2000L // Android 15需要2秒稳定时间从1.2秒增加到2秒
} else {
500L // 其他版本500ms
}
Thread.sleep(stabilizeTime)
// ✅ Android 15额外处理强制刷新Surface确保连接
if (Build.VERSION.SDK_INT >= 35) {
try {
Log.i(TAG, "🔧 Android 15强制刷新Surface连接")
// 通过重新设置Surface来确保连接
imageReader?.surface?.let { surface ->
// 触发Surface刷新
surface.toString() // 这会触发Surface的内部状态检查
}
Thread.sleep(300) // 给Surface刷新时间
} catch (e: Exception) {
Log.w(TAG, "⚠️ Android 15 Surface刷新失败${e.message}")
}
}
// ✅ Android 15验证尝试预先获取一帧图像确保VirtualDisplay工作正常
if (Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "🔧 Android 15验证预先测试图像获取...")
try {
val testImage = imageReader?.acquireLatestImage()
if (testImage != null) {
Log.i(TAG, "✅ Android 15 VirtualDisplay预验证成功")
testImage.close()
} else {
Log.w(TAG, "⚠️ Android 15 VirtualDisplay预验证暂无图像但这可能是正常的")
}
} catch (e: Exception) {
Log.w(TAG, "⚠️ Android 15 VirtualDisplay预验证异常${e.message}")
}
}
} catch (e: SecurityException) {
Log.e(TAG, "初始化MediaProjection资源失败权限问题", e)
// ✅ Android 15特殊处理检查是否是session重复使用错误
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只能使用一次")
// 标记session为已使用
markAndroid15SessionUsed()
// 清理资源并触发重新申请
cleanupVirtualDisplayOnly()
// 通知权限丢失,需要重新申请
Log.i(TAG, "📢 通知权限丢失,触发重新申请流程")
onMediaProjectionLost()
} else {
Log.w(TAG, "检测到其他权限问题,但不立即重新申请权限")
// 🛡️ 保守处理:只清理资源,不触发权限重新申请
cleanupVirtualDisplayOnly()
// 记录权限问题,但不采取激进措施
Log.i(TAG, "🛡️ 权限问题已记录,保持保守策略")
}
} 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)
// 如果有padding需要裁剪
if (rowPadding != 0) {
Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight)
} else {
bitmap
}
} catch (e: Exception) {
Log.e(TAG, "转换Image为Bitmap失败", e)
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)
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, "💡 需要重新申请用户权限才能继续使用屏幕捕获")
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为已使用")
}
} catch (e: Exception) {
Log.e(TAG, "标记Android 15 session失败", e)
}
}
/**
* Android 15新增权限丢失通知
*/
private fun onMediaProjectionLost() {
try {
Log.w(TAG, "📢 MediaProjection权限丢失通知AccessibilityService")
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权限丢失广播")
}
} catch (e: Exception) {
Log.e(TAG, "通知MediaProjection权限丢失失败", e)
}
}
/**
* 暂停屏幕捕获但保留权限(用于重连时避免发送失败)
*/
fun pauseCapture() {
try {
Log.i(TAG, "暂停屏幕捕获(保留权限)")
isCapturing = false
isPaused = true
// ✅ 持久化暂停状态
savePauseState()
// 只清理VirtualDisplay保留MediaProjection权限
cleanupVirtualDisplayOnly()
Log.i(TAG, "屏幕捕获已暂停,权限保留,状态已持久化")
} catch (e: Exception) {
Log.e(TAG, "暂停屏幕捕获失败", e)
}
}
/**
* 恢复屏幕捕获
*/
fun resumeCapture() {
try {
if (!isPaused) {
Log.d(TAG, "屏幕捕获未暂停,无需恢复")
return
}
Log.i(TAG, "恢复屏幕捕获")
isPaused = false
// ✅ 持久化恢复状态
savePauseState()
startCapture()
} catch (e: Exception) {
Log.e(TAG, "恢复屏幕捕获失败", e)
}
}
/**
* 检查是否暂停
*/
fun isPaused(): Boolean {
return isPaused
}
/**
* 获取最后成功发送时间
*/
fun getLastSuccessfulSendTime(): Long? {
return lastSuccessfulSendTime
}
/**
* 停止屏幕捕获
*/
fun stopCapture() {
try {
Log.i(TAG, "停止屏幕捕获")
isCapturing = false
isPaused = false // 清理暂停状态
// ✅ 清理持久化的暂停状态
try {
val sp = service.getSharedPreferences(PAUSE_STATE_PREF, android.content.Context.MODE_PRIVATE)
sp.edit().putBoolean(KEY_IS_PAUSED, false).apply()
Log.d(TAG, "✅ 已清理持久化的暂停状态")
} catch (e: Exception) {
Log.w(TAG, "⚠️ 清理持久化暂停状态失败", e)
}
// 🔧 停止队列处理协程
stopQueueProcessor()
// ❌ 修复只清理VirtualDisplay保留MediaProjection权限防止Android 15权限丢失
// 正常停止时不应该销毁权限,只有用户主动停止或应用退出时才销毁
cleanupVirtualDisplayOnly() // 只清理显示资源,保留权限
Log.i(TAG, "屏幕捕获已停止(权限保留)")
// 关闭截图执行器
try { screenshotExecutor.shutdownNow() } catch (_: Exception) {}
} catch (e: Exception) {
Log.e(TAG, "停止屏幕捕获失败", e)
}
}
/**
* 强制停止屏幕捕获并清理权限(仅在用户主动停止时使用)
*/
fun forceStopCapture() {
try {
Log.i(TAG, "强制停止屏幕捕获")
isCapturing = false
isPaused = false // 清理暂停状态
// ✅ 清理持久化的暂停状态
try {
val sp = service.getSharedPreferences(PAUSE_STATE_PREF, android.content.Context.MODE_PRIVATE)
sp.edit().putBoolean(KEY_IS_PAUSED, false).apply()
Log.d(TAG, "✅ 已清理持久化的暂停状态")
} catch (e: Exception) {
Log.w(TAG, "⚠️ 清理持久化暂停状态失败", e)
}
// 🔧 停止队列处理协程
stopQueueProcessor()
// 强制清理MediaProjection权限
forceCleanupMediaProjectionResources()
Log.i(TAG, "屏幕捕获已强制停止")
// 关闭截图执行器
try { screenshotExecutor.shutdownNow() } catch (_: Exception) {}
} catch (e: Exception) {
Log.e(TAG, "强制停止屏幕捕获失败", e)
}
}
/**
* 设置MediaProjection
*/
fun setMediaProjection(projection: android.media.projection.MediaProjection) {
Log.i(TAG, "✅ MediaProjection已设置到ScreenCaptureManager")
this.mediaProjection = projection
MediaProjectionHolder.setMediaProjection(projection)
}
/**
* 🎯 智能压缩Bitmap为JPEG - 动态调整质量以平衡画质和传输效率
*/
private fun compressBitmap(bitmap: Bitmap): ByteArray {
val outputStream = ByteArrayOutputStream()
try {
// 计算缩放比例,确保不超过最大尺寸
val scaledBitmap = scaleDownBitmap(bitmap)
// 🎯 智能压缩:根据数据大小动态调整质量
var quality = dynamicQuality
var compressedData: ByteArray
var attempts = 0
val maxAttempts = 3
val targetSize = 150 * 1024 // 目标150KB平衡质量和传输效率
do {
outputStream.reset()
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
compressedData = outputStream.toByteArray()
attempts++
if (compressedData.size > targetSize && attempts < maxAttempts) {
// 数据过大,降低质量重新压缩
quality = maxOf(25, quality - 15) // 最低不低于25
Log.v(TAG, "🔧 数据过大(${compressedData.size} bytes),降低质量到$quality 重新压缩")
} else {
break
}
} while (attempts < maxAttempts)
// 如果缩放了释放缩放后的bitmap
if (scaledBitmap != bitmap) {
scaledBitmap.recycle()
}
Log.d(TAG, "🎯 智能压缩完成: 原始${bitmap.width}x${bitmap.height} -> 输出${compressedData.size} bytes (质量$quality, 尝试${attempts}次)")
} catch (e: Exception) {
Log.e(TAG, "❌ 智能压缩失败,使用基础压缩", e)
// 如果智能压缩失败,使用基础压缩
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++
// 🔧 启动队列处理器(只启动一次)
startQueueProcessor()
// 🎯 优化队列清理策略,减少画面跳跃,提供更平滑的体验
when {
screenDataQueue.size >= 13 -> {
// 队列接近满载适度清理2帧保持流畅避免5帧跳跃
Log.w(TAG, "⚠️ 队列接近满载(${screenDataQueue.size}/15),轻度清理旧数据保持流畅")
repeat(2) {
screenDataQueue.poll()?.let {
droppedFrameCount++
}
}
}
screenDataQueue.size >= 11 -> {
// 中等负载清理1帧防止积压
Log.v(TAG, "🔧 队列中等负载(${screenDataQueue.size}/15)清理1帧防止积压")
screenDataQueue.poll()?.let {
droppedFrameCount++
}
}
}
// 尝试添加新帧,如果满了就丢弃旧帧
if (!screenDataQueue.offer(frameData)) {
// 队列已满,丢弃最旧的帧并添加新帧
screenDataQueue.poll()?.let {
droppedFrameCount++
}
if (!screenDataQueue.offer(frameData)) {
Log.e(TAG, "❌ 队列清理后仍无法添加新帧,跳过此帧")
droppedFrameCount++
return
}
}
// 🔧 定期内存检查和清理
checkAndCleanMemory()
} catch (e: Exception) {
Log.e(TAG, "❌ 发送帧数据失败", e)
}
}
/**
* 🔧 启动单一的队列处理协程,避免协程泄漏
*/
private fun startQueueProcessor() {
if (queueProcessingStarted.compareAndSet(false, true)) {
Log.d(TAG, "🚀 启动屏幕数据队列处理协程")
queueProcessorJob = serviceScope.launch {
while (isCapturing && isActive) {
try {
val frameData = screenDataQueue.poll()
if (frameData != null) {
processFrameData(frameData)
} else {
// 队列为空短暂休眠避免CPU占用过高
delay(10)
}
} catch (e: Exception) {
Log.e(TAG, "❌ 队列处理协程异常", e)
delay(100) // 出错时延长休眠时间
}
}
Log.d(TAG, "🛑 屏幕数据队列处理协程结束")
queueProcessingStarted.set(false)
}
}
}
/**
* 🔧 停止队列处理协程
*/
private fun stopQueueProcessor() {
try {
Log.d(TAG, "🛑 停止屏幕数据队列处理协程")
queueProcessorJob?.cancel()
queueProcessorJob = null
queueProcessingStarted.set(false)
// 清空队列中剩余的数据
val remainingFrames = screenDataQueue.size
screenDataQueue.clear()
if (remainingFrames > 0) {
Log.d(TAG, "🗑️ 清空队列中剩余的${remainingFrames}帧数据")
}
} catch (e: Exception) {
Log.e(TAG, "❌ 停止队列处理协程失败", e)
}
}
/**
* 🔧 处理单帧数据
*/
private suspend fun processFrameData(frameData: ByteArray) {
try {
var success = false
// ✅ 优先使用Socket.IO v4官方客户端发送屏幕数据
val socketIOManager = service.getSocketIOManager()
if (socketIOManager != null && socketIOManager.isConnected()) {
socketIOManager.sendScreenData(frameData)
Log.v(TAG, "✅ Socket.IO v4发送帧数据: ${frameData.size} bytes")
success = true
} else {
Log.w(TAG, "⚠️ Socket.IO连接不可用无法发送屏幕数据")
}
// 记录成功发送时间
if (success) {
lastSuccessfulSendTime = System.currentTimeMillis()
}
// 🔧 确保frameData被GC回收
frameData.fill(0) // 清空数组内容
} catch (e: Exception) {
Log.e(TAG, "❌ 处理帧数据失败", e)
}
}
/**
* 🔧 内存检查和清理机制
*/
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)")
// 🚨 内存使用率过高时触发清理
if (memoryUsagePercent > maxMemoryUsagePercent) {
Log.w(TAG, "🚨 内存使用率过高(${(memoryUsagePercent * 100).toInt()}%),触发紧急清理")
performEmergencyCleanup()
}
// 🔧 定期清理无效的WeakReference
cleanupWeakReferences()
// 🔧 每分钟记录一次统计信息
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()}%")
}
} catch (e: Exception) {
Log.e(TAG, "❌ 内存检查失败", e)
}
}
/**
* 🚨 紧急内存清理
*/
private fun performEmergencyCleanup() {
try {
Log.w(TAG, "🚨 执行紧急内存清理")
// 1. 清空屏幕数据队列
val queueSize = screenDataQueue.size
screenDataQueue.clear()
Log.w(TAG, "🗑️ 清空屏幕数据队列,释放${queueSize}帧数据")
// 2. 清理缓存的图像
lastValidBitmap?.recycle()
lastValidBitmap = null
Log.w(TAG, "🗑️ 清理缓存图像")
// 3. 强制回收弱引用中的资源
cleanupWeakReferences(true)
// 4. 降低临时图像质量(如果需要的话)
Log.w(TAG, "🗑️ 临时降低图像质量以减少内存压力")
// 5. 建议系统进行垃圾回收
System.gc()
Log.w(TAG, "✅ 紧急内存清理完成")
} catch (e: Exception) {
Log.e(TAG, "❌ 紧急内存清理失败", e)
}
}
/**
* 🔧 清理WeakReference
*/
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}")
}
} catch (e: Exception) {
Log.e(TAG, "❌ 清理WeakReference失败", e)
}
}
/**
* 🔧 跟踪Bitmap资源
*/
private fun trackBitmap(bitmap: Bitmap): Bitmap {
activeBitmaps.add(WeakReference(bitmap))
return bitmap
}
/**
* 🔧 跟踪Image资源
*/
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) // 出错时延长间隔
}
}
}
}
/**
* ✅ 生成权限恢复提示测试图像
*/
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)
// 状态信息
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
}
}
/**
* ✅ 触发智能权限恢复机制 - 使用智能权限管理器
*/
private fun triggerPermissionRecovery() {
try {
Log.i(TAG, "🧠 尝试使用智能权限管理器进行权限恢复")
// 首先尝试智能权限管理器
val smartManager = com.hikoncont.manager.SmartMediaProjectionManager.getInstance(service)
val currentProjection = smartManager.getCurrentMediaProjection()
if (currentProjection != null) {
Log.i(TAG, "✅ 智能管理器找到有效的MediaProjection直接使用")
setMediaProjection(currentProjection)
return
}
// 智能管理器没有有效权限,检查是否可以静默恢复
val permissionData = com.hikoncont.MediaProjectionHolder.getPermissionData()
if (permissionData != null) {
val (resultCode, resultData) = permissionData
if (resultData != null) {
Log.i(TAG, "🔧 尝试通过智能管理器静默恢复权限")
val recovered = smartManager.setMediaProjection(resultCode, resultData)
if (recovered) {
Log.i(TAG, "✅ 智能权限恢复成功")
val newProjection = smartManager.getCurrentMediaProjection()
if (newProjection != null) {
setMediaProjection(newProjection)
return
}
}
}
}
// 智能恢复失败,回退到传统方式但更智能
Log.w(TAG, "⚠️ 智能权限恢复失败,回退到传统恢复机制")
triggerTraditionalPermissionRecovery()
} catch (e: Exception) {
Log.e(TAG, "❌ 智能权限恢复失败,回退到传统方式", e)
triggerTraditionalPermissionRecovery()
}
}
/**
* 传统权限恢复机制(作为智能恢复的备用方案)
*/
private fun triggerTraditionalPermissionRecovery() {
try {
Log.i(TAG, "🔧 开始传统MediaProjection权限恢复流程")
// ✅ 检查权限申请状态避免在权限申请达到上限时继续启动MainActivity
val accessibilityService = com.hikoncont.service.AccessibilityRemoteService.getInstance()
if (accessibilityService != null) {
if (accessibilityService.areAllPermissionsCompleted() || accessibilityService.isPermissionRequestInProgress()) {
Log.i(TAG, "⚠️ 权限申请已完成或正在进行中,跳过权限恢复流程")
return
}
}
// 使用协程避免阻塞主线程
captureScope.launch {
try {
// ❌ 修复权限恢复时不要清理MediaProjection权限
// 只清理VirtualDisplay等显示资源保留权限以防Android 15权限丢失
cleanupVirtualDisplayOnly() // 只清理显示资源,保留权限
Log.i(TAG, "🚀 启动MainActivity重新申请MediaProjection权限保留现有权限防止丢失")
// 启动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权限恢复流程已启动")
} catch (e: Exception) {
Log.e(TAG, "❌ 传统MediaProjection权限恢复失败", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ 触发传统权限恢复失败", e)
}
}
/**
* 只清理VirtualDisplay和ImageReader保留MediaProjection权限
*/
private fun cleanupVirtualDisplayOnly() {
try {
// ✅ 修复释放顺序:先释放 VirtualDisplay断开对 Surface 的引用),
// 等待系统 WindowManager 完成挂起的布局操作,再关闭 ImageReader释放 Surface
// 原来的顺序会导致 system_server 的 WindowManager 在异步镜像操作中
// 访问已释放的 Surface抛出 "Surface has already been released" 异常。
// 1. 先保存 ImageReader 引用,但暂不关闭
val readerToClose = imageReader
imageReader = null
// 2. 释放 VirtualDisplay断开它对 ImageReader.surface 的引用
virtualDisplay?.release()
virtualDisplay = null
// 3. 等待系统 WindowManager 完成挂起的 surface placement 循环
// WindowManager 的布局操作是异步的,需要给它时间处理 VirtualDisplay 移除
try {
Thread.sleep(100)
} catch (_: InterruptedException) {}
// 4. 现在安全关闭 ImageReader释放底层 Surface
readerToClose?.close()
// 清理缓存的图像
lastValidBitmap?.recycle()
lastValidBitmap = null
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设备
// 这会导致权限永久失效,只有在用户主动停止或应用完全退出时才清理权限
// 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
// ❌ 修复应用完全退出时才强制清理MediaProjection权限
// 正常情况下只清理VirtualDisplay保留权限以防Android 15权限丢失
cleanupVirtualDisplayOnly() // 只清理显示资源,保留权限
// 取消所有协程
captureScope.cancel()
// 停止后台线程
handlerThread.quitSafely()
Log.i(TAG, "屏幕捕获资源清理完成(权限保留)")
} catch (e: Exception) {
Log.e(TAG, "清理屏幕捕获资源失败", e)
}
}
/**
* 强制释放所有资源(仅在应用完全退出时使用)
*/
fun forceRelease() {
try {
Log.i(TAG, "强制清理屏幕捕获资源")
isCapturing = false
// 强制清理MediaProjection权限
forceCleanupMediaProjectionResources()
// 取消所有协程
captureScope.cancel()
// 停止后台线程
handlerThread.quitSafely()
Log.i(TAG, "屏幕捕获资源强制清理完成")
} catch (e: Exception) {
Log.e(TAG, "强制清理屏幕捕获资源失败", e)
}
}
/**
* 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以解决单次令牌限制
*/
private fun regenerateMediaProjectionForAndroid15(): Boolean {
return try {
if (Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "🔄 Android 15重新生成MediaProjection令牌")
val permissionData = MediaProjectionHolder.getPermissionData()
if (permissionData != null) {
val (resultCode, resultData) = permissionData
if (resultData != null) {
// 通过Android 15管理器重新创建
val accessibilityService = com.hikoncont.service.AccessibilityRemoteService.getInstance()
val android15Manager = accessibilityService?.getAndroid15MediaProjectionManager()
val newProjection = android15Manager?.createMediaProjectionWithCallback(resultCode, resultData)
if (newProjection != null) {
// 更新本地引用
mediaProjection = newProjection
MediaProjectionHolder.setMediaProjection(newProjection)
Log.i(TAG, "✅ Android 15 MediaProjection重新生成成功")
return true
} else {
Log.e(TAG, "❌ Android 15 MediaProjection重新生成失败创建返回null")
}
} else {
Log.e(TAG, "❌ Android 15重新生成失败权限数据中Intent为null")
}
} else {
Log.e(TAG, "❌ Android 15重新生成失败无权限数据")
}
}
false
} catch (e: Exception) {
Log.e(TAG, "❌ Android 15重新生成MediaProjection异常", e)
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以获取图像")
// ✅ 更新重新创建统计
val currentTime = System.currentTimeMillis()
lastVirtualDisplayRecreationTime = currentTime
consecutiveRecreationCount++
// ✅ 设置重新创建状态,抑制权限保活检查
isVirtualDisplayRecreating = true
Log.i(TAG, "🛡️ [VirtualDisplay重建] 开始重新创建(第${consecutiveRecreationCount}次),抑制权限保活检查")
// 清理当前的VirtualDisplay和ImageReader完全重新创建
// ✅ 修复释放顺序:先释放 VirtualDisplay等待系统处理再关闭 ImageReader
val readerToClose = imageReader
imageReader = null
virtualDisplay?.release()
virtualDisplay = null
// 等待系统 WindowManager 完成挂起的布局操作后再释放 Surface
Thread.sleep(200)
readerToClose?.close()
// 等待系统完全清理
Thread.sleep(300)
Log.i(TAG, "🔧 Android 15完全重新创建ImageReader和VirtualDisplay")
// 重新创建ImageReader
imageReader = android.media.ImageReader.newInstance(
screenWidth, screenHeight,
android.graphics.PixelFormat.RGBA_8888, 4 // 增加到4个缓存
)
// 添加监听器
imageReader?.setOnImageAvailableListener({ reader ->
Log.v(TAG, "🔧 Android 15重新初始化后: ImageReader有新图像可用")
}, backgroundHandler)
Log.i(TAG, "🔧 Android 15ImageReader重新创建完成")
// 重新创建VirtualDisplay
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
)
if (virtualDisplay != null) {
Log.i(TAG, "✅ Android 15 VirtualDisplay完全重新创建成功")
// ✅ Android 15VirtualDisplay重新创建成功后触发权限申请处理
Log.i(TAG, "🔧 Android 15VirtualDisplay重新创建成功触发权限申请处理")
triggerAndroid15PermissionRequest("重新初始化VirtualDisplay")
// 等待更长时间让新的VirtualDisplay完全稳定
Thread.sleep(1500)
// 进行二次验证
Log.i(TAG, "🔧 Android 15进行重新初始化后的验证")
val testImage = imageReader?.acquireLatestImage()
if (testImage != null) {
Log.i(TAG, "✅ Android 15重新初始化后验证成功图像可用")
testImage.close()
} else {
Log.w(TAG, "⚠️ Android 15重新初始化后验证仍无图像")
}
// ✅ 延迟重置状态,给系统时间完成权限检查
Thread.sleep(2000) // 等待2秒让系统完成所有权限检查
isVirtualDisplayRecreating = false
Log.i(TAG, "🛡️ [VirtualDisplay重建] 重新创建完成,恢复权限保活检查")
return true
} else {
Log.e(TAG, "❌ Android 15 VirtualDisplay重新创建失败")
isVirtualDisplayRecreating = false
return false
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ Android 15重新初始化VirtualDisplay失败", e)
// 确保异常情况下也重置状态
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尝试强制刷新图像获取")
// 方法1触发VirtualDisplay刷新
virtualDisplay?.surface?.let { surface ->
try {
// 强制触发surface渲染
Log.v(TAG, "🔧 Android 15触发Surface刷新")
} 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})")
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}")
// 更新缓存
lastValidBitmap?.recycle()
lastValidBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
lastCaptureTime = System.currentTimeMillis()
return bitmap
}
} catch (e: Exception) {
Log.e(TAG, "Android 15强制刷新图像转换失败", e)
} finally {
latestImage?.close()
}
}
Log.w(TAG, "⚠️ Android 15强制刷新未获得有效图像")
return null
} else {
return null
}
} catch (e: Exception) {
Log.e(TAG, "❌ Android 15强制刷新失败", e)
return null
}
}
/**
* Android 15设备注册成功后图像传输验证
*/
fun verifyAndroid15ImageTransmission() {
if (Build.VERSION.SDK_INT >= 35 && !android15ImageTransmissionVerified) {
Log.i(TAG, "🔍 Android 15设备注册成功开始图像传输验证...")
serviceScope.launch {
delay(android15ImageVerificationDelay)
try {
// 检查当前图像传输状态
val hasValidImage = checkImageTransmissionStatus()
if (!hasValidImage) {
Log.w(TAG, "⚠️ Android 15图像传输验证失败尝试修复...")
repairAndroid15ImageTransmission()
} else {
Log.i(TAG, "✅ Android 15图像传输验证成功")
android15ImageTransmissionVerified = true
}
} catch (e: Exception) {
Log.e(TAG, "❌ Android 15图像传输验证异常", e)
}
}
}
}
/**
* 检查图像传输状态
*/
private fun checkImageTransmissionStatus(): Boolean {
try {
// 检查VirtualDisplay是否存在
if (!isVirtualDisplayCreated()) {
Log.w(TAG, "❌ VirtualDisplay未创建")
return false
}
// 尝试获取最新图像
val latestImage = getLatestImage()
if (latestImage == null) {
Log.w(TAG, "❌ 无法获取最新图像")
return false
}
// 检查图像是否为有效图像(非测试图像)
val bitmap = convertImageToBitmap(latestImage)
if (bitmap == null) {
Log.w(TAG, "❌ 图像转换为Bitmap失败")
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")
return false
}
Log.i(TAG, "✅ 图像传输状态检查通过")
return true
} catch (e: Exception) {
Log.e(TAG, "❌ 检查图像传输状态失败", e)
return false
}
}
/**
* 修复Android 15图像传输问题
*/
private suspend fun repairAndroid15ImageTransmission() {
try {
Log.i(TAG, "🔧 开始修复Android 15图像传输...")
// 步骤1强制刷新Surface
Log.i(TAG, "📱 步骤1强制刷新Surface")
refreshSurfaceForAndroid15()
delay(2000)
if (checkImageTransmissionStatus()) {
Log.i(TAG, "✅ Surface刷新修复成功")
android15ImageTransmissionVerified = true
return
}
// 步骤2重新初始化ImageReader
Log.i(TAG, "📱 步骤2重新初始化ImageReader")
reinitializeImageReaderForAndroid15()
delay(3000)
if (checkImageTransmissionStatus()) {
Log.i(TAG, "✅ ImageReader重新初始化修复成功")
android15ImageTransmissionVerified = true
return
}
// 步骤3完整重建VirtualDisplay
Log.i(TAG, "📱 步骤3完整重建VirtualDisplay")
recreateVirtualDisplayForAndroid15()
delay(3000)
if (checkImageTransmissionStatus()) {
Log.i(TAG, "✅ VirtualDisplay重建修复成功")
android15ImageTransmissionVerified = true
return
}
Log.w(TAG, "⚠️ Android 15图像传输修复失败将依赖后续自动恢复机制")
} catch (e: Exception) {
Log.e(TAG, "❌ Android 15图像传输修复异常", e)
}
}
/**
* 为Android 15强制刷新Surface
* ✅ 修复:不能直接 release Surface必须先断开 VirtualDisplay 的引用
*/
private suspend fun refreshSurfaceForAndroid15() {
try {
Log.d(TAG, "🔄 Android 15强制刷新Surface — 通过重建 VirtualDisplay + ImageReader")
// 正确做法:通过 cleanupVirtualDisplayOnly 安全释放,再重建
cleanupVirtualDisplayOnly()
delay(300)
setupMediaProjectionResources()
} catch (e: Exception) {
Log.e(TAG, "❌ Android 15 Surface刷新失败", e)
}
}
/**
* 为Android 15重新初始化ImageReader
* ✅ 修复:不能在 VirtualDisplay 还持有 Surface 引用时释放 ImageReader
* 改为先创建新 ImageReader再切换 VirtualDisplay 的 Surface最后释放旧 ImageReader
*/
private suspend fun reinitializeImageReaderForAndroid15() {
try {
Log.d(TAG, "🔄 Android 15重新初始化ImageReader")
// 保存旧 ImageReader 引用
val oldReader = imageReader
// 先创建新 ImageReader
createImageReader()
delay(300)
// 如果VirtualDisplay存在先切换到新 Surface再释放旧的
virtualDisplay?.let { display ->
imageReader?.let { newReader ->
display.surface = newReader.surface
Log.d(TAG, "✅ Android 15已重新关联新ImageReader和VirtualDisplay")
}
}
// 等待系统完成 Surface 切换
delay(200)
// 现在安全释放旧 ImageReader
try {
oldReader?.close()
Log.d(TAG, "✅ 旧 ImageReader 已安全释放")
} catch (e: Exception) {
Log.w(TAG, "⚠️ 释放旧 ImageReader 异常", e)
}
} catch (e: Exception) {
Log.e(TAG, "❌ Android 15 ImageReader重新初始化失败", e)
}
}
/**
* 为Android 15完整重建VirtualDisplay
*/
private suspend fun recreateVirtualDisplayForAndroid15() {
try {
Log.d(TAG, "🔄 Android 15完整重建VirtualDisplay")
// 停止当前屏幕捕获
stopScreenCapture()
delay(2000)
// 重新启动屏幕捕获
if (startScreenCapture()) {
Log.i(TAG, "✅ Android 15 VirtualDisplay重建成功")
} else {
Log.w(TAG, "⚠️ Android 15 VirtualDisplay重建失败")
}
} catch (e: Exception) {
Log.e(TAG, "❌ Android 15 VirtualDisplay重建异常", e)
}
}
/**
* 检查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}")
} catch (e: Exception) {
Log.e(TAG, "❌ 创建ImageReader失败", e)
}
}
/**
* 释放ImageReader
*/
private fun releaseImageReader() {
try {
imageReader?.close()
imageReader = null
Log.d(TAG, "✅ 释放ImageReader")
} catch (e: Exception) {
Log.e(TAG, "❌ 释放ImageReader失败", e)
}
}
/**
* 开始屏幕捕获(别名方法)
*/
fun startScreenCapture(): Boolean {
return try {
startCapture()
true
} catch (e: Exception) {
Log.e(TAG, "❌ 启动屏幕捕获失败", e)
false
}
}
/**
* 停止屏幕捕获(别名方法)
*/
fun stopScreenCapture() {
try {
stopCapture()
} catch (e: Exception) {
Log.e(TAG, "❌ 停止屏幕捕获失败", e)
}
}
/**
* 获取当前屏幕截图
*/
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
*/
fun isVirtualDisplayRecreating(): Boolean {
return isVirtualDisplayRecreating
}
/**
* ✅ 智能判断是否应该重新创建VirtualDisplay
*/
private fun shouldRecreateVirtualDisplay(): Boolean {
val currentTime = System.currentTimeMillis()
// 检查冷却期
if (virtualDisplayRecreationCooldown) {
Log.i(TAG, "🛡️ VirtualDisplay重新创建处于冷却期跳过")
return false
}
// 检查最小间隔
if (currentTime - lastVirtualDisplayRecreationTime < minRecreationInterval) {
val remainingTime = minRecreationInterval - (currentTime - lastVirtualDisplayRecreationTime)
Log.i(TAG, "🛡️ VirtualDisplay重新创建间隔不足还需等待${remainingTime}ms")
return false
}
// 检查连续重新创建次数
if (consecutiveRecreationCount >= maxConsecutiveRecreations) {
Log.w(TAG, "🛡️ VirtualDisplay连续重新创建次数过多(${consecutiveRecreationCount}/${maxConsecutiveRecreations}),进入冷却期")
virtualDisplayRecreationCooldown = true
// 5分钟后重置冷却期
serviceScope.launch {
delay(300000) // 5分钟
virtualDisplayRecreationCooldown = false
consecutiveRecreationCount = 0
Log.i(TAG, "🔄 VirtualDisplay重新创建冷却期结束重置计数")
}
return false
}
// 检查失败次数是否达到阈值
if (consecutiveImageFailures < maxImageFailuresBeforeRecreation) {
Log.i(TAG, "🛡️ 图像失败次数未达到阈值(${consecutiveImageFailures}/${maxImageFailuresBeforeRecreation}),暂不重新创建")
return false
}
// 检查VirtualDisplay和ImageReader是否存在
if (virtualDisplay == null || imageReader == null) {
Log.w(TAG, "⚠️ VirtualDisplay或ImageReader为null需要重新创建")
return true
}
// 所有条件都满足,可以重新创建
Log.i(TAG, "✅ 满足VirtualDisplay重新创建条件失败${consecutiveImageFailures}次,上次重建${currentTime - lastVirtualDisplayRecreationTime}ms前")
return true
}
/**
* ✅ Android 15触发权限申请流程
*/
private fun triggerAndroid15PermissionRequest(reason: String) {
try {
Log.i(TAG, "🎯 [权限申请] Android 15触发权限申请$reason")
// 🚨 Android 11+特殊处理统一使用无障碍截图API跳过权限申请触发
if (Build.VERSION.SDK_INT >= 30) {
Log.i(TAG, "📱 Android 11+设备使用无障碍截图API跳过权限申请触发")
return
}
// 获取PermissionGranter实例
val permissionGranter = try {
(service as? AccessibilityRemoteService)?.getPermissionGranter()
} catch (e: Exception) {
Log.e(TAG, "❌ 无法获取PermissionGranter实例", e)
null
}
if (permissionGranter != null) {
// ✅ 强制设置权限申请状态,绕过现有权限检查
Log.i(TAG, "🎯 [权限申请] 强制设置MediaProjection申请状态为true准备处理权限弹窗")
// 使用反射或直接设置绕过setMediaProjectionRequesting中的权限检查
try {
// 直接设置内部状态,绕过权限检查
val field = permissionGranter.javaClass.getDeclaredField("isRequestingMediaProjection")
field.isAccessible = true
field.setBoolean(permissionGranter, true)
Log.i(TAG, "🎯 [权限申请] 已直接设置权限申请状态,绕过权限检查")
} catch (e: Exception) {
Log.w(TAG, "⚠️ 无法直接设置权限状态,使用常规方法", e)
permissionGranter.setMediaProjectionRequesting(true)
}
Log.i(TAG, "🎯 [权限申请] Android 15权限申请流程已触发等待自动处理权限弹窗")
// 启动异步监控,在权限处理完成后重置状态
serviceScope.launch {
delay(15000) // 15秒后自动重置状态给权限处理更多时间
try {
permissionGranter.setMediaProjectionRequesting(false)
Log.i(TAG, "🎯 [权限申请] 15秒后自动重置权限申请状态")
} catch (e: Exception) {
Log.w(TAG, "重置权限申请状态失败", e)
}
}
} else {
Log.w(TAG, "⚠️ [权限申请] 无法获取PermissionGranter跳过权限申请触发")
}
} catch (e: Exception) {
Log.e(TAG, "❌ [权限申请] 触发Android 15权限申请失败", e)
}
}
/**
* 🚨 Android 11+专用:生成测试图像
*/
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)
// 副标题
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()
}
}
}