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

3

3

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?

Bourn answered 17/3, 2021 at 21:21 Comment(0)
B
1

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>
Bourn answered 2/4, 2022 at 17:8 Comment(0)
D
2
  1. There is a common approach to detect a window overlay using FLAG_WINDOW_IS_OBSCURED (or FLAG_WINDOW_IS_PARTIALLY_OBSCURED for API 29+):
    https://stackoverflow.com/search?q=FLAG_WINDOW_IS_OBSCURED \
  2. There is no API to detect a particular app.

Regarding Chrome, you can find the implementation in the source code: ModalDialogView.java#L203

Dronski answered 23/3, 2021 at 7:54 Comment(5)
1. So it's a callback to touch events? Nothing more? 2. Too bad. About Chrome, this is great finding. Seems they did the touch event for the buttons alone for some reason, but used the one that triggers no matter where SAW is covering the screen. No wonder it shows even when it doesn't make much sense...Bourn
@androiddeveloper I believe there were some architecture limitations that forced developers to did it in that way.Dronski
You mean about the touch event?Bourn
Yeah, any view instance holds the flag if a window is obscured.Dronski
OK thank you. Sadly I can't grant bounty (or accept an answer) to more than one person, so you get an upvote for the effort and for finding such a hard thing to find of Chrome. Was nice to see it's possible.Bourn
B
1

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>
Bourn answered 2/4, 2022 at 17:8 Comment(0)
E
0

Actually you can ask MotionEvents if the window is obscured (an alert is being shown over them) so you can tell that you're obscured, but you cannot tell which app is doing it

There are some similar questions that has required explanations How to detect when my Activity has been obscured?

Android detect or block floating/overlaying apps

I'm not sure about item 4, but usually application who use accessibility to reas the screen and show you something magical (like that banned Voodoo app) also require SAW permission and as much as I can remember, Accessibility services are just some callbacks.

  1. You put the permission in manifest and when you want to hide SAWs simply call setHideOverlayWindows(true)

https://developer.android.google.cn/about/versions/12/features#hide-application-overlay-windows

Emmy answered 23/3, 2021 at 1:32 Comment(5)
1. It seems like a callback for touching. Are there alternatives? A general callback for when it changes its state (like livedata) ? 2. Can you check which app is using SAW? 3. Anything more? 4. Sadly I couldn't find about this. I think one of the flags will give the app SAW as a part of granting accessibility, or at least its capabilities. 5. Docs don't say it, but do you think it's forever, till you call it again? Or maybe it stays as long as the app has focus (or in foreground)? And it gets back when it got focus again?Bourn
1. I don't think so 2. No, unfortunately 3. I'm not aware of 5. Your activity should be resumed so your hide request would take placeEmmy
Too bad. That's probably why Chrome just says to check the settings, which is quite annoying. I will now grant you the bounty and mark as accepted. Will upvote it too if you find about accessibility, because I couldn't find anything about it but I'm pretty sure there is something about it.Bourn
If I've found anything I'll edit my answer but I've never seen such a thing, but I guess if you have Accessibility, you can launch SAW permission and grand yourself SAW permission:DEmmy
OK thank you. What I meant is that I think accessibility can let you do something similar to SAW without holding (and probably without even declaring) SAW permission.Bourn

© 2022 - 2024 — McMap. All rights reserved.