package nl.sense_os.service.commonsense.senddata; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import nl.sense_os.service.SenseService; import nl.sense_os.service.MsgHandler; import nl.sense_os.service.R; import nl.sense_os.service.commonsense.SenseApi; import nl.sense_os.service.constants.SenseDataTypes; import nl.sense_os.service.constants.SensePrefs; import nl.sense_os.service.constants.SensePrefs.Main; import nl.sense_os.service.constants.SenseUrls; import nl.sense_os.service.constants.SensorData.DataPoint; import nl.sense_os.service.storage.LocalStorage; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; /** * Handler for transmit tasks of recently added data. Updates {@link DataPoint#TRANSMIT_STATE} of * the data points after the transmission is completed successfully. Note that this handler is * re-usable: every time the handler receives a message, it gets the latest data in a Cursor and * sends it to CommonSense. * * @author Steven Mulder <steven@sense-os.nl> */ public class BufferTransmitHandler extends Handler { class SensorDataEntry { String sensorId; String sensorName; String sensorDescription; JSONArray data; } private static final String TAG = "BatchDataTransmitHandler"; private static final int MAX_POST_DATA = 10000; private final Uri contentUri; private final WeakReference<Context> ctxRef; private final WeakReference<LocalStorage> storageRef; private final String url; private final DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.ENGLISH); private final NumberFormat dateFormatter = new DecimalFormat("##########.###", symbols); public BufferTransmitHandler(Context context, LocalStorage storage, Looper looper) { super(looper); this.ctxRef = new WeakReference<Context>(context); this.storageRef = new WeakReference<LocalStorage>(storage); contentUri = Uri.parse("content://" + context.getString(R.string.local_storage_authority) + DataPoint.CONTENT_URI_PATH); SharedPreferences mainPrefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); boolean devMode = mainPrefs.getBoolean(Main.Advanced.DEV_MODE, false); url = devMode ? SenseUrls.SENSOR_DATA_MULTIPLE_DEV : SenseUrls.SENSOR_DATA_MULTIPLE; } /** * Cleans up after transmission is over. Closes the Cursor with the data and releases the wake * lock. Should always be called after transmission, even if the attempt failed. * * @param cursor */ private void cleanup(Cursor cursor, WakeLock wakeLock) { if (null != cursor) { cursor.close(); cursor = null; } if (null != wakeLock) { wakeLock.release(); wakeLock = null; } } private List<SensorDataEntry> getSensorDataList(Cursor cursor) throws IOException, JSONException { // map of transmission entries, indexed by the sensor name and description Map<String, SensorDataEntry> map = new HashMap<String, SensorDataEntry>(); String name, description, dataType, value, deviceUuid, valuePath; long timestamp; int points = 0; while ((points < MAX_POST_DATA) && !cursor.isAfterLast()) { // get the data point details try { name = cursor.getString(cursor.getColumnIndexOrThrow(DataPoint.SENSOR_NAME)); description = cursor.getString(cursor .getColumnIndexOrThrow(DataPoint.SENSOR_DESCRIPTION)); valuePath = cursor.getString(cursor.getColumnIndexOrThrow(DataPoint.VALUE_PATH)); dataType = cursor.getString(cursor.getColumnIndexOrThrow(DataPoint.DATA_TYPE)); value = cursor.getString(cursor.getColumnIndexOrThrow(DataPoint.VALUE)); timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(DataPoint.TIMESTAMP)); deviceUuid = cursor.getString(cursor.getColumnIndexOrThrow(DataPoint.DEVICE_UUID)); // set default sensor ID if it is missing deviceUuid = deviceUuid != null ? deviceUuid : SenseApi.getDefaultDeviceUuid(ctxRef .get()); } catch (IllegalArgumentException e) { // something is wrong with this data point, skip it Log.w(TAG, "Exception getting data point details from cursor: '" + e.getMessage() + "'. Skip data point..."); cursor.moveToNext(); continue; } /* * "normal" data is added to the map until we reach the max amount of points */ if (!dataType.equals(SenseDataTypes.FILE)) { // construct JSON representation of the value JSONObject jsonDataPoint = new JSONObject(); jsonDataPoint.put("date", dateFormatter.format(timestamp / 1000d)); jsonDataPoint.put("value", value); jsonDataPoint.put("value_path", valuePath); // put the new value Object in the appropriate sensor's data String key = name + description; SensorDataEntry sensorEntry = map.get(key); JSONArray data = null; if (sensorEntry == null) { sensorEntry = new SensorDataEntry(); String id = SenseApi.getSensorId(ctxRef.get(), name, description, dataType, deviceUuid); if (null == id) { // skip sensor data that does not have a sensor ID yet Log.w(TAG, "cannot find sensor ID for " + name + " (" + description + ")"); cursor.moveToNext(); continue; } sensorEntry.sensorId = id; sensorEntry.sensorName = name; sensorEntry.sensorDescription = description; data = new JSONArray(); } else { data = sensorEntry.data; } data.put(jsonDataPoint); sensorEntry.data = data; map.put(key, sensorEntry); // count the added point to the total number of sensor data points++; } else { // if the data type is a "file", we need special handling sendFile(name, description, dataType, deviceUuid, value, timestamp); } cursor.moveToNext(); } return new ArrayList<BufferTransmitHandler.SensorDataEntry>(map.values()); } /** * @return Cursor with the data points that have to be sent to CommonSense. */ private Cursor getUnsentData() { try { String where = DataPoint.TRANSMIT_STATE + "=0"; String sortOrder = DataPoint.TIMESTAMP + " ASC"; Cursor unsent = storageRef.get().query(contentUri, null, where, null, sortOrder); if (null != unsent) { Log.v(TAG, "Found " + unsent.getCount() + " unsent data points in local storage"); } else { Log.w(TAG, "Failed to get unsent recent data points from local storage"); } return unsent; } catch (IllegalArgumentException e) { Log.e(TAG, "Error querying Local Storage!", e); return null; } } @Override public void handleMessage(Message msg) { String cookie = msg.getData().getString("cookie"); // check if our references are still valid if (null == ctxRef.get() || null == storageRef.get()) { // parent service has died return; } WakeLock wakeLock = null; Cursor cursor = null; try { // make sure the device stays awake while transmitting PowerManager powerMgr = (PowerManager) ctxRef.get().getSystemService( Context.POWER_SERVICE); wakeLock = powerMgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); wakeLock.acquire(); cursor = getUnsentData(); if (null != cursor && cursor.moveToFirst()) { transmit(cursor, cookie); } else { // nothing to transmit } } catch (Exception e) { if (null != e.getMessage()) { Log.e(TAG, "Exception sending buffered data: '" + e.getMessage() + "'. Data will be resent later."); } else { Log.e(TAG, "Exception sending cursor data. Data will be resent later.", e); } } finally { cleanup(cursor, wakeLock); } } /** * Performs cleanup tasks after transmission was successfully completed. Should update the data * point records to show that they have been sent to CommonSense. * * @param sensorDatas * List of data that was sent to CommonSense. Contains all the data points that were * transmitted. * @throws Exception */ private void onTransmitSuccess(List<SensorDataEntry> sensorDatas) throws JSONException{ // log our great success Log.i(TAG, "Sent recent sensor data from the local storage!"); // new content values with updated transmit state ContentValues values = new ContentValues(); values.put(DataPoint.TRANSMIT_STATE, 1); for (SensorDataEntry sensorData : sensorDatas) { // get the name of the sensor, to use in the ContentResolver query String sensorName = sensorData.sensorName; String description = sensorData.sensorDescription; // select points for this sensor, between the first and the last time stamp JSONArray dataPoints = sensorData.data; String frstTimeStamp = dataPoints.getJSONObject(0).getString("date"); String lastTimeStamp = dataPoints.getJSONObject(dataPoints.length() - 1).getString( "date"); long min = Math.round(Double.parseDouble(frstTimeStamp) * 1000); long max = Math.round(Double.parseDouble(lastTimeStamp) * 1000); String where = DataPoint.SENSOR_NAME + "='" + sensorName + "'" + " AND " + DataPoint.SENSOR_DESCRIPTION + "='" + description + "'" + " AND " + DataPoint.TIMESTAMP + ">=" + min + " AND " + DataPoint.TIMESTAMP + " <=" + max; // update points in local storage try { int updated = storageRef.get().update(contentUri, values, where, null); if (updated == dataPoints.length()) { Log.v(TAG, "Updated all " + updated + " '" + sensorName + "' data points in the local storage"); } else { Log.w(TAG, "Wrong number of '" + sensorName + "' data points updated after transmission! " + updated + " vs. " + dataPoints.length()); } } catch (IllegalArgumentException e) { Log.e(TAG, "Error updating points in Local Storage!", e); } } } /** * POSTs the sensor data points to the main sensor data URL at CommonSense. * * @param cookie * * @param transmission * JSON Object with data points for transmission * @return true if successfully sent * @throws JSONException * @throws MalformedURLException */ private boolean postData(String cookie, JSONObject transmission) throws JSONException, MalformedURLException { Map<String, String> response = null; try { response = SenseApi.request(ctxRef.get(), url, transmission, cookie); } catch (IOException e) { // handle failure later } boolean result = false; if (response == null) { // Error when sending Log.w(TAG, "Failed to send buffered data points.\nData will be retried later."); result = false; } else if (response.get(SenseApi.RESPONSE_CODE).compareToIgnoreCase("201") != 0) { // incorrect status code String statusCode = response.get(SenseApi.RESPONSE_CODE); // if un-authorized: relogin if (statusCode.compareToIgnoreCase("403") == 0) { Log.e(TAG, "You are not logged into sense. In order to use sense service, please login using SwanLake app"); final Intent serviceIntent = new Intent(ctxRef.get().getString( R.string.action_sense_service)); serviceIntent.putExtra(SenseService.EXTRA_RELOGIN, true); ctxRef.get().startService(serviceIntent); } // Show the HTTP response Code Log.w(TAG, "Failed to send buffered data points: " + statusCode + ", Response content: '" + response.get(SenseApi.RESPONSE_CONTENT) + "'\n" + "Data will be retried later"); result = false; } else { // Data sent successfully result = true; } return result; } private void sendFile(String name, String description, String dataType, String deviceUuid, String value, long timestamp) throws JSONException { // create sensor data JSON object with only 1 data point JSONObject sensorData = new JSONObject(); JSONArray dataArray = new JSONArray(); JSONObject data = new JSONObject(); data.put("value", value); data.put("date", dateFormatter.format(timestamp / 1000d)); dataArray.put(data); sensorData.put("data", dataArray); // send data point through MsgHandler Context context = ctxRef.get(); MsgHandler.sendSensorData(context, name, description, dataType, deviceUuid, sensorData); } /** * Transmits the data points from {@link #cursor} to CommonSense. Any "file" type data points * will be sent separately via * {@link MsgHandler#sendSensorData(String, String, String, JSONObject)}. * * @param cookie * @param cursor * * @throws JSONException * @throws IOException */ private void transmit(Cursor cursor, String cookie) throws JSONException, IOException { // continue until all points in the cursor have been sent List<SensorDataEntry> sensorDataList = null; while (!cursor.isAfterLast()) { // organize the data into a hash map sorted by sensor sensorDataList = getSensorDataList(cursor); if (sensorDataList.size() < 1) { // nothing to transmit continue; } // prepare the main JSON object for transmission JSONArray sensors = new JSONArray(); for (SensorDataEntry sensorDataEntry : sensorDataList) { JSONObject transmissionEntry = new JSONObject(); transmissionEntry.put("sensor_id", sensorDataEntry.sensorId); transmissionEntry.put("sensor_name", sensorDataEntry.sensorName); transmissionEntry.put("data", sensorDataEntry.data); sensors.put(transmissionEntry); } JSONObject transmission = new JSONObject(); transmission.put("sensors", sensors); // perform the actual POST request boolean result = postData(cookie, transmission); if (result) { onTransmitSuccess(sensorDataList); } else { // abort! abort! break; } } } }