package kc.spark.pixels.android.cloud.requestservice;
import static org.solemnsilence.util.Py.list;
import static org.solemnsilence.util.Py.truthy;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import kc.get.pixel.list.android.R;
import kc.spark.pixels.android.app.AppConfig;
import kc.spark.pixels.android.cloud.ApiFacade;
import kc.spark.pixels.android.cloud.ApiUrlHelper;
import kc.spark.pixels.android.cloud.WebHelpers;
import kc.spark.pixels.android.cloud.ApiFacade.ApiResponseReceiver;
import kc.spark.pixels.android.cloud.login.TokenRequest;
import kc.spark.pixels.android.cloud.login.TokenResponse;
import kc.spark.pixels.android.cloud.login.TokenTool;
import kc.spark.pixels.android.storage.Prefs;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.solemnsilence.util.TLog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.content.LocalBroadcastManager;
import com.google.gson.Gson;
import com.squareup.okhttp.OkHttpClient;
/**
* IntentService which performs the actual HTTP calls to talk to the Spark
* Cloud.
*
* You probably only need to poke around in here to look at the post/put/get
* methods, or if you're debugging.
*
*/
public class SimpleSparkApiService extends ClearableIntentService {
/**
* Key to retrieve the API response from the Bundle for the ResultReceiver
*/
public static final String EXTRA_API_RESPONSE_JSON = "EXTRA_API_RESPONSE_JSON";
public static final String EXTRA_RESULT_CODE = "EXTRA_RESULT_CODE";
public static final String EXTRA_ERROR_MSG = "EXTRA_ERROR_MSG";
/**
* The status code returned when a request could not be made, i.e.: an
* IOException was raised because a socket couldn't be opened, etc.
*/
public static final int REQUEST_FAILURE_CODE = -1;
/**
* Perform a POST request with the given args -- see {@link ApiFacade} for
* examples.
*
* @param ctx
* any Context
* @param resourcePathSegments
* the URL path as a String array (not including version string).
* e.g.: if your path was
* "/v1/devices/0123456789abcdef01234567/myFunction", you'd use:
* new String[] { "devices", "0123456789abcdef01234567",
* "myFunction" }
* @param formEncodingBodyData
* the data to post, as key-value pairs to be encoded as form
* data.
* @param resultReceiver
* Optional; specifies the ResultReceiver instance to use for
* receiving the result. Using a subclass of
* {@link ApiResponseReceiver} here is recommended for
* simplicity.
* @param broadcastName
* Optional; specifies the "action" string for a broadcast to be
* sent via {@link LocalBroadcastManager}. See
* {@link #processResponse(Response, Intent)} for more info.
*/
public static void post(Context ctx, String[] resourcePathSegments,
Bundle formEncodingBodyData, ResultReceiver resultReceiver, String broadcastName) {
ctx.startService(
buildRestRequestIntent(ctx, resourcePathSegments, formEncodingBodyData,
resultReceiver, broadcastName)
.setAction(ACTION_POST));
}
/**
* Perform a PUT request with the given args -- see {@link ApiFacade} for
* examples
*
* @param ctx
* any Context
* @param resourcePathSegments
* the URL path as a String array (not including version string).
* e.g.: if your path was
* "/v1/devices/0123456789abcdef01234567/myFunction", you'd use:
* new String[] { "devices", "0123456789abcdef01234567",
* "myFunction" }
* @param params
* the data to PUT, as key-value pairs in a Bundle
* @param resultReceiver
* Optional; specifies the ResultReceiver instance to use for
* receiving the result. Using a subclass of
* {@link ApiResponseReceiver} here is recommended for
* simplicity.
* @param broadcastName
* Optional; specifies the "action" string for a broadcast to be
* sent via {@link LocalBroadcastManager}. See
* {@link #processResponse(Response, Intent)} for more info.
*/
public static void put(Context ctx, String[] resourcePathSegments, Bundle params,
ResultReceiver resultReceiver, String broadcastName) {
ctx.startService(
// null post data
buildRestRequestIntent(ctx, resourcePathSegments, params, resultReceiver,
broadcastName)
.setAction(ACTION_PUT));
}
/**
* Perform a GET request -- see {@link ApiFacade} for examples
*
* @param ctx
* any Context
* @param resourcePathSegments
* the URL path as a String array (not including version string).
* e.g.: if your path was
* "/v1/devices/0123456789abcdef01234567/myFunction", you'd use:
* new String[] { "devices", "0123456789abcdef01234567",
* "myFunction" }
* @param params
* the URL params, as key-value pairs in a Bundle
* @param resultReceiver
* Optional; specifies the ResultReceiver instance to use for
* receiving the result. Using a subclass of
* {@link ApiResponseReceiver} here is recommended for
* simplicity.
* @param broadcastName
* Optional; specifies the "action" string for a broadcast to be
* sent via {@link LocalBroadcastManager}. See
* {@link #processResponse(Response, Intent)} for more info.
*/
public static void get(Context ctx, String[] resourcePathSegments, Bundle params,
ResultReceiver resultReceiver, String broadcastName) {
ctx.startService(
buildRestRequestIntent(ctx, resourcePathSegments, params, resultReceiver,
broadcastName)
.setAction(ACTION_GET));
}
// Logging in is handled a little differently, since it requires a number of
// different behaviors
public static void logIn(Context ctx, String username, String password) {
Intent intent = new Intent(ctx, SimpleSparkApiService.class)
.putExtra("username", username)
.putExtra("password", password)
.setAction(ACTION_LOG_IN);
ctx.startService(intent);
}
private static Intent buildRestRequestIntent(Context ctx, String[] resourcePathSegments,
Bundle params, ResultReceiver resultReceiver, String broadcastAction) {
Intent intent = new Intent(ctx, SimpleSparkApiService.class)
.putExtra(EXTRA_RESOURCE_PATH_SEGMENTS, resourcePathSegments);
if (params != null) {
intent.putExtra(EXTRA_REQUEST_QUERY_PARAMS, params);
}
if (resultReceiver != null) {
intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver);
}
if (broadcastAction != null) {
intent.putExtra(EXTRA_BROADCAST_ACTION, broadcastAction);
}
return intent;
}
private static final String NS = SimpleSparkApiService.class.getCanonicalName() + ".";
private static final String ACTION_GET = NS + "ACTION_GET";
private static final String ACTION_POST = NS + "ACTION_POST";
private static final String ACTION_PUT = NS + "ACTION_PUT";
private static final String ACTION_LOG_IN = NS + "ACTION_LOG_IN";
private static final String EXTRA_RESOURCE_PATH_SEGMENTS = NS + "EXTRA_RESOURCE_PATH_SEGMENTS";
private static final String EXTRA_REQUEST_QUERY_PARAMS = NS + "EXTRA_REQUEST_QUERY_PARAMS";
private static final String EXTRA_RESULT_RECEIVER = NS + "EXTRA_RESULT_RECEIVER";
private static final String EXTRA_BROADCAST_ACTION = NS + "EXTRA_BROADCAST_ACTION";
Gson gson;
Prefs prefs;
OkHttpClient okHttpClient;
LocalBroadcastManager localBroadcastManager;
// IntentServices are always single-threaded, so it's safe to just keep
// re-using the same output stream for capturing responses
ByteArrayOutputStream reusableResponseStream = new ByteArrayOutputStream(8192);
TokenTool tokenTool;
boolean authFailed = false;
String authToken = null;
public SimpleSparkApiService() {
super(SimpleSparkApiService.class.getSimpleName());
gson = WebHelpers.getGson();
okHttpClient = WebHelpers.getOkClient();
tokenTool = new TokenTool(gson, okHttpClient);
prefs = Prefs.getInstance();
}
@Override
public void onCreate() {
super.onCreate();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
// Don't redeliver intents, it's not critical for this app, and it's a
// great way to cause crash loops if something is wrong with your
// Intents.
setIntentRedelivery(false);
}
@Override
protected void onHandleIntent(Intent intent) {
if (ACTION_LOG_IN.equals(intent.getAction())) {
String username = intent.getStringExtra("username");
String password = intent.getStringExtra("password");
logIn(username, password);
return;
}
String token = getAuthToken();
if (!truthy(token)) {
log.d("Making request without token...");
}
Bundle extras = intent.getExtras();
String action = intent.getAction();
Response response = null;
if (ACTION_GET.equals(action)) {
URL url = buildGetUrl(extras, token);
response = get(url);
} else if (ACTION_POST.equals(action)) {
URL url = buildPostUrl(extras, token);
String postData = getPostData(extras);
response = post(url, postData);
} else if (ACTION_PUT.equals(action)) {
URL url = buildPutUrl(extras);
Bundle queryParams = extras.getBundle(EXTRA_REQUEST_QUERY_PARAMS);
queryParams.putString(AppConfig.getApiParamAccessToken(), token);
String putString = URLEncodedUtils.format(
bundleParamsToNameValuePairs(queryParams),
HTTP.UTF_8);
response = put(url, putString);
} else {
log.wtf("Received intent with unrecognized action: " + action);
}
processResponse(response, intent);
}
// returns the status code of the request
private int logIn(String username, String password) {
TokenRequest tokenRequest = new TokenRequest(username, password);
TokenResponse response = tokenTool.requestToken(tokenRequest);
log.d("Token response received, status code: " + response.getStatusCode());
Intent bcast = new Intent(ApiFacade.BROADCAST_LOG_IN_FINISHED);
if (response.getStatusCode() == -1 || response.getStatusCode() >= 300) {
log.e("Error requesting token: " + response.errorDescription);
bcast.putExtra(EXTRA_ERROR_MSG, response.errorDescription);
bcast.putExtra(EXTRA_RESULT_CODE, response.getStatusCode());
} else {
prefs.saveUsername(username);
prefs.saveCompletedFirstLogin(true);
prefs.saveToken(response.accessToken);
bcast.putExtra(EXTRA_RESULT_CODE, response.getStatusCode());
authFailed = false;
}
localBroadcastManager.sendBroadcast(bcast);
return response.getStatusCode();
}
void processResponse(Response response, Intent intent) {
Bundle extras = intent.getExtras();
Bundle resultBundle = new Bundle();
int resultCode = REQUEST_FAILURE_CODE;
String error = getString(R.string.error_communicating_with_server);
if (response != null) {
resultCode = response.responseCode;
resultBundle.putString(EXTRA_API_RESPONSE_JSON, response.apiResponse);
resultBundle.putInt(EXTRA_RESULT_CODE, resultCode);
} else {
resultBundle.putString(EXTRA_ERROR_MSG, error);
}
ResultReceiver receiver = extras.getParcelable(EXTRA_RESULT_RECEIVER);
if (receiver != null) {
receiver.send(resultCode, resultBundle);
}
String bcastAction = extras.getString(EXTRA_BROADCAST_ACTION);
if (truthy(bcastAction)) {
Intent responseIntent = new Intent()
.replaceExtras(resultBundle)
.setAction(bcastAction);
localBroadcastManager.sendBroadcast(responseIntent);
}
}
Response post(URL url, String stringData) {
return performRequestWithInputData(url, "POST", stringData);
}
Response put(URL url, String stringData) {
return performRequestWithInputData(url, "PUT", stringData);
}
Response performRequestWithInputData(URL url, String httpMethod, String stringData) {
HttpURLConnection connection = okHttpClient.open(url);
OutputStream out = null;
InputStream in = null;
int responseCode = -1;
String responseData = "";
try {
try {
connection.setRequestMethod(httpMethod);
connection.setDoOutput(true);
out = connection.getOutputStream();
out.write(stringData.getBytes(HTTP.UTF_8));
out.close();
responseCode = connection.getResponseCode();
in = connection.getInputStream();
responseData = readAsUtf8String(in);
} finally {
// Clean up.
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
}
} catch (IOException e) {
log.e("Error trying to make " + connection.getRequestMethod() + " request");
}
return new Response(responseCode, responseData);
}
Response get(URL url) {
HttpURLConnection connection = okHttpClient.open(url);
InputStream in = null;
int responseCode = -1;
String responseData = "";
// Java I/O throws *exception*al parties!
try {
try {
responseCode = connection.getResponseCode();
in = connection.getInputStream();
responseData = readAsUtf8String(in);
} finally {
if (in != null) {
in.close();
}
}
} catch (IOException e) {
log.e("Error trying to make GET request");
}
return new Response(responseCode, responseData);
}
URL buildGetUrl(Bundle intentExtras, String token) {
Uri.Builder uriBuilder = ApiUrlHelper.buildUri(token,
intentExtras.getStringArray(EXTRA_RESOURCE_PATH_SEGMENTS));
Bundle queryParams = intentExtras.getBundle(EXTRA_REQUEST_QUERY_PARAMS);
if (queryParams != null) {
for (NameValuePair param : bundleParamsToNameValuePairs(queryParams)) {
uriBuilder.appendQueryParameter(param.getName(), param.getValue());
}
}
return ApiUrlHelper.convertToURL(uriBuilder);
}
URL buildPutUrl(Bundle intentExtras) {
Uri.Builder uriBuilder = ApiUrlHelper.buildUri(null,
intentExtras.getStringArray(EXTRA_RESOURCE_PATH_SEGMENTS));
return ApiUrlHelper.convertToURL(uriBuilder);
}
URL buildPostUrl(Bundle intentExtras, String token) {
Uri.Builder uriBuilder = ApiUrlHelper.buildUri(token,
intentExtras.getStringArray(EXTRA_RESOURCE_PATH_SEGMENTS));
return ApiUrlHelper.convertToURL(uriBuilder);
}
List<NameValuePair> bundleParamsToNameValuePairs(Bundle params) {
List<NameValuePair> paramList = list();
for (String key : params.keySet()) {
Object value = params.get(key);
if (value != null) {
paramList.add(new BasicNameValuePair(key, value.toString()));
}
}
return paramList;
}
String getPostData(Bundle intentExtras) {
String postString = "";
Bundle queryParams = intentExtras.getBundle(EXTRA_REQUEST_QUERY_PARAMS);
if (queryParams != null) {
String putString = URLEncodedUtils.format(
bundleParamsToNameValuePairs(queryParams),
HTTP.UTF_8);
postString = putString;
}
return postString;
}
String readAsUtf8String(InputStream in) throws IOException {
reusableResponseStream.reset();
byte[] buffer = new byte[1024];
for (int count; (count = in.read(buffer)) != -1;) {
reusableResponseStream.write(buffer, 0, count);
}
return reusableResponseStream.toString(HTTP.UTF_8);
}
String getAuthToken() {
if (!truthy(authToken)) {
authToken = prefs.getToken();
}
return authToken;
}
private static class Response {
public final int responseCode;
public final String apiResponse;
public Response(int responseCode, String apiResponse) {
this.responseCode = responseCode;
this.apiResponse = apiResponse;
}
@Override
public String toString() {
return "RequestResult [resultCode=" + responseCode + ", apiResponse=" + apiResponse
+ "]";
}
}
private static final TLog log = new TLog(SimpleSparkApiService.class);
}