package com.rubenvandeven.emotionhero; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.PointF; import android.graphics.Rect; import android.util.Log; import com.google.gson.Gson; 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.SyncHttpClient; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Locale; import java.util.TimeZone; import cz.msebera.android.httpclient.Header; import cz.msebera.android.httpclient.HeaderElement; import cz.msebera.android.httpclient.ParseException; import cz.msebera.android.httpclient.entity.StringEntity; /** * Created by ruben on 01/09/16. * * A way to interact with api.emotionhero.com */ public class ApiRestClient { private static final String BASE_URL = "https://api.emotionhero.com"; /** * For some reason validation of SSL certificate needs to be disabled (using true). * Otherwise async-http-client tries to validate other virtualhost (rubenvandeven.com) with api.emotionhero.com * which obviously is not the same domain. For some reason the connection is handles properly * by apache (so it detects the right VirtualHost...) ... odd :-( */ private static AsyncHttpClient client = new AsyncHttpClient(true, 80, 443); private String jwt; private Player player; public ApiRestClient(Player player) { this.player = player; } public void registerIfNeeded() { if(player.getJWT() == null) { requestWithJWT(null, null, null, null, null); } } /** * For now call register endpoint. Given JWT should have a long enough lifetime (for now) * @todo However, custom token endpoint should be used eventually * @param method * @param url * @param postBody * @param getParams * @param responseHandler */ public void requestWithJWT(final String method, final String url, final StringEntity postBody, final RequestParams getParams, final AsyncHttpResponseHandler responseHandler) { // sync call, so we can return a value! Log.v("API", "register"); client.post(BASE_URL + "/api/register", null, new JsonHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { // If the response is JSONObject instead of expected JSONArray try { String token = response.getString("jwt"); String remoteId = response.getString("id"); Log.d("API", "Token " + token); Log.d("API", "RemoteId" + remoteId); getPlayer().setJWT(token); getPlayer().setRemoteId(remoteId); if(method != null) { ApiRestClient.this.request( method, url, postBody, getParams, responseHandler); } } catch (JSONException e) { // responseHandler.sendFailureMessage(500, null, null); // failure!! // retrying later probably.... Log.e("API", "Failed request"); e.printStackTrace(); } } @Override public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) { handleOnFailure(statusCode, headers, responseString, throwable); } @Override public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject response) { onFailure(statusCode, headers, response == null ? null:response.toString(), throwable); } }); } public Player getPlayer() { return player; } class TokenHeader implements Header { /** * Get the name of the Header. * * @return the name of the Header, never {@code null} */ @Override public String getName() { return "X-Access-Token"; } /** * Get the value of the Header. * * @return the value of the Header, may be {@code null} */ @Override public String getValue() { return "Bearer " + player.getJWT(); } /** * Parses the value. * * @return an array of {@link HeaderElement} entries, may be empty, but is never {@code null} * @throws ParseException */ @Override public HeaderElement[] getElements() throws ParseException { return new HeaderElement[]{}; } } public void request(String method, String url, StringEntity postBody, RequestParams requestParams, AsyncHttpResponseHandler responseHandler) { Header[] headers = new Header[]{new TokenHeader()}; Log.d("API", "Do request to: " + url); if(method == "post") { if(postBody != null) { client.post(player.getContext(), url, headers, postBody,"application/json", responseHandler); } else { // let content type be determinded by sender client.post(player.getContext(), url, headers, requestParams, null, responseHandler); } } else { client.get(player.getContext(), url, headers, requestParams, responseHandler); } } public void get(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) { String jwt = player.getJWT(); if(jwt != null) { request("get", getAbsoluteUrl(url), null, params, responseHandler); } else { requestWithJWT("get", getAbsoluteUrl(url), null, params, responseHandler); } } /** * Post string body with JWT (stringentity can be json-data) * @param url * @param postBody * @param responseHandler */ public void post(String url, StringEntity postBody, AsyncHttpResponseHandler responseHandler) { String jwt = player.getJWT(); if(jwt != null) { request("post", getAbsoluteUrl(url), postBody, null, responseHandler); } else { requestWithJWT("post", getAbsoluteUrl(url), postBody, null, responseHandler); } } /** * Post params (ie. multipart) with JWT (stringentity can be json-data) * @param url * @param params * @param responseHandler */ public void post(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) { String jwt = player.getJWT(); if(jwt != null) { request("post", getAbsoluteUrl(url), null, params, responseHandler); } else { requestWithJWT("post", getAbsoluteUrl(url), null, params, responseHandler); } } private static String getAbsoluteUrl(String relativeUrl) { return BASE_URL + relativeUrl; } public void syncGame(final Game game) { if(game.remoteId != null) return; RequestParams params = new RequestParams(); JSONObject j = new JSONObject(); try { j.put("lvl_id", game.scenario.id); j.put("score", game.score); j.put("bonus", game.bonus); TimeZone tz = TimeZone.getTimeZone("UTC"); DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); df.setTimeZone(tz); j.put("time", df.format(game.time)); j.put("lost_face_time", game.lostFaceTime); JSONArray jHits = new JSONArray(); j.put("hits", jHits); for(Hit hit: game.hits.values()) { JSONObject jHit = new JSONObject(); jHit.put("id", hit.id); jHit.put("target_index", hit.target.index); jHit.put("score", hit.score); jHit.put("bonus", hit.bonus); jHit.put("glasses", hit.glasses); jHit.put("ethnicity", hit.ethnicity); jHit.put("age", hit.age); jHit.put("gender", hit.gender); JSONObject jExpressions = new JSONObject(); jHit.put("expressions", jExpressions); JSONObject jEmotions = new JSONObject(); jHit.put("emotions", jEmotions); JSONObject jPoints = new JSONObject(); jHit.put("points", jPoints); for(Expression e: Expression.values()) { jExpressions.put(e.getDbName(), hit.expressions.get(e)); } for(Emotion e: Emotion.values()) { jEmotions.put(e.getDbName(), hit.emotions.get(e)); } int i=0; for(PointF p: hit.points) { JSONObject jPoint = new JSONObject(); jPoint.put("x", p.x); jPoint.put("y", p.y); jPoints.put(""+i, jPoint); i++; } jHits.put(jHit); } JSONArray jAchievements = new JSONArray(); j.put("achievements", jAchievements); for(Achievement achievement: game.achievements) { jAchievements.put(achievement.getId()); } } catch (JSONException e) { e.printStackTrace(); } StringEntity postBody = null; try { postBody = new StringEntity(j.toString()); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } this.post("/games", postBody, new JsonHttpResponseHandler(){ @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { Log.d("API",response.toString()); GameOpenHelper gameHelper = new GameOpenHelper(player.getContext()); // set remote ids on hits and game. try { game.remoteId = response.getString("id"); JSONObject hits = response.getJSONObject("hits"); gameHelper.saveRemoteId(game); for(Hit hit: game.hits.values()) { hit.remoteId = hits.getString(Long.toString(hit.id)); gameHelper.saveRemoteId(hit); } // Achievement checking now in-game. /*JSONArray jAchievements = response.getJSONArray("achievements"); if(jAchievements.length() > 0){ ArrayList achievements = new ArrayList(jAchievements.length()); for (int ai = 0; ai < jAchievements .length(); ai++) { achievements.add(AchievementCollection.getInstance().get(jAchievements.getInt(ai))); } game.achievements = achievements; gameHelper.saveAchievementsForGame(game); }*/ } catch (JSONException e) { Log.e("API","Invalid data: " + response.toString()); e.printStackTrace(); } } @Override public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) { handleOnFailure(statusCode, headers, responseString, throwable); } @Override public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject response) { onFailure(statusCode, headers, response == null ? null:response.toString(), throwable); } }); } /** * 1. Cut out the essential parts of the image (privacy) * 2. send to server in one batch * 3. if successful, remove from device to save storage. * @param hits */ public void sendHitImages(final Collection hits) { // 1. Cut out the essential parts GameOpenHelper gameHelper = player.getGameOpenHelper(); if(hits == null || hits.size() < 1) { return; } RequestParams params = new RequestParams(); boolean noImages = true; for(Hit hit: hits) { Bitmap img = gameHelper.getImageForHit(hit); if(img == null) { Log.e("API", "no image for hit " + hit.id); continue; } noImages = false; // Crop: http://stackoverflow.com/a/31698091 int margin = (int) ((hit.points[10].x - hit.points[5].x) * 0.05); int left = (int) (hit.points[5].x) - margin; int right = (int) (hit.points[10].x) + margin; int top = (int) (hit.points[6].y < hit.points[9].y ? hit.points[6].y : hit.points[9].y) - 2*margin; // a bit of forehead int bottom = (int) (hit.points[5].y > hit.points[10].y ? hit.points[5].y : hit.points[10].y) + margin; Rect rect = new Rect(left, top, right, bottom); // Be sure that there is at least 1px to slice. if(!(rect.left < rect.right && rect.top < rect.bottom)) { Log.e("API", "Error in point positions."+" left: " + rect.left + " right: " + rect.right + " top: " + rect.top + " bottom: " + rect.bottom ); continue; // strange bug... skip it and drop the file anyway } // Create our resulting image (150--50),(75--25) = 200x100px Bitmap croppedBmp = Bitmap.createBitmap(rect.right-rect.left, rect.bottom-rect.top, Bitmap.Config.ARGB_8888); // draw source bitmap into resulting image at given position: new Canvas(croppedBmp).drawBitmap(img, -rect.left, -rect.top, null); ByteArrayOutputStream stream = new ByteArrayOutputStream(); croppedBmp.compress(Bitmap.CompressFormat.JPEG, 90, stream); byte[] imageBytes = stream.toByteArray(); params.put(hit.remoteId + ":brows", new ByteArrayInputStream(imageBytes), hit.remoteId + "-brows.jpg", "image/jpeg"); Log.v("API", "add param: " + hit.remoteId + ":brows - length:" + imageBytes.length ); // NOSE // Crop: http://stackoverflow.com/a/31698091 // reuse margin of brows left = (int) hit.points[13].x - margin; right = (int) hit.points[15].x + margin; top = (int) hit.points[11].y - margin; bottom = (int) hit.points[14].y + margin; Rect rect2 = new Rect(left, top, right, bottom); // Be sure that there is at least 1px to slice. if(!(rect2.left < rect2.right && rect2.top < rect2.bottom)) { Log.e("API", "Error in point positions."+" left: " + rect2.left + " right: " + rect2.right + " top: " + rect2.top + " bottom: " + rect2.bottom ); continue; // strange bug... skip it and drop the file anyway } // Create our resulting image (150--50),(75--25) = 200x100px croppedBmp = Bitmap.createBitmap(rect2.right-rect2.left, rect2.bottom-rect2.top, Bitmap.Config.ARGB_8888); // draw source bitmap into resulting image at given position: new Canvas(croppedBmp).drawBitmap(img, -rect2.left, -rect2.top, null); ByteArrayOutputStream stream2 = new ByteArrayOutputStream(); croppedBmp.compress(Bitmap.CompressFormat.JPEG, 90, stream2); byte[] imageBytes2 = stream2.toByteArray(); params.put(hit.remoteId + ":nose", new ByteArrayInputStream(imageBytes2), hit.remoteId + "-nose.jpg", "image/jpeg"); Log.v("API", "add param: " + hit.remoteId + ":nose - length:" + imageBytes.length ); // RIGHT MOUTH CORNER // Crop: http://stackoverflow.com/a/31698091 // reuse margin of brows to make a square image // left = (int) hit.points[24].x - margin; // right = (int) hit.points[24].x + 3 * margin; // top = (int) hit.points[24].y - 2 * margin; // bottom = (int) hit.points[24].y + 2 * margin; // Rect rect3 = new Rect(left, top, right, bottom); //// Be sure that there is at least 1px to slice. // if(!(rect3.left < rect3.right && rect3.top < rect3.bottom)) { // Log.e("API", "Error in point positions."+" left: " + rect3.left + " right: " + rect3.right + " top: " + rect3.top + " bottom: " + rect3.bottom ); // continue; // strange bug... skip it and drop the file anyway // } // // croppedBmp = Bitmap.createBitmap(rect3.right-rect3.left, rect3.bottom-rect3.top, Bitmap.Config.ARGB_8888); //// draw source bitmap into resulting image at given position: // new Canvas(croppedBmp).drawBitmap(img, -rect3.left, -rect3.top, null); // // ByteArrayOutputStream stream3 = new ByteArrayOutputStream(); // croppedBmp.compress(Bitmap.CompressFormat.JPEG, 90, stream3); // byte[] imageBytes3 = stream3.toByteArray(); // // params.put(hit.remoteId + ":mouth_right", new ByteArrayInputStream(imageBytes3), hit.remoteId + "-mouth_right.jpg", "image/jpeg"); // Log.v("API", "add param: " + hit.remoteId + ":mouth_right - length:" + imageBytes.length ); // LEFT MOUTH CORNER (doing whole mouth :-) // Crop: http://stackoverflow.com/a/31698091 // reuse margin of brows to make a square image left = (int) hit.points[20].x - margin; right = (int) hit.points[24].x + margin; top = (int) (hit.points[21].y < hit.points[23].y ? hit.points[21].y : hit.points[23].y) - margin; bottom = (int) hit.points[26].y + margin; Rect rect4 = new Rect(left, top, right, bottom); // Be sure that there is at least 1px to slice. if(!(rect4.left < rect4.right && rect4.top < rect4.bottom)) { Log.e("API", "Error in point positions."+" left: " + rect4.left + " right: " + rect4.right + " top: " + rect4.top + " bottom: " + rect4.bottom ); continue; // strange bug... skip it and drop the file anyway } croppedBmp = Bitmap.createBitmap(rect4.right-rect4.left, rect4.bottom-rect4.top, Bitmap.Config.ARGB_8888); // draw source bitmap into resulting image at given position: new Canvas(croppedBmp).drawBitmap(img, -rect4.left, -rect4.top, null); ByteArrayOutputStream stream4 = new ByteArrayOutputStream(); croppedBmp.compress(Bitmap.CompressFormat.JPEG, 90, stream4); byte[] imageBytes4 = stream4.toByteArray(); params.put(hit.remoteId + ":mouth_left", new ByteArrayInputStream(imageBytes4), hit.remoteId + "-mouth_left.jpg", "image/jpeg"); Log.v("API", "add param: " + hit.remoteId + ":mouth_left - length:" + imageBytes.length ); } // don't post if there are no images // this happens ie. when images are all successfully send if(noImages) { return; } post("/images", params,new JsonHttpResponseHandler(){ @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { // 3. remove images :-) for(Hit hit: hits) { Log.v("API", "clear image for hit: "+ hit.id); player.getGameOpenHelper().clearImageForHit(hit); } } @Override public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) { handleOnFailure(statusCode, headers, responseString, throwable); } @Override public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject response) { onFailure(statusCode, headers, response == null ? null:response.toString(), throwable); } }); } public static void handleOnFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) { Log.e("API", "FAILURE ON REQUEST!"); Log.e("API", throwable.getMessage()); Log.e("API", "Status: "+statusCode); if(headers == null){ Log.e("API", "\tNULL"); } else { for(Header header: headers) { Log.e("API", "\t" + header.getName() + ": " + header.getValue()); } } Log.e("API", "Response:"); if(responseString == null) { Log.e("API", "\tNULL!"); } else { int maxLogSize = 1000; for(int i = 0; i <= responseString.length() / maxLogSize; i++) { int start = i * maxLogSize; int end = (i+1) * maxLogSize; end = end > responseString.length() ? responseString.length() : end; Log.e("API", responseString.substring(start, end)); } } } }