构建USSD响应的无障碍服务:对话框隐藏与控件ID定位问题
问题描述
我正尝试在应用中实现一个无障碍服务类,完成以下任务:
- 发送USSD请求(例如
*121#) - 读取响应内容
- 在响应中查找指定字符串
- 提取该字符串前的数字
- 使用提取的数字进行回复
目前代码已完成前4步,但无法完成回复操作,存在两个问题:
- 发送USSD请求后,USSD菜单/响应对话框会弹出到前台,需将其隐藏到后台才能进行回复操作;
- 不知道如何获取USSD对话框中输入框和发送按钮的实际ID,无法与控件进行交互。
以下是当前实现的代码:
public class AccServices extends AccessibilityService { private static final String TAG = "AccServices"; private static String ussdCode; @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && "com.example.testingaccessibility.ACTION_SEND_USSD".equals(intent.getAction())) { ussdCode = intent.getStringExtra("ussd_code"); Log.d(TAG, "Received USSD Code: " + ussdCode); // Start USSD dialer if (ussdCode != null) { String ussdDialCode = ussdCode.replace("#", Uri.encode("#")); Intent callIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + ussdDialCode)); callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(callIntent); } } return START_NOT_STICKY; } @Override public void onAccessibilityEvent(AccessibilityEvent event) { Log.d(TAG, "onAccessibilityEvent"); if (event.getClassName() != null && event.getClassName().equals("android.app.AlertDialog")) { List<CharSequence> eventText = event.getText(); if (eventText != null && eventText.size() > 0) { StringBuilder ussdMessage = new StringBuilder(); for (CharSequence text : eventText) { ussdMessage.append(text); } Log.d(TAG, "USSD Message: " + ussdMessage.toString()); Intent intent = new Intent("com.example.testingaccessibility.ACTION_USSD_RECEIVED"); intent.putExtra("ussd_message", ussdMessage.toString()); sendBroadcast(intent); // Find the number before "GP STAR" String response = ussdMessage.toString(); String numberBeforeGpStar = extractNumberBeforeGpStar(response); if (numberBeforeGpStar != null) { respondToUssd(numberBeforeGpStar); } performGlobalAction(GLOBAL_ACTION_BACK); } } } private String extractNumberBeforeGpStar(String response) { Pattern pattern = Pattern.compile("(\\d+)\\s+GP STAR"); Matcher matcher = pattern.matcher(response); if (matcher.find()) { String numberBeforeGpStar = matcher.group(1); // Show a Toast message with the number before "GP STAR" Toast.makeText(this, "Number before GP STAR: " + numberBeforeGpStar, Toast.LENGTH_LONG).show(); return numberBeforeGpStar; } return null; } private void respondToUssd(String response) { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); if (rootNode != null) { List<AccessibilityNodeInfo> inputNodes = rootNode.findAccessibilityNodeInfosByViewId("com.android.phone:id/inputField"); // Replace with actual input field ID if (inputNodes != null && !inputNodes.isEmpty()) { AccessibilityNodeInfo inputNode = inputNodes.get(0); Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, response); inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); // Find and click the send button List<AccessibilityNodeInfo> sendNodes = rootNode.findAccessibilityNodeInfosByViewId("com.android.phone:id/sendButton"); // Replace with actual send button ID if (sendNodes != null && !sendNodes.isEmpty()) { AccessibilityNodeInfo sendNode = sendNodes.get(0); sendNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } rootNode.recycle(); } } @Override public void onInterrupt() { Log.d(TAG, "Accessibility service interrupted"); } @Override protected void onServiceConnected() { super.onServiceConnected(); Log.d(TAG, "Accessibility service connected"); AccessibilityServiceInfo info = new AccessibilityServiceInfo(); info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; info.packageNames = new String[]{"com.android.phone"}; info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; info.notificationTimeout = 0; setServiceInfo(info); } }
解决方案
问题1:处理USSD对话框前台显示的问题
你当前在onAccessibilityEvent中提前调用performGlobalAction(GLOBAL_ACTION_BACK),会直接关闭对话框,导致后续无法找到输入控件。正确逻辑是完成回复操作后再关闭对话框:
- 修改
onAccessibilityEvent,移除提前关闭的代码:
@Override public void onAccessibilityEvent(AccessibilityEvent event) { Log.d(TAG, "onAccessibilityEvent"); if (event.getClassName() != null && event.getClassName().equals("android.app.AlertDialog")) { List<CharSequence> eventText = event.getText(); if (eventText != null && eventText.size() > 0) { StringBuilder ussdMessage = new StringBuilder(); for (CharSequence text : eventText) { ussdMessage.append(text); } Log.d(TAG, "USSD Message: " + ussdMessage.toString()); Intent intent = new Intent("com.example.testingaccessibility.ACTION_USSD_RECEIVED"); intent.putExtra("ussd_message", ussdMessage.toString()); sendBroadcast(intent); String response = ussdMessage.toString(); String numberBeforeGpStar = extractNumberBeforeGpStar(response); if (numberBeforeGpStar != null) { respondToUssd(numberBeforeGpStar); } // 移除这里的performGlobalAction(GLOBAL_ACTION_BACK) } } }
- 在
respondToUssd方法末尾添加关闭操作:
private void respondToUssd(String response) { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); if (rootNode != null) { // 输入和发送逻辑保持不变 // 完成发送后关闭对话框 performGlobalAction(GLOBAL_ACTION_BACK); rootNode.recycle(); } }
注意:部分设备的USSD流程是先弹出纯文本响应框,点击后才进入带输入框的界面,这种情况需要监听两次AccessibilityEvent,通过文本内容或控件结构区分状态。
问题2:获取USSD控件的实际ID
不同厂商电话应用的控件ID差异很大,不建议硬编码,推荐两种兼容方案:
方案1:通过控件类型和文本查找
不需要依赖ID,通过控件类型(如EditText、Button)和文本内容定位:
private void respondToUssd(String response) { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); if (rootNode != null) { // 查找输入框 AccessibilityNodeInfo inputNode = findEditTextNode(rootNode); if (inputNode != null) { Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, response); inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); // 查找发送按钮 AccessibilityNodeInfo sendNode = findSendButtonNode(rootNode); if (sendNode != null) { sendNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } performGlobalAction(GLOBAL_ACTION_BACK); rootNode.recycle(); } } // 递归查找EditText控件 private AccessibilityNodeInfo findEditTextNode(AccessibilityNodeInfo node) { if (node.getClassName().equals(EditText.class.getName())) { return node; } for (int i = 0; i < node.getChildCount(); i++) { AccessibilityNodeInfo child = node.getChild(i); if (child != null) { AccessibilityNodeInfo result = findEditTextNode(child); if (result != null) { return result; } child.recycle(); } } return null; } // 递归查找发送按钮(匹配中文/英文文本) private AccessibilityNodeInfo findSendButtonNode(AccessibilityNodeInfo node) { if (node.getClassName().equals(Button.class.getName())) { CharSequence text = node.getText(); if (text != null && (text.toString().contains("发送") || text.toString().equalsIgnoreCase("send"))) { return node; } } for (int i = 0; i < node.getChildCount(); i++) { AccessibilityNodeInfo child = node.getChild(i); if (child != null) { AccessibilityNodeInfo result = findSendButtonNode(child); if (result != null) { return result; } child.recycle(); } } return null; }
方案2:导出布局获取真实ID
如果需要精准定位,可通过以下步骤获取控件ID:
- 开启开发者选项,打开「显示布局边界」和「指针位置」;
- 触发USSD对话框,执行ADB命令导出布局:
adb shell uiautomator dump /sdcard/ussd_layout.xml - 拉取布局文件到本地查看:
adb pull /sdcard/ussd_layout.xml . - 在XML文件中找到输入框和按钮的
resource-id,替换代码中的硬编码值。
额外优化建议
- 添加延迟处理:部分设备USSD界面加载有延迟,可在
respondToUssd中用Handler.postDelayed确保控件加载完成; - 兼容多厂商:部分设备的USSD对话框不是
AlertDialog,需在onAccessibilityEvent中增加对其他类名的判断; - 权限检查:确保已申请
CALL_PHONE权限,且无障碍服务已在系统设置中启用。
内容的提问来源于stack exchange,提问作者Sohan
相关产品推荐
相关产品推荐

