package com.kth.baasio.entity.push;
import com.google.android.gcm.GCMRegistrar;
import com.kth.baasio.Baas;
import com.kth.baasio.BuildConfig;
import com.kth.baasio.callback.BaasioAsyncTask;
import com.kth.baasio.callback.BaasioCallback;
import com.kth.baasio.callback.BaasioDeviceAsyncTask;
import com.kth.baasio.callback.BaasioDeviceCallback;
import com.kth.baasio.callback.BaasioResponseCallback;
import com.kth.baasio.exception.BaasioError;
import com.kth.baasio.exception.BaasioException;
import com.kth.baasio.preferences.BaasioPreferences;
import com.kth.baasio.response.BaasioResponse;
import com.kth.baasio.utils.ObjectUtils;
import com.kth.common.utils.LogUtils;
import org.springframework.http.HttpMethod;
import android.content.Context;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.regex.Pattern;
public class BaasioPush {
enum REG_STATE {
CREATE_DEVICE, UPDATE_DEVICE_BY_REGID, UPDATE_DEVICE_BY_UUID
};
private static final String TAG = LogUtils.makeLogTag(BaasioPush.class);
private static final int MAX_ATTEMPTS = 5;
private static final int BACKOFF_MILLIS = 4000;
private static final int BACKOFF_MILLIS_DEFAULT = 3000;
private static final Random sRandom = new Random();
private static final String TAG_REGEXP = "^[a-zA-Z0-9-_]*$";
static List<String> getTagList(String tagString) {
List<String> result = new ArrayList<String>();
String[] tags = tagString.split("\\,");
for (String tag : tags) {
tag = tag.toLowerCase(Locale.getDefault()).trim();
if (!ObjectUtils.isEmpty(tag)) {
result.add(tag);
}
}
return result;
}
public BaasioPush() {
}
private static boolean needRegister(Context context, String signedInUsername, String regId,
String oldRegId, String newTags) {
boolean bResult = true;
if (GCMRegistrar.isRegisteredOnServer(context)) {
String registeredUsername = BaasioPreferences.getRegisteredUserName(context);
if (registeredUsername.equals(signedInUsername)) {
String curTags = BaasioPreferences.getRegisteredTags(context);
if (curTags.equals(newTags)) {
if (oldRegId.equals(regId)) {
LogUtils.LOGV(TAG, "BaasioPush.register() called but already registered.");
bResult = false;
} else {
LogUtils.LOGV(
TAG,
"BaasioPush.register() called. Already registered on the GCM server. But, need to register again because regId changed.");
}
} else {
LogUtils.LOGV(
TAG,
"BaasioPush.register() called. Already registered on the GCM server. But, need to register again because tags changed.");
}
} else {
LogUtils.LOGV(
TAG,
"BaasioPush.register() called. Already registered on the GCM server. But, need to register again because username changed.");
}
}
return bResult;
}
public static BaasioDevice register(Context context, String regId) throws BaasioException {
if (!Baas.io().isGcmEnabled()) {
throw new BaasioException(BaasioError.ERROR_GCM_DISABLED);
}
GCMRegistrar.checkDevice(context);
if (BuildConfig.DEBUG) {
GCMRegistrar.checkManifest(context);
}
String signedInUsername = "";
if (!ObjectUtils.isEmpty(Baas.io().getSignedInUser())) {
signedInUsername = Baas.io().getSignedInUser().getUsername();
}
String newTags = BaasioPreferences.getNeedRegisteredTags(context);
String oldRegId = BaasioPreferences.getRegisteredRegId(context);
if (!needRegister(context, signedInUsername, regId, oldRegId, newTags)) {
throw new BaasioException(BaasioError.ERROR_GCM_ALREADY_REGISTERED);
}
BaasioDevice device = new BaasioDevice();
if (ObjectUtils.isEmpty(device.getType())) {
throw new IllegalArgumentException(BaasioError.ERROR_MISSING_TYPE);
}
if (ObjectUtils.isEmpty(device.getPlatform())) {
device.setPlatform("G");
}
if (ObjectUtils.isEmpty(regId)) {
throw new IllegalArgumentException(BaasioError.ERROR_GCM_MISSING_REGID);
}
device.setToken(regId);
String tagString = BaasioPreferences.getNeedRegisteredTags(context);
List<String> tags = getTagList(tagString);
device.setTags(tags);
long backoff = BACKOFF_MILLIS + sRandom.nextInt(1000);
REG_STATE eREG_STATE = REG_STATE.CREATE_DEVICE;
if (!ObjectUtils.isEmpty(oldRegId)) {
if (!oldRegId.equals(regId)) {
LogUtils.LOGV(TAG, "RegId changed!!!!");
LogUtils.LOGV(TAG, "New RegId: " + regId);
LogUtils.LOGV(TAG, "Old RegId: " + oldRegId);
} else {
LogUtils.LOGV(TAG, "New and old regId are same!!!");
LogUtils.LOGV(TAG, "RegId: " + oldRegId);
}
eREG_STATE = REG_STATE.UPDATE_DEVICE_BY_REGID;
}
for (int i = 1; i <= MAX_ATTEMPTS; i++) {
LogUtils.LOGV(TAG, "#" + i + " Attempt..");
REG_STATE curState = eREG_STATE;
try {
BaasioResponse response = null;
switch (eREG_STATE) {
case CREATE_DEVICE: {
LogUtils.LOGV(TAG, "POST /devices");
LogUtils.LOGV(TAG, "Request: " + device.toString());
response = Baas.io().apiRequest(HttpMethod.POST, null, device, "devices");
LogUtils.LOGV(TAG, "Response: " + response.toString());
break;
}
case UPDATE_DEVICE_BY_REGID: {
LogUtils.LOGV(TAG, "PUT /devices/" + oldRegId);
LogUtils.LOGV(TAG, "Request: " + device.toString());
response = Baas.io().apiRequest(HttpMethod.PUT, null, device, "devices",
oldRegId);
LogUtils.LOGV(TAG, "Response: " + response.toString());
break;
}
case UPDATE_DEVICE_BY_UUID: {
String deviceUuid = BaasioPreferences.getDeviceUuidForPush(context);
LogUtils.LOGV(TAG, "PUT /devices/" + deviceUuid);
LogUtils.LOGV(TAG, "Request: " + device.toString());
response = Baas.io().apiRequest(HttpMethod.PUT, null, device, "devices",
deviceUuid);
LogUtils.LOGV(TAG, "Response: " + response.toString());
break;
}
}
if (response != null) {
BaasioDevice entity = response.getFirstEntity().toType(BaasioDevice.class);
if (!ObjectUtils.isEmpty(entity)) {
BaasioPreferences
.setRegisteredSenderId(context, Baas.io().getGcmSenderId());
GCMRegistrar.setRegisteredOnServer(context, true);
BaasioPreferences.setRegisteredTags(context, tagString);
BaasioPreferences.setRegisteredUserName(context, signedInUsername);
String newDeviceUuid = entity.getUuid().toString();
BaasioPreferences.setDeviceUuidForPush(context, newDeviceUuid);
BaasioPreferences.setRegisteredRegId(context, regId);
return entity;
}
throw new BaasioException(BaasioError.ERROR_UNKNOWN_NORESULT_ENTITY);
}
} catch (BaasioException e) {
LogUtils.LOGV(TAG, "Failed to register on attempt " + i, e);
String statusCode = e.getStatusCode();
if (!ObjectUtils.isEmpty(statusCode)) {
if (eREG_STATE == REG_STATE.CREATE_DEVICE) {
if (statusCode.equals("400") && e.getErrorCode() == 913) {
// 이미 regId가 등록되어 있음. 하지만 태그 정보가 업데이트되어 있는지 알 수 없으니
// Retry
LogUtils.LOGV(
TAG,
"Already registered on the GCM server. But, need to register again because other data could be changed.");
oldRegId = regId;
i--;
eREG_STATE = REG_STATE.UPDATE_DEVICE_BY_REGID;
}
} else if (eREG_STATE == REG_STATE.UPDATE_DEVICE_BY_REGID) {
if (statusCode.equals("400")
&& (e.getErrorCode() == 620 || e.getErrorCode() == 103)) {
String deviceUuid = BaasioPreferences.getDeviceUuidForPush(context);
if (!ObjectUtils.isEmpty(deviceUuid)) {
eREG_STATE = REG_STATE.UPDATE_DEVICE_BY_UUID;
i--;
} else {
LogUtils.LOGE(TAG,
"Failed to register. This should not happen. Give up registering.(400)");
break;
}
} else if (statusCode.equals("404")) {
LogUtils.LOGE(TAG,
"Failed to register. This should not happen. Give up registering.(404)");
break;
}
} else if (eREG_STATE == REG_STATE.UPDATE_DEVICE_BY_UUID) {
if (statusCode.equals("404")) {
eREG_STATE = REG_STATE.CREATE_DEVICE;
i--;
} else if (statusCode.equals("400")
&& (e.getErrorCode() == 620 || e.getErrorCode() == 103)) {
eREG_STATE = REG_STATE.CREATE_DEVICE;
i--;
}
}
}
// Here we are simplifying and retrying on any error; in a real
// application, it should retry only on unrecoverable errors
// (like HTTP error code 503).
if (i >= MAX_ATTEMPTS) {
LogUtils.LOGE(TAG,
"Failed all attempts to register. Next time application launched, will try again.");
break;
}
if (curState == eREG_STATE) {
try {
LogUtils.LOGV(TAG, "Sleeping for " + backoff + " ms before retry");
Thread.sleep(backoff);
} catch (InterruptedException e1) {
// Activity finished before we complete - exit.
LogUtils.LOGD(TAG, "Thread interrupted: abort remaining retries!");
Thread.currentThread().interrupt();
return null;
}
// increase backoff exponentially
backoff *= 2;
} else {
try {
LogUtils.LOGV(TAG, "Sleeping for " + BACKOFF_MILLIS_DEFAULT
+ " ms before retry");
Thread.sleep(BACKOFF_MILLIS_DEFAULT);
} catch (InterruptedException e1) {
// Activity finished before we complete - exit.
LogUtils.LOGD(TAG, "Thread interrupted: abort remaining retries!");
Thread.currentThread().interrupt();
return null;
}
}
}
}
return null;
}
private static boolean compareArrays(String[] arr1, String[] arr2) {
if (ObjectUtils.isEmpty(arr1) || ObjectUtils.isEmpty(arr2)) {
return false;
}
Arrays.sort(arr1);
Arrays.sort(arr2);
return Arrays.equals(arr1, arr2);
}
private static boolean needRegisterSenderId(Context context, String regId) {
if (TextUtils.isEmpty(regId)) {
LogUtils.LOGD(TAG, "RegId is empty. Need register Sender ID.");
return true;
}
String[] oldSenderIds = BaasioPreferences.getRegisteredSenderId(context);
String[] newSenderIds = Baas.io().getGcmSenderId();
if (!compareArrays(oldSenderIds, newSenderIds)) {
LogUtils.LOGD(TAG, "SenderID is different. Need register Sender ID.");
return true;
}
return false;
}
/**
* Register device. If server is not available(HTTP status 5xx), it will
* retry 5 times. Executes asynchronously in background and the callbacks
* are called in the UI thread.
*
* @param context Context
* @param callback GCM registration result callback
* @return registration task
*/
public static BaasioDeviceAsyncTask registerInBackground(final Context context,
final BaasioDeviceCallback callback) {
if (!Baas.io().isGcmEnabled()) {
if (callback != null) {
callback.onException(new BaasioException(BaasioError.ERROR_GCM_DISABLED));
}
return null;
}
final String regId = GCMRegistrar.getRegistrationId(context);
if (needRegisterSenderId(context, regId)) {
GCMRegistrar.register(context, Baas.io().getGcmSenderId());
} else {
BaasioDeviceAsyncTask task = new BaasioDeviceAsyncTask(callback) {
@Override
public BaasioDevice doTask() throws BaasioException {
BaasioDevice device = register(context, regId);
if (ObjectUtils.isEmpty(device)) {
GCMRegistrar.unregister(context);
}
return device;
}
};
task.execute();
return task;
}
return null;
}
/**
* Register device with tags. If server is not available(HTTP status 5xx),
* it will retry 5 times. Executes asynchronously in background and the
* callbacks are called in the UI thread.
*
* @param context Context
* @param tags Tags. The max length of each tag is 36.
* @param callback GCM registration result callback
* @return registration task
*/
public static BaasioDeviceAsyncTask registerWithTagsInBackground(final Context context,
String tags, final BaasioDeviceCallback callback) {
if (!Baas.io().isGcmEnabled()) {
if (callback != null) {
callback.onException(new BaasioException(BaasioError.ERROR_GCM_DISABLED));
}
return null;
}
List<String> tagList = getTagList(tags);
for (String tag : tagList) {
if (tag.length() > 36) {
throw new IllegalArgumentException(BaasioError.ERROR_GCM_TAG_LENGTH_EXCEED);
}
Pattern pattern = Pattern.compile(TAG_REGEXP);
if (!pattern.matcher(tag).matches()) {
throw new IllegalArgumentException(BaasioError.ERROR_GCM_TAG_PATTERN_MISS_MATCHED);
}
}
BaasioPreferences.setNeedRegisteredTags(context, tags);
return registerInBackground(context, callback);
}
/**
* Unregister device. If request failed, it will not retry.
*
* @param context Context
*/
public static BaasioResponse unregister(Context context) throws BaasioException {
if (!Baas.io().isGcmEnabled()) {
throw new BaasioException(BaasioError.ERROR_GCM_DISABLED);
}
if (!GCMRegistrar.isRegisteredOnServer(context)) {
throw new BaasioException(BaasioError.ERROR_GCM_ALREADY_UNREGISTERED);
}
String deviceUuid = BaasioPreferences.getDeviceUuidForPush(context);
String oldRegId = BaasioPreferences.getRegisteredRegId(context);
BaasioPreferences.setDeviceUuidForPush(context, "");
BaasioPreferences.setNeedRegisteredTags(context, "");
BaasioPreferences.setRegisteredUserName(context, "");
BaasioPreferences.setRegisteredTags(context, "");
BaasioPreferences.setRegisteredRegId(context, "");
GCMRegistrar.setRegisteredOnServer(context, false);
if (!ObjectUtils.isEmpty(deviceUuid)) {
LogUtils.LOGV(TAG, "DELETE /devices/" + deviceUuid);
BaasioResponse response = Baas.io().apiRequest(HttpMethod.DELETE, null, null,
"devices", deviceUuid);
if (response != null) {
LogUtils.LOGV(TAG, "Response: " + response.toString());
return response;
} else {
throw new BaasioException(BaasioError.ERROR_UNKNOWN_NO_RESPONSE_DATA);
}
} else {
if (!ObjectUtils.isEmpty(oldRegId)) {
LogUtils.LOGV(TAG, "DELETE /devices/" + oldRegId);
BaasioResponse response = Baas.io().apiRequest(HttpMethod.DELETE, null, null,
"devices", oldRegId);
if (response != null) {
LogUtils.LOGV(TAG, "Response: " + response.toString());
return response;
} else {
throw new BaasioException(BaasioError.ERROR_UNKNOWN_NO_RESPONSE_DATA);
}
}
}
throw new BaasioException(BaasioError.ERROR_GCM_MISSING_REGID);
}
/**
* Unregister device. However, server is not available(HTTP status 5xx), it
* will not retry. Executes asynchronously in background and the callbacks
* are called in the UI thread.
*
* @param context Context
* @param callback GCM unregistration result callback
*/
public static void unregisterInBackground(final Context context,
final BaasioResponseCallback callback) {
if (!Baas.io().isGcmEnabled()) {
if (callback != null) {
callback.onException(new BaasioException(BaasioError.ERROR_GCM_DISABLED));
}
return;
}
(new BaasioAsyncTask<BaasioResponse>(callback) {
@Override
public BaasioResponse doTask() throws BaasioException {
return unregister(context);
}
}).execute();
}
/**
* Send a push message.
*
* @param message push message
*/
public static BaasioMessage sendPush(BaasioMessage message) throws BaasioException {
if (ObjectUtils.isEmpty(message)) {
throw new IllegalArgumentException(BaasioError.ERROR_MISSING_MESSAGE);
}
if (ObjectUtils.isEmpty(message.getTarget())) {
throw new IllegalArgumentException(BaasioError.ERROR_MISSING_TARGET);
}
BaasioResponse response = Baas.io().apiRequest(HttpMethod.POST, null, message, "pushes");
if (response != null) {
BaasioMessage entity = response.getFirstEntity().toType(BaasioMessage.class);
if (!ObjectUtils.isEmpty(entity)) {
return entity;
}
throw new BaasioException(BaasioError.ERROR_UNKNOWN_NORESULT_ENTITY);
}
throw new BaasioException(BaasioError.ERROR_UNKNOWN_NO_RESPONSE_DATA);
}
/**
* Send a push message. Executes asynchronously in background and the
* callbacks are called in the UI thread.
*
* @param message Push message
* @param callback Result callback
*/
public static void sendPushInBackground(final BaasioMessage message,
final BaasioCallback<BaasioMessage> callback) {
(new BaasioAsyncTask<BaasioMessage>(callback) {
@Override
public BaasioMessage doTask() throws BaasioException {
return sendPush(message);
}
}).execute();
}
}