/************************************************************************************************** * Copyright (C) 2010 Sense Observation Systems, Rotterdam, the Netherlands. All rights reserved. * *************************************************************************************************/ package nl.sense_os.service.commonsense; import android.annotation.TargetApi; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Build; import android.telephony.TelephonyManager; import android.util.Log; import nl.sense_os.service.SenseServiceStub; import nl.sense_os.service.constants.SenseDataTypes; import nl.sense_os.service.constants.SensePrefs; import nl.sense_os.service.constants.SensePrefs.Auth; import nl.sense_os.service.constants.SensePrefs.Main.Advanced; import nl.sense_os.service.constants.SenseUrls; import nl.sense_os.service.constants.SensorData.SensorNames; import nl.sense_os.service.push.GCMReceiver; import org.apache.http.conn.ssl.SSLSocketFactory; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.zip.GZIPOutputStream; import javax.net.ssl.HttpsURLConnection; /** * Main interface for communicating with the CommonSense API. * * @author Steven Mulder <steven@sense-os.nl> */ public class SenseApi { private static final long CACHE_REFRESH = 1000l * 60 * 60; // 1 hour private static final String TAG = "SenseApi"; /** Device UUID for sensors that are not physical sensors, i.e. not connected to any device */ public static final String NO_DEVICE_UUID = "no_device_uuid"; /** * Key for getting the http response code from the Map object that is returned by * {@link SenseApi#request(Context, String, JSONObject, String)} */ public static final String RESPONSE_CODE = "http response code"; /** * Key for getting the response content from the Map object that is returned by * {@link SenseApi#request(Context, String, JSONObject, String)} */ public static final String RESPONSE_CONTENT = "content"; private static SharedPreferences sAuthPrefs; private static SharedPreferences sMainPrefs; private static TelephonyManager sTelManager; private static String APPLICATION_KEY; /** * Gets a list of all registered sensors for a user at the CommonSense API. Uses caching for * increased performance. * * @param context * Application context, used for getting preferences. * @return The list of sensors * @throws IOException * In case of communication failure to CommonSense * @throws JSONException * In case of unparseable response from CommonSense */ public static JSONArray getAllSensors(Context context) throws IOException, JSONException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } // try to get list of sensors from the cache try { String cachedSensors = sAuthPrefs.getString(Auth.SENSOR_LIST_COMPLETE, null); long cacheTime = sAuthPrefs.getLong(Auth.SENSOR_LIST_COMPLETE_TIME, 0); boolean isOutdated = System.currentTimeMillis() - cacheTime > CACHE_REFRESH; // return cached list of it is still valid if (false == isOutdated && null != cachedSensors) { return new JSONArray(cachedSensors); } } catch (Exception e) { // unlikely to ever happen. Just get the list from CommonSense instead Log.w(TAG, "Failed to get list of sensors from cache!", e); } // if we make it here, the list was not in the cache Log.v(TAG, "List of sensor IDs is missing or outdated, refreshing..."); boolean done = false; JSONArray result = new JSONArray(); int page = 0; while (!done) { // request fresh list of sensors for this device from CommonSense String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); if (devMode) { Log.v(TAG, "Using development server to get registered sensors"); } String url = devMode ? SenseUrls.ALL_SENSORS_DEV : SenseUrls.ALL_SENSORS; url += "&page=" + page; Map<String, String> response = SenseApi.request(context, url, null, cookie); String responseCode = response.get(RESPONSE_CODE); if (!"200".equals(responseCode)) { Log.w(TAG, "Failed to get list of sensors! Response code: " + responseCode); throw new IOException("Incorrect response from CommonSense: " + responseCode); } // parse response and store the list JSONObject content = new JSONObject(response.get(RESPONSE_CONTENT)); JSONArray sensorList = content.getJSONArray("sensors"); // put the sensor list in the result array for (int i = 0; i < sensorList.length(); i++) { result.put(sensorList.getJSONObject(i)); } if (sensorList.length() < SenseUrls.PAGE_SIZE) { // all sensors received done = true; } else { // get the next page page++; } } // store the new sensor list Editor authEditor = sAuthPrefs.edit(); authEditor.putString(Auth.SENSOR_LIST_COMPLETE, result.toString()); authEditor.putLong(Auth.SENSOR_LIST_COMPLETE_TIME, System.currentTimeMillis()); authEditor.commit(); return result; } /** * Gets the sensors that are connected to another sensor. Typically used * * @param context * @param sensorId * @return List of IDs for connected sensors * @throws IOException * @throws JSONException */ public static List<String> getConnectedSensors(Context context, String sensorId) throws IOException, JSONException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } // request fresh list of sensors for this device from CommonSense String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); if (devMode) { Log.v(TAG, "Using development server to get connected sensors"); } String url = devMode ? SenseUrls.CONNECTED_SENSORS_DEV : SenseUrls.CONNECTED_SENSORS; url = url.replace("%1", sensorId); Map<String, String> response = SenseApi.request(context, url, null, cookie); String responseCode = response.get(RESPONSE_CODE); if (!"200".equals(responseCode)) { Log.w(TAG, "Failed to get list of connected sensors! Response code: " + responseCode); throw new IOException("Incorrect response from CommonSense: " + responseCode); } // parse response and store the list JSONObject content = new JSONObject(response.get(RESPONSE_CONTENT)); JSONArray sensorList = content.getJSONArray("sensors"); List<String> result = new ArrayList<String>(); for (int i = 0; i < sensorList.length(); i++) { JSONObject sensor = sensorList.getJSONObject(i); result.add(sensor.getString("id")); } return result; } /** * @param context * Context for accessing phone details * @return The default device type, i.e. the phone's model String */ public static String getDefaultDeviceType(Context context) { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } return sAuthPrefs.getString(Auth.DEVICE_TYPE, Build.MODEL); } /** * @param context * Context for accessing phone details * @return The default device UUID, e.g. the phone's IMEI String */ @TargetApi(9) public static String getDefaultDeviceUuid(Context context) { if (null == sTelManager) { sTelManager = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)); } String uuid = sTelManager.getDeviceId(); if (null == uuid) { // device has no IMEI, try using the Android serial code if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { uuid = Build.SERIAL; } else { Log.w(TAG, "Cannot get reliable device UUID!"); } } return uuid; } /** * Get device configuration from commonSense * * @throws JSONException * @throws IOException */ public static String getDeviceConfiguration(Context context) throws IOException, JSONException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); String url = devMode ? SenseUrls.DEVICE_CONFIGURATION_DEV : SenseUrls.DEVICE_CONFIGURATION; // Get the device ID String device_id = getDeviceId(context); url = url.replaceFirst("%1", device_id); Map<String, String> response = SenseApi.request(context, url, null, cookie); String responseCode = response.get(RESPONSE_CODE); if (!"200".equals(responseCode)) { Log.w(TAG, "Failed to get device configuration! Response code: " + responseCode); throw new IOException("Incorrect response from CommonSense: " + responseCode); } return response.get(RESPONSE_CONTENT); } /** * Get specific configuration from commonSense * * @throws JSONException * @throws IOException * */ public static String getDeviceConfiguration(Context context, String configuration_id) throws IOException, JSONException { final SharedPreferences authPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); final SharedPreferences prefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); String cookie = authPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = prefs.getBoolean(Advanced.DEV_MODE, false); String url = devMode ? SenseUrls.CONFIGURATION_DEV : SenseUrls.CONFIGURATION; url = url.replaceFirst("%1", configuration_id); Map<String, String> response = SenseApi.request(context, url, null, cookie); String responseCode = response.get(RESPONSE_CODE); if (!"200".equals(responseCode)) { Log.w(TAG, "Failed to get Requirement! Response code: " + responseCode); throw new IOException("Incorrect response from CommonSense: " + responseCode); } return response.get(RESPONSE_CONTENT); } /** * Get this device_id registered in common sense * * @param context * @throws IOException * @throws JSONException */ public static String getDeviceId(Context context) throws IOException, JSONException { String device_id = null; if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); String url = devMode ? SenseUrls.DEVICES_DEV : SenseUrls.DEVICES; Map<String, String> response = SenseApi.request(context, url, null, cookie); String responseCode = response.get(RESPONSE_CODE); if (!"200".equals(responseCode)) { Log.w(TAG, "Failed to get devices data: " + responseCode); throw new IOException("Incorrect response from CommonSense: " + responseCode); } // check registration result JSONObject res = new JSONObject(response.get(RESPONSE_CONTENT)); JSONArray arr = res.getJSONArray("devices"); String deviceType = getDefaultDeviceType(context); String deviceUUID = getDefaultDeviceUuid(context); for (int i = 0; i < arr.length(); i++) { JSONObject row = arr.getJSONObject(i); String cid = row.getString("id"); String ctype = row.getString("type"); String cuuid = row.getString("uuid"); if (deviceType.equals(ctype) && deviceUUID.equals(cuuid)) { device_id = cid; break; } } return device_id; } private static List<JSONObject> getMatchingSensors(Context context, String name, String description, String dataType) throws IOException, JSONException { // get list of all registered sensors for this device JSONArray sensors = getAllSensors(context); // check sensors with similar names and descriptions in the list List<JSONObject> result = new ArrayList<JSONObject>(); JSONObject sensor; for (int i = 0; i < sensors.length(); i++) { sensor = (JSONObject) sensors.get(i); if (sensor.getString("name").equalsIgnoreCase(name) && ((null == description) || sensor.getString("device_type").equalsIgnoreCase( description)) && ((null == dataType) || sensor.getString("data_type").equalsIgnoreCase( dataType))) { result.add(sensor); } else if (name.equals(SensorNames.ACCELEROMETER) || name.equals(SensorNames.ORIENT) || name.equals(SensorNames.GYRO) || name.equals(SensorNames.LIN_ACCELERATION) || name.equals(SensorNames.MAGNETIC_FIELD) || name.equals(SensorNames.ACCELEROMETER_EPI) || name.equals(SensorNames.ACCELEROMETER_BURST) || name.equals(SensorNames.GYRO_BURST) || name.equals(SensorNames.LINEAR_BURST)) { // special case to take care of changed motion sensor descriptions since Gingerbread if (name.equals(sensor.getString("name"))) { // use inexact match result.add(sensor); } } } return result; } /** * Gets the sensor ID at CommonSense , which can be used to modify the sensor information and * data. * * @param context * Context for getting preferences * @param name * Sensor name, to match with registered sensors. * @param description * Sensor description (previously 'device_type'), to match with registered sensors. * @param dataType * Sensor data type, to match with registered sensors. * @param deviceUuid * (Optional) UUID of the device that should hold the sensor. * @return String with the sensor's ID, or null if the sensor does not exist at CommonSense * (yet). * @throws IOException * If the request to CommonSense failed. * @throws JSONException * If the response from CommonSense could not be parsed. */ public static String getSensorId(Context context, String name, String description, String dataType, String deviceUuid) throws IOException, JSONException { // get list of sensors with matching description List<JSONObject> sensors = getMatchingSensors(context, name, description, dataType); // check the devices that the sensors are connected to String id = null; JSONObject device; for (JSONObject sensor : sensors) { if (null != deviceUuid && !NO_DEVICE_UUID.equals(deviceUuid)) { // check if the device UUID matches device = sensor.optJSONObject("device"); if (null != device && deviceUuid.equals(device.optString("uuid"))) { id = sensor.getString("id"); break; } } else { // we do not care about the device, just accept the first match we find id = sensor.getString("id"); break; } } if (null == id) { Log.d(TAG, "missing id for " + name + " (" + description + ") @ " + deviceUuid); } return id; } /** * Gets the URL at CommonSense to which the data must be sent. * * @param context * Context for getting preferences * @param name * Sensor name, to match with registered sensors. * @param description * Sensor description (previously 'device_type'), to match with registered sensors. * @param dataType * Sensor data type, to match with registered sensors. * @param deviceUuid * (Optional) UUID of the device that holds the sensor. Set null to use the default * device. * @return String with the sensor's URL, or null if sensor does not have an ID (yet) * @throws JSONException * If there was unexpected response getting the sensor ID. * @throws IOException * If there was a problem during communication with CommonSense. */ public static String getSensorUrl(Context context, String name, String description, String dataType, String deviceUuid) throws IOException, JSONException { String id = getSensorId(context, name, description, dataType, deviceUuid); if (id == null) { Log.w(TAG, "Failed to get URL for sensor '" + name + "': sensor ID is not available"); return null; } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); // found the right sensor if (dataType.equals(SenseDataTypes.FILE)) { String url = devMode ? SenseUrls.SENSOR_FILE_DEV : SenseUrls.SENSOR_FILE; return url.replaceFirst("%1", id); } else { String url = devMode ? SenseUrls.SENSOR_DATA_DEV : SenseUrls.SENSOR_DATA; return url.replaceFirst("%1", id); } } /** * @param context * Context for getting preferences * @return The current CommonSense session ID * @throws IllegalAccessException * if the app ID is not valid */ public static String getCookie(Context context) throws IllegalAccessException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } return sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); } /** * @param context * Context for getting preferences * @return The current CommonSense session ID * @throws IllegalAccessException * if the app ID is not valid */ public static String getSessionId(Context context) throws IllegalAccessException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } return sAuthPrefs.getString(Auth.LOGIN_SESSION_ID, null); } /** * Gets user details from CommonSense * * @param context * Context for getting preferences * @return JSONObject with user if successful, null otherwise * @throws JSONException * In case of unparseable response from CommonSense * @throws IOException * In case of communication failure to CommonSense */ public static JSONObject getUser(Context context) throws IOException, JSONException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); String url = devMode ? SenseUrls.CURRENT_USER_DEV : SenseUrls.CURRENT_USER; // perform actual request Map<String, String> response = SenseApi.request(context, url, null, cookie); String responseCode = response.get(RESPONSE_CODE); JSONObject result = null; if ("200".equalsIgnoreCase(responseCode)) { result = new JSONObject(response.get(RESPONSE_CONTENT)).getJSONObject("user"); } else { Log.w(TAG, "Failed to get user! Response code: " + responseCode); throw new IOException("Incorrect response from CommonSense: " + responseCode); } return result; } /** * @param hashMe * "clear" password String to be hashed before sending it to CommonSense * @return Hashed String */ public static String hashPassword(String hashMe) { final byte[] unhashedBytes = hashMe.getBytes(); try { final MessageDigest algorithm = MessageDigest.getInstance("MD5"); algorithm.reset(); algorithm.update(unhashedBytes); final byte[] hashedBytes = algorithm.digest(); final StringBuffer hexString = new StringBuffer(); for (final byte element : hashedBytes) { final String hex = Integer.toHexString(0xFF & element); if (hex.length() == 1) { hexString.append(0); } hexString.append(hex); } return hexString.toString(); } catch (final NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } /** * Joins a group * * @param context * Context for getting preferences * @param groupId * Id of the group to join * @return true if joined successfully, false otherwise * @throws JSONException * In case of unparseable response from CommonSense * @throws IOException * In case of communication failure to CommonSense */ public static boolean joinGroup(Context context, String groupId) throws JSONException, IOException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); // get userId String userId = getUser(context).getString("id"); String url = devMode ? SenseUrls.GROUP_USERS_DEV : SenseUrls.GROUP_USERS; url = url.replaceFirst("%1", groupId); // create JSON object to POST final JSONObject data = new JSONObject(); final JSONArray users = new JSONArray(); final JSONObject item = new JSONObject(); final JSONObject user = new JSONObject(); user.put("id", userId); item.put("user", user); users.put(item); data.put("users", users); // perform actual request Map<String, String> response = SenseApi.request(context, url, data, cookie); String responseCode = response.get(RESPONSE_CODE); boolean result = false; if ("201".equalsIgnoreCase(responseCode)) { result = true; } else { Log.w(TAG, "Failed to join group! Response code: " + responseCode + "Response: " + response); result = false; } return result; } /** * Tries to log in at CommonSense using the supplied username and password. After login, the * cookie containing the session ID is stored in the preferences. * * @param context * Context for getting preferences * @param username * Username for authentication * @param password * Hashed password for authentication * @return 0 if login completed successfully, -2 if login was forbidden, and -1 for any other * errors. * @throws JSONException * In case of unparseable response from CommonSense * @throws IOException * In case of communication failure to CommonSense * @see SenseServiceStub#changeLogin(String, String, nl.sense_os.service.ISenseServiceCallback) */ public static int login(Context context, String username, String password) throws JSONException, IOException { // preferences if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); if (devMode) { Log.v(TAG, "Using development server to log in"); } final String url = devMode ? SenseUrls.LOGIN_DEV : SenseUrls.LOGIN; final JSONObject user = new JSONObject(); user.put("username", username); user.put("password", password); // TODO disable compressed login Boolean useCompressed = sMainPrefs.getBoolean(SensePrefs.Main.Advanced.COMPRESS, false); sMainPrefs.edit().putBoolean(SensePrefs.Main.Advanced.COMPRESS, false).commit(); // perform actual request // set the application id for the login call APPLICATION_KEY = sMainPrefs.getString(SensePrefs.Main.APPLICATION_KEY, null); Map<String, String> response = request(context, url, user, null); APPLICATION_KEY = null; // set previous value sMainPrefs.edit().putBoolean(SensePrefs.Main.Advanced.COMPRESS, useCompressed).commit(); // if response code is not 200 (OK), the login was incorrect String responseCode = response.get(RESPONSE_CODE); int result = -1; if ("403".equalsIgnoreCase(responseCode)) { Log.w(TAG, "CommonSense login refused! Response: forbidden!"); result = -2; } else if (!"200".equalsIgnoreCase(responseCode)) { Log.w(TAG, "CommonSense login failed! Response: " + responseCode); result = -1; } else { // received 200 response result = 0; } // create a cookie from the session_id String session_id = response.get("session-id"); String cookie = ""; if (result == 0 && session_id == null) { // something went horribly wrong Log.w(TAG, "CommonSense login failed: no cookie received?!"); result = -1; } else cookie = "session_id="+session_id+"; domain=.sense-os.nl"; // handle result Editor authEditor = sAuthPrefs.edit(); switch (result) { case 0: // logged in authEditor.putString(Auth.LOGIN_COOKIE, cookie); authEditor.putString(Auth.LOGIN_SESSION_ID, session_id); authEditor.commit(); break; case -1: // error break; case -2: // unauthorized authEditor.remove(Auth.LOGIN_COOKIE); authEditor.remove(Auth.LOGIN_SESSION_ID); authEditor.commit(); break; default: Log.e(TAG, "Unexpected login result: " + result); } return result; } /** * Push GCM Registration ID for current device * * @param context * Application context, used to read preferences. * @param registrationId * Registration ID given by google * @throws IOException * @throws JSONException * @throws IllegalStateException */ public static void registerGCMId(Context context, String registrationId) throws IOException, JSONException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } // Check if already synced with common sense String pref_registration_id = sAuthPrefs.getString(Auth.GCM_REGISTRATION_ID, ""); if (registrationId.equals(pref_registration_id)) { // GCM registration id is already sync with commonSense return; } String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); String url = devMode ? SenseUrls.REGISTER_GCM_ID_DEV : SenseUrls.REGISTER_GCM_ID; // Get the device ID String device_id = getDeviceId(context); if (device_id == null) { // register GCM ID failed, can not get the device ID for this device return; } url = url.replaceFirst("%1", device_id); final JSONObject data = new JSONObject(); data.put("registration_id", registrationId); Map<String, String> response = SenseApi.request(context, url, data, cookie); String responseCode = response.get(RESPONSE_CODE); if (!"200".equals(responseCode)) { throw new IOException("Incorrect response from CommonSense: " + responseCode); } // check registration result JSONObject res = new JSONObject(response.get(RESPONSE_CONTENT)); if (!registrationId.equals(res.getJSONObject("device").getString(GCMReceiver.KEY_GCM_ID))) { throw new IllegalStateException("GCM registration_id not match with response"); } Editor authEditor = sAuthPrefs.edit(); authEditor.putString(Auth.GCM_REGISTRATION_ID, registrationId); authEditor.commit(); } /** * Registers a new sensor for this device at CommonSense. Also connects the sensor to this * device. * * @param context * The application context, used to retrieve preferences. * @param name * The name of the sensor. * @param displayName * The sensor's pretty display name. * @param description * The sensor description (previously "device_type"). * @param dataType * The sensor data type. * @param value * An example sensor value, used to determine the data structure for JSON type * sensors. * @param deviceType * (Optional) Type of the device that holds the sensor. Set null to use the default * device. * @param deviceUuid * (Optional) UUID of the device that holds the sensor. Set null to use the default * device. * @return The new sensor ID at CommonSense, or <code>null</code> if the registration failed. * @throws JSONException * In case of invalid sensor details or if the request returned unparseable * response. * @throws IOException * In case of communication failure during creation of the sensor. */ public static String registerSensor(Context context, String name, String displayName, String description, String dataType, String value, String deviceType, String deviceUuid) throws JSONException, IOException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); // prepare request to create new sensor String url = devMode ? SenseUrls.CREATE_SENSOR_DEV : SenseUrls.SENSORS; JSONObject postData = new JSONObject(); JSONObject sensor = new JSONObject(); sensor.put("name", name); sensor.put("device_type", description); sensor.put("display_name", displayName); sensor.put("pager_type", ""); sensor.put("data_type", dataType); if (dataType.compareToIgnoreCase(SenseDataTypes.JSON) == 0 || dataType.compareToIgnoreCase(SenseDataTypes.JSON_TIME_SERIES) == 0) { JSONObject dataStructJSon = null; try { dataStructJSon = new JSONObject(value); JSONArray fieldNames = dataStructJSon.names(); for (int x = 0; x < fieldNames.length(); x++) { String fieldName = fieldNames.getString(x); int start = dataStructJSon.get(fieldName).getClass().getName().lastIndexOf("."); dataStructJSon.put(fieldName, dataStructJSon.get(fieldName).getClass() .getName().substring(start + 1)); } } catch (JSONException e) { // apparently the data structure cannot be parsed from the value dataStructJSon = new JSONObject(); } sensor.put("data_structure", dataStructJSon.toString().replaceAll("\"", "\\\"")); } postData.put("sensor", sensor); // perform actual request Map<String, String> response = request(context, url, postData, cookie); // check response code String code = response.get(RESPONSE_CODE); if (!"201".equals(code)) { Log.w(TAG, "Failed to register sensor at CommonSense! Response code: " + code); throw new IOException("Incorrect response from CommonSense: " + code); } // retrieve the newly created sensor ID String locationHeader = response.get("location"); String[] split = locationHeader.split("/"); String id = split[split.length - 1]; // see if sensor should also be connected to a device at CommonSense if (NO_DEVICE_UUID.equals(deviceUuid)) { JSONObject device = new JSONObject(); device.put("type", deviceType); device.put("uuid", deviceUuid); postData.put("device", device); // store the new sensor in the preferences sensor.put("id", id); sensor.put("device", device); JSONArray sensors = getAllSensors(context); sensors.put(sensor); Editor authEditor = sAuthPrefs.edit(); authEditor.putString(Auth.SENSOR_LIST_COMPLETE, sensors.toString()); authEditor.commit(); return id; } // get device properties from preferences, so it matches the properties in CommonSense if (null == deviceUuid) { deviceUuid = getDefaultDeviceUuid(context); deviceType = getDefaultDeviceType(context); } // add sensor to this device at CommonSense url = devMode ? SenseUrls.SENSOR_DEVICE_DEV : SenseUrls.SENSOR_DEVICE; url = url.replaceFirst("%1", id); postData = new JSONObject(); JSONObject device = new JSONObject(); device.put("type", deviceType); device.put("uuid", deviceUuid); postData.put("device", device); response = request(context, url, postData, cookie); // check response code code = response.get(RESPONSE_CODE); if (!"201".equals(code)) { Log.w(TAG, "Failed to add sensor to device at CommonSense! Response code: " + code); throw new IOException("Incorrect response from CommonSense: " + code); } // store the new sensor in the preferences sensor.put("id", id); sensor.put("device", device); JSONArray sensors = getAllSensors(context); sensors.put(sensor); Editor authEditor = sAuthPrefs.edit(); authEditor.putString(Auth.SENSOR_LIST_COMPLETE, sensors.toString()); authEditor.commit(); Log.v(TAG, "Created sensor: '" + name + "' for device: '" + deviceType + "'"); // return the new sensor ID return id; } /** * Tries to register a new user at CommonSense. * * @param context * Context for getting preferences * @param username * Username to register * @param password * Hashed password for the new user * @return 0 if registration completed successfully, -2 if the user already exists, and -1 for * any other unexpected responses. * @throws JSONException * In case of unparseable response from CommonSense * @throws IOException * In case of communication failure to CommonSense * @see SenseServiceStub#register(String, String, String, String, String, String, String, * String, String, nl.sense_os.service.ISenseServiceCallback) */ public static int registerUser(Context context, String username, String password, String name, String surname, String email, String mobile) throws JSONException, IOException { if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); final String url = devMode ? SenseUrls.REGISTER_DEV : SenseUrls.REGISTER; // create JSON object to POST final JSONObject data = new JSONObject(); final JSONObject user = new JSONObject(); user.put("username", username); user.put("password", password); if (null != name) { user.put("name", name); } if (null != surname) { user.put("surname", surname); } if (null != email) { user.put("email", email); } if (null != mobile) { user.put("mobile", mobile); } data.put("user", user); // perform actual request Map<String, String> response = SenseApi.request(context, url, data, null); String responseCode = response.get(RESPONSE_CODE); int result = -1; if ("201".equalsIgnoreCase(responseCode)) { result = 0; } else if ("409".equalsIgnoreCase(responseCode)) { Log.w(TAG, "Failed to register new user! User already exists"); result = -2; } else { Log.w(TAG, "Failed to register new user! Response code: " + responseCode); result = -1; } return result; } /** * Performs request at CommonSense API. Returns the response code, content, and headers. * * @param context * Application context, used to read preferences. * @param urlString * Complete URL to perform request to. * @param content * (Optional) Content for the request. If the content is not null, the request method * is automatically POST. The default method is GET. * @param cookie * (Optional) Cookie header for the request. * @return Map with SenseApi.KEY_CONTENT and SenseApi.KEY_RESPONSE_CODE fields, plus fields for * all response headers. * @throws IOException */ public static Map<String, String> request(Context context, String urlString, JSONObject content, String cookie) throws IOException { HttpURLConnection urlConnection = null; HashMap<String, String> result = new HashMap<String, String>(); try { // get compression preference if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } final boolean compress = sMainPrefs.getBoolean(Advanced.COMPRESS, true); // open new URL connection channel. URL url = new URL(urlString); if ("https".equals(url.getProtocol().toLowerCase(Locale.ENGLISH))) { HttpsURLConnection https = (HttpsURLConnection) url.openConnection(); https.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); urlConnection = https; } else { urlConnection = (HttpURLConnection) url.openConnection(); } // some parameters urlConnection.setUseCaches(false); urlConnection.setInstanceFollowRedirects(false); urlConnection.setRequestProperty("Accept", "application/json"); // set cookie (if available) if (null != cookie) { urlConnection.setRequestProperty("Cookie", cookie); } // set the application id if(null != APPLICATION_KEY) urlConnection.setRequestProperty("APPLICATION-KEY", APPLICATION_KEY); // send content (if available) if (null != content) { urlConnection.setDoOutput(true); // When no charset is given in the Content-Type header "ISO-8859-1" should be // assumed (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1). // Because we're uploading UTF-8 the charset should be set to UTF-8. urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); // send content DataOutputStream printout; if (compress) { // do not set content size urlConnection.setRequestProperty("Transfer-Encoding", "chunked"); urlConnection.setRequestProperty("Content-Encoding", "gzip"); GZIPOutputStream zipStream = new GZIPOutputStream( urlConnection.getOutputStream()); printout = new DataOutputStream(zipStream); } else { // set content size // The content length should be in bytes. We cannot use string length here // because that counts the number of chars while not accounting for multibyte // chars int contentLength = content.toString().getBytes("UTF-8").length; urlConnection.setFixedLengthStreamingMode(contentLength); urlConnection.setRequestProperty("Content-Length", "" + contentLength); printout = new DataOutputStream(urlConnection.getOutputStream()); } // Write the string in UTF-8 encoding printout.write(content.toString().getBytes("UTF-8")); printout.flush(); printout.close(); } // get response, or read error message InputStream inputStream; try { inputStream = urlConnection.getInputStream(); } catch (IOException e) { inputStream = urlConnection.getErrorStream(); } if (null == inputStream) { throw new IOException("could not get InputStream"); } BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream), 1024); String line; StringBuffer responseContent = new StringBuffer(); while ((line = reader.readLine()) != null) { responseContent.append(line); responseContent.append('\r'); } result.put(RESPONSE_CONTENT, responseContent.toString()); result.put(RESPONSE_CODE, Integer.toString(urlConnection.getResponseCode())); // clean up reader.close(); reader = null; inputStream.close(); inputStream = null; // get headers Map<String, List<String>> headerFields = urlConnection.getHeaderFields(); String key, valueString; List<String> value; for (Entry<String, List<String>> entry : headerFields.entrySet()) { key = entry.getKey(); value = entry.getValue(); if (null != key && null != value) { key = key.toLowerCase(Locale.ENGLISH); valueString = value.toString(); valueString = valueString.substring(1, valueString.length() - 1); result.put(key, valueString); } } return result; } finally { if (urlConnection != null) { urlConnection.disconnect(); } } } /** * Request a password reset for the given email address. * * This function does not use the authentication API and is therefore deprecated * * @param context * Application context, used for getting preferences. * @param email * Email address for the account that you want to regain access to. * @return <code>true</code> if the request wasw accepted * @throws IOException * In case of communication failure to CommonSense * @throws JSONException * In case of unparseable response from CommonSense */ @Deprecated public static boolean resetPassword(Context context, String email) throws IOException, JSONException { if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); // prepare request String url = devMode ? SenseUrls.FORGOT_PASSWORD_DEV : SenseUrls.FORGOT_PASSWORD; JSONObject content = new JSONObject(); content.put("email", email); // perform request Map<String, String> result = request(context, url, content, null); // check response code String responseCode = result.get(RESPONSE_CODE); if ("200".equals(responseCode)) { return true; } else { Log.w(TAG, "Failed to request password reset! Response code " + responseCode); return false; } } /** * Request a password reset for the given username. * * @param context * Application context, used for getting preferences. * @param email * Email address for the account that you want to regain access to. * @return <code>true</code> if the request wasw accepted * @throws IOException * In case of communication failure to CommonSense * @throws JSONException * In case of unparseable response from CommonSense */ public static boolean resetPasswordRequest(Context context, String username) throws IOException, JSONException { if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); // prepare request String url = devMode ? SenseUrls.RESET_PASSWORD_REQUEST_DEV : SenseUrls.RESET_PASSWORD_REQUEST; JSONObject content = new JSONObject(); content.put("username", username); // TODO disable compressed Boolean useCompressed = sMainPrefs.getBoolean(SensePrefs.Main.Advanced.COMPRESS, false); sMainPrefs.edit().putBoolean(SensePrefs.Main.Advanced.COMPRESS, false).commit(); // perform actual request // set the application id for the login call APPLICATION_KEY = sMainPrefs.getString(SensePrefs.Main.APPLICATION_KEY, null); // perform request Map<String, String> result = request(context, url, content, null); APPLICATION_KEY = null; // set previous value sMainPrefs.edit().putBoolean(SensePrefs.Main.Advanced.COMPRESS, useCompressed).commit(); // check response code String responseCode = result.get(RESPONSE_CODE); if ("202".equals(responseCode)) { return true; } else { Log.w(TAG, "Failed to request password reset! Response code " + responseCode); return false; } } /** * Change the password of the current user. * * @param context * Application context, used for getting preferences. * @param current_password * The current (hashed) password of the user * @param new_password * The new (hashed) password of the user * @return <code>true</code> if the password was changed * @throws IOException * In case of communication failure to CommonSense * @throws JSONException * In case of unparseable response from CommonSense */ public static boolean changePassword(Context context, String current_password, String new_password) throws IOException, JSONException { if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); // prepare request String url = devMode ? SenseUrls.CHANGE_PASSWORD_DEV : SenseUrls.CHANGE_PASSWORD; String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); JSONObject content = new JSONObject(); content.put("current_password", current_password); content.put("new_password", new_password); // perform request Map<String, String> result = request(context, url, content, cookie); // check response code String responseCode = result.get(RESPONSE_CODE); if ("200".equals(responseCode)) { return true; } else { Log.w(TAG, "Failed to change the password! Response code " + responseCode); return false; } } /** * Shares a sensor with a user or group * * @param context * Context for getting preferences * @param sensorId * Id of the sensor to share * @param userId * Id of the user or group to share the sensor with * @return true if shared successfully, false otherwise * @throws JSONException * In case of unparseable response from CommonSense * @throws IOException * In case of communication failure to CommonSense */ public static boolean shareSensor(Context context, String sensorId, String userId) throws JSONException, IOException { if (null == sAuthPrefs) { sAuthPrefs = context.getSharedPreferences(SensePrefs.AUTH_PREFS, Context.MODE_PRIVATE); } if (null == sMainPrefs) { sMainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); } String cookie = sAuthPrefs.getString(Auth.LOGIN_COOKIE, null); boolean devMode = sMainPrefs.getBoolean(Advanced.DEV_MODE, false); String url = devMode ? SenseUrls.SENSOR_USERS_DEV : SenseUrls.SENSOR_USERS; url = url.replaceFirst("%1", sensorId); // create JSON object to POST final JSONObject data = new JSONObject(); final JSONObject user = new JSONObject(); user.put("id", userId); data.put("user", user); // perform actual request Map<String, String> response = SenseApi.request(context, url, data, cookie); String responseCode = response.get(RESPONSE_CODE); boolean result = false; if ("201".equalsIgnoreCase(responseCode)) { result = true; } else { Log.w(TAG, "Failed to share sensor! Response code: " + responseCode); result = false; } return result; } }