- MediaProjectionHolder添加全局创建锁safeGetOrCreateProjection() - 所有创建点统一通过安全入口,禁止直接调用getMediaProjection() - 重复创建新实例会导致系统stop旧实例触发onStop死循环 - Android15MediaProjectionManager/SmartMediaProjectionManager的onStop回调添加isCreating检查 - RemoteControlForegroundService备用方案改用安全创建入口 - AccessibilityRemoteService静默恢复改用安全创建入口 - 添加最小创建间隔5秒防止短时间内重复创建
3075 lines
129 KiB
Kotlin
3075 lines
129 KiB
Kotlin
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 = 3000L // ✅ 调整为3000ms,无障碍服务截图间隔3秒
|
||
|
||
// ✅ 持久化暂停状态相关常量
|
||
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次失败才考虑重新创建
|
||
|
||
// ✅ VirtualDisplay重建次数限制,防止无限重建循环
|
||
private var virtualDisplayRebuildCount = 0
|
||
private val MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS = 3
|
||
|
||
// 🔑 新增:AccessibilityService截图模式开关
|
||
private var useAccessibilityScreenshot = false
|
||
|
||
// 📊 自适应画质:运行时可调参数(覆盖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}")
|
||
}
|
||
|
||
/**
|
||
* ✅ 安全回收lastValidBitmap,防止多线程并发导致的native crash (SIGSEGV)
|
||
*
|
||
* 问题根因:多个协程(startMediaProjectionCapture、startAccessibilityScreenCapture、
|
||
* captureWithMediaProjection等)并发访问lastValidBitmap,可能导致:
|
||
* 1. double-recycle:Bitmap已被其他线程recycle但引用未置null
|
||
* 2. Hardware Bitmap的底层HardwareBuffer已被释放
|
||
* 两种情况都会在native层BitmapWrapper::freePixels()触发SIGSEGV
|
||
*/
|
||
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}")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ✅ 安全回收任意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}")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* <20> 清空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}")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 🔑 启用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
|
||
virtualDisplayRebuildCount = 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 {
|
||
// 安全获取Image,防止maxImages溢出
|
||
val image = try {
|
||
imageReader?.acquireLatestImage()
|
||
} catch (e: IllegalStateException) {
|
||
Log.w(TAG, "⚠️ 流式采集ImageReader缓冲区已满,清空: ${e.message}")
|
||
drainImageReader()
|
||
null
|
||
}
|
||
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)
|
||
|
||
safeRecycleLastValidBitmap()
|
||
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
|
||
}
|
||
|
||
// 发送缓存的有效帧保持画面连续(如果有的话)
|
||
if (lastValidBitmap != null && !lastValidBitmap!!.isRecycled) {
|
||
val cacheAge = System.currentTimeMillis() - lastCaptureTime
|
||
if (cacheAge < 10000) {
|
||
val cachedJpeg = compressBitmap(lastValidBitmap!!)
|
||
if (cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
|
||
sendFrameToServer(cachedJpeg)
|
||
Log.d(TAG, "📸 使用缓存帧替代黑屏帧 (${cacheAge}ms前)")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
bitmap.recycle()
|
||
}
|
||
} finally {
|
||
image.close()
|
||
}
|
||
} else {
|
||
consecutiveFailures++
|
||
|
||
// ✅ 无新帧时发送缓存帧,保持画面连续
|
||
if (lastValidBitmap != null && !lastValidBitmap!!.isRecycled) {
|
||
val cacheAge = System.currentTimeMillis() - lastCaptureTime
|
||
if (cacheAge < 10000) {
|
||
val cachedJpeg = compressBitmap(lastValidBitmap!!)
|
||
if (cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
|
||
sendFrameToServer(cachedJpeg)
|
||
Log.d(TAG, "📸 无新帧,发送缓存帧 (${cacheAge}ms前)")
|
||
}
|
||
}
|
||
}
|
||
|
||
// 连续失败过多,尝试重建 VirtualDisplay
|
||
if (consecutiveFailures >= maxImageFailuresBeforeRecreation) {
|
||
virtualDisplayRebuildCount++
|
||
Log.w(TAG, "⚠️ 连续 ${consecutiveFailures} 次无法获取有效帧,重建 VirtualDisplay (${virtualDisplayRebuildCount}/${MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS})")
|
||
|
||
// 重建次数超限,回退到无障碍截图
|
||
if (virtualDisplayRebuildCount > MAX_VIRTUAL_DISPLAY_REBUILD_ATTEMPTS) {
|
||
Log.w(TAG, "🚨 重建次数已达上限,回退到无障碍截图")
|
||
cleanupVirtualDisplayOnly()
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||
enableAccessibilityScreenshotMode()
|
||
startAccessibilityScreenCapture()
|
||
} else {
|
||
startFallbackCapture()
|
||
}
|
||
return@launch
|
||
}
|
||
|
||
cleanupVirtualDisplayOnly()
|
||
delay(500)
|
||
setupMediaProjectionResources()
|
||
consecutiveFailures = 0
|
||
|
||
if (virtualDisplay == null) {
|
||
Log.w(TAG, "⚠️ VirtualDisplay 重建失败,回退到无障碍截图")
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||
enableAccessibilityScreenshotMode()
|
||
startAccessibilityScreenCapture()
|
||
}
|
||
return@launch
|
||
}
|
||
}
|
||
}
|
||
|
||
// 按动态帧率控制采集间隔
|
||
delay(1000L / dynamicFps)
|
||
|
||
} catch (e: CancellationException) {
|
||
throw e
|
||
} catch (e: Exception) {
|
||
Log.w(TAG, "⚠️ MediaProjection 帧采集异常: ${e.message}")
|
||
consecutiveFailures++
|
||
delay(100)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 确保 MediaProjection 可用
|
||
*
|
||
* 🚨 核心修复:禁止重复调用 getMediaProjection(resultCode, resultData)
|
||
* 每次调用都会创建新实例,系统会自动 stop 旧实例,触发 onStop 回调,
|
||
* 形成"权限丢失→恢复→再丢失"的死循环,这是权限频繁掉落的根因。
|
||
*
|
||
* 只从 MediaProjectionHolder 获取已有对象,不重新创建。
|
||
*/
|
||
private fun ensureMediaProjection(): Boolean {
|
||
if (mediaProjection != null) return true
|
||
|
||
// 从全局 Holder 获取已有的 MediaProjection 对象
|
||
mediaProjection = MediaProjectionHolder.getMediaProjection()
|
||
if (mediaProjection != null) {
|
||
Log.i(TAG, "✅ 从 MediaProjectionHolder 获取到已有 MediaProjection")
|
||
return true
|
||
}
|
||
|
||
// 🚨 不再重复调用 getMediaProjection() 创建新实例
|
||
// 如果 Holder 中没有有效对象,说明权限确实未授予或已过期
|
||
// 应由 MainActivity 的权限申请流程统一创建
|
||
Log.w(TAG, "⚠️ MediaProjectionHolder 中无有效 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 {
|
||
// 清理旧的缓存
|
||
safeRecycleLastValidBitmap()
|
||
// 保存当前成功的截图副本
|
||
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++
|
||
if (consecutiveFailures % 50 == 1) {
|
||
Log.d(TAG, "📱 无障碍截图失败(${consecutiveFailures}次),跳过本帧等待下次")
|
||
}
|
||
}
|
||
|
||
// ✅ 截图成功时按帧率延迟,失败时短延迟后立即重试
|
||
if (consecutiveFailures == 0) {
|
||
delay(1000 / dynamicFps.toLong())
|
||
} else {
|
||
delay(50) // 失败时50ms后重试,让系统截图间隔限制自己控制节奏
|
||
}
|
||
|
||
} catch (e: CancellationException) {
|
||
throw e
|
||
} catch (e: Exception) {
|
||
Log.w(TAG, "⚠️ 屏幕捕获失败: ${e.message}")
|
||
consecutiveFailures++
|
||
delay(100) // ✅ 出错时进一步缩短间隔,保持流畅度(从200ms改为100ms)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 🚨 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) {
|
||
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 {
|
||
// ✅ 修复:检查截图间隔,防止截图间隔太短
|
||
val currentTime = System.currentTimeMillis()
|
||
val timeSinceLastScreenshot = currentTime - lastScreenshotTime
|
||
|
||
if (timeSinceLastScreenshot < MIN_CAPTURE_INTERVAL) {
|
||
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 {
|
||
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()
|
||
|
||
// 🔑 关键修复:正确提取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) {
|
||
Log.e(TAG, "无障碍服务截图失败,错误码: $failureErrorCode")
|
||
errorCode = failureErrorCode
|
||
|
||
// 🚨 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 -> "截图间隔太短"
|
||
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")
|
||
}
|
||
|
||
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, "无障碍服务截图超时")
|
||
// 🚨 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未初始化,Holder中无有效对象")
|
||
|
||
// 🚨 核心修复:不再重复调用 getMediaProjection() 创建新实例
|
||
// 重复创建会导致系统 stop 旧实例,触发 onStop 回调死循环
|
||
// 权限应由 MainActivity 统一申请
|
||
|
||
// ✅ 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
|
||
}
|
||
}
|
||
}
|
||
|
||
// 🛡️ 保守策略:只在实际使用失败时才进行权限检测,避免过度检测
|
||
// 移除主动的可用性测试,让权限在实际使用中自然暴露问题
|
||
|
||
// 初始化ImageReader和VirtualDisplay(仅第一次)
|
||
if (imageReader == null || virtualDisplay == null) {
|
||
setupMediaProjectionResources()
|
||
}
|
||
|
||
// 尝试获取最新图像,带缓存机制
|
||
val currentTime = System.currentTimeMillis()
|
||
var newBitmap: Bitmap? = null
|
||
|
||
// ✅ 尝试获取新图像,用安全方式防止maxImages溢出
|
||
var retryCount = 0
|
||
val maxRetries = if (Build.VERSION.SDK_INT >= 35) 5 else 2
|
||
|
||
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
|
||
}
|
||
|
||
if (image != null) {
|
||
try {
|
||
newBitmap = convertImageToBitmap(image)
|
||
if (newBitmap != null) {
|
||
trackBitmap(newBitmap)
|
||
Log.d(TAG, "MediaProjection获取新图像: ${newBitmap.width}x${newBitmap.height}")
|
||
|
||
if (consecutiveImageFailures > 0) {
|
||
Log.d(TAG, "✅ 图像获取成功,重置失败计数(之前${consecutiveImageFailures}次)")
|
||
consecutiveImageFailures = 0
|
||
}
|
||
|
||
safeRecycleLastValidBitmap()
|
||
lastValidBitmap = newBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||
lastValidBitmap?.let { trackBitmap(it) }
|
||
lastCaptureTime = currentTime
|
||
|
||
return newBitmap
|
||
}
|
||
} catch (e: Exception) {
|
||
Log.e(TAG, "转换图像失败", e)
|
||
} finally {
|
||
// 确保Image被close,释放ImageReader缓冲区槽位
|
||
try { image.close() } catch (_: Exception) {}
|
||
}
|
||
}
|
||
|
||
val waitTime = if (Build.VERSION.SDK_INT >= 35) {
|
||
if (retryCount == 0) 50L else (retryCount * 30L)
|
||
} else {
|
||
20L
|
||
}
|
||
Thread.sleep(waitTime)
|
||
retryCount++
|
||
}
|
||
|
||
// 如果获取不到新图像,检查是否可以使用缓存
|
||
if (lastValidBitmap != null) {
|
||
val timeSinceLastCapture = currentTime - lastCaptureTime
|
||
if (timeSinceLastCapture < 30000) {
|
||
Log.d(TAG, "使用缓存图像 (${timeSinceLastCapture}ms前) - 静止页面")
|
||
return lastValidBitmap!!.copy(Bitmap.Config.ARGB_8888, false)
|
||
} else {
|
||
Log.w(TAG, "缓存图像过期 (${timeSinceLastCapture}ms前),清理缓存")
|
||
safeRecycleLastValidBitmap()
|
||
}
|
||
}
|
||
|
||
// ✅ 智能重新初始化逻辑:增加失败计数和多层检查
|
||
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重新初始化后成功获取图像")
|
||
safeRecycleLastValidBitmap()
|
||
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()
|
||
|
||
// ✅ 创建ImageReader,bufferCount至少4个
|
||
// acquireLatestImage()内部需要先acquireNextImage再遍历找最新帧,
|
||
// bufferCount=2时如果有1个未close的Image就会触发maxImages异常
|
||
val bufferCount = if (Build.VERSION.SDK_INT >= 35) {
|
||
5 // Android 15需要更多缓存确保图像连续性
|
||
} else {
|
||
4 // 其他版本也需要足够缓存,避免acquireLatestImage的maxImages异常
|
||
}
|
||
|
||
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 15:VirtualDisplay创建成功后触发权限申请处理
|
||
if (Build.VERSION.SDK_INT >= 35) {
|
||
Log.i(TAG, "🔧 Android 15:VirtualDisplay创建成功,触发权限申请处理")
|
||
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 {
|
||
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)
|
||
}
|
||
}
|
||
} catch (e: CancellationException) {
|
||
Log.d(TAG, "🛑 队列处理协程被取消(正常停止)")
|
||
} finally {
|
||
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) { // 5fps,150帧约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. 清理缓存的图像
|
||
safeRecycleLastValidBitmap()
|
||
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
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ✅ 触发智能权限恢复机制
|
||
*
|
||
* 🚨 核心修复:优先从 Holder 获取已有对象,
|
||
* 避免通过 SmartManager 重复创建新实例导致旧实例被 stop
|
||
*/
|
||
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
|
||
}
|
||
|
||
// ✅ 第二步:从智能管理器获取
|
||
val smartManager = com.hikoncont.manager.SmartMediaProjectionManager.getInstance(service)
|
||
val currentProjection = smartManager.getCurrentMediaProjection()
|
||
if (currentProjection != null) {
|
||
Log.i(TAG, "✅ 智能管理器找到有效的MediaProjection,直接使用")
|
||
setMediaProjection(currentProjection)
|
||
return
|
||
}
|
||
|
||
// ✅ 第三步:都没有有效对象,回退到传统恢复(重新申请权限)
|
||
Log.w(TAG, "⚠️ 无有效MediaProjection对象,回退到传统恢复机制")
|
||
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
|
||
virtualDisplay?.release()
|
||
virtualDisplay = null
|
||
|
||
// 清理ImageReader
|
||
imageReader?.close()
|
||
imageReader = null
|
||
|
||
// ✅ 安全清理缓存的Bitmap,防止多线程并发导致native crash (SIGSEGV)
|
||
safeRecycleLastValidBitmap()
|
||
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以解决单次令牌限制
|
||
*
|
||
* 🚨 核心修复:只从 Holder 获取已有对象,禁止重复创建
|
||
*/
|
||
private fun regenerateMediaProjectionForAndroid15(): Boolean {
|
||
return try {
|
||
if (Build.VERSION.SDK_INT >= 35) {
|
||
Log.i(TAG, "🔄 Android 15:尝试获取可用的MediaProjection")
|
||
|
||
// 只从 Holder 获取已有对象
|
||
val existingProjection = MediaProjectionHolder.getMediaProjection()
|
||
if (existingProjection != null) {
|
||
Log.i(TAG, "✅ Holder中已有有效MediaProjection,直接复用")
|
||
mediaProjection = existingProjection
|
||
return true
|
||
}
|
||
|
||
// 🚨 不再重新创建!避免死循环
|
||
Log.w(TAG, "❌ Holder中无有效MediaProjection,等待权限重新授予")
|
||
}
|
||
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?.release()
|
||
virtualDisplay = null
|
||
imageReader?.close()
|
||
imageReader = null
|
||
|
||
// 等待系统完全清理
|
||
Thread.sleep(500)
|
||
|
||
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 15:ImageReader重新创建完成")
|
||
|
||
// 重新创建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 15:VirtualDisplay重新创建成功后触发权限申请处理
|
||
Log.i(TAG, "🔧 Android 15:VirtualDisplay重新创建成功,触发权限申请处理")
|
||
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}")
|
||
|
||
// 更新缓存
|
||
safeRecycleLastValidBitmap()
|
||
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
|
||
*/
|
||
private suspend fun refreshSurfaceForAndroid15() {
|
||
try {
|
||
imageReader?.let { reader ->
|
||
val surface = reader.surface
|
||
if (surface.isValid) {
|
||
Log.d(TAG, "🔄 Android 15强制刷新Surface")
|
||
// 通过重设Surface的方式强制刷新
|
||
surface.release()
|
||
delay(500)
|
||
|
||
// 重新创建ImageReader(如果需要)
|
||
if (isVirtualDisplayCreated()) {
|
||
createImageReader()
|
||
}
|
||
}
|
||
}
|
||
} catch (e: Exception) {
|
||
Log.e(TAG, "❌ Android 15 Surface刷新失败", e)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 为Android 15重新初始化ImageReader
|
||
*/
|
||
private suspend fun reinitializeImageReaderForAndroid15() {
|
||
try {
|
||
Log.d(TAG, "🔄 Android 15重新初始化ImageReader")
|
||
|
||
// 释放现有ImageReader
|
||
releaseImageReader()
|
||
delay(1000)
|
||
|
||
// 重新创建ImageReader
|
||
createImageReader()
|
||
delay(1000)
|
||
|
||
// 如果VirtualDisplay存在,重新关联Surface
|
||
virtualDisplay?.let { display ->
|
||
imageReader?.let { reader ->
|
||
display.surface = reader.surface
|
||
Log.d(TAG, "✅ Android 15已重新关联ImageReader和VirtualDisplay")
|
||
}
|
||
}
|
||
|
||
} 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()
|
||
}
|
||
}
|
||
} |