Skip to main content

TerminalHelper APIs

All terminal helper functions work by binding to the TerminalHelperService.
This page will show you how to send requests and receive responses. We use a standard Android ServiceConnection for the communication.

Flow

  1. Bind to Service
  2. Wait for onServiceConnected callback from the ServiceConnection
  3. Register a client to listen to Rubean events
  4. Perform an action of your choice
  5. Unregister your client as a listener to Rubean events
  6. Unbind the service to remove the connection

Methods

GetStatus()

Mandatory function

Before performing a transaction with PhonePOS, you must call this method and wait for the response.

New error codes

With the release of PhonePOS 4.X (MPoC) we have introduced additional error codes that make it possible for integrators to better react on internal states of PhonePOS.

As a response you will receive the current state of PhonePOS and the included terminal component is currently in. In case the terminal process is not running, it will be automatically started by this call. When the terminal is already running in the background you will receive the response in 10 - 100ms.
Only when the terminal is not running in the background the response will take a longer time. On regular devices you can expect around 30 seconds as a maximum startup time. Please keep in mind that very old devices with slow processors and multiple apps in memory might take up to 2 minutes only for the initial GetStatus call.

Context

The GetStatus() command needs to be sent from a foreground context.
This is a limitation from Android 10 upwards (API level 29+), see Android Documentation.

Communication

Send an ACTIONS.GET_STATUS to the TerminalHelperService.
You will receive one of the following status codes:

CodeNameDescription
0STATUS_TERMINAL_OPERATIONALPersonalization, Terminal startup and initialization have been completed.
1STATUS_PERSONALIZATION_NOT_DONE_YETTerminal has not been personalized yet.
2STATUS_TERMINAL_NOT_OPERATIONALThe personalization generally finished but there are still issues that prevent full operation.
102STATUS_APP_IS_OUTDATEDThe app needs to be updated because the internal key box cycle has expired. Available from version 3.08.01.12 and up.

See Personalisation API for further documentation on terminal status codes.

Reset()

This method will delete the personalisation of the terminal and delete all of PhonePOS data. A new personalisation can be performed after.

Reset Callback from PhonePOS >= 3.14.2.18

From PhonePOS 3.14.02.18 you will receive a callback when the reset function has finished. It is highly recommended to wait for the callback until you perform another personalisation action.

Communication

Send an ACTIONS.RESET_PERSO to the TerminalHelperService. From PhonePOS 3.14.02.18 you will receive a callback.

GetSDKInfo()

New feature

Available from PhonePOS version 3.09.02.02 and up

This method retrieves several pieces of information: the version number of the PhonePOS build, the terminal identification (TID) if the terminal has already been personalized, and the orientation currently set on the SDK.

Communication

Send an action ACTIONS.GET_SDK_INFO to the TerminalHelperService. You will receive a json response with key GET_SDK_INFO_RESPONSE.

Example response json.

{
"terminalId": "TID123456789",
"orientation": "PORTRAIT",
"sdkGeneralInfo": {
"version": "4.01.15.0198_81"
}
}
note

If the terminal is not personalized, the terminalId field in json response will contain an empty string value.

UpdateOrientation()

New feature

Available from PhonePOS version 3.10.01.07 and up

The UpdateOrientation method sends a message to update the screen orientation of the SDK. It prepares and dispatches a message containing the orientation information, allowing the SDK to respond accordingly.

Supported Parameters as String:

The orientation to which the screen should be set:

  • "PORTRAIT": Sets the screen orientation to portrait.
  • "LANDSCAPE": Sets the screen orientation to landscape.
  • "NONE": Resets the screen orientation to the defaults.

Example

    public void messageUpdateOrientation(String orientation) {
Message message = Message.obtain(null, UPDATE_SCREEN_ORIENTATION);
Bundle bundle = new Bundle();
bundle.putString(UPDATE_SCREEN_ORIENTATION_RESPONSE, orientation);
message.setData(bundle);
sendMessage(message);
}

The Handler's response handling should look like this:

if (msg.what == UPDATE_SCREEN_ORIENTATION) {
boolean isSuccess = msg.getData().getBoolean(UPDATE_SCREEN_ORIENTATION_RESPONSE, false);
// Handle isSuccess
}

UpdateTheme()

The PhonePOS personalisation display can be themed by sending a JSON stylesheet with the action ACTIONS.UPDATE_THEME to the TerminalHelperService.

New feature

Available from PhonePOS version 3.12.01.11 and up

Themes can be configured anytime in the process and are permanently stored within PhonePOS until the next theme update.

Communication

A theme should be provided in a JSON format as follows:

{
"day": {
// custom logo shown on activation screen (PNG/JPG in base64 encoding)
"logoBase64": "...",
"activationLogoEnabled": true,

// progress spinner color
"colorAccentAnimation": "#hex",

// button colors
"colorButton": "#hex",
"colorButtonText": "#hex",

// bar colors
"colorStatusBar": "#hex",
"colorNavigationBar": "#hex",
"colorActionBar": "#hex"
}
}

If you wish to leave certain elements, such as the navigation bar color, unchanged, simply omit the corresponding entry in the JSON. Keep in mind that boolean values default to false. Currently only the day theme is supported.

You can create the above-mentioned JSON with this POJO (using Gson & Lombok):

@Data
@Builder(setterPrefix = "with")
public class AppTheme {
@SerializedName("day")
private Stylesheet day;

@Data
@Builder(setterPrefix = "with")
private static class Stylesheet {
@SerializedName("activationLogoEnabled")
public boolean activationLogoEnabled;

@SerializedName("logoBase64")
public String logoBase64;

@SerializedName("colorAccentAnimation")
public String colorAccentAnimation;

@SerializedName("colorButton")
public String colorButton;

@SerializedName("colorButtonText")
public String colorButtonText;

@SerializedName("colorStatusBar")
public String colorStatusBar;

@SerializedName("colorNavigationBar")
public String colorNavigationBar;

@SerializedName("colorActionBar")
public String colorActionBar;
}
}
Reset behaviour

Please note that the theming settings are not affected by the reset function. If you want to reset the theme to the default, please call the same theme update api again with an empty stylesheet (e.g. null).

Request changes

If the design options are not sufficient for your use case, you can contact our support team and we will analyse further proceedings.

AttestationPreflight()

A device attestation is required before every transaction. Executing this call at the right time can make the startup of transactions significantly faster. For further description and guidance please have a look at MPoC attestation processing changes.

Communication

Send an action ACTIONS.ATTESTATION_PREFLIGHT to the TerminalHelperService.

public void messageRequestPreflight() {
sendMessage(Message.obtain(null, ATTESTATION_PREFLIGHT));
}

No response will be sent.

info

This command can be called in every PhonePOS version but will only have an effect with MPoC releases of the SDK (version 4.XX.XX.XX).

Example implementation

This is an example of a TerminalHelperInteractor.
You are not required to follow this example, but it is the fastest way to achieve the terminal communication.

public class TerminalHelperServiceClient {
private static final String TAG = TerminalHelperServiceClient.class.toString();
private final Messenger responseMessenger;
private final Context context;
private boolean isServiceBound = false;
private Messenger requestMessenger;
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
requestMessenger = new Messenger(service);
isServiceBound = true;
messageRegister();
}

@Override
public void onServiceDisconnected(ComponentName name) {
isServiceBound = false;
Log.i(TAG, "TerminalHelperServiceClient disconnected");
}
};

public TerminalHelperServiceClient(Context context) {
this.context = context;
responseMessenger = new Messenger(new TerminalHelperServiceHandler());
}

private Intent getTerminalHelperServiceIntent() {
Intent intent = new Intent();
// -> APK: <packageName> is the packageName of the PhonePOS APK
// -> SDK: <packageName> is your own packageName (context.getPackageName())
intent.setComponent(new ComponentName("<packageName>", "com.rubean.sdkphonepos.TerminalHelperService"));
return intent;
}

/**
* Initiates the binding process to a service. This method internally calls the asynchronous
* method `context.bindService` to establish a connection to a service specified by the
* intent returned from `getTerminalHelperServiceIntent()`.
* <p>
* A `ServiceConnection` object, which was created during the initialization, is passed as a parameter
* to handle the connection lifecycle callbacks. It is crucial to wait for the `ServiceConnection.onServiceConnected()`
* callback to be invoked before attempting to send messages to the service.
* <p>
* If the service binding is initiated successfully, a log entry will indicate the start of the binding process.
* Otherwise, it will log an error suggesting to check if the correct application ID is configured in the manifest.
* <p>
* Note:
* The `bindService` method is asynchronous. More information can be found in the official
* documentation: https://developer.android.com/reference/android/content/Context#bindService(android.content.Intent,%20android.content.ServiceConnection,%20int)
*
* @catch Exception if the binding process fails, an error message will be logged.
*/
public void bindToService() {
if (!isServiceBound) {
Intent intent = getTerminalHelperServiceIntent();

try {
// This call is asynchronous
// Read more at https://developer.android.com/reference/android/content/Context#bindService(android.content.Intent,%20android.content.ServiceConnection,%20int)
boolean bound = context.bindService(getTerminalHelperServiceIntent(), serviceConnection, Context.BIND_AUTO_CREATE);

Log.i(TAG, "Start of binding process to " + intent.getComponent() + " was successful: " + bound);
if (!bound) {
// see APK Setup instructions
Log.e(TAG, "Correct application id configured in the manifest query section?");
}
} catch (Exception exception) {
Log.e(TAG, "Binding to service failed with " + exception.getMessage());
}
}
}

public void unbindFromService() {
try {
messageUnregister();
context.unbindService(serviceConnection);
} catch (Exception exception) {
Log.e(TAG, "Failed to unbind TerminalHelperServiceClient");
} finally {
isServiceBound = false;
}
}

private void messageRegister() {
sendMessage(Message.obtain(null, REGISTER_CLIENT));
}

private void messageUnregister() {
sendMessage(Message.obtain(null, UNREGISTER_CLIENT));
}

public void messageGetStatus() {
sendMessage(Message.obtain(null, GET_STATUS));
}

public void messageGetSDKInfo() {
sendMessage(Message.obtain(null, GET_SDK_INFO));
}

public void messageResetPerso() {
sendMessage(Message.obtain(null, RESET_PERSO));
}

// supported orientation: "PORTRAIT", "LANDSCAPE", "NONE"
public void messageUpdateOrientation(String orientation) {
Message message = Message.obtain(null, UPDATE_SCREEN_ORIENTATION);
Bundle bundle = new Bundle();
bundle.putString(UPDATE_SCREEN_ORIENTATION_RESPONSE, orientation);
message.setData(bundle);
sendMessage(message);
}

public void messageUpdateTheme(String themeJson) {
Message message = Message.obtain(null, UPDATE_THEME);
Bundle bundle = new Bundle();
bundle.putString(UPDATE_THEME_RESPONSE, themeJson);
message.setData(bundle);
sendMessage(message);
}

public void messageUpdateNfcReaderPlatformSound(boolean enabled) {
Message message = Message.obtain(null, UPDATE_NFC_READER_PLATFORM_SOUNDS);
Bundle bundle = new Bundle();
bundle.putBoolean(UPDATE_NFC_READER_PLATFORM_SOUNDS_RESPONSE, enabled);
message.setData(bundle);
sendMessage(message);
}

public void messageRequestTerminalRecovery() {
sendMessage(Message.obtain(null, TERMINAL_RECOVERY));
}

public void messageRequestPreflight() {
sendMessage(Message.obtain(null, ATTESTATION_PREFLIGHT));
}

private void sendMessage(Message message) {
try {
message.replyTo = responseMessenger;
requestMessenger.send(message);
} catch (RemoteException remoteException) {
Log.e(TAG, "Failed to send message", remoteException);
} catch (NullPointerException nullPointerException) {
// was the service successfully bound?
Log.e(TAG, "TerminalHelperServiceClient not properly initialized");
}
}
}
public class TerminalHelperServiceHandler extends Handler {
private final static String TAG = TerminalHelperServiceHandler.class.toString();

public TerminalHelperServiceHandler() {
super(Looper.getMainLooper());
}

@Override
public void handleMessage(Message msg) {
Log.i(TAG, "IncomingHandler.handleMessage(" + msg.toString() + ")");

// Important, always filter for the right action
if (msg.what == GET_STATUS) {
int statusCode = msg.arg1;

if (STATUS_TERMINAL_OPERATIONAL == statusCode) {
boolean isUpdateComingSoon = msg.getData().getBoolean(UPDATE_REMINDER_EXTRA, false);
// if isUpdateComingSoon is true, you can notify the user in your app that there soon will be a mandatory update
// Handle, e.g. start a payment
} else if (STATUS_TERMINAL_NOT_OPERATIONAL == statusCode) {
// If STATUS_TERMINAL_NOT_OPERATIONAL is returned, you will also receive a reason
int reason = msg.getData().getInt(TERMINAL_STATUS_ERROR_MESSAGE_NAME, -1);
if (reason == TERMINAL_LOW_BATTERY) {
// e.g. instruct the user to charge their phone
}
// ... depending on your need, handle more reasons
// see codes and possible reactions in the TerminalErrorCodes below
} else if (STATUS_PERSONALIZATION_NOT_DONE_YET == statusCode) {
// Handle, e.g. start a personalization
} else if (STATUS_PERSONALIZATION_IN_PROGRESS == statusCode) {
// Handle, e.g. if you want to abort the ongoing personalisation, perform a reset
} else if (STATUS_APP_IS_OUTDATED == statusCode) {
// Handle, e.g. notify user that phonepos requires an update
}

} else if (msg.what == RESET_PERSO) {
boolean isSuccess = msg.getData().getBoolean(GET_SDK_INFO_RESPONSE, false);
// Handle isSuccess
} else if (msg.what == GET_SDK_INFO) {
String sdkInfoJson = msg.getData().getString(GET_SDK_INFO_RESPONSE);
// Handle sdkInfoJson
} else if (msg.what == UPDATE_SCREEN_ORIENTATION) {
boolean isSuccess = msg.getData().getBoolean(UPDATE_SCREEN_ORIENTATION_RESPONSE, false);
// Handle isSuccess
} else if (msg.what == UPDATE_THEME) {
boolean isSuccess = msg.getData().getBoolean(UPDATE_THEME_RESPONSE, false);
// Handle isSuccess
} else if (msg.what == TERMINAL_RECOVERY) {
// Recovery returns a response equivalent to a GET_STATUS, representing the state after recovery
// You can therefore handle GET_STATUS and RECOVER_TERMINAL responses in the same way
} else if (msg.what == UPDATE_OPTIONS) {
boolean isSuccess = msg.getData().getBoolean(OPTIONS_STATUS_EXTRA, false);
String updatedOptionsJson = msg.getData().getString(OPTIONS_EXTRA);
// Handle isSuccess and updatedOptionsJson
} else if (msg.what == UPDATE_NFC_READER_PLATFORM_SOUNDS) {
boolean isSuccess = msg.getData().getBoolean(UPDATE_NFC_READER_PLATFORM_SOUNDS_RESPONSE, false);
// Handle isSuccess
}
}
}
Package name

To bind to the TerminalHelperService you need to provide a package name to the intent.
If you implement the SDK this will be your own application id.
If you implement the APK this will be the application id of the APK that will be provided to you.

Filtering for msg.what

Please make sure to filter for msg.what to always interpret responses correctly.
Messages should be processed according to their action.

Fatal Errors

All TerminalHelperService methods are designed to handle common errors gracefully. Fatal errors will lead to a mostly empty response with msg.what set to the requested action and msg.arg2 holding an error code.
Please report to our support if this occurs.

Communication Constants

The constants used in the example code are listed below:

public interface TerminalHelperServiceCommunication {
interface ACTIONS {
int REGISTER_CLIENT = 0;
int UNREGISTER_CLIENT = 1;
int GET_STATUS = 3;
int RESET_PERSO = 4;
int GET_SDK_INFO = 10;
int UPDATE_SCREEN_ORIENTATION = 11;
int UPDATE_THEME = 12;
int ATTESTATION_PREFLIGHT = 15;
int UPDATE_NFC_READER_PLATFORM_SOUNDS = 16;
int TERMINAL_RECOVERY = 19;
int UPDATE_OPTIONS = 20;
}

interface TerminalStatusCodes {
// see descriptions in Personalisation API
int STATUS_TERMINAL_OPERATIONAL = 0;
int STATUS_PERSONALIZATION_NOT_DONE_YET = 1;
int STATUS_TERMINAL_NOT_OPERATIONAL = 2;
int STATUS_PERSONALIZATION_IN_PROGRESS = 3;
int STATUS_APP_IS_OUTDATED = 102;
}

interface ExtraNames {
String OPTIONS_EXTRA = "options";
String GET_SDK_INFO_RESPONSE = "sdk_info";
String UPDATE_SCREEN_ORIENTATION_RESPONSE = "screen_orientation";
String UPDATE_THEME_RESPONSE = "theme";
String OPTIONS_STATUS_EXTRA = "options_update_status";
String UPDATE_REMINDER_EXTRA = "update_reminder";
String UPDATE_NFC_READER_PLATFORM_SOUNDS_RESPONSE = "nfc_reader_platform_sounds_enabled";
String TERMINAL_STATUS_ERROR_MESSAGE_NAME = "terminal_status_error_message";
}

interface TerminalErrorCodes {
// see descriptions in Personalisation API
int TERMINAL_LOW_BATTERY = 2310;

// These additional error codes are available with PhonePOS 4.X
int INSECURE_KEYSTORE = 2316;
int PERSONALISATION_RECOVERY_REQUIRED_SECURITY = 2318;
int PERSONALISATION_RECOVERY_REQUIRED_CONFIGURATION = 2319;
}
}