diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae2ff2..d581fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ OpenSeizureDetector Android App - Change Log ============================================ - V3.2.0 - (NEXT VERSION!) - - Added neural network based data analysis. + V3.2.0 - jan2020 + - Modified data logging to use sqlite database rather than text files. + - Added facility to upload data to remote server. + - Added support for additional GarminSD settings data fields to record the watch app version number etc and + log that info to the SysLog file. + + V3.1.11 - 23oct2019 - Updated network data source so it displays heart rate data if it is available. diff --git a/app/src/main/java/uk/org/openseizuredetector/DBQueryActivity.java b/app/src/main/java/uk/org/openseizuredetector/DBQueryActivity.java index 0727601..d653e6b 100644 --- a/app/src/main/java/uk/org/openseizuredetector/DBQueryActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/DBQueryActivity.java @@ -1,13 +1,114 @@ package uk.org.openseizuredetector; +import android.app.DatePickerDialog; +import android.app.TimePickerDialog; +import android.content.Context; +import android.os.Handler; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.DatePicker; +import android.widget.EditText; +import android.widget.TimePicker; + +import java.util.Calendar; + +public class DBQueryActivity extends AppCompatActivity + implements View.OnClickListener { + String TAG = "DBQueryActivity"; + Button mDateBtn; + Button mTimeBtn; + Button mExportBtn; + EditText mDateTxt; + EditText mTimeTxt; + EditText mDurationTxt; + + int mYear; + int mMonth; + int mDay; + int mHour; + int mMinute; + double mDuration; + + OsdUtil mUtil; + Handler mHandler; -public class DBQueryActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_dbquery); + + mHandler = new Handler(); + mUtil = new OsdUtil(this, mHandler); + + mDateBtn = (Button)findViewById(R.id.dateBtn); + mDateBtn.setOnClickListener(this); + mTimeBtn = (Button)findViewById(R.id.timeBtn); + mTimeBtn.setOnClickListener(this); + mExportBtn = (Button)findViewById(R.id.exportBtn); + mExportBtn.setOnClickListener(this); + mDateTxt = (EditText)findViewById(R.id.endDateText); + mTimeTxt = (EditText)findViewById(R.id.endTimeText); + mDurationTxt = (EditText)findViewById(R.id.durationText); + + // Get Current Date + final Calendar c = Calendar.getInstance(); + mYear = c.get(Calendar.YEAR); + mMonth = c.get(Calendar.MONTH); + mDay = c.get(Calendar.DAY_OF_MONTH); + mHour = c.get(Calendar.HOUR_OF_DAY); + mMinute = c.get(Calendar.MINUTE); + + mDateTxt.setText(String.format("%02d-%02d-%04d",mDay, mMonth+1, mYear)); + mTimeTxt.setText(String.format("%02d:%02d:%02d", mHour, mMinute, 00)); + mDuration = 2.0; + mDurationTxt.setText(String.format("%03.1f", mDuration)); + + } + + @Override + public void onClick(View view) { + Log.v(TAG, "onClick()"); + if (view == mDateBtn) { + DatePickerDialog datePickerDialog = new DatePickerDialog(this, + new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePicker view, int year, + int monthOfYear, int dayOfMonth) { + mDay = dayOfMonth; + mMonth = monthOfYear; + mYear = year; + mDateTxt.setText(String.format("%02d-%02d-%04d",mDay, mMonth+1, mYear)); + } + }, mYear, mMonth, mDay); + datePickerDialog.show(); + } + if (view == mTimeBtn) { + // Launch Time Picker Dialog + TimePickerDialog timePickerDialog = new TimePickerDialog(this, + new TimePickerDialog.OnTimeSetListener() { + @Override + public void onTimeSet(TimePicker view, int hourOfDay, + int minute) { + mHour = hourOfDay; + mMinute = minute; + mTimeTxt.setText(String.format("%02d:%02d:%02d", mHour, mMinute, 00)); + } + }, mHour, mMinute, false); + timePickerDialog.show(); + } + if (view == mExportBtn) { + mDateTxt.setText(String.format("%02d-%02d-%04d",mDay, mMonth+1, mYear)); + mTimeTxt.setText(String.format("%02d:%02d:%02d", mHour, mMinute, 00)); + mDuration = Double.parseDouble(mDurationTxt.getText().toString()); + + + mUtil.showToast(String.format("EndDate=%s %s, Duration=%3.1f hrs", + mDateTxt.getText().toString(), mTimeTxt.getText().toString() ,mDuration)); + + } } } diff --git a/app/src/main/java/uk/org/openseizuredetector/LogManager.java b/app/src/main/java/uk/org/openseizuredetector/LogManager.java index 2ddc211..761d694 100644 --- a/app/src/main/java/uk/org/openseizuredetector/LogManager.java +++ b/app/src/main/java/uk/org/openseizuredetector/LogManager.java @@ -28,9 +28,25 @@ import android.database.DatabaseUtils; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.os.AsyncTask; +import android.os.CountDownTimer; +import android.os.Handler; import android.text.format.Time; import android.util.Log; +import org.apache.commons.codec.binary.Base64; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + import static android.database.sqlite.SQLiteDatabase.openOrCreateDatabase; /** @@ -47,7 +63,10 @@ public class LogManager { private int mOSDWearerId; private String mOSDUrl; private OsdDbHelper mOSDDb; + private RemoteLogTimer mRemoteLogTimer; private Context mContext; + private OsdUtil mUtil; + public LogManager(boolean logRemote, boolean logRemoteMobile, @@ -64,6 +83,9 @@ public class LogManager { mOSDUrl = OSDUrl; mContext = context; + Handler handler = new Handler(); + mUtil = new OsdUtil(mContext, handler); + try { mOSDDb = new OsdDbHelper(mDbTableName, mContext); if (!checkTableExists(mOSDDb, mDbTableName)) { @@ -72,9 +94,12 @@ public class LogManager { } catch (SQLException e) { Log.e(TAG, "Failed to open Database: " + e.toString()); } + + startRemoteLogTimer(); } + private boolean checkTableExists(OsdDbHelper osdDb, String osdTableName) { Cursor c = null; boolean tableExists = false; @@ -126,8 +151,142 @@ public class LogManager { } + public void writeToRemoteServer() { + Log.v(TAG,"writeToRemoteServer()"); + if (!mLogRemote) { + Log.v(TAG,"mLogRemote not set, not doing anything"); + return; + } + + if (!mLogRemoteMobile) { + // Check network state - are we using mobile data? + if (mUtil.isMobileDataActive()) { + Log.v(TAG,"Using mobile data, so not doing anything"); + return; + } + } + + if (!mUtil.isNetworkConnected()) { + Log.v(TAG,"No network connection - doing nothing"); + return; + } + + Log.v(TAG,"Requirements for remote logging met!"); + uploadSdData(); + } + + /** + * Upload a batch of seizure detector data records to the server.. + * Uses the UploadSdDataTask class to upload the data in the + * background. DownloadSdDataTask.onPostExecute() is called on completion. + */ + public void uploadSdData() { + Log.v(TAG, "uploadSdData()"); + String dataStr = "data string to upload"; + //new PostDataTask().execute("http://" + mOSDUrl + ":8080/data", dataStr, mOSDUname, mOSDPasswd); + new PostDataTask().execute("http://192.168.43.175:8765/datapoints/add", dataStr, mOSDUname, mOSDPasswd); + } + + private class PostDataTask extends AsyncTask { + @Override + protected String doInBackground(String... params) { + // params comes from the execute() call: + // params[0] is the url, + // params[1] is the data to send. + // params[2] is the user name + // params[3] is the password + int MAXLEN = 500; // Maximum length of response that we will accept (bytes) + InputStream is = null; + String urlStr = params[0]; + String dataStr = params[1]; + String uname = params[2]; + String passwd = params[3]; + String resultStr = "Not Initialised"; + Log.v(TAG,"doInBackgound(): url="+urlStr+" data="+dataStr+" uname="+uname+" passwd="+passwd); + try { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setReadTimeout(2000 /* milliseconds */); + conn.setConnectTimeout(5000 /* milliseconds */); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json; utf-8"); + conn.setRequestProperty("Accept", "application/json"); + String auth = uname + ":" + passwd; + byte[] encodedAuth = Base64.encodeBase64(auth.getBytes("utf-8")); + String authHeaderValue = "Basic " + new String(encodedAuth); + conn.setRequestProperty("Authorization", authHeaderValue); + conn.setDoInput(true); + + // Put our data into the outputstream associated with the connection. + OutputStream os = conn.getOutputStream(); + byte[] input = dataStr.getBytes("utf-8"); + os.write(input, 0, input.length); + + // Starts the query + conn.connect(); + int response = conn.getResponseCode(); + Log.d(TAG, "The response code is: " + response); + is = conn.getInputStream(); + + // Convert the InputStream into a string + Reader reader = new InputStreamReader(is, "UTF-8"); + char[] buffer = new char[MAXLEN]; + reader.read(buffer); + resultStr = new String(buffer); + + } catch (IOException e) { + Log.v(TAG,"doInBackground(): IOException - "+e.toString()); + resultStr = "Error"+e.toString(); + + // Makes sure that the InputStream is closed after the app is + // finished using it. + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + Log.v(TAG,"doInBackground(): IOException - "+e.toString()); + resultStr = "Error"+e.toString(); + } + } + } + + if (resultStr.startsWith("Unable to retrieve web page")) { + Log.v(TAG,"doInBackground() - Unable to retrieve data"); + } else { + Log.v(TAG,"doInBackground(): result = "+resultStr); + } + return (resultStr); + + } + // onPostExecute displays the results of the AsyncTask. + @Override + protected void onPostExecute(String result) { + Log.v(TAG,"onPostExecute() - result = "+result); + } + } + + + + public void close() { mOSDDb.close(); + stopRemoteLogTimer(); + } + + + public JSONObject queryDatapoints(String endDateStr, Double duration) { + Log.d(TAG,"queryDatapoints() - endDateStr="+endDateStr); + Cursor c = null; + try { + c = mOSDDb.getWritableDatabase().query(mDbTableName, null, + null, null, null, null, null); + //c.query("Select * from ? where DataTime < ?", mDbTableName, endDateStr); + } + catch (Exception e) { + Log.d(TAG, mDbTableName+" doesn't exist :((("); + } + return(null); } public class OsdDbHelper extends SQLiteOpenHelper { @@ -170,4 +329,54 @@ public class LogManager { onUpgrade(db, oldVersion, newVersion); } } + + /* + * Start the timer that will send and SMS alert after a given period. + */ + private void startRemoteLogTimer() { + if (mRemoteLogTimer != null) { + Log.v(TAG, "startRemoteLogTimer -timer already running - cancelling it"); + mRemoteLogTimer.cancel(); + mRemoteLogTimer = null; + } + Log.v(TAG, "startRemoteLogTimer() - starting RemoteLogTimer"); + mRemoteLogTimer = + new RemoteLogTimer(10 * 1000, 1000); + mRemoteLogTimer.start(); + } + + + /* + * Cancel the SMS timer to prevent the SMS message being sent.. + */ + public void stopRemoteLogTimer() { + if (mRemoteLogTimer != null) { + Log.v(TAG, "stopRemoteLogTimer(): cancelling Remote Log timer"); + mRemoteLogTimer.cancel(); + mRemoteLogTimer = null; + } + } + /** + * Inhibit fault alarm initiation for a period to avoid spurious warning + * beeps caused by short term network interruptions. + */ + private class RemoteLogTimer extends CountDownTimer { + public RemoteLogTimer(long startTime, long interval) { + super(startTime, interval); + } + + @Override + public void onTick(long l) { + // Do Nothing + } + + @Override + public void onFinish() { + Log.v(TAG, "mRemoteLogTimer - onFinish"); + writeToRemoteServer(); + start(); + } + + } + } diff --git a/app/src/main/java/uk/org/openseizuredetector/MainActivity.java b/app/src/main/java/uk/org/openseizuredetector/MainActivity.java index c74edd2..ee54f45 100644 --- a/app/src/main/java/uk/org/openseizuredetector/MainActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/MainActivity.java @@ -238,6 +238,17 @@ public class MainActivity extends AppCompatActivity { mConnection.mSdServer.sendSMSAlarm(); } return true; + case R.id.action_export: + Log.i(TAG, "action_export"); + try { + Intent i = new Intent( + MainActivity.this, + DBQueryActivity.class); + this.startActivity(i); + } catch (Exception ex) { + Log.i(TAG, "exception starting export activity " + ex.toString()); + } + return true; case R.id.action_logs: Log.i(TAG, "action_logs"); try { diff --git a/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java b/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java index 26c46bd..4b236fd 100644 --- a/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java +++ b/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java @@ -49,6 +49,8 @@ import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Environment; @@ -295,6 +297,24 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac return null; } + public boolean isMobileDataActive() { + // return true if we are using mobile data, otherwise return false + ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) { + return true; + } else { + return false; + } + } + + public boolean isNetworkConnected() { + // return true if we have a network connection, otherwise false. + ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return (activeNetwork.isConnected()); + } + /** * Display a Toast message on screen. * diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java index 1c5b20e..0199691 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java @@ -354,6 +354,10 @@ public class SdDataSourceGarmin extends SdDataSource { // Returns a message string that is passed back to the watch. public String updateFromJSON(String jsonStr) { String retVal = "undefined"; + String watchPartNo; + String watchFwVersion; + String sdVersion; + String sdName; Log.v(TAG,"updateFromJSON - "+jsonStr); try { @@ -397,6 +401,23 @@ public class SdDataSourceGarmin extends SdDataSource { mSampleFreq = (short)dataObject.getInt("sampleFreq"); mSdData.batteryPc = (short)dataObject.getInt("battery"); Log.v(TAG,"updateFromJSON - mSamplePeriod="+mSamplePeriod+" mSampleFreq="+mSampleFreq); + mUtil.writeToSysLogFile("SdDataSourceGarmin.updateFromJSON - Settings Received"); + mUtil.writeToSysLogFile(" * mSamplePeriod="+mSamplePeriod+" mSampleFreq="+mSampleFreq); + mUtil.writeToSysLogFile(" * batteryPc = "+mSdData.batteryPc); + + try { + watchPartNo = dataObject.getString("watchPartNo"); + watchFwVersion = dataObject.getString("watchFwVersion"); + sdVersion = dataObject.getString("sdVersion"); + sdName = dataObject.getString("sdName"); + mUtil.writeToSysLogFile(" * sdName = "+sdName+" version "+sdVersion); + mUtil.writeToSysLogFile(" * watchPartNo = "+watchPartNo+" fwVersion "+watchFwVersion); + } catch (Exception e) { + Log.e(TAG,"updateFromJSON - Error Parsing V3.2 JSON String - "+e.toString()); + mUtil.writeToSysLogFile("updateFromJSON - Error Parsing V3.2 JSON String - "+e.toString()); + mUtil.writeToSysLogFile(" This is probably because of an out of date watch app - please upgrade!"); + e.printStackTrace(); + } mSdData.haveSettings = true; mSdData.mSampleFreq = mSampleFreq; mWatchAppRunningCheck = true; @@ -407,6 +428,7 @@ public class SdDataSourceGarmin extends SdDataSource { } } catch (Exception e) { Log.e(TAG,"updateFromJSON - Error Parsing JSON String - "+e.toString()); + mUtil.writeToSysLogFile("updateFromJSON - Error Parsing JSON String - "+e.toString()); e.printStackTrace(); retVal = "ERROR"; } diff --git a/app/src/main/java/uk/org/openseizuredetector/SdServer.java b/app/src/main/java/uk/org/openseizuredetector/SdServer.java index cd2715d..de6f2d7 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdServer.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdServer.java @@ -371,6 +371,9 @@ public class SdServer extends Service implements SdDataReceiver { } */ + // Stop the log Manager + mLm.close(); + try { // Cancel the notification. @@ -386,9 +389,6 @@ public class SdServer extends Service implements SdDataReceiver { mToneGenerator.release(); mToneGenerator = null; - // Stop the log Manager - mLm.close(); - // stop this service. Log.v(TAG, "onDestroy(): calling stopSelf()"); mUtil.writeToSysLogFile("SdServer.onDestroy() - stopping self"); diff --git a/app/src/main/res/layout/activity_dbquery.xml b/app/src/main/res/layout/activity_dbquery.xml index 693abbc..3ab16be 100644 --- a/app/src/main/res/layout/activity_dbquery.xml +++ b/app/src/main/res/layout/activity_dbquery.xml @@ -1,9 +1,71 @@ - + android:orientation="vertical"> + + - \ No newline at end of file + +