HALDoc
HALDoc
HALDoc
Last updated by | Pedro Henrique Francisco Pereira | Jul 6, 2023 at 5:03 PM GMT-3
HAL is an app in the background responsible for taking care of all the hardware part of the POS, it works as a service that
bridges the communication between the other apps and the sdks of the device manufacturers, and also takes care of the
EMV, card reading and printing.
The whole point of the HAL concept is that it is a centralizer of code related to POS manufacturers. So all implementations
related to the manufacturer's SDK must be contained within the same project, and using design pattern tactics and
tools, the HAL will identify at runtime what hardware it is running on, and adapt the operation of the even for it to work on
that manufacturer.
Architecture
The HAL encapsulates all communication with the hardware and provides a communication lib so that other apps can
communicate with the hardware.
Although there is a repository with communication interfaces between product apps with HAL, it is not ideal that we have
several different apps, one for each manufacturer in different git repositories. This would break the HAL principle of being
a single apk that knows how to decide which dependencies to inject depending on the hardware being used. This also
impacts the scalability of the product in general, because it would require us to maintain and control the deployment for
several different apps, and if the scale of manufacturers increases, this solution becomes unfeasible.
That's why it's very important to keep all the code and logic inside the hal-service repository, and use design pattern
strategies so that HAL can orchestrate the operation on all possible hardware.
HAL support for several manufacturer
Despite all the idea of the HAL concept, it was built its first version fully coupled to the Newland SDK. So we need the
following work to be done:
1. Decoupling all the code related to Newland that exists within the hal-service repository, and through the use of
design pattern principles we can prepare the code so that it can also receive another manufacturer, and in runtime
the HAL knows identify what hardware it is running on, and choose the SDK and code for that hardware model. For
example if the POS hardware is from Newland, the HAL needs to choose classes and SDKs to work with that model. If
it's a Pax hardware model it's the same idea, the HAL should choose the SDKs and classes related to the HAL.
2. Add the code and sdks for the operation of more manufacturers, the first one that will be done is PAX
Pattern Proposal
To build this HAL ability to choose at runtime which Manufacturer the HAL will run on, we need to use a design pattern
strategy.
Within the various known patterns, the principle chosen as a starting point was the pattern called Strategy.
The Strategy Pattern is a design pattern that allows you to define a family of algorithms, encapsulate each one as an
object, and make them interchangeable at runtime. This pattern helps to simplify conditional statements and make code
more flexible by separating the behavior of an object from its implementation. It consists of three main components: the
Context, the Strategy, and the Concrete Strategy. The Context holds a reference to the current Strategy object, the
Strategy defines the common behavior, and the Concrete Strategy classes implement specific algorithms or behaviors.
if (sdk != null) {
manufacturerSDK = sdk
manufacturerType = type
}
return manufacturerSDK
}
}
We needed a class that the instance would remain in memory while the HAL app was running, we chose the Singleton
pattern for this purpose.
In order to be able to identify which manufacturer the HAL is running on, we use an Android feature which is the Build
class.
Through it, we can access:
Build.MANUFACTURER
So we get the name of the manufacturer. The manufacturer detector class will run the buildManufacturerSDK method
once, it will take the value of the manufacturer, and consult it in an Enum where all the manufacturers supported by the
HAL are mapped, and it will take the class that initializes the specific SDK from that manufacturer.
This is the enum that shows which manufacturers the HAL supports:
enum class ManufacturerType {
PAX {
override fun startup() =
PaxInitialize()
},
NEWLAND {
override fun startup() =
NewlandInitialize()
};
abstract fun startup(): ManufacturerInitialize
}
Detail that here we use a kotlin/java feature called anonymous enum , which is when the enum has an abstract method.
This method serves to do the to/from the manufacturer with the SDK initializer class of that manufacturer.
About the initializer classes, this is where we put the code that starts the SDK or .jar of the manufacturer, so if we have to
support a new manufacturer, just add a new item for the manufacturer to the enum, create an initializer class and extender
from interface called ManufacturerInitialize and build the call inside the start method.
public class PaxInitialize implements ManufacturerInitialize {
private static final String TAG = "INITIALIZER";
private static IDAL dal;
@Nullable
@Override
public Object start(@NonNull Context context) {
try {
dal = NeptuneLiteUser.getInstance().getDal(context);
Logger.getInstance().i(TAG, "Starting pax tools");
return dal;
} catch (Exception e) {
Logger.getInstance().e(TAG, "Not pax device");
return null;
}
}
}
In this way, the HAL will start the manufacturer SDK and, through the ManufacturerDetector, pass this on to the
manufacturer service and the other features within the HAL.
Services
Before this PoC HAL had a class called MainService which is an Android Service responsible for starting the newland SDK,
so there was an explicit coupling.
The challenge here was to find a way in which we could have a Service in the same repository, which would know in time
to run what the manufacturer was and change the code to the correct SDK.
As shown earlier, we chose the Strategy design pattern, so the first thing we did was mirror the Android Service api in an
interface created for the HAL.
public abstract class DeviceService<T> {
protected T sdk;
This interface mirrors the methods expected by an android service, and we use Java Generics to keep generic the type of
SDK in which each of the children of this interface would expect, for example if it is a Newland service, the expected SDK
is the NSDKModuleManager, in case for a Pax device the expected SDK is IDAL.
The next step was to isolate the MainService from the Newland code, so we moved all of the code into a class called
NewlandService that extends the abstract DeviceService class shown above.
In this way we also create a class called PaxService, where we place the implementation for the Pax devices.
public class PaxService extends DeviceService<IDAL> {
@Override
public void onStartCommand(Intent intent, int flags, int startId) {
Logger.getInstance().i(TAG, "onStartCommand: " + intent.getAction());
showNotificationIcon();
}
@Override
public void onCreate() {
Logger.getInstance().d(TAG, "onCreate");
}
@Override
public void onUnbind(Intent intent) {
Logger.getInstance().d(TAG, "onUnbind");
}
@Override
public void onDestroy() {
try {
Logger.getInstance().d(TAG, "onDestroy");
} catch (Exception e) {
Logger.getInstance().i(TAG, e.getMessage());
}
}
@Override
public IBinder onBind(Intent intent) {
Logger.getInstance().i(TAG, "onBind: " + intent.getAction());
@Override
public void showNotificationIcon() {
NotificationManager manager = null;
It is important to note that this service does not extend from android's Service class, but from HAL's DeviceService class.
Another important point is that we pass the type of SDK expected by the manufacturer for the implementation to work, in
this case from Pax it is IDAL
The next step was to create a layer, which was named Binder, which will be responsible for helping the MainService to
choose which DeviceService class it will choose at runtime.
class DeviceServiceBinder {
fun getService(context: Context?): DeviceService<out Any?> {
ManufacturerDetector.buildManufacturerSDK(context!!)
An important detail is that it is at this moment that we start that implementation of the ManufacturerDetector, through it
we discover the manufacturer, we call the initializer class of that manufacturer, and we return to the Binder class, the SDK
of that Manufacturer so that through a switch/case we can choose the correct service.
Finally, let's add the binder to our MainService class:
public class MainService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
service.onStartCommand(intent, flags, startId);
return super.onStartCommand(intent, flags, startId);
}
@Override
public boolean onUnbind(Intent intent) {
service.onUnbind(intent);
return super.onUnbind(intent);
}
@Override
public void onDestroy() {
service.onDestroy();
super.onDestroy();
}
@Override
public void onCreate() {
super.onCreate();
mContext = new WeakReference<>(this);
service = new DeviceServiceBinder().getService(this);
service.onCreate();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return service.onBind(intent);
}
Now MainService is not linked to any manufacturer, and through Binder and our ManufacturerDetector, it can identify
which manufacturer and choose the correct implementation for each of the android service methods.
How to decouple HAL features
With all the implementation of the ManufacturerDetector and also the MainService decoupled, it enables the HAL to at
least be able to start on any device. But it is necessary for us to decouple and implement a new manufacturer in the other
features of HAL.
HAL follows the package by feature pattern, so we have these packages that correspond to features:
apn
beeper
card
crypto
device
emv
initializer
pin
printer
reader
scanner
security
service
stats
Each of these packages needs to be decoupled and adapted for other manufacturers. We'll use the beeper package as an
example.
The existing class bound to Newland is called BeeperBinder:
public class BeeperBinder {
public BeeperBinder() {
if (duration < 0)
throw new IllegalArgumentException("duration");
try {
beeper.beep(freq, duration);
} catch(NSDKException e) {
e.printStackTrace();
throw new UnsupportedOperationException(e.getMessage());
}
}
};
}
We see that the newland code implementation is inside the UBeeper.Stub block.
The first step here is to create an interface that mirrors UBeeper
public interface ManufacturerBeeper {
void startBeep(int freq, int millis);
}
The next step is to move the code that is inside the UBeeper.Stub block in the BeeperBinder class, to a Newland specific
class.
In this case we create a class called NewlandBeeper:
public class NewlandBeeper implements ManufacturerBeeper{
@Override
public void startBeep(int freq, int millis) {
Logger.getInstance().i(TAG, "freq: " + freq + " duration: " + millis);
if (millis < 0)
throw new IllegalArgumentException("duration");
try {
beeper.beep(freq, millis);
} catch(NSDKException e) {
e.printStackTrace();
throw new UnsupportedOperationException(e.getMessage());
}
}
}
We can take advantage of the fact that we have already created the ManufacturerBeeper interface, so we can already
implement PaxBeeper with the Pax implementation.
public class PaxBeeper implements ManufacturerBeeper{
public PaxBeeper() {
wDal = new WeakReference<>((IDAL) ManufacturerDetector.INSTANCE.getManufacturerSDK());
}
@Override
public void startBeep(int freq, int millis) {
if( freq < 0 || freq > 4000 )
throw new IllegalArgumentException("frequencia fuera de rango");
And now it's time to create our Binder layer so that HAL knows when to choose NewlandBeeper or PaxBeeper.
class ManufacturerBeeperBinder {
fun getBeeper(): ManufacturerBeeper {
return when(ManufacturerDetector.manufacturerType) {
ManufacturerType.NEWLAND -> NewlandBeeper()
ManufacturerType.PAX -> PaxBeeper()
}
}
}
It is important to note that we again use the ManufacturerDetector to find out which manufacturer the HAL is running on.
Now we will decouple the UBeeper.Stub from the BeeperBinder class. We created a specific class for Ubeeper.Stub, so
that here we can insert the Binder created, so that HAL can choose the correct class at runtime:
public class BeeperImp extends UBeeper.Stub {
public BeeperImp() {
this.binder = new ManufacturerBeeperBinder().getBeeper();
}
@Override
public void startBeep(int i, int i1) throws RemoteException {
binder.startBeep(i, i1);
}
}
And finally we will change our BeeperBinder, so that it uses the BeeperImp class:
public class BeeperBinder {
public BeeperBinder() {
And that's it, we just took a feature that was fully coupled to newland, we decoupled it and started supporting more than
one manufacturer.
If we need to change something in the Pax or Newland specific code, we noticed that it is very easy to identify, in addition
to supporting other manufacturers it is also very easy to guide.
Next Steps
The next steps revolve around decoupling the code from the other packages, and starting to support the Pax
manufacturer.
For this it is necessary to follow the same steps that we used for the beeper package.
So for each of the packages we need to follow the following step by step:
1. In the existing binder class, identify which Stub is used, that is, the interface used in hal-lib.
2. Create an interface mirroring the hal-lib interface
3. Create a Newland-specific class by implementing the new interface, and moving the newland code from the Binder or
Stub class into this new class.
4. Create the Pax class implementing the same interface, and making the necessary implementations
5. Create a class for our Binder layer, and implement the method that through a switch/case can choose which class is
the correct manufacturer
6. In the stub class, implement the binder, and call the binder method in each of the stub methods