运行中USSD会话多次发送响应的Android开发问题
问题根源
- 系统抢了USSD会话控制权:发送USSD请求后,系统默认会弹出自带的USSD界面,直接接管会话,导致APP无法继续发送后续响应请求。
- 嵌套回调逻辑死板:当前代码用多层嵌套的
ResultReceiver处理响应,逻辑耦合度高,且未处理会话持续状态,系统弹窗出现后,后续交互直接失效。 - 未拦截系统弹窗:缺少阻止系统显示USSD界面的机制,APP无法独占会话控制权,自然无法完成多步交互。
解决方案
1. 用无障碍服务拦截系统USSD弹窗
要让APP持续控制会话,必须先拦截系统的USSD弹窗:
- 编写继承自
AccessibilityService的服务,在onAccessibilityEvent中检测系统USSD窗口(常见包名:com.android.phone或com.android.server.telecom),一旦检测到就调用返回键关闭弹窗。 - 在Manifest中注册该服务,声明对应权限和过滤规则。
2. 重构为状态化流程控制
放弃多层嵌套回调,改用状态机管理会话流程,比如分为「初始请求」「等待菜单响应」「等待PIN输入」等状态,每个状态对应独立处理逻辑,降低耦合度。
3. 确保会话上下文复用
调用handleUssdRequest时,需保证在同一个会话内执行。部分设备的该方法可能需要额外参数维持会话,且要在收到响应后立即发送下一个请求,避免会话超时或被系统接管。
代码实现示例
无障碍拦截服务
public class UssdInterceptorService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { String packageName = event.getPackageName().toString(); // 适配不同厂商的系统USSD窗口包名 if (packageName.equals("com.android.phone") || packageName.equals("com.android.server.telecom")) { // 拦截弹窗,不展示给用户 performGlobalAction(GLOBAL_ACTION_BACK); } } } @Override public void onInterrupt() {} @Override protected void onServiceConnected() { super.onServiceConnected(); AccessibilityServiceInfo info = new AccessibilityServiceInfo(); info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; info.packageNames = new String[]{"com.android.phone", "com.android.server.telecom"}; info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; setServiceInfo(info); } }
Manifest注册服务
<service android:name=".UssdInterceptorService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service>
无障碍服务配置文件(res/xml/accessibility_service_config.xml)
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/accessibility_description" android:packageNames="com.android.phone,com.android.server.telecom" />
重构后的USSD交互逻辑
private TelephonyManager tm; private Method handleUssdRequestMethod; private Object iTelephony; private String targetOfferAmount; private String userPin; // 初始化USSD相关实例 public void initUssd(Context context) { tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); try { Class<?> telephonyManagerClass = Class.forName(tm.getClass().getName()); Method getITelephony = telephonyManagerClass.getDeclaredMethod("getITelephony"); getITelephony.setAccessible(true); iTelephony = getITelephony.invoke(tm); Method[] methods = iTelephony.getClass().getMethods(); for (Method method : methods) { if (method.getName().equals("handleUssdRequest")) { handleUssdRequestMethod = method; handleUssdRequestMethod.setAccessible(true); break; } } } catch (Exception e) { e.printStackTrace(); } } // 启动USSD流程 public void startUssdFlow(Context context, String initialUssd, String offerAmount, String pin) { targetOfferAmount = offerAmount; userPin = pin; sendUssdRequest(0, initialUssd, response -> processUssdResponse(response)); } // 发送USSD请求的通用方法 private void sendUssdRequest(int simSlot, String ussd, UssdResponseCallback callback) { if (handleUssdRequestMethod == null || iTelephony == null) return; try { handleUssdRequestMethod.invoke(iTelephony, simSlot, ussd, new ResultReceiver(new Handler(Looper.getMainLooper())) { @Override protected void onReceiveResult(int resultCode, Bundle ussdResponse) { Object p = ussdResponse.getParcelable("USSD_RESPONSE"); if (p != null) { try { Method getReturnMessage = p.getClass().getMethod("getReturnMessage"); CharSequence message = (CharSequence) getReturnMessage.invoke(p); if (callback != null) callback.onResponse(message); } catch (Exception e) { e.printStackTrace(); } } } }); } catch (Exception e) { e.printStackTrace(); } } // 处理USSD响应的核心逻辑 private void processUssdResponse(CharSequence response) { String responseStr = response.toString(); if (responseStr.contains(targetOfferAmount)) { // 提取目标套餐对应的选项 String offerLine = findLineWithMyOffer(responseStr, targetOfferAmount); String option = extractNumberFromLine(offerLine); // 发送选项请求,之后发送PIN sendUssdRequest(0, option, nextResponse -> { sendUssdRequest(0, userPin, null); if (textViewUssdResponse != null) { textViewUssdResponse.setText(nextResponse); } }); } else { // 未找到目标套餐,发送99继续查找 sendUssdRequest(0, "99", this::processUssdResponse); } } // 响应回调接口 private interface UssdResponseCallback { void onResponse(CharSequence response); } // 原有工具方法保留 private String findLineWithMyOffer(String response, String offerAmount) { String[] lines = response.split("\n"); for (String line : lines) { if (line.contains(offerAmount)) return line; } return ""; } private String extractNumberFromLine(String line) { for (char c : line.toCharArray()) { if (Character.isDigit(c)) return String.valueOf(c); } return ""; }
注意事项
- 权限需手动开启:必须申请
ACCESSIBILITY_SERVICE权限,用户需前往系统设置为APP手动开启该权限。 - 适配不同厂商:不同品牌手机的USSD窗口包名可能不同,需测试适配(如部分华为机型包名有差异)。
- 避免会话超时:USSD会话存在超时限制,需确保每一步响应在超时前发送。
- 隐藏API兼容性:
handleUssdRequest是系统隐藏API,不同Android版本的调用规则可能变化,高版本系统可能限制调用,需做适配。
内容的提问来源于stack exchange,提问作者Sohan
相关产品推荐
相关产品推荐

