You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android如何后台运行交互式USSD会话并通过自定义覆盖布局隐藏系统USSD交互

核心问题根因

你碰到的「只要唤起自定义悬浮窗,USSD无障碍交互就失效」的现象,本质是两个系统机制的冲突:

  • 普通创建的覆盖类悬浮窗默认会抢占系统窗口的焦点优先级。而原生USSD弹窗本身是系统级的AlertDialog,一旦你的悬浮窗被设置为可获取焦点的窗口,系统会把USSD弹窗的窗口栈压到后台,导致无障碍服务接收到的事件源全部指向你的悬浮窗,而非真实的USSD交互界面
  • 你现有无障碍代码的事件过滤逻辑没有做窗口归属校验,碰到自身应用的悬浮窗触发窗口状态变更事件时,会直接走提前return的分支,根本走不到后续USSD交互的处理流程

可直接落地的实现方案

  • 修正悬浮窗配置,彻底剥离焦点抢占能力
    你展示「正在处理请求…」的自定义布局,本质只需要做纯视觉提示,完全不需要获得输入焦点、拦截触摸事件。给悬浮窗的WindowManager参数加上关键flag,就能从根源上避免干扰USSD弹窗:

    val overlayLayoutParams = WindowManager.LayoutParams(
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.WRAP_CONTENT,
        // Android 8.0+必须使用TYPE_APPLICATION_OVERLAY类型,低版本兼容旧类型
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        } else {
            WindowManager.LayoutParams.TYPE_PHONE
        },
        // 核心flag组合:纯展示、不抢焦点、不拦触摸
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
        WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
        WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
        PixelFormat.TRANSLUCENT
    )
    

    这样配置后的悬浮窗只会做视觉展示,完全不会改变USSD弹窗的窗口优先级、焦点分配逻辑,无障碍服务能正常接收到USSD窗口的所有事件。

  • 修正无障碍服务的事件过滤逻辑,避免误中断
    你现有代码的事件判断存在两个明显漏洞,哪怕不做悬浮窗,在部分机型上也会出现偶发失效:

    1. 硬绑定「类名包含AlertDialog」判断USSD窗口,不同厂商定制ROM的USSD弹窗类名差异极大,很多机型的USSD弹窗根本不会带AlertDialog标识,会直接导致事件被丢弃
    2. 没有排除自身应用的窗口事件,一旦悬浮窗触发窗口状态变更,就会直接触发提前return的逻辑,打断USSD处理流程
      修正后的判断逻辑参考:
    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        event ?: return
        // 第一步:直接过滤自身应用产生的所有窗口事件,避免悬浮窗干扰
        if (event.packageName == packageName) return
        
        val eventSource = event.source ?: return
        // 第二步:通过USSD窗口的固定交互特征做匹配,不硬绑定类名
        // 所有系统USSD弹窗必然包含输入框、取消按钮、发送按钮
        val isUssdInteractiveWindow = eventSource.findFocus(AccessibilityNodeInfo.FOCUS_INPUT) != null
                || eventSource.findAccessibilityNodeInfosByText("Send").isNotEmpty()
                || eventSource.findAccessibilityNodeInfosByText("Cancel").isNotEmpty()
        
        if (!isUssdInteractiveWindow) {
            eventSource.recycle()
            return
        }
        // 后续正常执行USSD文本解析、内容输入、按钮提交的原有逻辑即可
    }
    
  • 实现全后台USSD交互的增强优化
    上面的配置已经能保证交互流程完全正常,如果你需要让用户完全感知不到原生USSD弹窗的存在,不需要强行按返回键中断会话,可以在检测到USSD窗口激活时,通过无障碍能力把USSD窗口移出可视区域、设置为全透明,既保证会话正常走完,又不会被用户看到:

    // 检测到USSD窗口激活时执行窗口变换
    val transformParams = Bundle()
    transformParams.putFloat(AccessibilityNodeInfo.TRANSFORMATION_AMOUNT_ALPHA, 0f)
    transformParams.putInt(AccessibilityNodeInfo.TRANSFORMATION_TRANSLATION_X, 10000)
    transformParams.putInt(AccessibilityNodeInfo.TRANSFORMATION_TRANSLATION_Y, 10000)
    ussdWindowNode.performAction(AccessibilityNodeInfo.ACTION_SET_SCROLLABLE, transformParams)
    

    这个方案比强行按返回键稳定很多,不会导致运营商侧的USSD会话提前断开,能完整走完所有交互步骤,最终把查询结果回传到你的应用。

注意:部分国产ROM对无障碍的窗口变换操作做了权限限制,需要在系统无障碍设置页手动开启「窗口内容控制」权限,否则变换操作不会生效。


内容的提问来源于stack exchange,提问作者Kevin Bright

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.27 00:45:04