概述
在测试过程中发现,部分情况下当应用请求权限时,权限授予弹窗中的选项无法点击,有时候又可以。点击其他区域发现是可以正常响应,获取按键事件,发现触摸是有正常上报事件的,所以可以排除是触摸失灵导致。
问题分析
既然是权限授予弹窗无法点击,那么我们就去找权限管理代码看看弹窗点击的逻辑。
弹窗的布局文件如下:
//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 会主动去隐藏掉非系统的悬浮窗,这样本身也就没有遮挡的问题了。
但是我们应用是系统应用,这个方法就不起效果,所以需要特殊处理。