忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。
-- 服装学院的IT男
1. 问题描述
最新 GTS 套件为 12-R1 ,新增了一条用例,测试命令如下:
run gts-interactive -m GtsInteractiveDeviceControlsTestCases -t com.google.android.controls.gts.DeviceControlsHomeIsPresentInteractiveTest#testHomeAppIsPresentAfterInstallWithoutManualAdd
测试过程中会出现一下界面,最终因为点击授权按钮 “While using the app”(使用应用程序时)后弹窗不消失,导致测试无法进行,最终因超时结束。
这个问题出现在 T 平台上,目前其他平台还未发现,原因最后会解释。
整个文字分为以下4步:
-
- 问题描述
-
- 确认问题应用和具体界面
-
- 分析点击无效原因
-
- 根因分析
-
- 处理方案
踩了很多坑,比如想加日志单编发现权限应用的 APK 文件最终用的是 mainline 下的应用。
赶时间只想解决问题的直接看最后 4,5即可。
2. 确认问题应用和具体界面
2.1 资源文件信息
mFocusedApp=ActivityRecord{f85c003 u0 com.google.android.permissioncontroller/com.android.permissioncontroller.permission.ui.GrantPermissionsActivity} t31}
mFocusedWindow=Window{94352f1 u0 com.google.android.permissioncontroller/com.android.permissioncontroller.permission.ui.GrantPermissionsActivity}
应用为 com.google.android.permissioncontroller (GooglePermissionController.apk)是 mainline 下的文件,所以无法本地加日志单编 push APK 。 但是可以参考源码定位代码位置。
权限弹窗一般点击按钮 “While using the app”(使用应用程序时)。
# android/packages/modules/Permission/PermissionController/res/values/strings.xml
// 使用应用程序时
<string name="grant_dialog_button_allow_foreground">While using the app</string>
根据文案找到对应使用布局看布局和控件ID
# android/packages/modules/Permission/PermissionController/res/layout/grant_permissions.xml
<com.android.permissioncontroller.permission.ui.widget.SecureButton
android:id="@+id/permission_allow_foreground_only_button" // 控件ID
android:text="@string/grant_dialog_button_allow_foreground"
style="@style/PermissionGrantButtonAllowForeground" />
该按钮控件ID 为 = permission_allow_foreground_only_button
2.2 代码梳理
2.2.1 布局文件使用
grant_permissions.xml 搜到被 2 个文件使用,
android/packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/permission/ui/television/GrantPermissionsViewHandlerImpl.java 和 android/packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.kt
上面的为 TV 下面的是正常使用的,所以使用的是 GrantPermissionsViewHandlerImpl.kt
# GrantPermissionsViewHandlerImpl.kt
// “使用应用程序时”
import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_FOREGROUND_BUTTON
import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY
// 点击回调监听者,也就是 GrantPermissionsActivity
private var resultListener: GrantPermissionsViewHandler.ResultListener? = null
// 设置点击回调监听者
override fun setResultListener(
listener: GrantPermissionsViewHandler.ResultListener
): GrantPermissionsViewHandlerImpl {
resultListener = listener
return this
}
override fun createView(): View {
// Make this activity be Non-IME target to prevent hiding keyboard flicker when it show up.
mActivity.window.addFlags(LayoutParams.FLAG_ALT_FOCUSABLE_IM)
val useMaterial3PermissionGrantDialog = mActivity.resources
.getBoolean(R.bool.config_useMaterial3PermissionGrantDialog)
val rootView = if (useMaterial3PermissionGrantDialog || SdkLevel.isAtLeastT()) {
LayoutInflater.from(mActivity)
.inflate(R.layout.grant_permissions_material3, null) as ViewGroup
} else {
// 加载布局
LayoutInflater.from(mActivity)
.inflate(R.layout.grant_permissions, null) as ViewGroup
}
this.rootView = rootView
......
// 为各个按钮设置点击时间
val numButtons = BUTTON_RES_ID_TO_NUM.size()
for (i in 0 until numButtons) {
val button = rootView.findViewById<Button>(BUTTON_RES_ID_TO_NUM.keyAt(i))
button!!.setOnClickListener(this)
buttons[BUTTON_RES_ID_TO_NUM.valueAt(i)] = button
}
}
// 点击事件处理
override fun onClick(view: View) {
val id = view.id
......
// 找到映射
when (BUTTON_RES_ID_TO_NUM.get(id, -1)) {
......
ALLOW_FOREGROUND_BUTTON -> if (resultListener != null) {
view.performAccessibilityAction(
AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null)
// 点击回调,如果监听为空会报错
// 当前没有报错日志,说明正常走监听
resultListener!!.onPermissionGrantResult(groupName, affectedForegroundPermissions,
GRANTED_FOREGROUND_ONLY)
}
......
}
}
// 初始化
companion object {
......
// 控件ID的映射
private val BUTTON_RES_ID_TO_NUM = SparseIntArray()
private val LOCATION_RES_ID_TO_NUM = SparseIntArray()
init {
......
// “使用应用程序时”按钮的映射
BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_foreground_only_button,
ALLOW_FOREGROUND_BUTTON)
......
}
}
2.2.2 GrantPermissionsViewHandlerImpl 的创建
整个弹框的 Activity 在 GrantPermissionsActivity ,GrantPermissionsViewHandlerImpl 的创建逻辑也在这。
# android/packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java
/**
* An activity which displays runtime permission prompts on behalf of an app.
* 代表应用程序显示运行时权限提示的活动
* 实现 ResultListener 接口
*/
public class GrantPermissionsActivity extends SettingsActivity
implements GrantPermissionsViewHandler.ResultListener {
// 具体业务逻辑处理者
private GrantPermissionsViewHandler mViewHandler;
// ViewModel
private GrantPermissionsViewModel mViewModel;
@Override
public void onCreate(Bundle icicle) {
......
if (DeviceUtils.isTelevision(this)) {
// 电视
mViewHandler = new com.android.permissioncontroller.permission.ui.television
.GrantPermissionsViewHandlerImpl(this,
mTargetPackage).setResultListener(this);
} else if (DeviceUtils.isWear(this)) {
// 穿戴
mViewHandler = new GrantPermissionsWearViewHandler(this).setResultListener(this);
} else if (DeviceUtils.isAuto(this)) {
// 汽车
mViewHandler = new GrantPermissionsAutoViewHandler(this, mTargetPackage)
.setResultListener(this);
} else {
// 其他(手机)走这
mViewHandler = new com.android.permissioncontroller.permission.ui.handheld
.GrantPermissionsViewHandlerImpl(this, mTargetPackage,
Process.myUserHandle()).setResultListener(this); // 设置监听为当前
}
// 创建 ViewModel
GrantPermissionsViewModelFactory factory = new GrantPermissionsViewModelFactory(
getApplication(), mTargetPackage, mRequestedPermissions, mSessionId, icicle);
// GrantPermissionsViewModel
mViewModel = factory.create(GrantPermissionsViewModel.class);
mViewModel.getRequestInfosLiveData().observe(this, this::onRequestInfoLoad);
// UI 处理
mRootView = mViewHandler.createView();
mRootView.setVisibility(View.GONE);
......
}
}
-
- mViewHandler 的实际对象为 GrantPermissionsViewHandlerImpl
-
- mViewModel 的实际对象为 GrantPermissionsViewModel
3. 点击事件分析
基本信息前面已经分析完毕,现在开始分析当前问题原因,根据之前的分析基础,权限弹框中击按钮 “While using the app”(使用应用程序时)会触发 GrantPermissionsActivity::onPermissionGrantResult 方法。
3.1 点击事件实际处理流程
根据分析,传递的第三个参数为 GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY
# android/packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsViewHandler.java
@interface Result {}
int LINKED_TO_SETTINGS = -2;
int CANCELED = -1;
int GRANTED_ALWAYS = 0;
// 仅限APP 使用时授权
int GRANTED_FOREGROUND_ONLY = 1;
int DENIED = 2;
int DENIED_DO_NOT_ASK_AGAIN = 3;
int GRANTED_ONE_TIME = 4;
看 GrantPermissionsActivity::onPermissionGrantResult 。
# GrantPermissionsActivity.java
@Override
public void onPermissionGrantResult(String name,
@GrantPermissionsViewHandler.Result int result) {
// 当前逻辑这里返回false ,不影响主流程
if (checkKgm(name, null, result)) {
return;
}
if (name.equals(mPreMergeShownGroupName)) {
mPreMergeShownGroupName = null;
}
// 点击日志
logGrantPermissionActivityButtons(name, null, result);
mViewModel.onPermissionGrantResult(name, null, result);
showNextRequest();
// 返回键
if (result == CANCELED) {
setResultAndFinish();
}
}
// result = 1,但是没锁屏,所以返回 false
private boolean checkKgm(final String str, final List<String> list, final int i) {
KeyguardManager keyguardManager;
if ((i == 0 || i == 1 || i == 3) && (keyguardManager = (KeyguardManager) getSystemService(KeyguardManager.class)) != null && keyguardManager.isDeviceLocked()) {
......
return true;
}
return false;
}
这里肯定不会在 checkKgm 返回,也就是会走到后续逻辑。 简单整理点击事件调用链如下,
GrantPermissionsViewHandlerImpl::onClick
GrantPermissionsActivity::onPermissionGrantResult
GrantPermissionsActivity::logGrantPermissionActivityButtons
GrantPermissionsViewModel::onPermissionGrantResult
GrantPermissionsActivity::showNextRequest
后面的内容其实与当前案例无关,就不废话了,只看分析点击后执行的第一个方法 GrantPermissionsActivity::logGrantPermissionActivityButtons 的逻辑。
# GrantPermissionsActivity.java
private void logGrantPermissionActivityButtons(String permissionGroupName,
List<String> affectedForegroundPermissions, int grantResult) {
...... // 期间没有 return 逻辑,能走进来就会到 logClickedButtons
// 这里触发
mViewModel.logClickedButtons(permissionGroupName, selectedPrecision, clickedButton,
presentedButtons);
}
前面分析过 mViewModel 是 GrantPermissionsViewModel 的实例,根据关键字搜索到日志打印的地方为:
# GrantPermissionsViewModel.kt
fun logClickedButtons(
groupName: String?,
selectedPrecision: Int,
clickedButton: Int,
presentedButtons: Int
) {
if (groupName == null) {
// 这里为空会 return
return
}
if (!requestInfosLiveData.isInitialized || !packageInfoLiveData.isInitialized) {
// 没有这段,说明没走
Log.wtf(LOG_TAG, "Logged buttons presented and clicked permissionGroupName=" +
"$groupName package=$packageName presentedButtons=$presentedButtons " +
"clickedButton=$clickedButton sessionId=$sessionId, but requests were not yet" +
"initialized", IllegalStateException())
return
}
......
// 日志打印
Log.v(LOG_TAG, "Logged buttons presented and clicked permissionGroupName=" +
"$groupName uid=${packageInfo.uid} selectedLocations=$selectedLocations " +
"package=$packageName presentedButtons=$presentedButtons " +
"clickedButton=$clickedButton sessionId=$sessionId " +
"targetSdk=${packageInfo.targetSdkVersion}")
}
可以看到如果点击事件正常执行会有 "Logged buttons presented and clicked permissionGroupName= XX"的日志打印,搜索日志(与pass设备对比,比如pixel)发现正常点击授权按钮应该会有下面的日志输出:
Line 12151: 09-19 15:30:36.813 3891 3891 V GrantPermissionsViewModel: Logged buttons presented and clicked permissionGroupName=android.permission-group.LOCATION uid=10269 selectedPrecision=3 package=com.google.android.apps.chromecast.app presentedButtons=44 clickedButton=4 isPermissionRationaleShown=false sessionId=8182328744088758642 targetSdk=34
正常分析分体是逆向的,这一步在早起搜日志就发现了,只不过写问题分析记录还是顺序写比较易读
3.2 分析小结
到目前为止有2个怀疑:
-
- 点击事件没有到这个按钮
-
- 点击事件触发后业务逻辑出现问题,导致没有正确执行完
其中第一点很早就有怀疑了,但是确定权限窗口是焦点窗口,并且弹窗上的“确切位置”和“大致位置”这2个 checkbox 是可以正常点击的,所以早期分析就排除了焦点和事件分发问题,就排除了焦点和事件分发没有走到的可能性去分析第二点了。
但是第二点分析下来发现内部点击后的逻辑挺复杂,而且也不能加日志去确认,同时也发现测试打开的 APP 为 “Home” 属于 GMS 包下的应用,本以为是版本问题,通过升级版本也是 fail ,然后尝试不通过测试用例,自己手动打开APP,定位授权按钮正常点击,并且也有这段日志的打印。 所以再次把怀疑点放在按钮没有响应点击事件上
4. 根因分析
通过在 View.java 点击事件加上日志
# View.java
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 打印 当前 view
Log.d("biubiubiu", "performClick: "+getTag() + " this="+ this);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
问题场景点击日志如下,点击事件被 LinearLayout 消费了
Line 8530: 09-20 09:45:51.789 15149 15149 D biubiubiu: performClick: null this=android.widget.LinearLayout{843a555 V.E...C.. ...P.... 0,0-1026,1673 #7f0a01f5 app:id/grant_dialog}
手动打开该APP 点击按钮日志如下
09-20 09:58:35.135 15149 15149 D biubiubiu: performClick: null this=com.android.permissioncontroller.permission.ui.widget.SecureButton{bf07b02 VFED..C.. ...P.... 72,6-858,174 #7f0a0331 app:id/permission_allow_foreground_only_button}
permission_allow_foreground_only_button 就是我们点击按钮的控件 ID ,**因此可以确认问题原因就是点击事件没有被按钮消费而是被父容器消费了,**所以点击授权按钮没有任何反应。
这里涉及到了 View 的事件分发,当前场景出现问题的原因则是事件被派发到了子 View ,但是子 View 由于某些原因不处理这个事件,所以事件又传递到了父容器,最终被父容器消费了。
反编译应用发现按钮的控件是自定义的
/**
* A button which doesn't allow clicking when any part of the window is obscured
* 当窗口的任何部分被遮挡时,不允许点击的按钮
*/
public class SecureButton extends Button {
// 2个遮挡 flag
private static final int FLAGS_WINDOW_IS_OBSCURED =
MotionEvent.FLAG_WINDOW_IS_OBSCURED | MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED;
......
@Override
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
// 如果版本大于 S
if (SdkLevel.isAtLeastS()) {
// 不被遮挡,然后再是调用父类方法
// 当前由于第一个条件 false 所以直接返回false
return (event.getFlags() & FLAGS_WINDOW_IS_OBSCURED) == 0
&& super.onFilterTouchEventForSecurity(event);
}
return super.onFilterTouchEventForSecurity(event);
}
}
这个自定义控件只重新了一个方法,并且根据注释可以知道,在大于等于 S 的设备上如果按钮上面被其他窗口遮挡,则不允许点击按钮。 毕竟权限这玩意对于安全很重要。
在事件分发逻辑最终会走到按钮 View 的 dispatchTouchEvent 方法,代码如下:
# View.java
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
......
// 符合条件则设置为true
result = true;
......
}
......
return result;
}
也就是说想要消费事件,最起码得先 onFilterTouchEventForSecurity 返回true,才能进入内部逻辑,然后满足条件才会将 result 设置为 true 。
默认返回 false 表示不处理这个事件,由于自定义 View 重写了 onFilterTouchEventForSecurity 方法,方法里又做了处理,所以当前当前这里最终会因为返回 false 导致按钮不处理点击事件,也就是当前问题的体现。
FLAG_WINDOW_IS_OBSCURED 和 FLAG_WINDOW_IS_PARTIALLY_OBSCURED 这2个 false 都是被折叠(具体区别在事件在遮挡区域还是不在)。
打印出 MotionEvent 后发现 pass 的设备 flag 如下:
fail 的如下:
可以看到是 FLAG_WINDOW_IS_PARTIALLY_OBSCURED 这个 flag 的,这是代码的分析,然后做以下2个更直观的测试可以确认问题:
-
- 观察 pass 的设备是不是测试窗口没有盖住权限窗口
-
- 把 fail 的设置通过 adb 命令调整屏幕属性和 pass 设备一直后,是不是权限窗口没有被遮挡,并且也能 pass
5. 处理方案
问题原因已经明确,这条 case 为 当前最新 GTS 套件(12-R1)新增用例,应该是还未考虑到这个场景,所以通过申请 waiver 的方式提给 google 。
当前最终大概率是通过更新套件的方式来解决这个问题。
至于为什么至于在 T 平台出现,目前发现的原因是上面测试窗口的提示文案, T 平台会有3行文案所以整个控件很高度比较大就会遮挡住权限弹窗,而 U 平台的文案只有一行。
所以 google 解决这个问题的最快方案也许就是减少一下 T 平台 setup 的文案。。。