android:filterTouchesWhenObscured="true" 引发的事故

279 阅读3分钟

设置 android:filterTouchesWhenObscured="true" 是为了防止悬浮窗攻击(如恶意应用通过覆盖真实按钮诱导用户点击),但语音助手常驻导致按钮被判定为 “被遮挡”,这是一个典型的 安全特性与用户体验的冲突。以下是解决方案:

一、问题本质

语音助手(如 Google Assistant、小爱同学)通常以 系统级悬浮窗 形式存在,导致:

  1. 按钮被判定为 “被遮挡”(即使悬浮窗位置不影响点击);
  2. filterTouchesWhenObscured=true 强制拦截所有触摸事件,导致按钮完全失效。

二、解决方案(优先级排序)

1. 缩小安全范围(推荐)

将 filterTouchesWhenObscured 仅应用于 真正敏感的操作(如支付、删除账户),退出登录可降级为 “普通操作”:

xml

<!-- 移除或改为 false -->
android:filterTouchesWhenObscured="false"

理由

  • 退出登录虽敏感,但通常有二次确认(如弹窗),风险相对可控;
  • 语音助手作为系统级应用,被恶意利用的概率极低。

2. 动态检测并调整(技术方案)

通过代码 实时检测语音助手是否遮挡按钮,仅在安全时允许点击:

java

// 在 Activity 中监听布局变化
logoutButton.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        // 检查按钮是否被遮挡
        if (isButtonObscuredByAssistant(logoutButton)) {
            logoutButton.setEnabled(false);
            logoutButton.setAlpha(0.5f); // 视觉提示
        } else {
            logoutButton.setEnabled(true);
            logoutButton.setAlpha(1.0f);
        }
    }
});

// 判断按钮是否被语音助手遮挡
private boolean isButtonObscuredByAssistant(View button) {
    // 获取按钮在屏幕上的位置
    int[] location = new int[2];
    button.getLocationOnScreen(location);
    Rect buttonRect = new Rect(
        location[0], 
        location[1], 
        location[0] + button.getWidth(), 
        location[1] + button.getHeight()
    );
    
    // 检查语音助手悬浮窗是否与按钮重叠
    // 注意:此处需要根据具体语音助手实现
    // 示例:假设语音助手在右下角,大小为 100x100
    Rect assistantRect = new Rect(
        screenWidth - 100, 
        screenHeight - 100, 
        screenWidth, 
        screenHeight
    );
    
    return Rect.intersects(buttonRect, assistantRect);
}

注意

  • 需要适配不同设备的语音助手位置(可能需配置文件);
  • 系统悬浮窗位置可能随用户操作变化,需持续监听。

3. 自定义按钮行为(高级方案)

创建自定义按钮,拦截 onTouchEvent 并手动处理遮挡逻辑:

java

public class SafeLogoutButton extends Button {
    public SafeLogoutButton(Context context) {
        super(context);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 检查是否被语音助手遮挡
        if (isObscuredByAssistant()) {
            // 若被遮挡,但用户明确点击按钮中心区域
            if (event.getAction() == MotionEvent.ACTION_UP && 
                isTouchInButtonCenter(event)) {
                // 记录用户意图,延迟处理(如 500ms 后再次检查)
                postDelayed(() -> {
                    if (!isObscuredByAssistant()) {
                        performClick(); // 安全时触发点击
                    }
                }, 500);
                return true;
            }
            return false; // 默认拦截
        }
        
        // 未被遮挡,正常处理
        return super.onTouchEvent(event);
    }
    
    // 其他辅助方法...
}

优点

  • 允许用户通过 “明确点击” 绕过临时遮挡;
  • 兼顾安全性和可用性。

4. 引导用户临时关闭语音助手(兜底方案)

当检测到按钮被遮挡时,显示提示:

java

logoutButton.setOnClickListener(v -> {
    if (isButtonObscuredByAssistant()) {
        new AlertDialog.Builder(this)
            .setMessage("语音助手遮挡了退出按钮,请暂时关闭语音助手后重试")
            .setPositiveButton("去关闭", (dialog, which) -> {
                // 跳转到语音助手设置页
                Intent intent = new Intent(Settings.ACTION_SETTINGS);
                startActivity(intent);
            })
            .setNegativeButton("取消", null)
            .show();
        return;
    }
    
    // 正常处理退出逻辑
    performLogout();
});

三、最佳实践

  1. 权衡安全与体验

    • 对于非核心敏感操作(如退出登录),优先保证可用性;
    • 对于支付、删除账户等操作,保留 filterTouchesWhenObscured=true
  2. 多维度验证

    • 结合二次确认(如弹窗、验证码);
    • 使用生物识别(指纹、人脸)增强敏感操作的安全性。
  3. 用户教育

    • 在隐私政策中说明悬浮窗风险;
    • 提示用户谨慎授予第三方应用悬浮窗权限。

总结

最推荐的方案是移除 filterTouchesWhenObscured=true,并通过其他方式(如二次确认、生物识别)保障安全。若必须保留该属性,建议结合动态检测或自定义按钮逻辑,在安全性和可用性之间找到平衡点。