应用程序如何检测当前正在使用 SAW 权限?

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

背景

我多次注意到,当我使用使用 SAW(系统警报-window,又名“显示在其他应用程序之上”)权限的应用程序(示例 here), and I open Chrome web browser and reach some website that requires a permission (example here, taken from here)时,它不会让我 grant/deny 权限:

问题

我找不到他们是怎么做到的,以及从哪个 Android 版本可以检查它。

我发现了什么

遗憾的是,尽管我搜索了很多,但实际上我还有更多问题。

例如,网络浏览器为何无法检测到哪个应用程序显示在顶部,并告诉我们禁用它?

或者,现在 Android 12 可能会到来,似乎有一个新的权限来阻止 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"

也许对于这种情况,没有多少人问过它,或者出于某种原因我没有选择正确的东西来寻找答案。

问题

  1. 如何在我显示某些内容时检测某些应用程序是否正在使用 SAW 权限?

  2. 有没有办法检测出是哪个应用做的?

  3. API 可以提供什么,从哪个版本可以使用?

  4. 我记得有人告诉我辅助功能可以用来在上面绘图。遗憾的是,我没有找到有关如何执行此操作的教程,也没有找到此类应用程序的示例。这个 API 也能检测到它们吗?或者这不被视为 SAW?在哪里可以找到有关如何操作的教程,以便我查看?

  5. 奖励:Android你如何使用新权限隐藏 SAW?

实际上,您可以询问 MotionEvents window 是否被遮挡(警告显示在它们上方),这样您就可以知道自己被遮挡了,但您无法判断哪个应用正在执行此操作

有一些类似的问题需要解释 How to detect when my Activity has been obscured?

Android detect or block floating/overlaying apps

我不确定第 4 项,但通常使用辅助功能来重新显示屏幕并向您展示一些神奇的东西(比如那个被禁止的 Voodoo 应用程序)的应用程序也需要 SAW 许可,据我所知,辅助功能服务只是一些回调。

  1. 您将权限放在清单中,当您想隐藏 SAW 时只需调用 setHideOverlayWindows(true)

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

  1. 对于 API 29+,有一种使用 FLAG_WINDOW_IS_OBSCURED (or FLAG_WINDOW_IS_PARTIALLY_OBSCURED 来检测 window 叠加层的常用方法:
    https://whosebug.com/search?q=FLAG_WINDOW_IS_OBSCURED\
  2. 没有API检测特定应用程序。

关于Chrome,可以在源码中找到实现: ModalDialogView.java#L203

好的,我制作了一个示例来展示检测它的所有方法,包括当您使用新的 API (setHideOverlayWindows) 隐藏顶部显示的应用程序时会发生什么:

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