package kc.spark.pixels.android.cloud; import java.lang.reflect.Type; import java.util.List; import kc.spark.pixels.android.app.DeviceState; import kc.spark.pixels.android.cloud.api.Device; import kc.spark.pixels.android.cloud.api.FunctionResponse; import kc.spark.pixels.android.cloud.api.SimpleResponse; import kc.spark.pixels.android.cloud.api.TinkerResponse; import kc.spark.pixels.android.cloud.requestservice.SimpleSparkApiService; import kc.spark.pixels.android.smartconfig.SmartConfigState; import kc.spark.pixels.android.ui.tinker.DigitalValue; import org.apache.http.HttpStatus; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.solemnsilence.util.TLog; import org.solemnsilence.util.Toaster; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.ResultReceiver; import android.support.v4.content.LocalBroadcastManager; import com.google.gson.reflect.TypeToken; /** * A simple interface for making requests and handling responses from the Spark * API. * * If you want to work with the Spark API from Android, this is the place to * start. See examples below like {@link #nameCore(String, String)}, * {@link #digitalWrite(String, String, DigitalValue, DigitalValue)}, etc, for * templates to work from. * */ public class ApiFacade { private static final TLog log = new TLog(ApiFacade.class); public static final int REQUEST_FAILURE_CODE = -1; // broadcast receiver actions public static final String BROADCAST_SIGN_UP_FINISHED = "BROADCAST_SIGN_UP_FINISHED"; public static final String BROADCAST_DEVICES_UPDATED = "BROADCAST_DEVICES_UPDATED"; public static final String BROADCAST_LOG_IN_FINISHED = "BROADCAST_LOG_IN_FINISHED"; public static final String BROADCAST_CORE_CLAIMED = "BROADCAST_CORE_CLAIMED"; public static final String BROADCAST_CORE_NAMING_REQUEST_COMPLETE = "BROADCAST_CORE_NAMING_REQUEST_COMPLETE"; public static final String BROADCAST_TINKER_RESPONSE_RECEIVED = "BROADCAST_TINKER_RESPONSE_RECEIVED"; public static final String BROADCAST_SHOULD_LOG_OUT = "BROADCAST_SHOULD_LOG_OUT"; public static final String BROADCAST_SERVICE_API_ERROR = "BROADCAST_SERVICE_API_ERROR"; public static final String BROADCAST_FUNCTION_RESPONSE_RECEIVED = "BROADCAST_FUNCTION_RESPONSE_RECEIVED"; public static final String EXTRA_ERROR_CODE = "EXTRA_ERROR_CODE"; public static final String EXTRA_TINKER_RESPONSE = "EXTRA_TINKER_RESPONSE"; public static final String EXTRA_FUNCTION_RESPONSE = "EXTRA_FUNCTION_RESPONSE"; private static ApiFacade instance = null; public static ApiFacade getInstance(Context context) { if (instance == null) { instance = new ApiFacade(context.getApplicationContext()); } return instance; } public static int getResultCode(Intent broadcastIntent) { int resultcode = broadcastIntent.getIntExtra(SimpleSparkApiService.EXTRA_RESULT_CODE, SimpleSparkApiService.REQUEST_FAILURE_CODE); return resultcode; } final Context ctx; final Handler handler; final LocalBroadcastManager broadcastMgr; private ApiFacade(Context context) { this.ctx = context.getApplicationContext(); this.handler = new Handler(); this.broadcastMgr = LocalBroadcastManager.getInstance(this.ctx); } public void signUp(String username, String password) { Bundle b = new Bundle(); b.putString("username", username); b.putString("password", password); SimpleSparkApiService.post(ctx, new String[] { "users" }, b, new SignUpResponseReceiver(handler, username, password), null); } public void logIn(String username, String password) { SimpleSparkApiService.logIn(ctx, username, password); } public void claimCore(String coreId) { log.i("Making request to claim core: " + coreId); Bundle b = new Bundle(); b.putString("id", coreId); SimpleSparkApiService.post(ctx, new String[] { "devices" }, b, new ClaimCoreResponseReceiver(handler, coreId), null); } public void requestUnheardCores() { SimpleSparkApiService.post(ctx, new String[] { "devices" }, null, new UnheardCoreCoreResponseReceiver(handler), null); } public void startSignalling(String coreId) { log.i("Making request for " + coreId + " to start signalling."); Bundle b = new Bundle(); b.putInt("signal", 1); SimpleSparkApiService.put(ctx, new String[] { "devices", coreId }, b, new SignalingResponseReceiver(handler, coreId, 1), null); } public void nameCore(String coreId, String name) { log.i("Renaming core " + coreId + " to " + name + " and instructing Core to cease any shouting of rainbows it may be doing."); Bundle b = new Bundle(); b.putInt("signal", 0); b.putString("name", name); SimpleSparkApiService.put(ctx, new String[] { "devices", coreId }, b, new CoreNamingResponseReceiver(handler, coreId, name), BROADCAST_CORE_NAMING_REQUEST_COMPLETE); } public void requestAllDevices() { log.d("Requesting update of all devices"); SimpleSparkApiService.get(ctx, new String[] { "devices" }, null, new DevicesResponseReceiver(handler), BROADCAST_DEVICES_UPDATED); } public void requestDevice(String coreId) { log.i("Requesting update for individual device: " + coreId); SimpleSparkApiService.get(ctx, new String[] { "devices", coreId }, null, new SingleDeviceResponseReceiver(handler), BROADCAST_DEVICES_UPDATED); } public void reflashTinker(String coreId) { Bundle b = new Bundle(); b.putString("app", "tinker"); SimpleSparkApiService.put(ctx, new String[] { "devices", coreId }, b, new ReflashTinkerResponseReceiver(handler, coreId), null); } public void reflashApp(String coreId, String appName) { Bundle b = new Bundle(); b.putString("app", appName); SimpleSparkApiService.put(ctx, new String[] { "devices", coreId }, b, new ReflashAppResponseReceiver(handler, coreId, appName), null); } public void digitalRead(String coreId, String pinId, DigitalValue oldValue) { TinkerReadValueReceiver receiver = new TinkerReadValueReceiver(handler, TinkerResponse.REQUEST_TYPE_READ, coreId, pinId, TinkerResponse.RESPONSE_TYPE_DIGITAL, oldValue.asInt()); Bundle args = new Bundle(); args.putString("params", pinId); SimpleSparkApiService.post(ctx, new String[] { "devices", coreId, "digitalread" }, args, receiver, null); } public void digitalWrite(String coreId, String pinId, DigitalValue oldValue, DigitalValue newValue) { TinkerWriteValueReceiver receiver = new TinkerWriteValueReceiver(handler, TinkerResponse.REQUEST_TYPE_WRITE, coreId, pinId, TinkerResponse.RESPONSE_TYPE_DIGITAL, oldValue.asInt(), newValue.asInt()); Bundle args = new Bundle(); args.putString("params", pinId + "," + newValue.name()); SimpleSparkApiService.post(ctx, new String[] { "devices", coreId, "digitalwrite" }, args, receiver, null); } public void analogRead(String coreId, String pinId, int oldValue) { TinkerReadValueReceiver receiver = new TinkerReadValueReceiver(handler, TinkerResponse.REQUEST_TYPE_READ, coreId, pinId, TinkerResponse.RESPONSE_TYPE_ANALOG, oldValue); Bundle args = new Bundle(); args.putString("params", pinId); SimpleSparkApiService.post(ctx, new String[] { "devices", coreId, "analogread" }, args, receiver, null); } public void analogWrite(String coreName, String pinId, int oldValue, int newValue) { TinkerWriteValueReceiver receiver = new TinkerWriteValueReceiver(handler, TinkerResponse.REQUEST_TYPE_WRITE, coreName, pinId, TinkerResponse.RESPONSE_TYPE_ANALOG, oldValue, newValue); Bundle args = new Bundle(); args.putString("params", pinId + "," + newValue); SimpleSparkApiService.post(ctx, new String[] { "devices", coreName, "analogwrite" }, args, receiver, null); } public void callSparkFunction(String coreName, String function, Bundle args, String responseType) { BaseFunctionValueReceiver Receiver = new BaseFunctionValueReceiver(handler, FunctionResponse.REQUEST_TYPE_VARIABLE, coreName, responseType); SimpleSparkApiService.post(ctx, new String[] { "devices", coreName, function }, args, Receiver, null); } public void getSparkVariable(String coreName, String variable, String responseType) { BaseFunctionValueReceiver Receiver = new BaseFunctionValueReceiver(handler, FunctionResponse.REQUEST_TYPE_VARIABLE, coreName, responseType); SimpleSparkApiService.get(ctx, new String[] { "devices", coreName, variable }, null, Receiver, null); } /** * This class is just to give a more clear, semantic interface to * ResultReceiver when using this Service, and provide a standard way of * delivering failure messages * */ public static abstract class ApiResponseReceiver extends ResultReceiver { public abstract void onRequestResponse(int statusCode, String jsonData); public ApiResponseReceiver(Handler handler) { super(handler); } @Override protected void onReceiveResult(int resultCode, Bundle resultData) { if (resultCode == REQUEST_FAILURE_CODE || resultCode >= 300 && shouldReportErrors()) { sendFailureBroadcast(resultCode); } this.onRequestResponse(resultCode, resultData.getString(SimpleSparkApiService.EXTRA_API_RESPONSE_JSON)); } private void sendFailureBroadcast(int errorCode) { String action = BROADCAST_SERVICE_API_ERROR; if (errorCode == 400) { // At least for now, 400 always means // "your token expired/was invalidated" action = BROADCAST_SHOULD_LOG_OUT; } Intent errIntent = new Intent(action) .putExtra(EXTRA_ERROR_CODE, errorCode); instance.broadcastMgr.sendBroadcast(errIntent); } public boolean shouldReportErrors() { return true; } } abstract class BaseTinkerValueReceiver extends ApiResponseReceiver { final int requestType; final String coreId; final String pinId; final int valueType; final int oldValue; abstract int getPinValue(int functionReturnValue); public BaseTinkerValueReceiver(Handler handler, int requestType, String coreId, String pinId, int valueType, int oldValue) { super(handler); this.requestType = requestType; this.coreId = coreId; this.pinId = pinId; this.valueType = valueType; this.oldValue = oldValue; } @Override public void onRequestResponse(int statusCode, String jsonData) { int newPinValue = oldValue; boolean hasErrors = false; if (statusCode == HttpStatus.SC_OK) { try { JSONObject json = new JSONObject(jsonData); if (json.has("return_value")) { int returnVal = json.getInt("return_value"); newPinValue = getPinValue(returnVal); } else if (json.has("error")) { hasErrors = true; } } catch (JSONException e) { log.e("Unable to get result from response JSON"); hasErrors = true; } } else { log.w("Pin value update failed! Response code: " + statusCode); } TinkerResponse response = new TinkerResponse(requestType, coreId, pinId, valueType, newPinValue, hasErrors); Intent intent = new Intent(BROADCAST_TINKER_RESPONSE_RECEIVED) .putExtra(EXTRA_TINKER_RESPONSE, response); instance.broadcastMgr.sendBroadcast(intent); } } class BaseFunctionValueReceiver extends ApiResponseReceiver { final int requestType; final String coreId; final String responseType; public BaseFunctionValueReceiver(Handler handler, int requestType, String coreId, String responseType){ super(handler); this.requestType = requestType; this.responseType = responseType; this.coreId = coreId; } @Override public void onRequestResponse(int statusCode, String jsonData) { String returnString = null; log.d("received JSON: " + jsonData); boolean hasErrors = false; if (statusCode == HttpStatus.SC_OK) { try { JSONObject json = new JSONObject(jsonData); if (json.has("return_value")) { returnString = json.getString("return_value"); } else if (json.has("result")) { returnString = json.getString("result"); } else if (json.has("error")) { hasErrors = true; } } catch (JSONException e) { log.e("Unable to get result from response JSON"); hasErrors = true; } } else { log.w("Function call failed! Response code: " + statusCode); } FunctionResponse response = new FunctionResponse(requestType, coreId, responseType, returnString, hasErrors); Intent intent = new Intent(BROADCAST_FUNCTION_RESPONSE_RECEIVED) .putExtra(EXTRA_FUNCTION_RESPONSE, response); instance.broadcastMgr.sendBroadcast(intent); } } class TinkerReadValueReceiver extends BaseTinkerValueReceiver { public TinkerReadValueReceiver(Handler handler, int requestType, String coreId, String pinId, int valueType, int oldValue) { super(handler, requestType, coreId, pinId, valueType, oldValue); } @Override int getPinValue(int functionReturnValue) { if (functionReturnValue == -1) { return oldValue; } else { return functionReturnValue; } } } class TinkerWriteValueReceiver extends BaseTinkerValueReceiver { final int newValue; public TinkerWriteValueReceiver(Handler handler, int requestType, String coreId, String pinId, int valueType, int oldValue, int newValue) { super(handler, requestType, coreId, pinId, valueType, oldValue); this.newValue = newValue; } @Override int getPinValue(int functionReturnValue) { if (functionReturnValue == -1) { return oldValue; } else { return newValue; } } } class SignUpResponseReceiver extends ApiResponseReceiver { String username; String password; public SignUpResponseReceiver(Handler handler, String username, String password) { super(handler); this.username = username; this.password = password; } @Override public void onRequestResponse(int statusCode, String jsonData) { SimpleResponse simpleResponse = null; try { simpleResponse = WebHelpers.getGson().fromJson(jsonData, SimpleResponse.class); } catch (Exception e) { log.w("Error trying to read sign up response."); } int statusCodeToReport = SimpleSparkApiService.REQUEST_FAILURE_CODE; String errorMessageToReturn = "User " + username + " exists, but the password is incorrect."; if (statusCode == HttpStatus.SC_OK && simpleResponse != null && simpleResponse.ok) { log.i("Sign up succeeded"); statusCodeToReport = statusCode; errorMessageToReturn = null; } else { log.w("Registration failed! Response code: " + statusCode); } if (simpleResponse != null) { Intent intent = new Intent(BROADCAST_SIGN_UP_FINISHED) .putExtra(SimpleSparkApiService.EXTRA_RESULT_CODE, statusCodeToReport); if (errorMessageToReturn != null) { intent.putExtra(SimpleSparkApiService.EXTRA_ERROR_MSG, errorMessageToReturn); } instance.broadcastMgr.sendBroadcast(intent); ApiFacade.instance.logIn(username, password); } } } class CoreNamingResponseReceiver extends ApiResponseReceiver { final String coreId; final String name; public CoreNamingResponseReceiver(Handler handler, String coreId, String name) { super(handler); this.coreId = coreId; this.name = name; } @Override public void onRequestResponse(int statusCode, String jsonData) { SmartConfigState.removeClaimedButPossiblyUnnamedDeviceIds(coreId); if (statusCode == HttpStatus.SC_OK) { log.i("Naming request succeeded: " + coreId + ", name: " + name); DeviceState.renameDevice(coreId, name); requestDevice(coreId); } else { log.w("Naming request failed: " + coreId + ", name: " + name + ", response code: " + statusCode); } } } class SignalingResponseReceiver extends ApiResponseReceiver { final String coreId; final int signal; public SignalingResponseReceiver(Handler handler, String coreId, int signal) { super(handler); this.coreId = coreId; this.signal = signal; } @Override public void onRequestResponse(int statusCode, String jsonData) { if (statusCode == HttpStatus.SC_OK) { log.i("Signaling request succeeded: " + coreId + ", value: " + signal); } else { log.w("Signaling request failed: " + coreId + ", value: " + signal + ", response code: " + statusCode); } } } class UnheardCoreCoreResponseReceiver extends ApiResponseReceiver { public UnheardCoreCoreResponseReceiver(Handler handler) { super(handler); } @Override public void onRequestResponse(int statusCode, String jsonData) { try { JSONObject json = new JSONObject(jsonData); JSONArray jsonArray = json.getJSONArray("devices"); for (int i = 0; i < jsonArray.length(); i++) { JSONObject idObj = jsonArray.getJSONObject(i); String unheardCoreId = idObj.getString("id"); log.d("Got ID of core which was 'unheard' via mDNS/CoAP: " + unheardCoreId); SmartConfigState.addSmartConfigFoundId(unheardCoreId); instance.claimCore(unheardCoreId); } } catch (JSONException e) { log.e("Bad JSON response trying to get the IDs of cores which weren't heard from via mDNS/CoAP"); } } @Override public boolean shouldReportErrors() { return false; } } class ClaimCoreResponseReceiver extends ApiResponseReceiver { final String coreId; public ClaimCoreResponseReceiver(Handler handler, String coreId) { super(handler); this.coreId = coreId; } @Override public void onRequestResponse(int statusCode, String jsonData) { if (statusCode == HttpStatus.SC_OK) { log.i("Claiming Core succeeded: " + coreId); SmartConfigState.addClaimedButPossiblyUnnamedDeviceId(coreId); broadcastMgr.sendBroadcast(new Intent(BROADCAST_CORE_CLAIMED)); } else { log.w("Claiming Core failed! Response code: " + statusCode); } } @Override public boolean shouldReportErrors() { return false; } } class SingleDeviceResponseReceiver extends ApiResponseReceiver { public SingleDeviceResponseReceiver(Handler handler) { super(handler); } @Override public void onRequestResponse(int statusCode, String jsonData) { if (statusCode == HttpStatus.SC_OK) { try { Device updated = WebHelpers.getGson().fromJson(jsonData, Device.class); DeviceState.updateSingleDevice(updated, true); } catch (Exception e) { log.e("Error parsing resposne JSON from single device request."); } } else { log.w("Device request failed! Response code: " + statusCode); } } } class DevicesResponseReceiver extends ApiResponseReceiver { public DevicesResponseReceiver(Handler handler) { super(handler); } @Override public void onRequestResponse(int statusCode, String jsonData) { if (statusCode == HttpStatus.SC_OK) { Type listType = new TypeToken<List<Device>>() { }.getType(); List<Device> devices = WebHelpers.getGson().fromJson(jsonData, listType); DeviceState.updateAllKnownDevices(devices); } else { log.w("Device list failed! Response code: " + statusCode); } } } class ReflashTinkerResponseReceiver extends ApiResponseReceiver { final String coreId; public ReflashTinkerResponseReceiver(Handler handler, String coreId) { super(handler); this.coreId = coreId; } @Override public void onRequestResponse(int statusCode, String jsonData) { if (statusCode == HttpStatus.SC_OK) { log.d("Request to reflash Tinker succeeded: " + coreId); Device d = DeviceState.getDeviceById(coreId); Toaster.s(ctx, "Re-flashing " + d.name + " with Tinker"); } else { log.w("Request to reflash Tinker failed: " + coreId + ", response code: " + statusCode); } } } class ReflashAppResponseReceiver extends ApiResponseReceiver { final String coreId; final String appName; public ReflashAppResponseReceiver(Handler handler, String coreId, String appName) { super(handler); this.coreId = coreId; this.appName = appName; } @Override public void onRequestResponse(int statusCode, String jsonData) { if (statusCode == HttpStatus.SC_OK) { log.d("Request to reflash " + appName + "succeeded: " + coreId); Device d = DeviceState.getDeviceById(coreId); Toaster.s(ctx, "Re-flashing " + d.name + " with " + appName); } else { log.w("Request to reflash " + appName + " failed: " + coreId + ", response code: " + statusCode); } } } }