diff --git a/app/release/app-release-4.1.9.apk b/app/release/app-release-4.1.12.apk similarity index 67% rename from app/release/app-release-4.1.9.apk rename to app/release/app-release-4.1.12.apk index 803d40c..3b16726 100644 Binary files a/app/release/app-release-4.1.9.apk and b/app/release/app-release-4.1.12.apk differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 676845d..6d178ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ + android:versionCode="128" + android:versionName="4.2.1b"> + @@ -22,6 +23,10 @@ + + + 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..02c6577 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"; @@ -97,7 +105,7 @@ public class LogManager { public double mNDATimeRemaining; // hours public double mNDALogPeriodHours = 24.0; // hours private static Context mContext; - private OsdUtil mUtil; + private static OsdUtil mUtil; public static WebApiConnection mWac; public static final boolean USE_FIREBASE_BACKEND = false; @@ -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,32 @@ 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); } + /** + * exportToCsvFile - 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. + * @param callback function to be called on completion of the task (returns true on success, false on error) + */ + public void exportToCsvFile(Date endDate, double duration, Uri uri, BooleanCallback callback) { + Log.v(TAG, "exportToCsvFile(): uri=" + uri.toString()); + new ExportDataTask(endDate, duration, uri, (boolean retVal) -> { + Log.v(TAG, "exportToCsvFile - returned " + retVal); + callback.accept(retVal); + }).execute(); + return; + } + + + /** * Return an array list of objects representing the events in the database by calling the specified callback function. @@ -486,7 +518,8 @@ public class LogManager { * @param includeWarnings - whether to include warnings in the list of events, or just alarm conditions. * @return True on successful start or false if call fails. */ - public boolean getEventsList(boolean includeWarnings, ArrayListCallback callback) { + public boolean getEventsList(boolean includeWarnings, ArrayListCallback + callback) { Log.v(TAG, "getEventsList - includeWarnings=" + includeWarnings); ArrayList> eventsList = new ArrayList<>(); @@ -572,7 +605,8 @@ public class LogManager { * @param includeWarnings - whether to include warnings in the list of events, or just alarm conditions. * @return True on successful start or false if call fails. */ - public boolean getNextEventToUpload(boolean includeWarnings, WebApiConnection.LongCallback callback) { + public boolean getNextEventToUpload(boolean includeWarnings, WebApiConnection. + LongCallback callback) { Log.v(TAG, "getNextEventToUpload - includeWarnings=" + includeWarnings); String[] whereArgsStatus = getEventWhereArgs(includeWarnings); @@ -619,7 +653,8 @@ public class LogManager { * * @return True on successful start or false if call fails. */ - public boolean getNearestDatapointToDate(String dateStr, WebApiConnection.LongCallback callback) { + public boolean getNearestDatapointToDate(String + dateStr, WebApiConnection.LongCallback callback) { Log.v(TAG, "getNextEventToDate - dateStr=" + dateStr); String[] columns = {"*", "(julianday(dataTime)-julianday(datetime('" + dateStr + "'))) as ddiff"}; //SQLStr = "SELECT *, (julianday(dataTime)-julianday(datetime('" + dateStr + "'))) as ddiff from " + mDbTableName + " order by ABS(ddiff) asc;"; @@ -652,7 +687,8 @@ public class LogManager { * @param includeWarnings - whether to include warnings in the list of events, or just alarm conditions. * @return True on successful start or false if call fails. */ - public boolean getLocalEventsCount(boolean includeWarnings, WebApiConnection.LongCallback callback) { + public boolean getLocalEventsCount(boolean includeWarnings, WebApiConnection. + LongCallback callback) { //Log.v(TAG, "getLocalEventsCount- includeWarnings=" + includeWarnings); String[] whereArgs = getEventWhereArgs(includeWarnings); String whereClause = getEventWhereClause(includeWarnings); @@ -755,6 +791,158 @@ public class LogManager { } } + //query(String table, String[] columns, String selection, String[] selectionArgs, + // String groupBy, String having, String orderBy) + + /** + * Exports the contents of the local datapoints table between given dates + * to a .csv file. + * Use as new ExportDataTask(xxx,xxx,xx,xxxx).execute() + */ + static private class ExportDataTask extends AsyncTask { + BooleanCallback mCallback; + Date mEndDate; + double mDuration; + Uri mUri; + + + ExportDataTask(Date endDate, double duration, Uri uri, BooleanCallback callback) { + Log.i(TAG,"ExportDataTask constructor()"); + this.mCallback = callback; + mEndDate = endDate; + mDuration = duration; + mUri = uri; + } + + @Override + protected Boolean doInBackground(Void... params) { + Log.v(TAG, "ExportDataTask.doInBackground()"); + long endDateMillis = mEndDate.getTime(); + long durationMillis = (long) (mDuration * 3600. * 1000); + long startDateMillis = endDateMillis - durationMillis; + Log.v(TAG, "exportDataTask() - 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, "ExportDataTask.doInBackground - sDateStr=" + sDateStr + " eDateStr=" + eDateStr); + String[] columns = {"*"}; + String whereClause = "DataTime>? AND DataTime= mNDALogPeriodHours) { Log.i(TAG, "mNDATimer - onFinish - NDA logging period completed - switching off NDA Logging"); @@ -1307,8 +1496,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..dbeec10 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", "0.6 "); + 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 12dd401..32ea311 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,11 +3,7 @@ OpenSeizureDetector "\n - \mV4.2.1 - 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 - Fixed a small number of user reported issues (https://github.com/OpenSeizureDetector/Android_Pebble_SD/issues?q=is%3Aissue+milestone%3AV4.1.8) + \nV4.2.1 - Added Machine Learning Model Manager so models can be updated easily. " Please enable the new <b>Data Sharing</b> feature to help improve OpenSeizureDetector!<br/> @@ -509,6 +505,14 @@ AI Model ID ID number of machine learning (AI) model to be used - users should not edit this, but use the model Manager page instead. MlModelManager + 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. + First Fragment Second Fragment diff --git a/app/src/main/res/xml/seizure_detector_prefs.xml b/app/src/main/res/xml/seizure_detector_prefs.xml index 4b5da03..4f74e74 100644 --- a/app/src/main/res/xml/seizure_detector_prefs.xml +++ b/app/src/main/res/xml/seizure_detector_prefs.xml @@ -211,5 +211,22 @@ android:title="@string/fall_window_title" /> + + + + +