Talking to the payment app
Your FCS app and the mada payment app are processes on the same terminal, so the integration is
app-to-app: you hand a transaction over by Intent and it hands the result back by broadcast.
Adding the SDK
The SkyBand ECR SDK ships as an Android archive. Put it in your module's libs/ folder and
reference it directly:
dependencies {
implementation files('libs/SkyBandSDK-release.aar')
}
The filename varies between releases — some are shipped as skyband-ecr-sdk.aar. Use whatever
name your package arrived with rather than renaming it, so the version you are running stays
identifiable from the build file.
It has to be the AAR. The archive carries a native library that packs the request, so there is
no pure-Java path to getPackData — a jar of the Java classes alone will build and then fail at
runtime.
None of what follows is optional. A forecourt integration that gets the receiver wrong looks
exactly like one that works, right up until the first authorisation goes unanswered with a
nozzle in someone's hand.
The response comes back to a fixed class name
The payment app does not broadcast its result by action. It builds an explicit
ComponentName from the packageName you sent, plus the literal suffix
.GetPortBroadcastReceiver, and delivers there.
If your package is com.example.forecourt, the response goes to:
com.example.forecourt/com.example.forecourt.GetPortBroadcastReceiver
An explicit intent addressed to a component cannot reach a receiver registered at runtime
with registerReceiver(). It has to be declared in your manifest, under that exact name.
<receiver
android:name=".GetPortBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.skyband.pos.perform.port" />
</intent-filter>
</receiver>
package com.example.forecourt; // your ROOT package - the string you sent
public class GetPortBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
byte[] data = intent.getByteArrayExtra("app-to-app-response");
if (data == null || data.length == 0) return; // the port handshake carries none
String raw = new String(data).replace("\uFFFD", ";");
// parse - section 3
}
}
Three rules, none of them optional:
- The class must be named
GetPortBroadcastReceiver. The name is part of the protocol. - It must sit in your application's root package, matching the
packageNameyou send. - It must be
exported="true", because the sender is a different application.
Important
The symptom of getting this wrong is a transaction that looks entirely
successful from outside — the payment app opens, reads the card, authorises, closes — and
your application never hears anything. On a forecourt that means a hold placed against a
customer's card that the pump never learns about, so it is never captured and never voided —
a customer who drives away with fuel nobody was charged for, or with money held that nothing
will release.
Sending a request
Intent intent = getPackageManager().getLaunchIntentForPackage("com.skyband.pos.app");
intent.putExtra("message", "ecr-txn-event");
intent.putExtra("request", packed);
intent.putExtra("packageName", getPackageName()); // the response is addressed from this
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
Important
On Android 11 and above
getLaunchIntentForPackagereturnsnullunless the
payment app is declared in<queries>. A missing entry is indistinguishable from the app not
being installed, which sends you looking in the wrong place entirely.<queries> <package android:name="com.skyband.pos.app" /> </queries>
Building the request data
Fields are semicolon-delimited and terminated with !. A pre-authorisation:
String ecrRef = nextEcrRef(); // your counter, echoed back untouched
String date = new SimpleDateFormat("ddMMyyHHmmss", Locale.US).format(new Date());
String etp = printReceiptOnTerminal ? "1" : "0";
// Pre Authorization: dateTimeStamp;authAmount;etpInput;ecrRefNo!
String reqData = date + ";" + authAmountMinorUnits + ";" + etp + ";" + ecrRef + "!";
byte[] packed = CLibraryLoad.getInstance().getPackData(reqData, 3, signature);
Amounts are in minor units. A SAR 300 full-tank ceiling is 30000. Sending 300 holds
three riyals, which is the kind of mistake that passes testing on a pump nobody is watching.
The signature argument is not required for app-to-app integrations. Pass what your
implementation already uses and do not build logic on it.
Updated 3 days ago
