Files
and-bak/app/src/main/java/com/hikoncont/manager/SmartMediaProjectionManager.kt
wdvipa 39bc5b47a0 fix: 修复屏幕录制权限频繁掉落问题
- 根因:多处代码重复调用getMediaProjection()创建新实例,系统自动stop旧实例触发onStop回调,形成权限丢失恢复再丢失的死循环
- ScreenCaptureManager.ensureMediaProjection():禁止重复创建,只从Holder获取已有对象
- ScreenCaptureManager.captureWithMediaProjection():移除重复创建逻辑
- ScreenCaptureManager.triggerPermissionRecovery():优先从Holder和SmartManager获取已有对象
- ScreenCaptureManager.regenerateMediaProjectionForAndroid15():优先复用Holder中已有对象
- SmartMediaProjectionManager.attemptSilentRecovery():先检查Holder是否已有有效对象
- Android15MediaProjectionManager.attemptSilentRecovery():先检查Holder复用已有对象
- Android15MediaProjectionManager.determineStopReason():修复误判逻辑,Holder中仍有有效对象时识别为旧实例被替换而非用户主动停止
- AccessibilityRemoteService.attemptAndroid15SilentRecovery():优先复用Holder中已有对象
- AccessibilityRemoteService.handleMediaProjectionGranted():Android 11+优先从Holder获取
- RemoteControlForegroundService.handleStartMediaProjection():优先检查Holder避免重复创建
2026-02-15 00:22:47 +08:00

668 lines
22 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.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.BroadcastReceiver
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.hikoncont.MediaProjectionHolder
import com.hikoncont.service.AccessibilityRemoteService
import kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
/**
* 智能MediaProjection管理器
*
* 专门解决Android 15投屏权限丢失问题
* 1. 智能区分用户主动停止和系统自动停止
* 2. 多层级权限保持策略
* 3. 非侵入式权限恢复机制
* 4. 向下兼容所有Android版本
*/
class SmartMediaProjectionManager(
private val context: Context
) {
companion object {
private const val TAG = "SmartMediaProjection"
// 恢复策略配置
private const val MAX_SILENT_RECOVERY_ATTEMPTS = 5 // 最大静默恢复次数
private const val MAX_USER_RECOVERY_ATTEMPTS = 3 // 最大用户确认恢复次数
private const val SILENT_RECOVERY_DELAY = 3000L // 静默恢复延迟3秒
private const val USER_RECOVERY_DELAY = 30000L // 用户恢复延迟30秒
private const val PERMISSION_CHECK_INTERVAL = 10000L // 权限检查间隔10秒
// Android版本特定配置
private const val ANDROID_15_MIN_RECOVERY_INTERVAL = 120000L // Android 15最小恢复间隔2分钟
@Volatile
private var instance: SmartMediaProjectionManager? = null
fun getInstance(context: Context): SmartMediaProjectionManager {
return instance ?: synchronized(this) {
instance ?: SmartMediaProjectionManager(context.applicationContext).also { instance = it }
}
}
}
// 状态管理
private var mediaProjection: MediaProjection? = null
private var mediaProjectionManager: MediaProjectionManager? = null
private val isInitialized = AtomicBoolean(false)
private val isRecovering = AtomicBoolean(false)
private val silentRecoveryAttempts = AtomicInteger(0)
private val userRecoveryAttempts = AtomicInteger(0)
// 时间记录
@Volatile private var lastPermissionLostTime = 0L
@Volatile private var lastRecoveryTime = 0L
@Volatile private var lastUserInteractionTime = 0L
// 协程管理
private val managerScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var monitoringJob: Job? = null
// 回调接口
interface PermissionStateListener {
fun onPermissionLost(reason: LossReason)
fun onPermissionRecovered()
fun onPermissionFailedToRecover()
}
enum class LossReason {
USER_STOPPED, // 用户主动停止
SYSTEM_LOCK_SCREEN, // 系统锁屏停止
SYSTEM_AUTO_STOP, // 系统其他原因停止
APP_BACKGROUNDED, // 应用后台化
UNKNOWN // 未知原因
}
private val listeners = mutableSetOf<PermissionStateListener>()
// 系统状态监听
private val systemStateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> {
Log.i(TAG, "📱 设备锁屏,标记可能的权限丢失原因")
lastUserInteractionTime = System.currentTimeMillis()
}
Intent.ACTION_USER_PRESENT -> {
Log.i(TAG, "📱 用户解锁,检查权限状态")
checkPermissionStatusAfterUnlock()
}
"android.mycustrecev.USER_STOPPED_PROJECTION" -> {
Log.i(TAG, "👤 收到用户主动停止投屏信号")
handleUserStoppedProjection()
}
}
}
}
/**
* 智能MediaProjection回调
* 能够智能判断权限丢失的原因
*/
private val smartCallback = object : MediaProjection.Callback() {
override fun onStop() {
Log.w(TAG, "🛑 MediaProjection权限丢失")
val currentTime = System.currentTimeMillis()
lastPermissionLostTime = currentTime
// 智能判断丢失原因
val lossReason = determineLossReason(currentTime)
// 清理当前MediaProjection引用
cleanupCurrentProjection()
// 通知监听器
notifyPermissionLost(lossReason)
// 根据丢失原因选择恢复策略
selectRecoveryStrategy(lossReason)
}
}
/**
* 初始化智能管理器
*/
fun initialize(): Boolean {
return try {
if (isInitialized.get()) {
Log.d(TAG, "管理器已初始化")
return true
}
Log.i(TAG, "🚀 初始化智能MediaProjection管理器")
// 初始化MediaProjectionManager
mediaProjectionManager = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager
if (mediaProjectionManager == null) {
Log.e(TAG, "❌ 无法获取MediaProjectionManager")
return false
}
// 注册系统状态监听
registerSystemStateReceiver()
// 检查现有权限
checkAndRestoreExistingPermission()
// 启动权限状态监控
startPermissionMonitoring()
isInitialized.set(true)
Log.i(TAG, "✅ 智能MediaProjection管理器初始化成功")
true
} catch (e: Exception) {
Log.e(TAG, "❌ 初始化失败", e)
false
}
}
/**
* 智能判断权限丢失原因
*/
private fun determineLossReason(lossTime: Long): LossReason {
return when {
// 用户最近与状态栏交互(可能点击了停止按钮)
lossTime - lastUserInteractionTime < 5000 -> {
Log.i(TAG, "🤔 判断为用户主动停止(最近有用户交互)")
LossReason.USER_STOPPED
}
// 锁屏相关的权限丢失
lossTime - lastUserInteractionTime < 2000 -> {
Log.i(TAG, "🔒 判断为系统锁屏自动停止")
LossReason.SYSTEM_LOCK_SCREEN
}
// Android 15特有的系统自动停止
Build.VERSION.SDK_INT >= 35 -> {
Log.i(TAG, "🤖 Android 15系统自动停止")
LossReason.SYSTEM_AUTO_STOP
}
else -> {
Log.i(TAG, "❓ 未知原因的权限丢失")
LossReason.UNKNOWN
}
}
}
/**
* 根据丢失原因选择恢复策略
*/
private fun selectRecoveryStrategy(reason: LossReason) {
when (reason) {
LossReason.USER_STOPPED -> {
Log.i(TAG, "👤 用户主动停止,不进行自动恢复")
// 用户主动停止,重置恢复计数器但不自动恢复
resetRecoveryCounters()
}
LossReason.SYSTEM_LOCK_SCREEN -> {
Log.i(TAG, "🔒 锁屏导致的权限丢失,等待解锁后恢复")
// 等待用户解锁后再恢复
scheduleUnlockRecovery()
}
LossReason.SYSTEM_AUTO_STOP,
LossReason.APP_BACKGROUNDED,
LossReason.UNKNOWN -> {
Log.i(TAG, "🤖 系统自动停止,启动智能恢复")
// 系统原因,启动智能恢复
startSmartRecovery()
}
}
}
/**
* 启动智能恢复机制
*/
private fun startSmartRecovery() {
if (isRecovering.get()) {
Log.d(TAG, "🔄 恢复进程已在进行中")
return
}
managerScope.launch {
try {
isRecovering.set(true)
// 首先尝试静默恢复
if (attemptSilentRecovery()) {
Log.i(TAG, "✅ 静默恢复成功")
return@launch
}
// 静默恢复失败,根据情况决定是否提示用户
if (shouldPromptUserRecovery()) {
scheduleUserPromptedRecovery()
} else {
Log.i(TAG, "🤐 不满足用户提示条件,等待下次机会")
}
} catch (e: Exception) {
Log.e(TAG, "❌ 智能恢复失败", e)
} finally {
isRecovering.set(false)
}
}
}
/**
* 尝试静默恢复
*
* 🚨 核心修复:优先检查 Holder 中是否已有有效对象,
* 避免重复调用 getMediaProjection() 创建新实例导致旧实例被 stop
*/
private suspend fun attemptSilentRecovery(): Boolean {
val currentAttempts = silentRecoveryAttempts.get()
if (currentAttempts >= MAX_SILENT_RECOVERY_ATTEMPTS) {
Log.w(TAG, "⚠️ 已达到最大静默恢复次数($MAX_SILENT_RECOVERY_ATTEMPTS)")
return false
}
silentRecoveryAttempts.incrementAndGet()
Log.i(TAG, "🤫 尝试静默恢复 (${silentRecoveryAttempts.get()}/$MAX_SILENT_RECOVERY_ATTEMPTS)")
try {
// 延迟恢复,避免与系统操作冲突
delay(SILENT_RECOVERY_DELAY)
// ✅ 优先检查 Holder 中是否已有有效的 MediaProjection 对象
val existingProjection = MediaProjectionHolder.getMediaProjection()
if (existingProjection != null) {
Log.i(TAG, "✅ Holder中已有有效MediaProjection直接复用")
mediaProjection = existingProjection
notifyPermissionRecovered()
resetRecoveryCounters()
return true
}
// Holder 中无有效对象,尝试从权限数据重新创建(仅一次)
val permissionData = MediaProjectionHolder.getPermissionData()
if (permissionData != null) {
val (resultCode, resultData) = permissionData
if (resultData != null) {
val newProjection = createMediaProjectionSafely(resultCode, resultData)
if (newProjection != null) {
Log.i(TAG, "✅ 静默恢复成功")
notifyPermissionRecovered()
resetRecoveryCounters()
return true
}
}
}
Log.w(TAG, "❌ 静默恢复失败,权限数据不可用")
return false
} catch (e: Exception) {
Log.e(TAG, "❌ 静默恢复异常", e)
return false
}
}
/**
* 安全地创建MediaProjection
*/
private fun createMediaProjectionSafely(resultCode: Int, resultData: Intent): MediaProjection? {
return try {
val projection = mediaProjectionManager?.getMediaProjection(resultCode, resultData)
if (projection != null) {
// 注册智能回调
projection.registerCallback(smartCallback, Handler(Looper.getMainLooper()))
mediaProjection = projection
MediaProjectionHolder.setMediaProjection(projection)
Log.i(TAG, "✅ MediaProjection创建成功")
}
projection
} catch (e: Exception) {
Log.e(TAG, "❌ 创建MediaProjection失败", e)
null
}
}
/**
* 判断是否应该提示用户恢复
*/
private fun shouldPromptUserRecovery(): Boolean {
val currentTime = System.currentTimeMillis()
// 检查用户恢复次数
if (userRecoveryAttempts.get() >= MAX_USER_RECOVERY_ATTEMPTS) {
Log.w(TAG, "⚠️ 已达到最大用户恢复次数")
return false
}
// 检查最小恢复间隔Android 15需要更长间隔
val minInterval = if (Build.VERSION.SDK_INT >= 35) {
ANDROID_15_MIN_RECOVERY_INTERVAL
} else {
USER_RECOVERY_DELAY
}
if (currentTime - lastRecoveryTime < minInterval) {
Log.d(TAG, "🕐 未达到最小恢复间隔")
return false
}
// 检查是否有正在运行的AccessibilityService
if (!AccessibilityRemoteService.isServiceRunning()) {
Log.w(TAG, "⚠️ AccessibilityService未运行不提示用户恢复")
return false
}
return true
}
/**
* 安排用户提示恢复
*/
private fun scheduleUserPromptedRecovery() {
managerScope.launch {
try {
delay(USER_RECOVERY_DELAY)
userRecoveryAttempts.incrementAndGet()
lastRecoveryTime = System.currentTimeMillis()
Log.i(TAG, "🔔 启动用户提示恢复")
// 发送恢复请求广播(非侵入式)
val intent = Intent("android.mycustrecev.SMART_PERMISSION_RECOVERY").apply {
putExtra("recovery_type", "user_prompted")
putExtra("attempt", userRecoveryAttempts.get())
putExtra("max_attempts", MAX_USER_RECOVERY_ATTEMPTS)
}
context.sendBroadcast(intent)
} catch (e: Exception) {
Log.e(TAG, "❌ 用户提示恢复失败", e)
}
}
}
/**
* 处理解锁后的权限检查
*/
private fun checkPermissionStatusAfterUnlock() {
managerScope.launch {
try {
delay(2000) // 等待系统稳定
if (mediaProjection == null) {
Log.i(TAG, "🔓 解锁后检测到权限丢失,启动恢复")
startSmartRecovery()
}
} catch (e: Exception) {
Log.e(TAG, "❌ 解锁后权限检查失败", e)
}
}
}
/**
* 安排解锁恢复
*/
private fun scheduleUnlockRecovery() {
Log.i(TAG, "⏰ 安排解锁后恢复")
// 解锁恢复会在系统状态监听器中触发
}
/**
* 启动权限状态监控
*/
private fun startPermissionMonitoring() {
monitoringJob?.cancel()
monitoringJob = managerScope.launch {
while (isActive) {
try {
delay(PERMISSION_CHECK_INTERVAL)
// 检查MediaProjection是否仍然有效
val currentProjection = mediaProjection
if (currentProjection != null) {
// 执行轻量级状态检查
try {
// 这里可以添加一些检查逻辑来验证MediaProjection是否仍然有效
Log.v(TAG, "📊 权限状态检查:正常")
} catch (e: Exception) {
Log.w(TAG, "⚠️ 权限状态检查异常,可能已失效", e)
if (!isRecovering.get()) {
startSmartRecovery()
}
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ 权限监控异常", e)
}
}
}
}
/**
* 注册系统状态监听
*/
private fun registerSystemStateReceiver() {
val filter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_USER_PRESENT)
addAction("android.mycustrecev.USER_STOPPED_PROJECTION")
}
context.registerReceiver(systemStateReceiver, filter)
Log.i(TAG, "✅ 已注册系统状态监听")
}
/**
* 检查并恢复现有权限
*/
private fun checkAndRestoreExistingPermission() {
try {
val permissionData = MediaProjectionHolder.getPermissionData()
if (permissionData != null) {
val (resultCode, resultData) = permissionData
if (resultData != null) {
Log.i(TAG, "📱 发现现有权限数据,尝试恢复")
val projection = createMediaProjectionSafely(resultCode, resultData)
if (projection != null) {
Log.i(TAG, "✅ 现有权限恢复成功")
notifyPermissionRecovered()
}
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ 检查现有权限失败", e)
}
}
/**
* 处理用户主动停止投屏
*/
private fun handleUserStoppedProjection() {
Log.i(TAG, "👤 处理用户主动停止投屏")
lastUserInteractionTime = System.currentTimeMillis()
// 停止所有恢复机制
isRecovering.set(false)
resetRecoveryCounters()
// 清理MediaProjection
cleanupCurrentProjection()
}
/**
* 清理当前MediaProjection
*/
private fun cleanupCurrentProjection() {
try {
mediaProjection?.unregisterCallback(smartCallback)
mediaProjection = null
// 🚨 重要修复Android 15设备不要清理权限数据
if (android.os.Build.VERSION.SDK_INT >= 35) {
Log.i(TAG, "🛡️ Android 15设备仅清理MediaProjection引用保留权限数据")
// 不清理权限数据避免Android 15权限丢失
} else {
// 其他版本可以选择性清理,但建议保留
MediaProjectionHolder.clearMediaProjection()
Log.i(TAG, "MediaProjection引用和权限数据已清理")
}
} catch (e: Exception) {
Log.e(TAG, "❌ 清理MediaProjection失败", e)
}
}
/**
* 重置恢复计数器
*/
private fun resetRecoveryCounters() {
silentRecoveryAttempts.set(0)
userRecoveryAttempts.set(0)
Log.i(TAG, "🔄 恢复计数器已重置")
}
/**
* 添加权限状态监听器
*/
fun addPermissionStateListener(listener: PermissionStateListener) {
listeners.add(listener)
}
/**
* 移除权限状态监听器
*/
fun removePermissionStateListener(listener: PermissionStateListener) {
listeners.remove(listener)
}
/**
* 通知权限丢失
*/
private fun notifyPermissionLost(reason: LossReason) {
listeners.forEach { listener ->
try {
listener.onPermissionLost(reason)
} catch (e: Exception) {
Log.e(TAG, "❌ 通知权限丢失失败", e)
}
}
}
/**
* 通知权限恢复
*/
private fun notifyPermissionRecovered() {
listeners.forEach { listener ->
try {
listener.onPermissionRecovered()
} catch (e: Exception) {
Log.e(TAG, "❌ 通知权限恢复失败", e)
}
}
}
/**
* 获取当前MediaProjection
*/
fun getCurrentMediaProjection(): MediaProjection? {
return mediaProjection
}
/**
* 手动设置新的MediaProjection
*/
fun setMediaProjection(resultCode: Int, resultData: Intent): Boolean {
return try {
val projection = createMediaProjectionSafely(resultCode, resultData)
if (projection != null) {
resetRecoveryCounters()
notifyPermissionRecovered()
true
} else {
false
}
} catch (e: Exception) {
Log.e(TAG, "❌ 设置MediaProjection失败", e)
false
}
}
/**
* 手动停止MediaProjection
*/
fun stopMediaProjection() {
try {
Log.i(TAG, "🛑 用户手动停止MediaProjection")
// 发送用户停止信号
val intent = Intent("android.mycustrecev.USER_STOPPED_PROJECTION")
context.sendBroadcast(intent)
// 清理资源
mediaProjection?.stop()
cleanupCurrentProjection()
} catch (e: Exception) {
Log.e(TAG, "❌ 停止MediaProjection失败", e)
}
}
/**
* 获取管理器状态信息
*/
fun getStatusInfo(): Map<String, Any> {
return mapOf(
"initialized" to isInitialized.get(),
"hasPermission" to (mediaProjection != null),
"isRecovering" to isRecovering.get(),
"silentRecoveryAttempts" to silentRecoveryAttempts.get(),
"userRecoveryAttempts" to userRecoveryAttempts.get(),
"lastPermissionLostTime" to lastPermissionLostTime,
"lastRecoveryTime" to lastRecoveryTime,
"androidVersion" to Build.VERSION.SDK_INT
)
}
/**
* 清理资源
*/
fun cleanup() {
try {
Log.i(TAG, "🧹 清理智能MediaProjection管理器")
isInitialized.set(false)
monitoringJob?.cancel()
managerScope.cancel()
cleanupCurrentProjection()
context.unregisterReceiver(systemStateReceiver)
listeners.clear()
Log.i(TAG, "✅ 清理完成")
} catch (e: Exception) {
Log.e(TAG, "❌ 清理失败", e)
}
}
}