Android push notifications with InterSystems Ensemble using Road Police fines as a sample application

Many mobile applications that enable users to get information about road fines and pay them, send notifications about newly added fines. This functionality can be efficiently implemented using push notifications sent to users’ devices.

Our application was not an exception. The server side is based on the Ensemble platform that offers integrated support of push notifications starting from version 2015.1.

Some theory first

Push notifications provide a way of distributing information where data come from the provider to the user according to a set of predefined parameters.

In the most general case, the notification sending process for mobile devices looks the following way:

Notification of mobile app users is carried out using special push notification delivery services. However, notifications cannot be sent arbitrarily. The user must be subscribed to a push notifications channel or to notifications from a particular app.

Ensemble has the following instances for working with push notifications:

» EnsLib.PushNotifications.GCM.Operation — a business operation for sending push notifications to the Google Cloud Messaging Services (GCM) server. This operation also supports the delivery of a single message to several devices at once.

» EnsLib.PushNotifications.APNS.Operation — a business operation that sends a notification to the Apple Push Notifications server. Requires a separate SSL certificate for each destination application.

» EnsLib.PushNotifications.IdentityManager — an Ensemble business process that allows you to send messages to the user without thinking about their quantity and the types of destination devices. In essence, Identity Manager contains a table that maps one user’s identifier to all of their devices. The Identity Manager’s business process receives messages from other components of the production and forwards them to the router that dispatches all GCM messages to a GCM operation, and each APNS message to an APNS operation configured with a corresponding SSL certificate.

» EnsLib.PushNotifications.AppService – a business service that allows users to send push notifications generated outside of a production. In essence, the message itself can be generated somewhere inside Ensemble independently from a production, and the service makes it possible to send these messages from Ensemble. These classes are described in detail in the "Configuring and Using Ensemble Push Notifications" section of the Ensemble documentation.

Now, a few words on how we implemented the notification process

In our case, messages are generated by a specially designed business process within a production, which is why we didn’t need a service. Also, at this stage, we only have an Android app, which is why we haven’t used APNS operations yet. In fact, we used the lowest-level method of sending messages via a GCM operation. In the future, when working on the iOS version of the app, it will be convenient to work with notifications via Identity Manager in order to avoid analyzing the types and number of devices. However, we will now talk more about GCM.

In order to send notifications, you need to implement a process inside a production and include the necessary business operation. At the moment, we have two separate processes of push notification dispatch, each with its own logic: notifications about new fines, notifications about the expiry of fine discounts. We will describe each type individually.

First, a few words about the general data scheme and the settings needed for all notifications to work.

Let’s create an empty SSL configuration for the work of the operation and add it to the configuration of our business operation (for GCM only!).

Let’s add an operation of the EnsLib.PushNotifications.GCM.Operation class to our production and configure its parameters:

NotificationProtocol: HTTP

PushServer: http://android.googleapis.com/gcm/send

As the result, operation settings will look like this:

We need to keep the client identifier, devices (types and identifiers), list of documents (driver’s licenses and vehicle registration certificates). All this information is received from the client in a notification subscription request. Therefore, we need the following classes:

Client – for storing clients, App – for storing devices, Doc – for storing document data:

Class penalties.Data.Doc Extends %Persistent
{

///document type (VRC or DL)
Property type As %String;

///document identifier
Property value As %String;

}
Class penalties.Data.App Extends %Persistent
{

///device type (GCM or APNS)
Property Type As %String;

///device identifier
Property ID As %String(MAXLEN = 2048);

}
Class penalties.Data.Client Extends %Persistent
{

/// the client’s email address from Google Play Services used as an identifier
Property Email As %String;

///list of client’s devices
Property AppList As list Of penalties.Data.App;

///list of documents the client is subscribed to
Property Docs As list Of penalties.Data.Doc;

}

In order to be able to send notifications about new fines, we need to understand which fines the client has already seen, and which not. To do this, we use the class NotificationFlow, where we mark that the client has already received information about a particular finе.

Class penalties.Data.NotificationFlow Extends %Persistent
{

///client identifier (email in our case)
Property Client As %String;

///fine identifier
Property Penalty As %String;

/// Status
Property Sent As %Boolean;

}

For convenience, let’s omit the names of packages while referring to classes below. The content of these classes makes it clear how the process will look for new fines: for each client, we cycle through the list of documents, send queries to GIS GMP (main municipal system for state and municipal payments), check if the received fines are in NotificationFlow, and if they are, we remove them from the list. As the result, we end up with a list of fines that we need to notify the client about. Let’s cycle through the list of devices and send a push notification to each of them.

Top level:


 

where clientkey is a content property that by default equals to the ID of the first subscriber stored in the Client class.

The sub-process looks like this:


 

Let’s take a look inside of foreach blocks:


 

After this foreach block, we have a ready request called EnsLib.PushNotifications.NotificationRequest, and we only need to add some text to messages. This is done in the foreach block for our docs.


 

Finally, a small code fragment that fills 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 for fine discounts is implemented a bit differently. Here is what we have on the top level:


 

Fines with a discount are selected using the following code:

ClassMethod getSaleforNotify()
{
  //lets clean the temporary global just in case
  kill ^mtempArray
  set res=##class(%ResultSet).%New("%DynamicQuery:SQL")
  
  //let’s look for still unpaid fines with a discount
  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% off if paid by: ",2)
    set discDate=$extract(discDate,1,10)
    set date=$zdh(discDate,3)
    set dayscount=date-$p($h,",")

    //will send it 5,2,1 and 0 days before
    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")

    //let’s look for clients subscribed to the document
    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() {
      //let’s make a convenient list to cycle through
      set ^mtempArray($job,clRes.Data("Email"),res.Data("billNumber"))=res.Data("billDate")
    }
    do clRes.Close()
  }
  do res.Close()
}

As the result, we have a global with fines broken down by clients. What we need to do now is to cycle through this global and send a fine to each client, but make sure it hasn’t been paid somewhere else:


 

Let’s fall into the fines loop:


Basically, the difference between the processes is the following: in the first scenario, we cycle through all our clients, but in the second scenario, we only select clients with fines of a particular type; in the first case, we send a single notification with a summary (there are clients who somehow manage to get fined several times a day), in the second case, we notify about each discount option individually.

During debugging, we faced a problem with our messages that made us redefine some of our system methods. One of the parameters that is included in messages is the fine number that generally looks like this: “12345678901234567890”. The system classes that send notifications convert such strings into integers, but the GCM service that receives such large numbers gets really puzzled and, unfortunately, responds with a “Bad Request” error.

That is why we redefined the system class of the operation. Now we call our own method ConvertArrayToJSON, where we call ..Quote with the second parameter equal to 0 – that is, we don’t convert strings containing numbers into integers, but leave them in the string format:

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
}

We did not face any other problems while implementing this. The basic things that need to be done for notifications to be sent are:

Add the necessary operation

Design a process that fills the following properties of the request: AppIdentifier — Server API Key received after registering the service with GCM, Identifiers — list of device identifiers we will be using, Service — the type of devices we will be addressing (GCM in our case), Data — request data (keep in mind that the array has a key-value structure).

And that will be about it. The use of ready Ensemble components allowed us to spend just a couple of hours to implement this functionality, including debugging and testing.

As the result, we have happy clients who are timely informed about new fines and early payment discounts.


 

You can see this code in action in our Android and iOS apps.