Android端非FLAG_SECURE方案下,如何解决录屏/投屏前置启动的检测绕过问题
Android端非FLAG_SECURE方案下,如何解决录屏/投屏前置启动的检测绕过问题
我完全理解你的痛点——用DisplayManager监听外接显示器的方案在录屏/投屏提前启动时直接失效,又不想用FLAG_SECURE一刀切屏蔽截图,还想对齐iOS同事实现的用户体验。咱们一步步来优化现有方案,解决这个绕过问题。
现有方案的核心问题
你当前的DisplayListener只有在新增外接显示器时触发onDisplayAdded,如果录屏/投屏在APP启动前就已经运行,APP初始化时外接显示器已经存在了,自然不会触发这个回调。虽然你在startMonitoring里加了200ms延迟检测,但这个时机可能过早(比如APP还没完全初始化完成),或者某些场景下状态读取有延迟,导致漏检。
改进方案:多维度检测+全时机覆盖
要解决提前启动的漏检问题,我们需要从检测时机和检测维度两方面优化,同时加兜底方案:
1. 全场景触发检测
不仅在显示器新增/移除时检测,还要在以下关键节点主动触发全量检测:
- APP启动初始化完成时
- APP从后台回到前台(Activity
onResume)时 - 显示器状态发生任何变化(
onDisplayChanged)时 - (兜底)APP前台运行时定期轮询检测
2. 增强媒体投影检测的准确率
你当前用反射读取sys.service.media.projection的方式,不同厂商系统的属性名可能有差异,我们可以多检测几个常见的系统属性,提高兼容性。
3. 优化Overlay的显示逻辑
确保每次状态变化时,不仅显示/隐藏Overlay,还要在Activity切换时重新绑定Overlay,避免界面切换后Overlay失效。
调整后的完整代码示例
1. 优化ScreenRecordingDetectionManager
class ScreenRecordingDetectionManager(private val context: Context) : Application.ActivityLifecycleCallbacks { private val controller: ScreenSecurityController = (context as SmartApplication).getScreenSecurityController() ?: throw IllegalStateException("ScreenSecurityController not initialized") private val pollingHandler = Handler(Looper.getMainLooper()) private val pollingRunnable = Runnable { checkRecordingState() pollingHandler.postDelayed(pollingRunnable, 1500) // 1.5秒轮询一次,可根据需求调整 } private val displayListener = object : DisplayManager.DisplayListener { override fun onDisplayAdded(id: Int) { checkRecordingState() } override fun onDisplayRemoved(id: Int) { checkRecordingState() } override fun onDisplayChanged(id: Int) { checkRecordingState() // 显示器状态发生任何变化时,重新检测全量状态 } } // 统一检测录屏/投屏状态的核心方法 fun checkRecordingState() { val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager // 检测是否有外接显示器(录屏/投屏会新增虚拟显示器) val hasExternalDisplay = dm.displays.any { it.displayId != Display.DEFAULT_DISPLAY } // 检测媒体投影是否在运行 val isMediaProjectionActive = controller.isMediaProjectionRecording() if (hasExternalDisplay || isMediaProjectionActive) { controller.showSecurityOverlay() } else { controller.hideSecurityOverlay() } } fun startMonitoring() { val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager dm.registerDisplayListener(displayListener, null) // 立即触发一次全量检测,无需延迟 checkRecordingState() // 注册Activity生命周期监听,覆盖前后台切换场景 (context as? Application)?.registerActivityLifecycleCallbacks(this) } fun stopMonitoring() { val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager dm.unregisterDisplayListener(displayListener) (context as? Application)?.unregisterActivityLifecycleCallbacks(this) pollingHandler.removeCallbacks(pollingRunnable) } override fun onActivityResumed(activity: Activity) { // 回到前台时重启轮询+触发检测 pollingHandler.removeCallbacks(pollingRunnable) pollingHandler.postDelayed(pollingRunnable, 1500) checkRecordingState() } override fun onActivityPaused(activity: Activity) { // 后台时停止轮询,减少耗电 pollingHandler.removeCallbacks(pollingRunnable) } // 其他生命周期方法空实现 override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} override fun onActivityStarted(activity: Activity) {} override fun onActivityStopped(activity: Activity) {} override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} override fun onActivityDestroyed(activity: Activity) {} }
2. 优化ScreenSecurityController
class ScreenSecurityController(private val context: Context) : Application.ActivityLifecycleCallbacks { private var recordingOverlay: View? = null var isRecording = false private set fun showSecurityOverlay() { if (isRecording) return isRecording = true applyOverlayToCurrentActivity() } fun hideSecurityOverlay() { if (!isRecording) return isRecording = false recordingOverlay?.visibility = View.GONE } private fun applyOverlayToCurrentActivity() { val currentActivity = (context as? SmartApplication)?.currentActivity ?: return val root = currentActivity.findViewById<ViewGroup>(android.R.id.content) // 如果Overlay已存在且绑定到当前Activity,直接显示 if (recordingOverlay?.parent == root) { recordingOverlay?.visibility = View.VISIBLE return } // 重新创建并绑定Overlay到当前Activity recordingOverlay?.let { (it.parent as? ViewGroup)?.removeView(it) } recordingOverlay = LayoutInflater.from(currentActivity) .inflate(R.layout.screen_recording_overlay, root, false) root.addView(recordingOverlay) recordingOverlay?.bringToFront() recordingOverlay?.visibility = View.VISIBLE } override fun onActivityResumed(activity: Activity) { // 切换Activity时重新绑定Overlay,避免失效 if (isRecording) applyOverlayToCurrentActivity() } // 增强媒体投影检测:多检测几个常见系统属性,提高兼容性 @SuppressLint("PrivateApi") fun isMediaProjectionRecording(): Boolean { return try { val systemPropertiesClass = Class.forName("android.os.SystemProperties") val getMethod = systemPropertiesClass.getMethod("get", String::class.java) // 检测多个常见的录屏相关系统属性 val projectionState = getMethod.invoke(null, "sys.service.media.projection") as String val screenRecordState = getMethod.invoke(null, "persist.sys.screenrecord") as String val mediaProjectionActive = getMethod.invoke(null, "media_projection.active") as String projectionState.contains("running", ignoreCase = true) || screenRecordState == "1" || mediaProjectionActive == "1" } catch (e: Exception) { // 反射失败时默认返回false,避免崩溃 false } } // 其他生命周期方法空实现 override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} override fun onActivityStarted(activity: Activity) {} override fun onActivityPaused(activity: Activity) {} override fun onActivityStopped(activity: Activity) {} override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} override fun onActivityDestroyed(activity: Activity) {} }
3. 初始化与使用
在你的SmartApplication的onCreate里初始化并启动监控:
class SmartApplication : Application() { lateinit var screenSecurityController: ScreenSecurityController lateinit var screenRecordingDetectionManager: ScreenRecordingDetectionManager override fun onCreate() { super.onCreate() screenSecurityController = ScreenSecurityController(this) screenRecordingDetectionManager = ScreenRecordingDetectionManager(this) screenRecordingDetectionManager.startMonitoring() } fun getScreenSecurityController() = screenSecurityController }
方案说明与局限性
- 覆盖场景:这个方案可以覆盖绝大多数主流场景,包括提前启动录屏/投屏、APP内启动、前后台切换等,同时不会阻止用户截图。
- 兼容性:反射读取系统属性的方式可能在部分小众厂商系统上失效,你可以根据测试结果新增或调整检测的属性名。
- 耗电优化:轮询只在APP前台运行时开启,后台自动停止,不会造成额外的耗电压力。
虽然这个方案做不到FLAG_SECURE那样的100%防护,但已经能对齐iOS的核心体验,同时满足你不屏蔽截图的需求,是当前非FLAG_SECURE方案下的最优解了。




