package com.buddy.sdk;
import android.content.Context;
import android.location.Location;
import android.os.Looper;
import android.util.Log;
import com.buddy.sdk.models.LocationRange;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.AsyncHttpResponseHandler;
import com.loopj.android.http.JsonHttpResponseHandler;
import com.loopj.android.http.RequestHandle;
import com.loopj.android.http.RequestParams;
import com.loopj.android.http.ResponseHandlerInterface;
import com.loopj.android.http.SyncHttpClient;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
class BuddyServiceClientImpl implements BuddyServiceClient {
BuddyClientImpl _parent;
private class HttpMethodBase extends HttpEntityEnclosingRequestBase {
private String methodName;
public HttpMethodBase(final URI uri, final String methodName) {
super();
setURI(uri);
this.methodName = methodName;
}
@Override
public String getMethod() {
return methodName;
}
}
private class AsyncHttpClientWithPatchAndDelete extends AsyncHttpClient {
public RequestHandle patch(Context ctx, String url, Header[] headers, HttpEntity entity, String contentType, ResponseHandlerInterface responseHandler) {
HttpMethodBase patch = new HttpMethodBase(URI.create(url).normalize(), PATCH);
if (entity != null) {
patch.setEntity(entity);
}
if (headers != null) patch.setHeaders(headers);
return sendRequest((DefaultHttpClient) getHttpClient(), getHttpContext(), patch, contentType, responseHandler, ctx);
}
public RequestHandle delete(Context ctx, String url, Header[] headers, HttpEntity entity, String contentType, ResponseHandlerInterface responseHandler) {
HttpMethodBase patch = new HttpMethodBase(URI.create(url).normalize(), DELETE);
if (entity != null) {
patch.setEntity(entity);
}
if (headers != null) patch.setHeaders(headers);
return sendRequest((DefaultHttpClient) getHttpClient(), getHttpContext(), patch, contentType, responseHandler, ctx);
}
}
private class SyncHttpClientWithPatchAndDelete extends SyncHttpClient {
public RequestHandle patch(Context ctx, String url, Header[] headers, HttpEntity entity, String contentType, ResponseHandlerInterface responseHandler) {
HttpMethodBase patch = new HttpMethodBase(URI.create(url).normalize(), PATCH);
if (entity != null) {
patch.setEntity(entity);
}
if (headers != null) patch.setHeaders(headers);
return sendRequest((DefaultHttpClient) getHttpClient(), getHttpContext(), patch, contentType, responseHandler, ctx);
}
public RequestHandle delete(Context ctx, String url, Header[] headers, HttpEntity entity, String contentType, ResponseHandlerInterface responseHandler) {
HttpMethodBase patch = new HttpMethodBase(URI.create(url).normalize(), DELETE);
if (entity != null) {
patch.setEntity(entity);
}
if (headers != null) patch.setHeaders(headers);
return sendRequest((DefaultHttpClient) getHttpClient(), getHttpContext(), patch, contentType, responseHandler, ctx);
}
}
AsyncHttpClient client;
static Map<String, Method> clientMethods = new HashMap<String, Method>();
private boolean syncMode;
public BuddyServiceClientImpl(BuddyClientImpl parent) {
_parent = parent;
setSynchronousMode(false);
}
@Override
public void setSynchronousMode(boolean value) {
if (value != syncMode) {
client = null;
syncMode = value;
}
}
@Override
public boolean getSynchronousMode() {
return syncMode;
}
private AsyncHttpClient getHttpClient() {
boolean isSyncMode = Looper.myLooper() == null || syncMode;
if (client == null || (client instanceof SyncHttpClient) != isSyncMode) {
if (isSyncMode) {
client = new SyncHttpClientWithPatchAndDelete();
} else {
client = new AsyncHttpClientWithPatchAndDelete();
}
}
return client;
}
public static String toHexString(byte[] ba) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < ba.length; i++)
str.append(String.format("%02x", ba[i]));
return str.toString();
}
@Override
public String signString(String stringToSign, String secret) {
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
return toHexString(sha256_HMAC.doFinal(stringToSign.getBytes()));
} catch (NoSuchAlgorithmException e) {
return null;
} catch (java.security.InvalidKeyException keyE) {
return null;
}
}
private String signRequest(String verb, String Path, String AppId, String Secret) {
String fullPath = Path;
if (!Path.startsWith("/")) {
fullPath = String.format("/%s", Path);
}
String stringToSign = String.format("%s\n%s\n%s", verb.toUpperCase(Locale.US), AppId, fullPath);
return signString(stringToSign, Secret);
}
private static <T> BuddyResult<T> parseBuddyResponse(Class<T> type, int statusCode, String response) {
Gson gson =
new GsonBuilder()
.registerTypeAdapter(Date.class, new BuddyDateDeserializer())
.registerTypeAdapter(JsonEnvelope.class, new JsonEnvelopeDeserializer(type))
.create();
JsonEnvelope<T> result = gson.fromJson(response, JsonEnvelope.class);
result.status = statusCode;
return new BuddyResult<T>(result);
}
static Gson makeRequestSerializer() {
return new GsonBuilder()
.registerTypeAdapter(Location.class, new BuddyLocationSerializer())
.registerTypeAdapter(LocationRange.class, new BuddyLocationRangeSerializer())
.registerTypeAdapter(DateRange.class, new DateRangeSerializer())
.create();
}
private static boolean isFile(Object obj) {
return obj instanceof BuddyFile;
}
private Method getClientMethod(String verb) {
Method m = clientMethods.get(verb.toUpperCase(Locale.getDefault()));
if (m != null) {
return m;
}
try {
m = client.getClass().getMethod(verb.toLowerCase(Locale.getDefault()),
Context.class,
String.class,
Header[].class,
HttpEntity.class,
String.class,
ResponseHandlerInterface.class
);
} catch (NoSuchMethodException nsmEx) {
m = null;
}
clientMethods.put(verb.toUpperCase(Locale.getDefault()), m);
return m;
}
private final static String DefaultContentType = "application/json";
private void logResult(JSONObject result) {
String json = result.toString();
Log.d("BuddySdk", json);
}
private Object convertParameter(Object val) {
if (val instanceof DateRange) {
return DateRangeSerializer.serializeCore((DateRange) val);
} else if (val instanceof LocationRange) {
return BuddyLocationRangeSerializer.serializeCore((LocationRange) val);
}
return val;
}
private <T> BuddyFuture<BuddyResult<T>> makeRequestCore(String verb, String path, final String accessToken, final Map<? extends String, ? extends Object> callParams, final BuddyCallback<T> callback, final Class<T> clazz) {
final Map<? extends String,? extends Object> parameters = callParams == null ? new HashMap<String, Object>() : callParams;
List<Header> headerList = new ArrayList<Header>();
String root = _parent.getServiceRoot();
if (root.endsWith("/")) {
root = root.substring(0, root.length() - 1);
}
if (path.startsWith("/")) {
path = path.substring(1);
}
final String url = String.format("%s/%s", root, path);
final BuddyFuture<BuddyResult<T>> promise = new BuddyFuture<BuddyResult<T>>();
Class rClass = clazz;
if (rClass == null && callback != null) {
rClass = callback.getResultClass();
}
final Class resultClass = rClass;
final JsonHttpResponseHandler jsonHandler = new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
// here we need to deserialize the full result object,
// which could be arbitrarily complex, so we take the hit of converting
// back to a string, then running the full envelope
// through the parser.
logResult(response);
String json = response.toString();
BuddyResult<T> result = BuddyServiceClientImpl.<T>parseBuddyResponse(resultClass, statusCode, json);
if (callback != null) callback.completed(result);
promise.setValue(result);
}
@Override
public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
JsonEnvelope<T> env = null;
if (errorResponse != null) {
env = new JsonEnvelope<T>(errorResponse, null);
logResult(errorResponse);
} else {
env = new JsonEnvelope<T>();
env.error = "NoInternetConnection";
env.message = "No internet connection is available.";
}
BuddyResult<T> result = new BuddyResult<T>(env);
if (callback != null) callback.completed(result);
promise.setValue(result);
_parent.handleError(result);
}
@Override
public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
// something bad happened.
//
JsonEnvelope<T> env = null;
env = new JsonEnvelope<T>();
env.error = "UnexpectedServiceError";
env.message = responseString;
BuddyResult<T> result = new BuddyResult<T>(env);
if (callback != null) callback.completed(result);
promise.setValue(result);
}
};
final RequestParams requestParams = new RequestParams();
boolean isFile = resultClass != null && BuddyFile.class.isAssignableFrom(resultClass);
headerList.add(new BasicHeader("Accept", DefaultContentType));
if (isFile && verb.toUpperCase(Locale.getDefault()).equals(GET)) {
if ((accessToken != null) && (parameters == null || !parameters.containsKey("accessToken"))) {
requestParams.put("accessToken", accessToken);
}
} else if (accessToken != null) {
headerList.add(new BasicHeader("Authorization", String.format("Buddy %s", accessToken)));
}
this._parent.setDefaultParameters((Map<String, Object>) parameters);
Header[] headers = headerList.toArray(new Header[0]);
AsyncHttpClient httpClient = getHttpClient();
if (verb.toUpperCase(Locale.getDefault()).equals(GET)) {
if (parameters != null) {
for (Map.Entry<? extends String, ? extends Object> cursor : parameters.entrySet()) {
requestParams.put(cursor.getKey(), convertParameter(cursor.getValue()));
}
}
ResponseHandlerInterface handler = jsonHandler;
Log.d("BuddySdk", String.format("%s %s", verb, url));
if (isFile) {
handler = new AsyncHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
InputStream result = new ByteArrayInputStream(responseBody);
String contentTypeHeader = "application/octet-stream";
for (Header h : headers) {
if (h.getName().toLowerCase(Locale.getDefault()).equals("content-type")) {
contentTypeHeader = h.getValue();
}
}
BuddyFile file = new BuddyFile(result, contentTypeHeader);
JsonEnvelope env = new JsonEnvelope<T>();
env.result = file;
BuddyResult<T> r = new BuddyResult<T>(env);
if (callback != null) callback.completed(r);
promise.setValue(r);
}
@Override
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
jsonHandler.setUseSynchronousMode(this.getUseSynchronousMode());
// send failures over to the json handler.
jsonHandler.onFailure(statusCode, headers, responseBody, error);
}
};
}
httpClient.get(null, url, headers, requestParams, handler);
} else {
// loop through and pull out any files.
//
HttpEntity entity = null;
String contentType = DefaultContentType;
if (parameters != null) {
Map<String, Object> files = new HashMap<String, Object>();
Map<String, Object> nonFiles = new HashMap<String, Object>();
for (Map.Entry<? extends String, ? extends Object> cursor : parameters.entrySet()) {
Object obj = cursor.getValue();
if (isFile(obj)) {
files.put(cursor.getKey(), obj);
} else {
nonFiles.put(cursor.getKey(), obj);
}
}
String bodyJson = BuddyServiceClientImpl.makeRequestSerializer().toJson(nonFiles);
Log.d("BuddySdk", String.format("%s %s \r\n -> %s", verb, url, bodyJson));
if (files.size() > 0) {
InputStream stream = new ByteArrayInputStream(bodyJson.getBytes());
// here we do N entities:
// 1. is called body with all the non file items
// 2. each file
requestParams.put("body", stream, "body", "application/json");
// now put the files
for (Map.Entry<String, Object> cursor : files.entrySet()) {
BuddyFile file = (BuddyFile) cursor.getValue();
if (file.getStream() != null) {
requestParams.put(cursor.getKey(), file.getStream(), cursor.getKey(), file.getContentType());
} else if (file.getFile() != null) {
try {
requestParams.put(cursor.getKey(), file.getFile(), file.getContentType());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
try {
entity = requestParams.getEntity(new ResponseHandlerInterface() {
@Override
public void sendResponseMessage(HttpResponse response) throws IOException {
}
private boolean ups;
@Override
public boolean getUsePoolThread() {
return ups;
}
@Override
public void setUsePoolThread(boolean usePoolThread) {
boolean ups = usePoolThread;
}
private Object tag;
@Override
public Object getTag() {
return tag;
}
@Override
public void setTag(Object TAG) {
tag = TAG;
}
@Override
public void sendStartMessage() {
}
@Override
public void sendFinishMessage() {
}
@Override
public void sendProgressMessage(long bytesWritten, long bytesTotal) {
Log.d("BuddySdk", String.format("%d/%d", bytesWritten, bytesTotal));
}
@Override
public void onPreProcessResponse(ResponseHandlerInterface instance, HttpResponse response) {
}
@Override
public void onPostProcessResponse(ResponseHandlerInterface instance, HttpResponse response) {
}
@Override
public void sendCancelMessage() {
}
@Override
public void sendSuccessMessage(int statusCode, Header[] headers, byte[] responseBody) {
}
@Override
public void sendFailureMessage(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
}
@Override
public void sendRetryMessage(int retryNo) {
}
@Override
public URI getRequestURI() {
return null;
}
@Override
public Header[] getRequestHeaders() {
return new Header[0];
}
@Override
public void setRequestURI(URI requestURI) {
}
@Override
public void setRequestHeaders(Header[] requestHeaders) {
}
@Override
public void setUseSynchronousMode(boolean useSynchronousMode) {
}
@Override
public boolean getUseSynchronousMode() {
return syncMode;
}
});
} catch (IOException e) {
e.printStackTrace();
}
contentType = null;
} else {
try {
entity = new StringEntity(bodyJson);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
Method methodToInvoke = getClientMethod(verb);
if (methodToInvoke == null) {
jsonHandler.onFailure(0, new Header[0], "", new MethodNotSupportedException("Verb " + verb + " not supported."));
} else {
try {
try {
methodToInvoke.invoke(httpClient, null, url, headers, entity, contentType, jsonHandler);
} catch (InvocationTargetException e) {
JsonEnvelope<T> env = new JsonEnvelope<T>();
env.error = "UnexpectedSdkError";
env.status = 0;
env.errorCode = -1;
env.message = e.getTargetException().toString();
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.getTargetException().printStackTrace(pw);
Log.e("BuddySdk", sw.toString());
BuddyResult<T> newResult = new BuddyResult<T>(env);
if (callback != null) callback.completed(newResult);
promise.setValue(newResult);
}
} catch (IllegalAccessException e) {
}
}
}
return promise;
}
public <T> Future<BuddyResult<T>> makeRequest(final String verb, final String path, final Map<? extends String, ? extends Object> parameters, final BuddyCallback<T> callback, final Class<T> clazz) {
boolean autoRegister = true;
if (parameters != null) {
// should we disable auto register?
//
if (parameters.containsKey(BuddyClientImpl.NoRegisterDevice)) {
parameters.remove(BuddyClientImpl.NoRegisterDevice);
autoRegister = false;
}
}
final BuddyFuture<BuddyResult<T>> promise = new BuddyFuture<BuddyResult<T>>();
// get the access token.
//
_parent.getAccessToken(autoRegister, new AccessTokenCallback() {
@Override
public void completed(BuddyResult<Boolean> error, final String accessToken) {
if (error != null) {
// propagate the error
BuddyResult<T> newResult = error.convert((T) null);
if (callback != null) callback.completed(newResult);
promise.setValue(newResult);
} else {
String fullAccessToken = accessToken;
if (fullAccessToken != null && _parent.getSharedSecret() != null) {
String requestSig = signRequest(verb, path, _parent.getAppId(), _parent.getSharedSecret());
if (requestSig != null) {
fullAccessToken = String.format("%s %s", fullAccessToken, requestSig);
}
}
final BuddyFuture<BuddyResult<T>> innerPromise = BuddyServiceClientImpl.this.<T>makeRequestCore(verb, path, fullAccessToken, parameters, callback, clazz);
innerPromise.continueWith(new BuddyFutureCallback() {
@Override
public void completed(BuddyFuture future) {
try {
promise.setValue(innerPromise.get());
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
}
});
}
}
}
);
return promise;
}
}