Android12(S)授权弹窗被悬浮窗遮挡导致无法点击问题分析

概述

在测试过程中发现,部分情况下当应用请求权限时,权限授予弹窗中的选项无法点击,有时候又可以。点击其他区域发现是可以正常响应,获取按键事件,发现触摸是有正常上报事件的,所以可以排除是触摸失灵导致。

问题分析

既然是权限授予弹窗无法点击,那么我们就去找权限管理代码看看弹窗点击的逻辑。
弹窗的布局文件如下:

//path:packages/modules/Permission/PermissionController/res/layout/grant_permissions.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2015 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<!-- In (hopefully very rare) case dialog is too high: allow scrolling -->
<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    style="@style/PermissionGrantScrollView">

    <LinearLayout
        android:id="@+id/grant_singleton"
        android:importantForAccessibility="no"
        style="@style/PermissionGrantSingleton">

        <!-- The dialog -->
        <LinearLayout
            android:id="@+id/grant_dialog"
            android:theme="@style/Theme.PermissionGrantDialog"
            android:importantForAccessibility="no"
            style="@style/PermissionGrantDialog">

            <LinearLayout
                android:id="@+id/content_container"
                style="@style/PermissionGrantContent">

                <LinearLayout
                    style="@style/PermissionGrantDescription">

                    <ImageView
                        android:id="@+id/permission_icon"
                        style="@style/PermissionGrantTitleIcon" />

                    <TextView
                        android:id="@+id/permission_message"
                        style="@style/PermissionGrantTitleMessage" />

                </LinearLayout>

                <TextView
                    android:id="@+id/detail_message"
                    style="@style/PermissionGrantDetailMessage" />

            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/permission_location_accuracy">

                <RadioGroup
                    android:id="@+id/permission_location_accuracy_radio_group"
                    style="@style/PermissionLocationAccuracyRadioGroup">

                    <RadioButton
                        android:id="@+id/permission_location_accuracy_radio_fine"
                        android:text="@string/permgrouprequest_finelocation_imagetext"
                        style="@style/PermissionLocationAccuracyRadioFine"/>

                    <RadioButton
                        android:id="@+id/permission_location_accuracy_radio_coarse"
                        android:text="@string/permgrouprequest_coarselocation_imagetext"
                        style="@style/PermissionLocationAccuracyRadioCoarse" />
                </RadioGroup>

                <ImageView
                    android:id="@+id/permission_location_accuracy_fine_only"
                    android:contentDescription="@string/precise_image_description"
                    style="@style/PermissionLocationAccuracyFineImageView" />

                <ImageView
                    android:id="@+id/permission_location_accuracy_coarse_only"
                    android:contentDescription="@string/approximate_image_description"
                    style="@style/PermissionLocationAccuracyCoarseImageView" />

            </LinearLayout>

            <!-- Buttons on bottom of dialog -->
            <LinearLayout
                style="@style/PermissionGrantButtonList">

                <Space
                    style="@style/PermissionGrantButtonBarSpace"/>

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_allow_button"
                    android:text="@string/grant_dialog_button_allow"
                    style="@style/PermissionGrantButtonAllow" />

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_allow_foreground_only_button"
                    android:text="@string/grant_dialog_button_allow_foreground"
                    style="@style/PermissionGrantButtonAllowForeground" />

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_allow_one_time_button"
                    android:text="@string/grant_dialog_button_allow_one_time"
                    style="@style/PermissionGrantButtonAllowOneTime" />

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_deny_button"
                    android:text="@string/grant_dialog_button_deny"
                    style="@style/PermissionGrantButtonDeny" />

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_deny_and_dont_ask_again_button"
                    android:text="@string/grant_dialog_button_deny"
                    style="@style/PermissionGrantButtonDeny" />

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_no_upgrade_button"
                    android:text="@string/grant_dialog_button_no_upgrade"
                    style="@style/PermissionGrantButtonNoUpgrade" />

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_no_upgrade_and_dont_ask_again_button"
                    android:text="@string/grant_dialog_button_no_upgrade"
                    style="@style/PermissionGrantButtonNoUpgrade" />

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_no_upgrade_one_time_button"
                    android:text="@string/grant_dialog_button_no_upgrade_one_time"
                    style="@style/PermissionGrantButtonNoUpgrade" />

                <com.android.permissioncontroller.permission.ui.widget.SecureButton
                    android:id="@+id/permission_no_upgrade_one_time_and_dont_ask_again_button"
                    android:text="@string/grant_dialog_button_no_upgrade_one_time"
                    style="@style/PermissionGrantButtonNoUpgrade" />
            </LinearLayout>

        </LinearLayout>
    </LinearLayout>
</ScrollView>

发现点击按钮是自定义了一个com.android.permissioncontroller.permission.ui.widget.SecureButton,继续查看

package com.android.permissioncontroller.permission.ui.widget;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Button;

import com.android.modules.utils.build.SdkLevel;

/**
 * A button which doesn't allow clicking when any part of the window is obscured
 */
public class SecureButton extends Button {
    
    

    private static final int FLAGS_WINDOW_IS_OBSCURED =
            MotionEvent.FLAG_WINDOW_IS_OBSCURED | MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED;

    public SecureButton(Context context) {
    
    
        super(context);
    }

    public SecureButton(Context context, AttributeSet attrs) {
    
    
        super(context, attrs);
    }

    public SecureButton(Context context, AttributeSet attrs, int defStyleAttr) {
    
    
        super(context, attrs, defStyleAttr);
    }

    public SecureButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    
    
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    
    
        if (SdkLevel.isAtLeastS()) {
    
    
            return (event.getFlags() & FLAGS_WINDOW_IS_OBSCURED) == 0
                    && super.onFilterTouchEventForSecurity(event);
        }

        return super.onFilterTouchEventForSecurity(event);
    }
}

这个SecureButton 只是继承了Button,添加了onFilterTouchEventForSecurity 方法,对MotionEvent.FLAG_WINDOW_IS_OBSCURED | MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED这两个flag进行了判断。

查询资料可知,这是Android12新特性:
不信任的触摸事件被阻止
为了保持系统安全性和良好的用户体验,Android 12会阻止应用程序在覆盖层以不安全的方式遮盖应用程序时使用触摸事件。换句话说,系统会阻止通过某些窗口的触摸
侵害的应用
此更改会影响选择让触摸通过其窗口(例如通过使用 FLAG_NOT_TOUCHABLE 标志)的应用。几个示例包括但不限于以下示例:

  • 需要SYSTEM_ALERT_WINDOW 权限的叠加层
    ,例如使用TYPE_APPLICATION_OVERLAY,使用FLAG_NOT_TOUCHABLE标志的窗口 。

  • 使用该FLAG_NOT_TOUCHABLE标志的活动窗口。

  • Toast messages.

例外情况
在以下情况下,允许“通过”触摸:

  • 您的应用内的互动。您的应用会显示叠加层,并且叠加层仅在用户与您的应用进行交互时才会显示。
  • 受信任的窗口。这些窗口包括(但不限于)以下内容:
    辅助功能窗口
    输入法编辑器(IME)窗口
    助手窗口

注意:Windows类型 不受信任。TYPE_APPLICATION_OVERLAY

  • 隐形窗户。窗口的根视图为 GONE或 INVISIBLE。

  • 完全透明的窗口。该alpha窗口的 属性为0.0。

  • 足够透明的系统警报窗口。当组合的不透明度小于或等于系统对触摸的最大遮盖不透明度时,系统认为一组系统警报窗口是足够透明的。在Developer
    Preview 1中,最大不透明度为0.8,但是此值稍后可能在Developer Preview中更改。

在InputDispatcher 中可以找到触摸不被阻止的条件

//path:frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp

/**
 * Indicate whether one window handle should be considered as obscuring
 * another window handle. We only check a few preconditions. Actually
 * checking the bounds is left to the caller.
 */
static bool canBeObscuredBy(const sp<WindowInfoHandle>& windowHandle,
                            const sp<WindowInfoHandle>& otherHandle) {
    
    
    // Compare by token so cloned layers aren't counted
    if (haveSameToken(windowHandle, otherHandle)) {
    
    //两个窗口拥有相同的windowhandle 不被阻止
        return false;
    }
    auto info = windowHandle->getInfo();
    auto otherInfo = otherHandle->getInfo();
    if (!otherInfo->visible) {
    
    //另一个窗口不可见 触摸不被阻止
        return false;
    } else if (otherInfo->alpha == 0 && otherInfo->flags.test(WindowInfo::Flag::NOT_TOUCHABLE)) {
    
    //另一个窗口透明并且不可touch 触摸就不会被阻止
        // Those act as if they were invisible, so we don't need to flag them.
        // We do want to potentially flag touchable windows even if they have 0
        // opacity, since they can consume touches and alter the effects of the
        // user interaction (eg. apps that rely on
        // FLAG_WINDOW_IS_PARTIALLY_OBSCURED should still be told about those
        // windows), hence we also check for FLAG_NOT_TOUCHABLE.
        return false;
    } else if (info->ownerUid == otherInfo->ownerUid) {
    
    //两个窗口拥有相同的UID 触摸不被阻止
        // If ownerUid is the same we don't generate occlusion events as there
        // is no security boundary within an uid.
        return false;
    } else if (otherInfo->trustedOverlay) {
    
    //另一个窗口拥有trustedOverlay flag 触摸不被阻止
        return false;
    } else if (otherInfo->displayId != info->displayId) {
    
    //另一个窗口在其他屏幕上显示 触摸不被阻止
        return false;
    }
    return true;
}

以上可以分析出想要授权窗口的触摸事件不被阻止,那么只能让悬浮窗拥有trustedOverlay flag

解决方案

由上分析知,想要悬浮窗不影响授权框的点击,只能给悬浮窗设置不可点击,透明,或者拥有trustedOverlay flag。前面的不可点击,透明这些会改变悬浮窗的设计,不可取,那么久只能设置flag。
在windowmanager中有setTrustedOverlay 方法,可以给弹窗设置PRIVATE_FLAG_TRUSTED_OVERLAY flag 但是它是hide方法,并且需要INTERNAL_SYSTEM_WINDOW 权限,该方法只能系统应用使用。我们的应用本身是系统应用所以可以使用,问题能够解决。

 /**
         * Specifies that the window should be considered a trusted system overlay. Trusted system
         * overlays are ignored when considering whether windows are obscured during input
         * dispatch. Requires the {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW}
         * permission.
         *
         * {@see android.view.MotionEvent#FLAG_WINDOW_IS_OBSCURED}
         * {@see android.view.MotionEvent#FLAG_WINDOW_IS_PARTIALLY_OBSCURED}
         * @hide
         */
        public void setTrustedOverlay() {
    
    
            privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY;
        }

其实对于非系统应用,本身不会存在这个问题,授权弹窗其实在启动时会设置flag:getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS 会主动去隐藏掉非系统的悬浮窗,这样本身也就没有遮挡的问题了。
但是我们应用是系统应用,这个方法就不起效果,所以需要特殊处理。

猜你喜欢

转载自blog.csdn.net/weixin_40774418/article/details/126761169