You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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. 初始化与使用

在你的SmartApplicationonCreate里初始化并启动监控:

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方案下的最优解了。

火山引擎 最新活动