Search code examples
androidsystem-alert-window

How do apps detect that SAW permission is currently being used?


Background

I've noticed many times, that when I use an app that uses SAW (system-alert-window, AKA "display over other apps) permission (example here), and I open Chrome web browser and reach some website that requires a permission (example here, taken from here), it won't let me grant/deny the permission:

enter image description here

The problem

I can't find how they did it, and from which Android version it's possible to check it.

What I've found

Sadly as much as I've searched, I actually had more questions.

For example, how come the web browser can't detect which app is showing on top, and tell us to disable it?

Or, now that Android 12 might arrive, there seem to be a new permission to block SAW (here) :

HIDE_OVERLAY_WINDOWS Added in Android S

public static final String HIDE_OVERLAY_WINDOWS Allows an app to prevent non-system-overlay windows from being drawn on top of it

Constant Value: "android.permission.HIDE_OVERLAY_WINDOWS"

Perhaps for this case there aren't many that have asked about it, or for some reason I didn't choose the correct things to write in order to search for an answer.

The questions

  1. How can I detect if some app is using SAW permission while I show something?

  2. Is there a way to detect which app does that?

  3. What can the API offer for this, and from which version is it available?

  4. I remember I was told that accessibility can be used to draw on top. Sadly I failed to find a tutorial on how to do this, and also of an example of such apps. Would this API be able to detect them too? Or this isn't considered as SAW? Where can I find a tutorial on how to do it, so that I could check it out?

  5. Bonus: how on Android S do you use the new permission to hide SAW?


Solution

  • OK I've made a sample to show all ways to detect it, including what happens when you use the new API (setHideOverlayWindows) to hide apps that are shown on top:

    1. https://developer.android.com/reference/android/app/Activity#dispatchTouchEvent(android.view.MotionEvent)
    2. https://developer.android.com/reference/android/view/View#setOnTouchListener(android.view.View.OnTouchListener)
    3. https://developer.android.com/reference/android/view/View#onFilterTouchEventForSecurity(android.view.MotionEvent)

    manifest

    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.lb.detect_floating_app">
        <uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
        <application
            android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true"
            android:theme="@style/Theme.DetectFloatingApp">
            <activity
                android:name=".MainActivity" android:exported="true">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    </manifest>
    

    MainActivity.kt

    class MainActivity : AppCompatActivity() {
        var isObscured: Boolean? = null
        var isPartiallyObscured: Boolean? = null
        var isHidingFloatingApp = false
    
        @SuppressLint("ClickableViewAccessibility")
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            findViewById<View>(R.id.textView).setOnTouchListener(object : View.OnTouchListener {
                var isObscured: Boolean? = null
                var isPartiallyObscured: Boolean? = null
    
                override fun onTouch(p0: View?, ev: MotionEvent): Boolean {
                    val checkIsObscured = ev.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
                    val checkIsPartiallyObscured = ev.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
                    if (checkIsObscured != isObscured || checkIsPartiallyObscured != isPartiallyObscured) {
                        isObscured = checkIsObscured
                        isPartiallyObscured = checkIsPartiallyObscured
                        Log.d("AppLog", "${System.currentTimeMillis()} setOnTouchListener isObscured:$isObscured isPartiallyObscured:$isPartiallyObscured")
                    }
                    return false
                }
            })
            findViewById<View>(R.id.toggleHideFloatingAppButton).setOnClickListener {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    isHidingFloatingApp = !isHidingFloatingApp
                    window.setHideOverlayWindows(isHidingFloatingApp)
                } else
                    Toast.makeText(this, "need Android API 31", Toast.LENGTH_SHORT).show()
            }
        }
    
        override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
            val checkIsObscured = ev.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
            val checkIsPartiallyObscured = ev.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
            if (checkIsObscured != isObscured || checkIsPartiallyObscured != isPartiallyObscured) {
                isObscured = checkIsObscured
                isPartiallyObscured = checkIsPartiallyObscured
                Log.d("AppLog", "${System.currentTimeMillis()} dispatchTouchEvent isObscured:$isObscured isPartiallyObscured:$isPartiallyObscured")
            }
            return super.dispatchTouchEvent(ev)
        }
    }
    

    FloatingDetectorFrameLayout.kt

    class FloatingDetectorFrameLayout : FrameLayout {
        var isObscured: Boolean? = null
        var isPartiallyObscured: Boolean? = null
    
        constructor(context: Context) : super(context)
        constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
        constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
        constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
    
        override fun onFilterTouchEventForSecurity(ev: MotionEvent): Boolean {
            val checkIsObscured = ev.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
            val checkIsPartiallyObscured = ev.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
            if (checkIsObscured != isObscured || checkIsPartiallyObscured != isPartiallyObscured) {
                isObscured = checkIsObscured
                isPartiallyObscured = checkIsPartiallyObscured
                Log.d("AppLog", "${System.currentTimeMillis()} onFilterTouchEventForSecurity isObscured:$isObscured isPartiallyObscured:$isPartiallyObscured")
            }
            return super.onFilterTouchEventForSecurity(ev)
        }
    }
    

    activity_main.xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center"
        android:orientation="vertical" tools:context=".MainActivity">
    
    
        <com.lb.detect_floating_app.FloatingDetectorFrameLayout
            android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#f00"
            android:padding="100dp">
    
            <Button
                android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content"
                android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    
        </com.lb.detect_floating_app.FloatingDetectorFrameLayout>
    
        <Button android:id="@+id/toggleHideFloatingAppButton"
            android:text="toggle hide floating app"
            android:layout_width="wrap_content" android:layout_height="wrap_content"/>
    </LinearLayout>