Blog Infos
Author
Published
Topics
, , ,
Published

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 appearsSome 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:

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu