Multitasking Intrusion and Preventing Screenshots in Android Apps

Protect your user’s privacy and adhere to possible technical requirements

Tomáš Repčík
ProAndroidDev

--

The security of the user should be among the first things to tackle in every app, which manipulates with user’s data. In terms of finance apps, state services, healthcare and others, the developers should pay extra attention.

One of the technical requirements, which pops up usually is preventing users from taking screenshots or obscuring the multitasking preview of the app. I have gone through many paths and I have found it quite challenging to find a solution for obscuring the multitask preview while allowing users to take screenshots of the screen in Android.

Here are the various methods, which I am going to describe with pros, cons and some ideas:

  • FLAG_SECURE
  • setRecentsScreenshotEnabled
  • onWindowFocusChanged with FLAG_SECURE / custom view

FLAG_SECURE

To prevent users from taking screenshots and obscuring their multitask preview, there is a simple flag FLAG_SECURE for it:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE
)

setContent {
// compose content
}
}
}

You can set FLAG_SECURE in onCreate method and activity will be protected from taking screenshots and the preview in the recent apps will be obscured.

Dialogs and other popups have their own windows object and flag needs to be set upon their creation too.

FLAG_SECURE and lifecycle changes

Unfortunately, if you try to add FLAG_SECURE flag in onPause call, the app will not get obscured in the multitask preview and screenshots can be taken. It is because the preview is created already before the flag takes effect. In the end, the flag is ignored.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// compose content
}
}
override fun onPause() {
window.addFlags(
WindowManager.LayoutParams.FLAG_SECURE
)
super.onPause()
}
override fun onResume() {
super.onResume()
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}

However, this restriction does not apply to scenarios, where you apply the flag when the user actively uses the app. If the button is added, which adds and clears the flag, the functionality works as expected.

val flag = remember {
mutableStateOf(false)
}
Button(onClick = {
if (flag.value) {
window.clearFlags(
WindowManager.LayoutParams.FLAG_SECURE
)
} else {
window.addFlags(
WindowManager.LayoutParams.FLAG_SECURE
)
}
flag.value = flag.value.not()
}) {
Text(text = "Secure flag: ${flag.value}")
}

Ideas on how to use FLAG_SECURE

  • Set FLAG_SECURE in onCreate in one root activity
  • Separate sensitive content to individual activities and just set FLAG_SECURE in onCreate
  • Turn on the flag based on the context. If the sensitive information is visible, add the flag and remove it if it is not needed anymore

Be aware that forbidding screenshots can result in harder troubleshooting. For the development team it is important to have version of the app, where the flag is missed out intentionally. In production, the flag should be presented but more advanced Crashlytics and logging methods must be implemented to avoid awkward situations.

setRecentsScreenshotEnabled()

From Android 13 / SDK 33, Activity supports a new function called setRecentsScreenshotEnabled. This method prevents users from taking screenshots of the multitask preview of the app and obscures it without calling any flag of the screen. By default, the activity is set to true, but to disable screenshots of the preview, the app needs to set it to false.

User can still take screenshot of the app during active use.

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setRecentsScreenshotEnabled(false)
setContent {
// content
}
}
}

This is quite a simple and elegant solution, but it does not work with devices with a lower SDK than 33. Otherwise, it should be used as FLAG_SECURE in similar scenarios.

Issues with the methods above

The methods above are not perfect and that is why I dug deeper and found/created other ways to approach this topic. Here is a brief list of the most common issues.

  • Some phones do not obscure the multitask preview right away when the user moves the app to the background. The user must switch to another app and then the preview is obscured.
  • setRecentsScreenshotEnabled is available from SDK 33
  • Cannot rely on the lifecycle of the activity to use flags/methods

The following methods can be more clumsy / can work differently on various phones, but I think some people will find them useful and worth the try.

onWindowFocusChanged with FLAG_SECURE / dialog

onWindowFocusChanged is provided by the activity as a method, which can be overridden. The method is called when the user directly interacts with the activity. The activity loses focus e.g.:

  • User is asked for permission
  • User drags down the notification bar
  • User moves to multitask preview

The advantage of this method is that it is called before the activity creates the preview, so we can apply the methods above to obscure the preview / disable screenshots of the preview.

FLAG_SECURE version

For example, we can add FLAG_SECURE flag based on this trigger.

class MainActivity: ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// content
}
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}

Custom view version

At this stage, the UI can still be customized. We can change the screen or overlay of our app by which the contents get obscured. For demonstration, the example will use the dialog, but feel free to use any other UI component. The dialog has the advantage that you do not need to change anything UI-related underneath it.

class MainActivity : ComponentActivity() {
// placeholder for dialog
private var dialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// UI contents
}
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
dialog?.dismiss()
dialog = null
} else {
// style to make it full screen
Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen).also { dialog ->
this.dialog = dialog
// must be no focusable, so if the user comes back, the activity underneath it gets the focus
dialog.window?.setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
)
// your custom UI
dialog.setContentView(R.layout.activity_obscure_layout)
dialog.show()
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- R.layout.activity_obscure_layout -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/obscure_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black" />

The dialog needs to use full screen style to occupy full size.

Following the dialog creation, the dialog cannot obtain focus by adding FLAG_NOT_FOCUSABLE. The reason is if the user comes back to the app the activity will not get focused and the method will not get called, because the dialog will get focused. By adding this flag, the focus falls on the view underneath the dialog. The view behind the dialog is our activity, so the method gets triggered and dialog is dismissed.

Afterwards, the dialog can inflate any UI.

The issue with this approach is that every time the user is asked for permission or goes to check notifications, then this dialog appears. Some scoping for the permission is possible, but it is impossible to determine when the notification bar is pulled down. In the most cases the notifications occupies whole screen, but it is not guaranteed that the user will not see the custom UI to obscure the activity.

Not working implementations

I tried to achieve a similar effect via the lifecycle of the activity, but to no avail, unfortunately.

In this code snippet, I tried to replace the composable with empty composable, but when the onPause is called, it is already too late to change the screen. This will result in a multitask preview of proper UI.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var isObscured by remember { mutableStateOf(false) }
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleObserver = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> isObscured = false
Lifecycle.Event.ON_PAUSE -> isObscured = true
else -> Unit
}
}
DisposableEffect(key1 = lifecycleOwner) {
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
}
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
if (isObscured) {
Surface {}
} else {
SecuredContent(text = "Composable lifecycle")
}
}
}
}

The same goes for this code snippet, when I try to inflate XML view on top of the activity. The solution falls short because of the same problem as the code snippet above.

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_composable_layout)
findViewById<ComposeView>(R.id.main_composable).setContent {
SecuredContent(text = "Pause and Resume with XML layout")
}
}
override fun onPause() {
val mainComposable = findViewById<RelativeLayout>(R.id.main_container)
val obscureView = layoutInflater.inflate(R.layout.activity_obscure_layout, null)
mainComposable.addView(obscureView, mainComposable.width, mainComposable.height)
super.onPause()
}
override fun onResume() {
super.onResume()
val mainComposable = findViewById<RelativeLayout>(R.id.main_container)
mainComposable.removeView(findViewById<ComposeView>(R.id.obscure_layout))
}
}

Conclusion

Some last recommendations, what I would do:

  • Use FLAG_SECURE, if possible - it protects against taking screenshots, videos and obscures previews at the same time
  • Implement login/PIN/biometry and test it properly
  • Use/keep the private information of the user only if it is needed/desired from the use case. If you have nothing to hide, you do not have to worry about hiding stuff!

Thanks for reading and don’t forget to subscribe for more!

More from Android development:

Android development

20 stories

--

--

https://tomasrepcik.dev/ - Flutter app developer with experience in native app development and degree in biomedical engineering.