Push notifications on Android in InterSystems Ensemble on the example of traffic police fines

In many mobile applications that allow you to recognize fines and pay them, it is possible to receive information about new fines. For this, it is convenient to use the sending of push notifications to client devices.
Our application for paying fines was no exception. The server part is implemented on the Ensemble platform, in which, from version 2015.1, the built-in support for push notifications appeared very on time.
First, a little theory
Push notifications are one of the methods of disseminating information when data comes from the provider to the user based on the established parameters.
In general, for mobile devices, the notification process looks like this:

To notify users of mobile applications, notification delivery services are used, the data from which devices receive. And just so you can not send a notification. The user must be subscribed to the push notification channel or to notifications from a specific application.
Ensemble has the following entities for working with push notifications:
» EnsLib.PushNotifications.GCM.Operation - a business operation for sending push notifications to the Google Cloud Messaging Services (GCM) server. The operation also allows you to send one message to the application at once on several devices.
» EnsLib.PushNotifications.APNS.Operation- A business transaction that sends a notification to the Apple Push Notifications server. To send messages to each implemented application, you will need a separate SSL certificate.
» EnsLib.PushNotifications.IdentityManager - Ensemble business process. Allows you to send messages to the user without thinking about the number and types of his devices. Essentially, Identity Manager contains a table that maps all of its devices to one user ID. Identity Manager's business process receives messages from other product components and forwards them to a router, which in turn sends all GCM messages to a GCM operation, and each APNS message to an APNS operation configured with the corresponding SSL certificate.
"EnsLib.PushNotifications.AppService - a business service that allows you to send push messages generated outside the product. In fact, the message itself can be generated somewhere inside Ensemble, regardless of the product, while the service allows you to send these messages from Ensemble. All of these classes are described in detail in the Ensemble documentation " Configuring and Using Ensemble Push Notifications ".
Now about how we implemented the notification process
In our case, messages are generated by a specially developed business process inside the product, so the service was not useful to us. Also, at this stage we only have an Android application, so we have not yet used the APNS operation. In fact, we used the lowest level way to send directly through the GCM operation. In the future, when implementing the iOS version of the application, it will be convenient to implement work with notifications through Identity Manager so that you do not have to analyze the type and number of devices. But now let's talk more about GCM.
To send notifications, it is necessary to implement the process inside the product and connect the necessary business operation. Currently, we have two separate processes for sending push notifications, each with its own logic: notifications of new fines, notifications of the expiration of a discount on a fine. We will talk about each type in more detail.
First, about the general data scheme and settings necessary for all notifications to work.
- We create an empty SSL configuration for the operation to work, add it to the business transaction configuration (only for GCM!).
- We add the operation of the class EnsLib.PushNotifications.GCM.Operation to the products, configure its parameters:
NotificationProtocol: HTTP
PushServer: http://android.googleapis.com/gcm/send
The operation settings in the end look like this:

We need to save the client’s identifier, devices (identifiers and types), a list of documents (driver’s licenses and car registration certificates). We get all this information in requests from the client when subscribing to notifications. So, we need classes:
Client - for storing clients, App - for storing devices, Doc - for storing document data:
Class penalties.Data.Doc Extends %Persistent
{
///тип документа (СТС или ВУ)
Property type As %String;
///идентификатор документа
Property value As %String;
}
Class penalties.Data.App Extends %Persistent
{
///тип устройства (GCM или APNS)
Property Type As %String;
///идентификатор устройства
Property ID As %String(MAXLEN = 2048);
}
Class penalties.Data.Client Extends %Persistent
{
/// почтовый адрес клиента из Google Play Services, используем как идентификатор
Property Email As %String;
///список устройств клиента
Property AppList As list Of penalties.Data.App;
///список документов, на которые подписался клиент
Property Docs As list Of penalties.Data.Doc;
}
To send notifications of new fines, we need to understand which fines to send to the client and which ones he has already seen when entering the application. To do this, we have a NotificationFlow class, in which we note that the client has already received information about the fine.
Class penalties.Data.NotificationFlow Extends %Persistent
{
///идентификатор клиента (в нашем случае email)
Property Client As %String;
///идентификатор штрафа
Property Penalty As %String;
/// признак отправки
Property Sent As %Boolean;
}
For convenience, we will omit package names below when mentioning classes. The content of the classes makes it clear what the process of the new fines will look like: for each client we go through the list of documents, make a request for fines for them in the GIS GMP (State Information System on State and Municipal Payments), check the received fines for availability in NotificationFlow, if found - delete from the list, as a result, we form a list of fines that need to be notified to the client, go over the list of client devices and send a push notification to each of them.
Top level:

where clientkey is a context property, the default value of which is the identifier of the first in order client with a subscription stored in the Client class.
The subprocess looks like this:

Take a look inside the foreach blocks:

After this foreach block, we have a ready EnsLib.PushNotifications.NotificationRequest request, in which it remains to add the message text. This is done in the foreach block for Docs.

And a small piece of code filling in the request data:
ClassMethod getPenaltyforNotify(client As penalties.Data.Client, penaltyResponse As penalties.Operations.Response.getPenaltiesResponse, notificationRequest As EnsLib.PushNotifications.NotificationRequest)
{
set json="",count=0
set key="" for
{
set value=penaltyResponse.penalties.GetNext(.key)
quit:key=""
set find=0
set res=##class(%ResultSet).%New("%DynamicQuery:SQL")
set exec="SELECT * FROM penalties_Data.NotificationFlow WHERE (Penalty = ?) AND (Client = ?)"
set status=res.Prepare(exec)
set status=res.Execute(value.billNumber,client.Email)
if $$$ISERR(status) do res.%Close() kill res continue
while res.Next()
{
if res.Data("Sent") set find=1
}
do res.%Close() kill res
if find {do penaltyResponse.penalties.RemoveAt(key), penaltyResponse.penalties.GetPrevious(.key)}
else {
set count=count+1
do notificationRequest.Data.SetAt("single","pushType")
for prop="billNumber","billDate","validUntil","amount","addInfo","driverLicence","regCert"
{
set json=$property(value,prop)
set json=$tr(json,"""","")
if json="" continue
do notificationRequest.Data.SetAt(json,prop)
}
set json=""
set notObj=##class(penalties.Data.NotificationFlow).%New()
set notObj.Client=client.Email
set notObj.Penalty=value.billNumber
set notObj.Sent=1
do notObj.%Save()
}
}
if count>1 {
set keyn="" for {
do notificationRequest.Data.GetNext(.keyn)
quit:keyn=""
do notificationRequest.Data.RemoveAt(keyn)
}
do notificationRequest.Data.SetAt("multiple","pushType")
do notificationRequest.Data.SetAt(count,"penaltiesCount")
}
}
The process of discounts on the payment of fines is implemented somewhat differently. At the top level:

The selection of fines with a discount is performed by the following code:
ClassMethod getSaleforNotify()
{
//на всякий случай почистим временную глобаль
kill ^mtempArray
set res=##class(%ResultSet).%New("%DynamicQuery:SQL")
//поищем все еще не оплаченные штрафы со скидкой
set exec="SELECT * FROM penalties_Data.Penalty WHERE status!=2 AND addInfo LIKE '%Скидка%'"
set status=res.Prepare(exec)
set status=res.Execute()
if $$$ISERR(status) do res.%Close() kill res quit
while res.Next()
{
set discDate=$piece(res.Data("addInfo"),"Скидка 50% при оплате до: ",2)
set discDate=$extract(discDate,1,10)
set date=$zdh(discDate,3)
set dayscount=date-$p($h,",")
//отправлять будем за 5,2,1 и 0 дней
if '$lf($lb(5,2,1,0),dayscount) continue
set doc=$s(res.Data("regCert")'="":"sts||"_Res.Data("regCert"),1:"vu||"_Res.Data("driverLicence"))
set clRes=##class(%ResultSet).%New("%DynamicQuery:SQL")
//поищем клиентов, подписанных на документ
set clExec="SELECT * FROM penalties_Data.Client WHERE (Docs [ ?)"
set clStatus=clRes.Prepare(clExec)
set clStatus=clRes.Execute(doc)
if $$$ISERR(clStatus) do clRes.%Close() kill clRes quit
while clRes.Next()
{
//составим удобный список, по которому потом будем бегать
set ^mtempArray($job,clRes.Data("Email"),res.Data("billNumber"))=res.Data("billDate")
}
do clRes.Close()
}
do res.Close()
}
At the exit, we have a global with a breakdown of fines on customers. Now you need to go over this global and send each client his fine, after making sure that he has not yet been paid elsewhere:

Falling into a cycle of fines:

Actually, the difference between the processes is as follows: in the first case, we will definitely go over all our customers, in the second we select only customers who have fines of a certain type; in the first case, for several penalties, I send one notice with a total amount (there are clients who manage to pick up a lot of penalties in a day), in the second case, for each discount separately.
During debugging, we encountered a small feature of our messages, due to which we had to redefine some system methods. One of the parameters of our message, we pass the number of the fine, which in general terms looks something like “12345678901234567890”. The system classes of the operation for sending notifications convert such strings to numbers, and the GCM service, unfortunately, after receiving such a large number, is perplexed and returns “Bad Request”.
Therefore, we redefined the system class of the operation, in it we call our method ConvertArrayToJSON, inside which we call ..Quote with the second parameter equal to 0, that is, we do not convert strings consisting only of numbers to numbers, but leave them as strings:
Method ConvertArrayToJSON(ByRef pArray) As %String
{
#dim tOutput As %String = ""
#dim tSubscript As %String = ""
For {
Set tSubscript = $ORDER(pArray(tSubscript))
Quit:tSubscript=""
Set:tOutput'="" tOutput = tOutput _ ","
Set tOutput = tOutput _ ..Quote(tSubscript) _ ": "
If $GET(pArray(tSubscript))'="" {
#dim tValue = pArray(tSubscript)
If $LISTVALID(tValue) {
#dim tIndex As %Integer
// $LIST .. aka an array
// NOTE: This only handles an array of scalar values
Set tOutput = tOutput _ "[ "
For tIndex = 1:1:$LISTLENGTH(tValue) {
Set:tIndex>1 tOutput = tOutput _ ", "
Set tOutput = tOutput _ ..Quote($LISTGET(tValue,tIndex),0)
}
Set tOutput = tOutput _ " ]"
} Else {
// Simple string
Set tOutput = tOutput _ ..Quote(tValue,1)
}
} Else {
// Child elements
#dim tTemp
Kill tTemp
Merge tTemp = pArray(tSubscript)
Set tOutput = tOutput _ ..ConvertArrayToJSON(.tTemp)
}
}
Set tOutput = "{" _ tOutput _ "}"
Quit tOutput
}
No other problems were found during the implementation process. Total, the main things to do to send notifications:
- add the desired operation
- build a process that fills the following request properties: AppIdentifier - Server API Key obtained when registering a service in GCM, Identifiers - a list of identifiers of the devices we are accessing, Service - the type of device we are accessing (in our case, “GCM”), Data - query data itself (remember that the array is built on a key-value basis).
Actually, that's all. Due to the use of ready-made Ensemble components, the implementation of the process takes a couple of hours, including debugging and testing.
At the exit, we have satisfied customers who timely find out about new fines and remember the discounts on time.

You can see how it works in Android and iOS applications.