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

构建USSD响应的无障碍服务:对话框隐藏与控件ID定位问题

问题描述

我正尝试在应用中实现一个无障碍服务类,完成以下任务:

  • 发送USSD请求(例如*121#)
  • 读取响应内容
  • 在响应中查找指定字符串
  • 提取该字符串前的数字
  • 使用提取的数字进行回复

目前代码已完成前4步,但无法完成回复操作,存在两个问题:

  1. 发送USSD请求后,USSD菜单/响应对话框会弹出到前台,需将其隐藏到后台才能进行回复操作;
  2. 不知道如何获取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),会直接关闭对话框,导致后续无法找到输入控件。正确逻辑是完成回复操作后再关闭对话框:

  1. 修改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)
        }
    }
}
  1. 在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:

  1. 开启开发者选项,打开「显示布局边界」和「指针位置」;
  2. 触发USSD对话框,执行ADB命令导出布局:
    adb shell uiautomator dump /sdcard/ussd_layout.xml
    
  3. 拉取布局文件到本地查看:
    adb pull /sdcard/ussd_layout.xml .
    
  4. 在XML文件中找到输入框和按钮的resource-id,替换代码中的硬编码值。

额外优化建议

  • 添加延迟处理:部分设备USSD界面加载有延迟,可在respondToUssd中用Handler.postDelayed确保控件加载完成;
  • 兼容多厂商:部分设备的USSD对话框不是AlertDialog,需在onAccessibilityEvent中增加对其他类名的判断;
  • 权限检查:确保已申请CALL_PHONE权限,且无障碍服务已在系统设置中启用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 23:47:32