Mobile applications / Push notifications |
The token maintainer is a BDL Web Services server program that handles push token registration from mobile apps.
In order to implement a push notification mechanism, you need to set up a server part (token maintainer and push notification server), in conjunction with a push notification framework such as Google Cloud Messaging (GCM) or Apple Push Notification service (APNs). In addition, you need to handle notification events in your mobile app. This section describes how to implement the token maintainer, the server program that maintains the list of registered devices (i.e. registration tokens for GCM or device tokens for APNs).
The same code base can be used for Android (using GCM) and iOS (using APNs) applications: The token maintainer will basically handle RESTful HTTP requests coming from the internet for token registration and token un-registration. For each of these requests, the program will insert a new record or delete an existing record in a dedicated database table.
The push provider/server program can then query the tokens table to build the list of target devices for push notifications.
In the context of APNS, the token maintainer must also handle badge numbers for each registered device: When connsuming notification messages, the iOS app must inform the token maintainer that the badge number has changed. This function is implemented with the "badge_number" command.
The token maintainer is a Web Services server program which must be deployed behind a GAS to handle load balancing. You can, however, write code to test your program in development without a GAS.
The act of registering/unregistering push tokens is application specific: When registering tokens, you typically want to add application user information. Genero BDL allows you to implement a token maintainer in a simple way.
... MAIN CALL open_create_db() CALL handle_registrations() END MAIN FUNCTION open_create_db() DEFINE dbsrc VARCHAR(100), x INTEGER LET dbsrc = "tokendb+driver='dbmsqt'" CONNECT TO dbsrc WHENEVER ERROR CONTINUE SELECT COUNT(*) INTO x FROM tokens WHENEVER ERROR STOP IF SQLCA.SQLCODE<0 THEN CREATE TABLE tokens ( id INTEGER NOT NULL PRIMARY KEY, sender_id VARCHAR(150), registration_token VARCHAR(250) NOT NULL UNIQUE, badge_number INTEGER NOT NULL, app_user VARCHAR(50) NOT NULL, -- UNIQUE reg_date DATETIME YEAR TO FRACTION(3) NOT NULL ) END IF END FUNCTION
The next function is typical Web Service server code using the Web Services API to handle RESTful requests. Note that the TCP port is defined as a constant that is used to set FGLAPPSERVER automatically when not running behind the GAS:
IMPORT util IMPORT com CONSTANT DEFAULT_PORT = 9999 MAIN ... CALL handle_registrations() END MAIN FUNCTION handle_registrations() DEFINE req com.HTTPServiceRequest, url, method, version, content_type STRING, reg_data, reg_result STRING IF LENGTH(fgl_getenv("FGLAPPSERVER"))==0 THEN -- Normally, FGLAPPSERVER is set by the GAS DISPLAY SFMT("Setting FGLAPPSERVER to %1", DEFAULT_PORT) CALL fgl_setenv("FGLAPPSERVER", DEFAULT_PORT) END IF CALL com.WebServiceEngine.Start() WHILE TRUE TRY LET req = com.WebServiceEngine.getHTTPServiceRequest(20) CATCH IF STATUS==-15565 THEN CALL show_verb("TCP socket probably closed by GAS, stopping process...") EXIT PROGRAM 0 ELSE DISPLAY "Unexpected getHTTPServiceRequest() exception: ", STATUS DISPLAY "Reason: ", SQLCA.SQLERRM EXIT PROGRAM 1 END IF END TRY IF req IS NULL THEN -- timeout DISPLAY SFMT("HTTP request timeout...: %1", CURRENT YEAR TO FRACTION) CALL check_apns_feedback() CALL show_tokens() CONTINUE WHILE END IF LET url = req.getURL() LET method = req.getMethod() IF method IS NULL OR method != "POST" THEN IF method == "GET" THEN CALL req.sendTextResponse(200,NULL,"Hello from token maintainer...") ELSE DISPLAY SFMT("Unexpected HTTP request: %1", method) CALL req.sendTextResponse(400,NULL,"Only POST requests supported") END IF CONTINUE WHILE END IF LET version = req.getRequestVersion() IF version IS NULL OR version != "1.1" THEN DISPLAY SFMT("Unexpected HTTP request version: %1", version) CONTINUE WHILE END IF LET content_type = req.getRequestHeader("Content-Type") IF content_type IS NULL OR content_type NOT MATCHES "application/json*" -- ;Charset=UTF-8 THEN DISPLAY SFMT("Unexpected HTTP request header Content-Type: %1", content_type) CALL req.sendTextResponse(400,NULL,"Bad request") CONTINUE WHILE END IF TRY CALL req.readTextRequest() RETURNING reg_data CATCH DISPLAY SFMT("Unexpected HTTP request read exception: %1", STATUS) END TRY LET reg_result = process_command(url, reg_data) CALL req.setResponseCharset("UTF-8") CALL req.setResponseHeader("Content-Type","application/json") CALL req.sendTextResponse(200,NULL,reg_result) END WHILE END FUNCTION
FUNCTION process_command(url, data) DEFINE url, data STRING DEFINE data_rec RECORD sender_id VARCHAR(150), registration_token VARCHAR(250), app_user VARCHAR(50) END RECORD, p_id INTEGER, p_ts DATETIME YEAR TO FRACTION(3), result_rec RECORD status INTEGER, message STRING END RECORD, result STRING LET result_rec.status = 0 TRY CASE WHEN url MATCHES "*token_maintainer/register" CALL util.JSON.parse( data, data_rec ) SELECT id INTO p_id FROM tokens WHERE registration_token = data_rec.registration_token IF p_id > 0 THEN LET result_rec.status = 1 LET result_rec.message = SFMT("Token already registered:\n [%1]", data_rec.registration_token) GOTO pc_end END IF SELECT MAX(id) + 1 INTO p_id FROM tokens IF p_id IS NULL THEN LET p_id=1 END IF LET p_ts = util.Datetime.toUTC(CURRENT YEAR TO FRACTION(3)) WHENEVER ERROR CONTINUE INSERT INTO tokens VALUES( p_id, data_rec.sender_id, data_rec.registration_token, 0, data_rec.app_user, p_ts ) WHENEVER ERROR STOP IF SQLCA.SQLCODE==0 THEN LET result_rec.message = SFMT("Token is now registered:\n [%1]", data_rec.registration_token) ELSE LET result_rec.status = -2 LET result_rec.message = SFMT("Could not insert token in database:\n [%1]", data_rec.registration_token) END IF WHEN url MATCHES "*token_maintainer/unregister" CALL util.JSON.parse( data, data_rec ) DELETE FROM tokens WHERE registration_token = data_rec.registration_token IF SQLCA.SQLERRD[3]==1 THEN LET result_rec.message = SFMT("Token unregistered:\n [%1]", data_rec.registration_token) ELSE LET result_rec.status = -3 LET result_rec.message = SFMT("Could not find token in database:\n [%1]", data_rec.registration_token) END IF WHEN url MATCHES "*token_maintainer/badge_number" CALL util.JSON.parse( data, data_rec ) WHENEVER ERROR CONTINUE UPDATE tokens SET badge_number = data_rec.badge_number WHERE registration_token = data_rec.registration_token WHENEVER ERROR STOP IF SQLCA.SQLCODE==0 THEN LET result_rec.message = SFMT("Badge number update succeeded for Token:\n [%1]\n New value for badge number :[%2]\n", data_rec.registration_token, data_rec.badge_number) ELSE LET result_rec.status = -4 LET result_rec.message = SFMT("Could not update badge number for token in database:\n [%1]", data_rec.registration_token) END IF END CASE CATCH LET result_rec.status = -1 LET result_rec.message = SFMT("Failed to register token:\n [%1]", data_rec.registration_token) END TRY LABEL pc_end: DISPLAY result_rec.message LET result = util.JSON.stringify(result_rec) RETURN result END FUNCTION
FUNCTION show_tokens() DEFINE rec RECORD id INTEGER, sender_id VARCHAR(150), registration_token VARCHAR(250), badge_number INTEGER, app_user VARCHAR(50), reg_date DATETIME YEAR TO FRACTION(3) END RECORD DECLARE c1 CURSOR FOR SELECT * FROM tokens ORDER BY id FOREACH c1 INTO rec.* IF rec.sender_id IS NULL THEN LET rec.sender_id = "(null)" END IF DISPLAY " ", rec.id, ": ", rec.app_user[1,10], " / ", rec.sender_id[1,20],"... / ", "(",rec.badge_number USING "<<<<&", ") ", rec.registration_token[1,20],"..." END FOREACH IF rec.id == 0 THEN DISPLAY "No tokens registered yet..." END IF END FUNCTION
When using Apple Push Notification service, the device token maintainer can also handle device unregistration by querying the APNs feedback service. The APNs feedback service will provide the list of device tokens that are no longer valid because the app on the devices has unregistered.
To get the list of device tokens failed for remote notifications, send HTTP POST request to the following URL:
tcps://feedback.push.apple.com:2196
The token maintainer can use this service to clean up the token database.
FUNCTION check_apns_feedback() DEFINE req com.TCPRequest, resp com.TCPResponse, feedback DYNAMIC ARRAY OF RECORD timestamp INTEGER, deviceToken STRING END RECORD, timestamp DATETIME YEAR TO FRACTION(3), token VARCHAR(250), i INTEGER, data BYTE IF arg_val(1)!="APNS" THEN RETURN END IF DISPLAY "Checking APNS feedback service..." LOCATE data IN MEMORY TRY LET req = com.TCPRequest.create( "tcps://feedback.push.apple.com:2196" ) CALL req.setKeepConnection(true) CALL req.setTimeout(2) CALL req.doRequest() LET resp = req.getResponse() CALL resp.getDataResponse(data) CALL com.APNS.DecodeFeedback(data,feedback) FOR i=1 TO feedback.getLength() LET timestamp = util.Datetime.fromSecondsSinceEpoch(feedback[i].timestamp) LET timestamp = util.Datetime.toUTC(timestamp) LET token = feedback[i].deviceToken DELETE FROM tokens WHERE registration_token = token AND reg_date < timestamp END FOR CATCH CASE STATUS WHEN -15553 DISPLAY "APNS feedback: Timeout: No feedback message" WHEN -15566 DISPLAY "APNS feedback: Operation failed :", SQLCA.SQLERRM WHEN -15564 DISPLAY "APNS feedback: Server has shutdown" OTHERWISE DISPLAY "APNS feedback: ERROR :",STATUS END CASE END TRY END FUNCTION
For more details about APNs feedback service, see The Feedback Service in Apple's APNs documentation.