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窗口的所有事件。
修正无障碍服务的事件过滤逻辑,避免误中断
你现有代码的事件判断存在两个明显漏洞,哪怕不做悬浮窗,在部分机型上也会出现偶发失效:- 硬绑定「类名包含AlertDialog」判断USSD窗口,不同厂商定制ROM的USSD弹窗类名差异极大,很多机型的USSD弹窗根本不会带AlertDialog标识,会直接导致事件被丢弃
- 没有排除自身应用的窗口事件,一旦悬浮窗触发窗口状态变更,就会直接触发提前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
相关产品推荐
相关产品推荐

