I am trying to make a custom overlay for my application that will hide the screen if any screen share or unauthorized recording of my app is running.
The problem is, that I am trying to avoid using FLAG_SECURE, because it doesn't allow me to add custom elements to inform the user that the screen is being recorded and show them what is going on.
I have tried creating custom logic, and here is what I have achieved so far:
I managed to create a custom detection using DisplayManager, to catch whether recording is active. However, I found a bypass that I cannot fix. If you start recording before entering the app, your screen doesn't hide. Recording that starts outside of the app cannot be detected. The detection only works if recording starts while the app is in the background or on screen.
So far, I have tested sharing my screen and starting recording while in the app. This works great and looks really nice to inform the user what is actually happening. But this can be bypassed if recording starts outside the app before entering.
I have read forums and tried other approaches, but everyone says it is not possible without FLAG_SECURE. Using FLAG_SECURE would also block screenshots, which is not what I want. My goal is to achieve something similar to what my iOS colleague has implemented.
Here's the code I'm using:
class ScreenRecordingDetectionManager(private val context: Context) {
private val controller: ScreenSecurityController =
(context as SmartApplication).getScreenSecurityController()
?: throw IllegalStateException("ScreenSecurityController not initialized")
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(id: Int) {
if (id != Display.DEFAULT_DISPLAY) {
controller.showSecurityOverlay()
}
}
override fun onDisplayRemoved(id: Int) {
if (id != Display.DEFAULT_DISPLAY) {
controller.hideSecurityOverlay()
}
}
override fun onDisplayChanged(id: Int) {}
}
fun startMonitoring() {
val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
dm.registerDisplayListener(displayListener, null)
val handler = android.os.Handler(context.mainLooper)
handler.postDelayed({
val isExternalScreen =
dm.displays.any { it.displayId != Display.DEFAULT_DISPLAY }
val isMediaProjection =
controller.isMediaProjectionRecording()
if (isExternalScreen || isMediaProjection) {
controller.showSecurityOverlay()
}
}, 200)
}
}
class ScreenSecurityController(private val context: Context) :
Application.ActivityLifecycleCallbacks {
private var recordingOverlay: View? = null
private var isRecording = false
fun showSecurityOverlay() {
isRecording = true
applyOverlayToCurrentActivity()
}
fun hideSecurityOverlay() {
isRecording = false
recordingOverlay?.visibility = View.GONE
}
private fun applyOverlayToCurrentActivity() {
if (recordingOverlay != null && recordingOverlay?.parent != null)
return
val activity = (context as? SmartApplication)?.currentActivity ?: return
val root = activity.findViewById<ViewGroup>(android.R.id.content)
recordingOverlay = LayoutInflater.from(activity)
.inflate(R.layout.screen_recording_overlay, root, false)
root.addView(recordingOverlay)
recordingOverlay?.bringToFront()
recordingOverlay?.visibility = if (isRecording) View.VISIBLE else View.GONE
}
override fun onActivityResumed(activity: Activity) {
if (isRecording) applyOverlayToCurrentActivity()
}
override fun onActivityCreated(a: Activity, b: Bundle?) {}
override fun onActivityStarted(a: Activity) {}
override fun onActivityPaused(a: Activity) {}
override fun onActivityStopped(a: Activity) {}
override fun onActivitySaveInstanceState(a: Activity, outState: Bundle) {}
override fun onActivityDestroyed(a: Activity) {}
@SuppressLint("PrivateApi")
fun isMediaProjectionRecording(): Boolean {
return try {
val service = Class.forName("android.os.SystemProperties")
val getter = service.getMethod("get", String::class.java)
val value = getter.invoke(null, "sys.service.media.projection") as String
value.contains("running", ignoreCase = true)
} catch (e: Exception) {
false
}
}
}