diff --git a/app/release/app-release-4.1.9.apk b/app/release/app-release-4.1.11.apk similarity index 68% rename from app/release/app-release-4.1.9.apk rename to app/release/app-release-4.1.11.apk index 803d40c..0afab70 100644 Binary files a/app/release/app-release-4.1.9.apk and b/app/release/app-release-4.1.11.apk differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d5c5bf..983cefd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:versionName="4.1.11"> diff --git a/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java b/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java index 6da630f..0b6faf4 100644 --- a/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java @@ -209,17 +209,34 @@ public class ExportDataActivity extends AppCompatActivity // mDateTxt.getText().toString(), mTimeTxt.getText().toString(), mDuration)); Log.d(TAG, String.format("EndDate=%s %s, Duration=%3.1f hrs", mDateTxt.getText().toString(), mTimeTxt.getText().toString(), mDuration)); - ProgressBar pb = (ProgressBar) findViewById(R.id.exportPb); - pb.setIndeterminate(true); - pb.setVisibility(View.VISIBLE); - mExportBtn.setEnabled(false); - mExportBtn.setVisibility(View.INVISIBLE); + showProgressBar(); this.openFile(); } } + public void showProgressBar() { + ProgressBar pb = (ProgressBar) findViewById(R.id.exportPb); + pb.setIndeterminate(true); + pb.setVisibility(View.VISIBLE); + mExportBtn.setEnabled(false); + mExportBtn.setVisibility(View.INVISIBLE); + } + + public void hideProgressBar() { + runOnUiThread(new Runnable() { + public void run() { + ProgressBar pb = (ProgressBar) findViewById(R.id.exportPb); + pb.setIndeterminate(true); + pb.setVisibility(View.INVISIBLE); + mExportBtn.setEnabled(true); + mExportBtn.setVisibility(View.VISIBLE); + + } + }); + } + private void openFile() { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); @@ -248,96 +265,14 @@ public class ExportDataActivity extends AppCompatActivity // Perform operations on the document using its URI. //mUtil.showToast("URI="+uri.toString()); Log.v(TAG, "onActivityResult() - exporting to file " + uri.toString()); - exportToFile(uri); + mLm.exportToCsvFile(mEndDate, mDuration,uri, (boolean b)-> { + Log.v(TAG,"onActivityResult callback"); + hideProgressBar(); + }); } } super.onActivityResult(requestCode, resultCode, resultData); } - private void exportToFile(Uri uri) { - Log.v(TAG, "exportToFile(): uri=" + uri.toString()); - long endDateMillis = mEndDate.getTime(); - long durationMillis = (long) (mDuration * 3600. * 1000); - long startDateMillis = endDateMillis - durationMillis; - Log.v(TAG, "exportToFile() - endDateMillis=" + endDateMillis + ", startDateMillis=" + startDateMillis + ", durationMillis=" + durationMillis); - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - String sDateStr = dateFormat.format(new Date(startDateMillis)); - String eDateStr = dateFormat.format(new Date(endDateMillis)); - Log.v(TAG, "exportToFile() - sDateStr=" + sDateStr + " eDateStr=" + eDateStr); - mLm.getDatapointsByDate( - sDateStr, eDateStr, (String datapointsJsonStr) -> { - Log.v(TAG, "exportToFile() - datapoints=" + datapointsJsonStr); - // Open file for writing - try { - ParcelFileDescriptor pfd = this.getContentResolver(). - openFileDescriptor(uri, "w"); - FileOutputStream fileOutputStream = - new FileOutputStream(pfd.getFileDescriptor()); - fileOutputStream.write(("# dataTime, alarmState, hr, o2sat, accel*125\n").getBytes()); - JSONArray dataObj; - try { - dataObj = new JSONArray(datapointsJsonStr); - Log.v(TAG, "exportToFile() - dataObj length=" + dataObj.length()); - for (int i = 0; i < dataObj.length(); i++) { - JSONObject datapointJsonObj = dataObj.getJSONObject(i); - String dataJsonStr = datapointJsonObj.getString("dataJSON"); - Log.v(TAG, "exportToFile() - i=" + i + "dataJsonStr=" + dataJsonStr); - JSONObject dataJsonObj = new JSONObject(dataJsonStr); - JSONArray rawDataArr = dataJsonObj.getJSONArray("rawData"); - try { - fileOutputStream.write(dataJsonObj.getString("dataTime").getBytes(StandardCharsets.UTF_8)); - fileOutputStream.write(", ".getBytes(StandardCharsets.UTF_8)); - fileOutputStream.write(dataJsonObj.getString("alarmState").getBytes(StandardCharsets.UTF_8)); - fileOutputStream.write(", ".getBytes(StandardCharsets.UTF_8)); - fileOutputStream.write(dataJsonObj.getString("hr").getBytes(StandardCharsets.UTF_8)); - fileOutputStream.write(", ".getBytes(StandardCharsets.UTF_8)); - fileOutputStream.write(dataJsonObj.getString("o2Sat").getBytes(StandardCharsets.UTF_8)); - for (int j = 0; j < rawDataArr.length(); j++) { - fileOutputStream.write(", ".getBytes(StandardCharsets.UTF_8)); - fileOutputStream.write(rawDataArr.getString(j).getBytes(StandardCharsets.UTF_8)); - } - fileOutputStream.write("\n".getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - Log.e(TAG, "exportToFile() - ERROR Writing File: " + e.toString()); - //mUtil.showToast("ERROR WRITING FILE"); - } - - } - } catch (JSONException | NullPointerException e) { - Log.v(TAG, "createEventCallback(): Error Creating JSON Object from string " + datapointsJsonStr); - dataObj = null; - mUtil.showToast(getString(R.string.error_exporting_data)); - Log.e(TAG, "exportToFile() - JSONException: " + e.toString()); - } - // Let the document provider know you're done by closing the stream. - fileOutputStream.close(); - pfd.close(); - mUtil.showToast(getString(R.string.data_exported_ok)); - - } catch (FileNotFoundException e) { - e.printStackTrace(); - mUtil.showToast(getString(R.string.error_exporting_data)); - Log.e(TAG, "exportToFile() - FileNotFoundException: " + e.toString()); - } catch (IOException e) { - e.printStackTrace(); - mUtil.showToast(getString(R.string.error_exporting_data)); - Log.e(TAG, "exportToFile() - IOException: " + e.toString()); - } - runOnUiThread(new Runnable() { - public void run() { - ProgressBar pb = (ProgressBar) findViewById(R.id.exportPb); - pb.setIndeterminate(true); - pb.setVisibility(View.INVISIBLE); - mExportBtn.setEnabled(true); - mExportBtn.setVisibility(View.VISIBLE); - - } - }); - - }); - } - - - } diff --git a/app/src/main/java/uk/org/openseizuredetector/LogManager.java b/app/src/main/java/uk/org/openseizuredetector/LogManager.java index c4c7dd3..f3c3fb0 100644 --- a/app/src/main/java/uk/org/openseizuredetector/LogManager.java +++ b/app/src/main/java/uk/org/openseizuredetector/LogManager.java @@ -30,17 +30,25 @@ import android.database.DatabaseUtils; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; import android.os.AsyncTask; import android.os.CountDownTimer; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.preference.PreferenceManager; import android.text.format.Time; import android.util.Log; +import android.view.View; +import android.widget.ProgressBar; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -66,20 +74,20 @@ import java.util.HashMap; * - Query the local database to return all datapoints within +/- EventDuration/2 minutes of the event. * - Upload the datapoints, linking them to the new eventID. * - Mark all the uploaded datapoints as uploaded. - * + *

* Event statuses: - * 0 - OK - * 1 - WARNING - * 2 - ALARM - * 3 - FALL - * 4 - FAULT - * 5 - Manual Alarm - * 6 - NDA (Normal Daily Activities) - * - * NDA Timer creates an event periodically to record Normal Daily Activities (NDA), - * irrespective of the alarm state. This will upload a lot of data, so it will only run - * for 24 hours after being activated before shutting down requring the user to re-select - * the option to log NDA to re-start it. + * 0 - OK + * 1 - WARNING + * 2 - ALARM + * 3 - FALL + * 4 - FAULT + * 5 - Manual Alarm + * 6 - NDA (Normal Daily Activities) + *

+ * NDA Timer creates an event periodically to record Normal Daily Activities (NDA), + * irrespective of the alarm state. This will upload a lot of data, so it will only run + * for 24 hours after being activated before shutting down requring the user to re-select + * the option to log NDA to re-start it. */ public class LogManager { static final private String TAG = "LogManager"; @@ -122,6 +130,11 @@ public class LogManager { void accept(ArrayList> retVal); } + public interface BooleanCallback { + void accept(boolean retVal); + } + + public LogManager(Context context, boolean logRemote, boolean logRemoteMobile, String authToken, long eventDuration, long remoteLogPeriod, @@ -242,21 +255,21 @@ public class LogManager { String val; val = c.getString(c.getColumnIndex("id")); // We replace null values with empty string, otherwise they are completely excluded from output JSON. - event.put("id", val==null ? "" : val ); + event.put("id", val == null ? "" : val); val = c.getString(c.getColumnIndex("dataTime")); - event.put("dataTime", val==null ? "" : val); + event.put("dataTime", val == null ? "" : val); val = c.getString(c.getColumnIndex("status")); - event.put("status", val==null ? "" : val); + event.put("status", val == null ? "" : val); val = c.getString(c.getColumnIndex("type")); - event.put("type", val==null ? "" : val); + event.put("type", val == null ? "" : val); val = c.getString(c.getColumnIndex("subType")); - event.put("subType", val==null ? "" : val); + event.put("subType", val == null ? "" : val); val = c.getString(c.getColumnIndex("notes")); - event.put("desc", val==null ? "" : val); + event.put("desc", val == null ? "" : val); val = c.getString(c.getColumnIndex("dataJSON")); - event.put("dataJSON", val==null ? "" : val); + event.put("dataJSON", val == null ? "" : val); val = c.getString(c.getColumnIndex("uploaded")); - event.put("uploaded", val==null ? "" : val); + event.put("uploaded", val == null ? "" : val); c.moveToNext(); eventsArray.put(i, event); i++; @@ -342,7 +355,7 @@ public class LogManager { if (sdData.alarmState != 0) { Log.i(TAG, "writeDatapointToLocalDb(): adding event to local DB"); - createLocalEvent(dateStr,sdData.alarmState,null, null, null, sdData.toSettingsJSON()); + createLocalEvent(dateStr, sdData.alarmState, null, null, null, sdData.toSettingsJSON()); } } catch (SQLException e) { Log.e(TAG, "writeToLocalDb(): Error Writing Data: " + e.toString()); @@ -358,18 +371,18 @@ public class LogManager { public boolean createLocalEvent(String dataTime, long status, String type, String subType, String desc, String dataJSON) { // Expects dataTime to be in format: SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Log.d(TAG, "createLocalEvent() - dataTime=" + dataTime + ", status=" + status + ", dataJSON="+dataJSON); + Log.d(TAG, "createLocalEvent() - dataTime=" + dataTime + ", status=" + status + ", dataJSON=" + dataJSON); // Write Event to database ContentValues values = new ContentValues(); values.put("dataTime", dataTime); values.put("status", status); values.put("type", type); - values.put("subType",subType); - values.put("notes",desc); + values.put("subType", subType); + values.put("notes", desc); values.put("dataJSON", dataJSON); long newRowId = mOsdDb.insert(mEventsTableName, null, values); - Log.d(TAG, "createLocalEvent(): Created Row ID"+newRowId); + Log.d(TAG, "createLocalEvent(): Created Row ID" + newRowId); return true; } @@ -472,13 +485,118 @@ public class LogManager { if (cursor != null) { callback.accept(cursor2Json(cursor)); } else { - Log.w(TAG,"getDatapointsByDate() - returned null result"); + Log.w(TAG, "getDatapointsByDate() - returned null result"); callback.accept(null); } }).execute(); return (true); } + /** + * exportToFile - export datapoints data to a csv file on the android device. + * + * @param endDate end date of period to export (Date type) + * @param duration duration in hours of period to export (double) + * @param uri uri of file to save. + */ + public void exportToCsvFile(Date endDate, double duration, Uri uri, BooleanCallback callback) { + Log.v(TAG, "exportToCsvFile(): uri=" + uri.toString()); + long endDateMillis = endDate.getTime(); + long durationMillis = (long) (duration * 3600. * 1000); + long startDateMillis = endDateMillis - durationMillis; + Log.v(TAG, "exportToCsvFile() - endDateMillis=" + endDateMillis + ", startDateMillis=" + startDateMillis + ", durationMillis=" + durationMillis); + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String sDateStr = dateFormat.format(new Date(startDateMillis)); + String eDateStr = dateFormat.format(new Date(endDateMillis)); + Log.v(TAG, "exportToFile() - sDateStr=" + sDateStr + " eDateStr=" + eDateStr); + String[] columns = {"*"}; + String whereClause = "DataTime>? AND DataTime { + Log.v(TAG, "exportToCsvFile - returned " + cursor); + if (cursor != null) { + Log.d(TAG, "we got a cursor!"); + try { + ParcelFileDescriptor pfd = mContext.getContentResolver(). + openFileDescriptor(uri, "w"); + FileOutputStream fileOutputStream = + new FileOutputStream(pfd.getFileDescriptor()); + fileOutputStream.write(("# dataTime, alarmState, hr, o2sat, accel*125\n").getBytes()); + writeDatapointsToFile(cursor, fileOutputStream); + // Let the document provider know you're done by closing the stream. + fileOutputStream.close(); + pfd.close(); + callback.accept(true); + } catch (FileNotFoundException e) { + e.printStackTrace(); + mUtil.showToast(mContext.getString(R.string.error_exporting_data)); + Log.e(TAG, "exportToFile() - FileNotFoundException: " + e.toString()); + callback.accept(false); + } catch (IOException e) { + e.printStackTrace(); + mUtil.showToast(mContext.getString(R.string.error_exporting_data)); + Log.e(TAG, "exportToFile() - IOException: " + e.toString()); + callback.accept(false); + } + + } else { + Log.w(TAG, "exportToCsvFile() - returned null result"); + callback.accept(false); + } + }).execute(); + return; + } + + private void writeDatapointsToFile(Cursor c, FileOutputStream fileOutputStream) { + Log.v(TAG, "writeDatapointsToFile()"); + JSONArray dataObj; + String dataJsonStr; + JSONObject dataJsonObj; + JSONArray rawDataArr; + Log.d(TAG,"writeDatapointsToFile()" + c.getColumnNames()); + //for (int i=0;i= mNDALogPeriodHours) { Log.i(TAG, "mNDATimer - onFinish - NDA logging period completed - switching off NDA Logging"); @@ -1307,8 +1430,8 @@ public class LogManager { editor.apply(); } else { // Restart this timer. - Log.i(TAG,"NDATimer - tDiffMillis="+tDiffMillis+", tdiffHrs = "+tDiffHrs+ ", tnow="+tNow+", tstart="+mNDATimerStartTime+", NDALogPeriod="+mNDALogPeriodHours); - Log.i(TAG,"NDATimer - re-starting NDA timer"); + Log.i(TAG, "NDATimer - tDiffMillis=" + tDiffMillis + ", tdiffHrs = " + tDiffHrs + ", tnow=" + tNow + ", tstart=" + mNDATimerStartTime + ", NDALogPeriod=" + mNDALogPeriodHours); + Log.i(TAG, "NDATimer - re-starting NDA timer"); start(); } } diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java index 07e9cb7..de3904b 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java @@ -108,6 +108,10 @@ public abstract class SdDataSource { private Time mHrStatusTime; private double mHrFrozenPeriod = 60; // seconds private boolean mHrFrozenAlarm; + private boolean mFidgetDetectorEnabled; + private double mFidgetPeriod; + private double mFidgetThreshold; + private Time mLastFidget = null; public SdDataSource(Context context, Handler handler, SdDataReceiver sdDataReceiver) { @@ -723,6 +727,31 @@ public abstract class SdDataSource { } + private double calcRawDataStd(SdData sdData) { + /** + * Calculate the standard deviation in % of the rawData array in the SdData instance provided. + * It assumes that rawdata will contain 125 samples. + * Returns the standard deviation in %. + */ + // FIXME - assumes length of rawdata array is 125 data points + int j; + double sum = 0.0; + for (j = 0; j < 125; j++) { // FIXME - assumed length! + sum += sdData.rawData[j]; + } + double mean = sum / 125; + + double standardDeviation = 0.0; + for (j = 0; j < 125; j++) { // FIXME - assumed length! + standardDeviation += Math.pow(sdData.rawData[j] - mean, 2); + } + standardDeviation = Math.sqrt(standardDeviation / 125); // FIXME - assumed length! + + // Convert standard deviation from milli-g to % + standardDeviation = 100. * standardDeviation / mean; + return (standardDeviation); + } + /** * Checks the status of the connection to the watch, * and sets class variables for use by other functions. @@ -758,6 +787,23 @@ public abstract class SdDataSource { } } else { mSdData.watchAppRunning = true; + + // Check we have seen a fidget within the required period, or else assume a fault because watch is not being worn + if (mFidgetDetectorEnabled) { + if (mLastFidget == null) mLastFidget = tnow; // Initialise last fidget time on startup. + + double accStd = calcRawDataStd(mSdData); + if (accStd > mFidgetThreshold) { + mLastFidget = tnow; + } else { + Log.d(TAG,"onStatus() - Fidget Detector - low movement - is watch being worn?"); + tdiff = (tnow.toMillis(false) - mLastFidget.toMillis(false)); + if (tdiff > (mFidgetPeriod) * 60 * 1000) { + Log.e(TAG, "onStatus() - Fidget Not Detected - is watch being worn?"); + mSdDataReceiver.onSdDataFault(mSdData); + } + } + } } // if we have confirmation that the app is running, reset the @@ -877,6 +923,20 @@ public abstract class SdDataSource { toast.show(); } + // Parse the Fidget Detector settings. + try { + mFidgetDetectorEnabled = SP.getBoolean("FidgetDetectorEnabled", false); + mFidgetPeriod = readDoublePref(SP, "FidgetDetectorPeriod", "20"); // minutes + Log.v(TAG, "updatePrefs() - mFidgetPeriod = " + mFidgetPeriod); + mFidgetThreshold = readDoublePref(SP, "FidgetDetectorThreshold", "5"); + Log.d(TAG,"updatePrefs(): mFidgetThreshold="+mFidgetThreshold); + + } catch (Exception ex) { + Log.v(TAG, "updatePrefs() - Problem with FidgetDetector preferences!"); + Toast toast = Toast.makeText(mContext, "Problem Parsing FidgetPeriod Preference", Toast.LENGTH_SHORT); + toast.show(); + } + // Watch Settings String prefStr; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 645d309..9232bb0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ OpenSeizureDetector "\n - \mV4.1.11 - Changed target Android Version to 13 (SDK33) (Play Store policy). + \mV4.1.11 - Fixed issue with data export crashing when lots of data requested. Added simple 'Fidget Detector' to detect if watch has fallen off the wrist. Changed target Android Version to 13 (SDK33) (Play Store policy). \nV4.1.10 - Added warning if heart rate readings freeze and do not change for more than 1 minute. \nV4.1.9 - Fixed problem with average heart rate alarm Fixed issue with phone data source generating continuous alarms for Heart Rate or O2Sat @@ -503,4 +503,11 @@ *** ERROR Exporting Data *** Heart Rate measurement Frozen Warning Produce a fault warning if the heart rate measurement freezes and does not change for more than 1 minute. + Fidget Detector Settings + Enable Fidget Detector + Generates a fault if no movement has been detected for a specified period (signifying the watch has been removed) + The threshold (as % standard deviation) applied to each set of accelerometer data to determine if a \'Fidget\' has occurred. + Fidget Detector Threshold (%) + Fidget Detector Period (minutes) + A fault is generated if no movement (fidgets) are detected for more than the Fidget Detector Period. diff --git a/app/src/main/res/xml/seizure_detector_prefs.xml b/app/src/main/res/xml/seizure_detector_prefs.xml index d5bfa93..a7e1b60 100644 --- a/app/src/main/res/xml/seizure_detector_prefs.xml +++ b/app/src/main/res/xml/seizure_detector_prefs.xml @@ -186,5 +186,22 @@ android:title="@string/fall_window_title" /> + + + + +