package com.malcom.library.android.module.notifications.gcm;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.TimeZone;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.google.android.gcm.GCMRegistrar;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.malcom.library.android.module.core.MCMCoreAdapter;
import com.malcom.library.android.module.notifications.EnvironmentType;
import com.malcom.library.android.module.notifications.MCMNotificationModule;
import com.malcom.library.android.module.notifications.NotificationAck;
import com.malcom.library.android.module.notifications.NotificationRegistration;
import com.malcom.library.android.module.notifications.services.PendingAcksDeliveryService;
import com.malcom.library.android.utils.HttpDateUtils;
import com.malcom.library.android.utils.MCMUtils;
import com.malcom.library.android.utils.MalcomHttpOperations;
import com.malcom.library.android.utils.ToolBox;
import com.malcom.library.android.utils.ToolBox.HTTP_METHOD;
import com.malcom.library.android.utils.encoding.DigestUtils;
import com.malcom.library.android.utils.encoding.base64.Base64;
/**
* Helper class used to communicate with the Malcom server.
*
* - Device registration
* - Device un-registration
* - ACKs
*
* @author Malcom Ventures, S.L.
* @since 2012
*/
public final class MalcomServerUtilities {
//private enum HTTP_METHOD{POST,DELETE};
private static final int MAX_ATTEMPTS = 5;
private static final int MAX_ATTEMPTS_UNREG = 5;
private static final int BACKOFF_MILLI_SECONDS = 2000;
private static final Random random = new Random();
private static final String PARAM_DEVICE_REGID = "regId";
private static final String PARAM_DEVICEUDID = "deviceUdid";
private static final String PARAM_NOTIFICATION_ID = "notificationId";
private static final String PARAM_NOTIFICATION_SEGMENT_ID = "segmentId";
private static final String PARAM_ENVIRONMENT = "environment";
private static final String PARAM_APPLICATION_CODE = "appCode";
private static final String PARAM_APPLICATION_SECRETKEY = "appSecretKey";
private static final String PARAM_APPLICATION_ENVIRONMENT_TYPE = "appEnvironmentType";
/**
* Register this account/device pair within the server.
*
* @return whether the registration succeeded or not.
*
* @param context
* @param regId
* @param environment
* @param appCode
* @param appSecretKey
* @return
*/
public static boolean register(final Context context, final String regId, EnvironmentType environment,
final String appCode, final String appSecretKey) {
Log.i(MCMNotificationModule.TAG, "Registering device (regId = " + regId + ") ...");
long backoff = BACKOFF_MILLI_SECONDS + random.nextInt(1000);
String serverUrl = MCMCoreAdapter.getInstance().coreGetProperty(MCMCoreAdapter.PROPERTIES_MALCOM_BASEURL) + MCMNotificationModule.notification_registry;
Map<String, String> params = new HashMap<String, String>();
params.put(PARAM_DEVICE_REGID, regId);
params.put(PARAM_DEVICEUDID,ToolBox.device_getId(context));
params.put(PARAM_ENVIRONMENT, environment.name());
params.put(PARAM_APPLICATION_CODE, appCode);
params.put(PARAM_APPLICATION_SECRETKEY, appSecretKey);
// Once GCM returns a registration id, we need to register it in Malcom
// server. As the server might be down, we will retry it a couple
// times.
for (int i = 1; i <= MAX_ATTEMPTS; i++) {
if(!GCMRegistrar.isRegistered(context)){
break; //Is not registered, no sense doing registration.
}
Log.d(MCMNotificationModule.TAG, "Attempt #" + i + " to register");
try {
Log.d(MCMNotificationModule.TAG,"Registering in Malcom ("+i+"/"+MAX_ATTEMPTS+")...");
serverDoRegister(serverUrl, params);
GCMRegistrar.setRegisteredOnServer(context, true);
Log.d(MCMNotificationModule.TAG,"Device successfully registered in Malcom.");
return true;
} catch (Exception e) {
Log.e(MCMNotificationModule.TAG, "Failed to register on attempt " + i, e);
if (i == MAX_ATTEMPTS) {
break;
}
try {
Log.d(MCMNotificationModule.TAG, "Sleeping for " + backoff + " ms before retry");
Thread.sleep(backoff);
} catch (InterruptedException e1) {
// Activity finished before we complete - exit.
Log.d(MCMNotificationModule.TAG, "Thread interrupted: abort remaining retries!");
Thread.currentThread().interrupt();
return false;
}
// increase backoff exponentially
backoff *= 2;
}
}
if(!GCMRegistrar.isRegistered(context)){
Log.e(MCMNotificationModule.TAG,"Device registration with Malcom aborted. Device not registered!");
}else{
Log.e(MCMNotificationModule.TAG,"Device registration with Malcom failed!");
}
return false;
}
/**
* Unregister this account/device pair within the server.
*
* @param context
* @param regId
* @param appCode
* @param appSecretKey
* whether the un-registration succeeded or not.
*/
public static boolean unregister(final Context context, final String regId,
final String appCode, final String appSecretKey) {
Log.i(MCMNotificationModule.TAG, "Unregistering device from Malcom (regId = " + regId + ") ...");
long backoff = BACKOFF_MILLI_SECONDS + random.nextInt(1000);
for (int i = 1; i <= MAX_ATTEMPTS_UNREG; i++) {
if(GCMRegistrar.isRegistered(context)){
break; //Is registered, no sense doing un-registration.
}
Log.d(MCMNotificationModule.TAG, "Attempt #" + i + " to unregister");
try {
Map<String, String> params = new HashMap<String, String>();
params.put(PARAM_APPLICATION_CODE, appCode);
params.put(PARAM_APPLICATION_SECRETKEY, appSecretKey);
params.put(PARAM_DEVICEUDID, ToolBox.device_getId(context));
//Set the unregistration URL for later usage.
String deviceId = MCMUtils.getEncodedUDID(ToolBox.device_getId(context));
String serverUrl = MCMCoreAdapter.getInstance().coreGetProperty(MCMCoreAdapter.PROPERTIES_MALCOM_BASEURL) + MCMNotificationModule.notification_deregister;
serverUrl=serverUrl.replaceAll(MCMNotificationModule.notification_deregister_param_appCode, appCode);
serverUrl=serverUrl.replaceAll(MCMNotificationModule.notification_deregister_param_udid, deviceId);
System.out.println("Unregister url: "+serverUrl);
Log.d(MCMNotificationModule.TAG,"Un-Registering in Malcom ("+i+"/"+MAX_ATTEMPTS+")...");
serverDoUnRegister(serverUrl, params);
GCMRegistrar.setRegisteredOnServer(context, false);
Log.d(MCMNotificationModule.TAG,"Device successfully un-registered from Malcom.");
return true;
} catch (Exception e) {
Log.e(MCMNotificationModule.TAG, "Failed to un-register on attempt " + i, e);
if (i == MAX_ATTEMPTS) {
break;
}
try {
Log.d(MCMNotificationModule.TAG, "Sleeping for " + backoff + " ms before retry");
Thread.sleep(backoff);
} catch (InterruptedException e1) {
// Activity finished before we complete - exit.
Log.d(MCMNotificationModule.TAG, "Thread interrupted: abort remaining un-registration retries!");
Thread.currentThread().interrupt();
return false;
}
// increase backoff exponentially
backoff *= 2;
}
}
if(GCMRegistrar.isRegistered(context)){
Log.i(MCMNotificationModule.TAG,"Device un-registration with Malcom aborted!");
}else{
Log.e(MCMNotificationModule.TAG,"Device un-registration with Malcom failed!");
}
return false;
}
/**
* Sends the ACK to the server.
*
* @param context
* @param notId
* @param segmentId
* @param environmentType
* @param appCode
* @param appSecretKey
*/
public static void doAck(final Context context, final String notId, final String segmentId,
final String environmentType, final String appCode, final String appSecretKey){
Log.i(MCMNotificationModule.TAG, "Doing ACK (notId = " + notId + ") ...");
try {
Map<String, String> params = new HashMap<String, String>();
params.put(PARAM_APPLICATION_CODE, appCode);
params.put(PARAM_APPLICATION_SECRETKEY, appSecretKey);
params.put(PARAM_DEVICEUDID,ToolBox.device_getId(context));
params.put(PARAM_NOTIFICATION_ID,notId);
params.put(PARAM_NOTIFICATION_SEGMENT_ID,segmentId);
params.put(PARAM_APPLICATION_ENVIRONMENT_TYPE,environmentType);
//Set the unregistration URL for later usage.
String serverUrl = MCMCoreAdapter.getInstance().coreGetProperty(MCMCoreAdapter.PROPERTIES_MALCOM_BASEURL) + MCMNotificationModule.notification_ack;
serverDoAck(context, serverUrl, params);
Log.d(MCMNotificationModule.TAG,"Notification ACK successfully done.");
} catch (Exception e) {
Log.e(MCMNotificationModule.TAG, "Failed to ACK!.",e);
}
}
//AUXILIAR METHODS
/*
* Makes the registration POST request to the Malcom API.
*
* @param endpoint POST address.
* @param params request parameters.
*
* @throws IOException propagated from POST.
*/
private static void serverDoRegister(String endpoint, Map<String, String> params) throws Exception {
URL url;
try {
url = new URL(endpoint);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("invalid url: " + endpoint);
}
String jsonBody = null;
//Prepare the registration Object and get the JSON for the body
NotificationRegistration registration = new NotificationRegistration();
registration.setEnvironment(params.get(PARAM_ENVIRONMENT));
registration.setToken(params.get(PARAM_DEVICE_REGID));
registration.setUdid(params.get(PARAM_DEVICEUDID));
registration.setApplicationCode(params.get(PARAM_APPLICATION_CODE));
//...get the JSON body from the object using Google JSON library
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
jsonBody = "{\"NotificationRegistration\":" + gson.toJson(registration) + "}";
Log.v(MCMNotificationModule.TAG, "Sending device registration body: '" + jsonBody + "' to " + url);
String appCode = params.get(PARAM_APPLICATION_CODE);
String appSecretKey = params.get(PARAM_APPLICATION_SECRETKEY);
MalcomHttpOperations.sendPostToMalcom(endpoint, "/v3/notification/registry/application", jsonBody, appCode, appSecretKey);
}
/*
* Makes the un-registration (DELETE) request to the Malcom API.
*
* @param endpoint DELETE address.
* @param params request parameters.
*
* @throws IOException propagated from POST.
*/
private static void serverDoUnRegister(String endpoint, Map<String, String> params) throws Exception {
URL url;
try {
url = new URL(endpoint);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("invalid url: " + endpoint);
}
Log.v(MCMNotificationModule.TAG, "Sending device un-registration request to " + url);
try {
//Prepare required data for headers, these headers are requested by Malcom API.
String malcomDate = HttpDateUtils.formatDate(new Date());
String headers = "x-mcm-date:" + malcomDate+"\n";
//For unregistration i must pass the final url with corresponding application and udid.
//(and also add "/malcom-api/" before the endpoint of the service, why this?)
String resource = "/"+MCMNotificationModule.notification_deregister;
resource=resource.replaceAll(MCMNotificationModule.notification_deregister_param_appCode, params.get(PARAM_APPLICATION_CODE));
resource=resource.replaceAll(MCMNotificationModule.notification_deregister_param_udid, params.get(PARAM_DEVICEUDID));
System.out.println("Resource: "+resource);
String password = ToolBox.deliveries_getDataToSign(headers, null, null, "DELETE", resource, null);
password = DigestUtils.calculateRFC2104HMAC(password, params.get(PARAM_APPLICATION_SECRETKEY));
Map<String, String> headersData = new HashMap<String, String>();
headersData.put("Authorization", "basic " + new String(Base64.encode(new String(params.get(PARAM_APPLICATION_CODE) + ":" + password).getBytes())));
headersData.put("x-mcm-date", malcomDate);
System.out.println("Endpoint al desregistrar: "+endpoint);
ToolBox.net_httpclient_doAction(HTTP_METHOD.DELETE, endpoint, null, headersData);
} catch(Exception e) {
Log.e(MCMNotificationModule.TAG, "Error sending un-registration data to Malcom service url '"+url.toString()+"': "+e.getMessage(),e);
throw e;
}
}
/*
* Makes the ACK request (POST) to Malcom API.
*
* @param endpoint
* @param params
* @throws Exception
*/
private static void serverDoAck(final Context context, String endpoint, Map<String, String> params) throws Exception {
URL url;
try {
url = new URL(endpoint);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("invalid url: " + endpoint);
}
String jsonBody = null;
Date date = new Date();
//Prepare the registration Object and get the JSON for the body
NotificationAck ack = new NotificationAck();
ack.setApplicationCode(params.get(PARAM_APPLICATION_CODE));
ack.setUdid(params.get(PARAM_DEVICEUDID));
ack.setId(Long.valueOf(params.get(PARAM_NOTIFICATION_ID)));
if(params.get(PARAM_NOTIFICATION_SEGMENT_ID)!=null)
ack.setSegmentId(Long.valueOf(params.get(PARAM_NOTIFICATION_SEGMENT_ID)));
ack.setEnvironment(params.get(PARAM_APPLICATION_ENVIRONMENT_TYPE));
ack.setCreated(formatDate(date));
ack.setAckDate(formatDate(date));
//...get the JSON body from the object using Google JSON library
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
jsonBody = "{\"notificationReceipt\":" + gson.toJson(ack) + "}";
if(!ToolBox.network_haveNetworkConnection(context)){
Log.v(MCMNotificationModule.TAG, "Sending ACK aborted. No network available. Caching for later delivery.");
//Saves the failed delivered ack to disk.
cacheAck(context,jsonBody);
}else{
Log.v(MCMNotificationModule.TAG, "Sending ACK body: '" + jsonBody + "' to " + url);
try {
String appCode = params.get(PARAM_APPLICATION_CODE);
String appSecretKey = params.get(PARAM_APPLICATION_SECRETKEY);
MalcomHttpOperations.sendPostToMalcom(endpoint, MCMNotificationModule.notification_ack, jsonBody, appCode, appSecretKey);
} catch(Exception e) {
Log.e(MCMNotificationModule.TAG, "Error sending ACK to Malcom service url '"+url.toString()+"': "+e.getMessage(),e);
//Saves the failed delivered ack to disk.
cacheAck(context,jsonBody);
throw e;
}
}
//launches the service to send pending deliveries.
Intent ackSvcIntent = new Intent(context, PendingAcksDeliveryService.class);
context.startService(ackSvcIntent);
}
public static final String DEFAULT_DATE_HOUR_FORMAT = "yyyy-MM-dd'T'HH:mm";
private static String formatDate(Date date){
SimpleDateFormat f = new SimpleDateFormat(DEFAULT_DATE_HOUR_FORMAT);
f.setTimeZone(TimeZone.getTimeZone("UTC"));
return (f.format(new Date()));
}
/*
* Saves the ACK to disk for later delivery.
*
* @param context
* @param ackData
*/
private static synchronized void cacheAck(Context context, String ackData){
//Save the beacon for later send
try {
String name = MCMNotificationModule.CACHED_ACK_FILE_PREFIX +DigestUtils.md5Hex(ackData.getBytes());
ToolBox.storage_storeDataInInternalStorage(context, name, ackData.getBytes());
} catch (Exception e) {
Log.e(MCMNotificationModule.TAG,"Error saving the beacon for later delivery ("+e.getMessage()+")",e);
}
}
}