如何使用Delphi 10.4处理Android 7/9设备上的USSD响应并管理USSD会话
Managing USSD sessions in Delphi requires interacting with Android's native APIs, since FMX doesn't provide built-in support for USSD. The approach differs slightly between Android 7 (API 24-25) and Android 9 (API 28) due to changes in the platform's USSD handling framework. Below are step-by-step implementations for both versions.
For Android 9 (API 26+) Using TelecomManager
Android 8+ introduced the TelecomManager API, which provides official support for sending USSD requests and handling responses without relying on accessibility services.
Prerequisites
- Add Permissions: In your AndroidManifest.xml (via Delphi Project Options > Version Info > Custom Android Manifest), include these permissions:
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
Step 1: Implement the UssdResponseCallback
Create a callback class to handle USSD responses and session events:
unit UssdHandler; interface uses Androidapi.JNI.Telecom, Androidapi.JNI.JavaTypes, Androidapi.Helpers, Androidapi.JNI.GraphicsContentViewText; type TUssdResponseCallback = class(TJavaLocal, JUssdResponseCallback) private FPassword: string; public constructor Create(const Password: string); procedure onUssdResponseReceived(session: JUssdSession; response: JUssdResponse); cdecl; procedure onUssdRequestFailed(session: JUssdSession; errorCode: Integer); cdecl; end; implementation { TUssdResponseCallback } constructor TUssdResponseCallback.Create(const Password: string); begin inherited Create; FPassword := Password; end; procedure TUssdResponseCallback.onUssdRequestFailed(session: JUssdSession; errorCode: Integer); begin // Handle request failure (e.g., show error to user) TAndroidHelper.Activity.runOnUiThread( procedure begin ShowMessage('USSD request failed. Error code: ' + IntToStr(errorCode)); end ); end; procedure TUssdResponseCallback.onUssdResponseReceived(session: JUssdSession; response: JUssdResponse); var ResponseText: string; begin ResponseText := JStringToString(response.getMessage()); // Check if the response is the password prompt (adjust text to match your carrier's message) if Pos('Enter password', ResponseText) > 0 then begin // Send the password to the active USSD session session.sendUssdResponse(StringToJString(FPassword)); end else begin // Handle other responses (e.g., display balance to user) TAndroidHelper.Activity.runOnUiThread( procedure begin ShowMessage('USSD Response: ' + ResponseText); end ); end; end; end.
Step 2: Send Initial USSD Request
Call this method to trigger the initial balance query:
procedure SendInitialUssd(const Password: string); var TelecomManager: JTelecomManager; Callback: TUssdResponseCallback; begin {$IFDEF ANDROID} TelecomManager := TJTelecomManager.Wrap( TAndroidHelper.Context.getSystemService(TJContext.JavaClass.TELECOM_SERVICE) ); if TelecomManager = nil then begin ShowMessage('TelecomManager not available'); Exit; end; // Check if CALL_PHONE permission is granted if TAndroidHelper.Context.checkSelfPermission(TJManifest_permission.JavaClass.CALL_PHONE) <> TJPackageManager.JavaClass.PERMISSION_GRANTED then begin // Request permission if not granted TAndroidHelper.Activity.requestPermissions( TJavaObjectArray<JString>.Create(1, StringToJString(TJManifest_permission.JavaClass.CALL_PHONE)), 1001 // Request code for permission result ); Exit; end; Callback := TUssdResponseCallback.Create(Password); try // Send the initial USSD code (adjust to your carrier's exact code) TelecomManager.sendUssdRequest( StringToJString('#111*1*6*1#'), // Note: Replace spaces with * if needed (USSD codes typically use *) Callback, nil // Optional handler for async operations ); except Callback.Free; raise; end; {$ENDIF} end;
For Android 7 (API 24-25) Using Accessibility Service
Before API 26, there's no official USSD API, so we use an Accessibility Service to intercept the system's USSD dialog and automate input.
Prerequisites
Add Permissions & Service Declaration: Update your AndroidManifest.xml:
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" /> <service android:name="com.embarcadero.services.MyUssdAccessibilityService" 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/ussd_accessibility_config" /> </service>Create Accessibility Config: Add a
ussd_accessibility_config.xmlfile to your project'sres/xmlfolder:<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackGeneric" android:canRetrieveWindowContent="true" android:description="@string/ussd_service_desc" />Add Description String: Add a string to
res/values/strings.xml:<string name="ussd_service_desc">Automatically handles USSD password prompts for balance queries</string>
Step 1: Implement the Accessibility Service
Create the service class to detect and interact with USSD dialogs:
unit UssdAccessibilityService; interface uses Androidapi.JNI.AccessibilityService, Androidapi.JNI.JavaTypes, Androidapi.Helpers, Androidapi.JNI.GraphicsContentViewText; type TMyUssdAccessibilityService = class(TAndroidAccessibilityService) private FPassword: string; function FindNodeWithText(Node: JAccessibilityNodeInfo; Text: string): JAccessibilityNodeInfo; function FindInputField(Node: JAccessibilityNodeInfo): JAccessibilityNodeInfo; function FindSendButton(Node: JAccessibilityNodeInfo): JAccessibilityNodeInfo; protected procedure onAccessibilityEvent(event: JAccessibilityEvent); override; procedure onInterrupt; override; public class procedure SetPassword(const Password: string); function onServiceConnected: Boolean; override; end; implementation var GPassword: string; { TMyUssdAccessibilityService } class procedure TMyUssdAccessibilityService.SetPassword(const Password: string); begin GPassword := Password; end; function TMyUssdAccessibilityService.FindNodeWithText(Node: JAccessibilityNodeInfo; Text: string): JAccessibilityNodeInfo; var I: Integer; ChildNode: JAccessibilityNodeInfo; NodeText: string; begin Result := nil; if Node = nil then Exit; if Node.getText <> nil then begin NodeText := JStringToString(Node.getText); if NodeText.Contains(Text) then begin Result := Node; Exit; end; end; for I := 0 to Node.getChildCount - 1 do begin ChildNode := Node.getChild(I); Result := FindNodeWithText(ChildNode, Text); if Result <> nil then Break; end; end; function TMyUssdAccessibilityService.FindInputField(Node: JAccessibilityNodeInfo): JAccessibilityNodeInfo; var I: Integer; ChildNode: JAccessibilityNodeInfo; begin Result := nil; if Node = nil then Exit; if Node.getClassName <> nil then if JStringToString(Node.getClassName) = 'android.widget.EditText' then begin Result := Node; Exit; end; for I := 0 to Node.getChildCount - 1 do begin ChildNode := Node.getChild(I); Result := FindInputField(ChildNode); if Result <> nil then Break; end; end; function TMyUssdAccessibilityService.FindSendButton(Node: JAccessibilityNodeInfo): JAccessibilityNodeInfo; var I: Integer; ChildNode: JAccessibilityNodeInfo; NodeText: string; begin Result := nil; if Node = nil then Exit; if Node.getText <> nil then begin NodeText := JStringToString(Node.getText); if (NodeText = 'Send') or (NodeText = 'OK') then begin Result := Node; Exit; end; end; for I := 0 to Node.getChildCount - 1 do begin ChildNode := Node.getChild(I); Result := FindSendButton(ChildNode); if Result <> nil then Break; end; end; procedure TMyUssdAccessibilityService.onAccessibilityEvent(event: JAccessibilityEvent); var RootNode: JAccessibilityNodeInfo; PromptNode: JAccessibilityNodeInfo; InputNode: JAccessibilityNodeInfo; SendButton: JAccessibilityNodeInfo; Bundle: JBundle; begin inherited; if event.getEventType <> TJAccessibilityEvent.JavaClass.TYPE_WINDOW_STATE_CHANGED then Exit; RootNode := getRootInActiveWindow; if RootNode = nil then Exit; try // Check if this is the USSD dialog (package name may vary by device/carrier) if JStringToString(RootNode.getPackageName) <> 'com.android.phone' then Exit; // Look for the password prompt PromptNode := FindNodeWithText(RootNode, 'Enter password'); if PromptNode = nil then Exit; // Find input field and send button InputNode := FindInputField(RootNode); SendButton := FindSendButton(RootNode); if (InputNode <> nil) and (SendButton <> nil) then begin // Set password text Bundle := TJBundle.Create; Bundle.putString( TJAccessibilityNodeInfo.JavaClass.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, StringToJString(GPassword) ); InputNode.performAction(TJAccessibilityNodeInfo.JavaClass.ACTION_SET_TEXT, Bundle); // Click send button SendButton.performAction(TJAccessibilityNodeInfo.JavaClass.ACTION_CLICK); end; finally RootNode.recycle; end; end; procedure TMyUssdAccessibilityService.onInterrupt; begin inherited; end; function TMyUssdAccessibilityService.onServiceConnected: Boolean; var ServiceInfo: JAccessibilityServiceInfo; begin ServiceInfo := TJAccessibilityServiceInfo.JavaClass.init; ServiceInfo.eventTypes := TJAccessibilityEvent.JavaClass.TYPE_WINDOW_STATE_CHANGED or TJAccessibilityEvent.JavaClass.TYPE_WINDOW_CONTENT_CHANGED; ServiceInfo.feedbackType := TJAccessibilityServiceInfo.JavaClass.FEEDBACK_GENERIC; ServiceInfo.canRetrieveWindowContent := True; setServiceInfo(ServiceInfo); Result := True; end; end.
Step 2: Send Initial USSD Request & Configure Service
Set the password and prompt the user to enable accessibility permissions:
procedure PrepareUssdHandling(const Password: string); begin {$IFDEF ANDROID} TMyUssdAccessibilityService.SetPassword(Password); // Check if accessibility service is enabled if not TAndroidHelper.Context.getSystemService(TJContext.JavaClass.ACCESSIBILITY_SERVICE). isEnabled(TAndroidHelper.Context.getPackageName() + '/com.embarcadero.services.MyUssdAccessibilityService') then begin // Redirect user to accessibility settings ShowMessage('Please enable accessibility permissions for this app to handle USSD prompts.'); TAndroidHelper.Context.startActivity(TJIntent.JavaClass.init(TJSettings.JavaClass.ACTION_ACCESSIBILITY_SETTINGS)); end; {$ENDIF} end;Send the initial USSD code:
procedure SendInitialUssdAndroid7; var Intent: JIntent; EncodedUssd: string; begin {$IFDEF ANDROID} // Encode # as %23 for the tel URI EncodedUssd := StringReplace('#111*1*6*1#', '#', '%23', [rfReplaceAll]); Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_CALL, TJUri.JavaClass.parse('tel:' + StringToJString(EncodedUssd))); // Check CALL_PHONE permission if TAndroidHelper.Context.checkSelfPermission(TJManifest_permission.JavaClass.CALL_PHONE) = TJPackageManager.JavaClass.PERMISSION_GRANTED then TAndroidHelper.Context.startActivity(Intent) else TAndroidHelper.Activity.requestPermissions( TJavaObjectArray<JString>.Create(1, StringToJString(TJManifest_permission.JavaClass.CALL_PHONE)), 1002 ); {$ENDIF} end;
Key Notes & Pitfalls
- Carrier/Device Differences: USSD dialog package names, prompt texts, and button labels may vary by carrier or device. Adjust the text checks and package names accordingly.
- Permission Handling: Always check and request permissions at runtime (Delphi's
TAndroidHelpersimplifies this). - Session Management: For Android 9+, ensure the
TUssdResponseCallbackinstance isn't freed prematurely (store it in a form field or global variable if needed). - Accessibility Service Authorization: Users must manually enable the accessibility service for your app in system settings—your app should guide them to this screen.
内容的提问来源于stack exchange,提问作者Ranjavola Z Andrianarisaona

