From 0a57ff0ae0b6586f69214342082c39a9d3c124ca Mon Sep 17 00:00:00 2001 From: Graham Jones Date: Sat, 3 Jun 2023 19:30:04 +0100 Subject: [PATCH] Added ExportData function, and fixed issue with fault showing for HR and O2sat. --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 2 +- .../ExportDataActivity.java | 238 +++++++++++++++++- .../LogManagerControlActivity.java | 34 ++- .../org/openseizuredetector/SdDataSource.java | 9 + app/src/main/res/layout/activity_dbquery.xml | 6 + .../res/menu/log_manager_activity_menu.xml | 7 +- app/src/main/res/values/strings.xml | 5 +- build.gradle | 1 + 9 files changed, 276 insertions(+), 28 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 577ce75..bb6a7b7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,7 +73,7 @@ dependencies { //androidTestImplementation 'androidx.test:rules:1.1.1' //androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' //androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1' - + implementation 'com.techyourchance:threadposter:1.0.1' } repositories { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a247ce7..2a4db71 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:versionName="4.1.4b"> diff --git a/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java b/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java index d8b82fe..82e1002 100644 --- a/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java @@ -1,22 +1,57 @@ package uk.org.openseizuredetector; +import android.app.Activity; import android.app.DatePickerDialog; import android.app.TimePickerDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.SQLException; +import android.net.Uri; +import android.os.AsyncTask; import android.os.Handler; + import androidx.appcompat.app.AppCompatActivity; + import android.os.Bundle; +import android.os.IBinder; +import android.provider.DocumentsContract; 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 android.os.ParcelFileDescriptor; +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.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; +import java.util.Date; public class ExportDataActivity extends AppCompatActivity - implements View.OnClickListener { + implements View.OnClickListener { + public interface BooleanCallback { + void accept(Boolean retVal); + } + + String TAG = "ExportDataActivity"; + + // Request code for creating a PDF document. + private static final int FILE_REQUEST_CODE = 1353; + Button mDateBtn; Button mTimeBtn; Button mExportBtn; @@ -24,6 +59,8 @@ public class ExportDataActivity extends AppCompatActivity EditText mTimeTxt; EditText mDurationTxt; + Date mEndDate; + int mYear; int mMonth; int mDay; @@ -34,6 +71,10 @@ public class ExportDataActivity extends AppCompatActivity OsdUtil mUtil; Handler mHandler; + SdServiceConnection mConnection; + boolean mConnected; + LogManager mLm; + @Override protected void onCreate(Bundle savedInstanceState) { @@ -43,15 +84,16 @@ public class ExportDataActivity extends AppCompatActivity mHandler = new Handler(); mUtil = new OsdUtil(this, mHandler); - mDateBtn = (Button)findViewById(R.id.dateBtn); + mDateBtn = (Button) findViewById(R.id.dateBtn); mDateBtn.setOnClickListener(this); - mTimeBtn = (Button)findViewById(R.id.timeBtn); + mTimeBtn = (Button) findViewById(R.id.timeBtn); mTimeBtn.setOnClickListener(this); - mExportBtn = (Button)findViewById(R.id.exportBtn); + 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); + mExportBtn.setEnabled(false); + 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(); @@ -61,11 +103,64 @@ public class ExportDataActivity extends AppCompatActivity mHour = c.get(Calendar.HOUR_OF_DAY); mMinute = c.get(Calendar.MINUTE); - mDateTxt.setText(String.format("%02d-%02d-%04d",mDay, mMonth+1, mYear)); + 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)); + mConnection = new SdServiceConnection(getApplicationContext()); + mConnected = false; + + + } + + @Override + public void onStart() { + super.onStart(); + mUtil.bindToServer(getApplicationContext(), mConnection); + waitForConnection(); + + } + + @Override + protected void onStop() { + Log.v(TAG, "onStop()"); + super.onStop(); + mUtil.unbindFromServer(getApplicationContext(), mConnection); + } + + private void waitForConnection() { + // We want the UI to update as soon as it is displayed, but it takes a finite time for + // the mConnection to bind to the service, so we delay half a second to give it chance + // to connect before trying to update the UI for the first time (it happens again periodically using the uiTimer) + if (mConnection.mBound) { + Log.v(TAG, "waitForConnection - Bound!"); + initialiseServiceConnection(); + } else { + Log.v(TAG, "waitForConnection - waiting..."); + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + waitForConnection(); + } + }, 100); + } + } + + // FIXME - for some reason this never gets called, which is why we have the 'waitForConnection()' + // function that polls the connection until it is connected. + public void onServiceConnected(ComponentName name, IBinder service) { + Log.w(TAG, "onServiceConnected()"); + initialiseServiceConnection(); + } + + + private void initialiseServiceConnection() { + mConnected = true; + mExportBtn.setEnabled(true); + mLm = mConnection.mSdServer.mLm; + + //mUtil.showToast("Connected!!"); } @Override @@ -80,7 +175,7 @@ public class ExportDataActivity extends AppCompatActivity mDay = dayOfMonth; mMonth = monthOfYear; mYear = year; - mDateTxt.setText(String.format("%02d-%02d-%04d",mDay, mMonth+1, mYear)); + mDateTxt.setText(String.format("%02d-%02d-%04d", mDay, mMonth + 1, mYear)); } }, mYear, mMonth, mDay); datePickerDialog.show(); @@ -100,14 +195,133 @@ public class ExportDataActivity extends AppCompatActivity timePickerDialog.show(); } if (view == mExportBtn) { - mDateTxt.setText(String.format("%02d-%02d-%04d",mDay, mMonth+1, mYear)); + 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()); + String dateTimeStr = String.format("%04d-%02d-%02dT%02d:%02d:%02dZ", mYear, mMonth + 1, mDay, mHour, mMinute, 00); + //mUtil.showToast(dateTimeStr); + mEndDate = mUtil.string2date(dateTimeStr); + //mUtil.showToast(mEndDate.toString()); - mUtil.showToast(String.format("EndDate=%s %s, Duration=%3.1f hrs", - mDateTxt.getText().toString(), mTimeTxt.getText().toString() ,mDuration)); + //mUtil.showToast(String.format("EndDate=%s %s, Duration=%3.1f hrs", + // 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)); + + this.openFile(); } } + + private void openFile() { + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/csv"); + intent.putExtra(Intent.EXTRA_TITLE, "osd_data.csv"); + + //intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); + Log.v(TAG, "openFile() - showing open dialog"); + startActivityForResult(intent, FILE_REQUEST_CODE); + + } + + // Called when the file picker created in openFile() is closed. + @Override + public void onActivityResult(int requestCode, int resultCode, + Intent resultData) { + Log.v(TAG, "onActivityResult - requestCode=" + requestCode); + if (requestCode == FILE_REQUEST_CODE + && resultCode == Activity.RESULT_OK) { + // The result data contains a URI for the document or directory that + // the user selected. + Uri uri = null; + if (resultData != null) { + uri = resultData.getData(); + // Perform operations on the document using its URI. + //mUtil.showToast("URI="+uri.toString()); + Log.v(TAG, "onActivityResult() - exporting to file " + uri.toString()); + exportToFile(uri); + + } + } + 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(("Overwritten at " + System.currentTimeMillis() + + // "\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()); + } + }); + } + + + } diff --git a/app/src/main/java/uk/org/openseizuredetector/LogManagerControlActivity.java b/app/src/main/java/uk/org/openseizuredetector/LogManagerControlActivity.java index ad74128..43b5af1 100644 --- a/app/src/main/java/uk/org/openseizuredetector/LogManagerControlActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/LogManagerControlActivity.java @@ -200,7 +200,7 @@ public class LogManagerControlActivity extends AppCompatActivity { final CheckBox includeWarningsCb = (CheckBox) findViewById(R.id.include_warnings_cb); final CheckBox includeNDACb = (CheckBox) findViewById(R.id.include_nda_cb); getRemoteEvents(includeWarningsCb.isChecked(), includeNDACb.isChecked()); - ProgressBar pb = (ProgressBar)findViewById(R.id.remoteAccessPb); + ProgressBar pb = (ProgressBar) findViewById(R.id.remoteAccessPb); pb.setIndeterminate(true); pb.setVisibility(View.VISIBLE); // Populate events list - we only do it once when the activity is created because the query might slow down the UI. @@ -269,11 +269,11 @@ public class LogManagerControlActivity extends AppCompatActivity { eventHashMap.put("type", typeStr); eventHashMap.put("subType", subType); eventHashMap.put("desc", desc); - if ((osdAlarmState!=1 | includeWarnings) && - (osdAlarmState!=6 | includeNDA)) { + if ((osdAlarmState != 1 | includeWarnings) && + (osdAlarmState != 6 | includeNDA)) { mRemoteEventsList.add(eventHashMap); } else { - Log.v(TAG,"getRemoteEvents - skipping warning or NDA record"); + Log.v(TAG, "getRemoteEvents - skipping warning or NDA record"); } } Log.v(TAG, "getRemoteEvents() - set mRemoteEventsList(). Updating UI"); @@ -304,9 +304,9 @@ public class LogManagerControlActivity extends AppCompatActivity { TextView tv2 = (TextView) findViewById(R.id.num_local_datapoints_tv); tv2.setText(String.format("%d", datapointsCount)); }); - TextView tv3 = (TextView)findViewById(R.id.nda_time_remaining_tv); - tv3.setText(String.format("%.1f hrs",mLm.mNDATimeRemaining)); - Log.d(TAG,"mNDATimeRemaining = "+String.format("%.1f hrs",mLm.mNDATimeRemaining)); + TextView tv3 = (TextView) findViewById(R.id.nda_time_remaining_tv); + tv3.setText(String.format("%.1f hrs", mLm.mNDATimeRemaining)); + Log.d(TAG, "mNDATimeRemaining = " + String.format("%.1f hrs", mLm.mNDATimeRemaining)); } else { stopUpdating = false; } @@ -333,7 +333,7 @@ public class LogManagerControlActivity extends AppCompatActivity { } // Remote Database List View if (mRemoteEventsList != null) { - ProgressBar pb = (ProgressBar)findViewById(R.id.remoteAccessPb); + ProgressBar pb = (ProgressBar) findViewById(R.id.remoteAccessPb); pb.setIndeterminate(false); pb.setVisibility(View.INVISIBLE); ListView lv = (ListView) findViewById(R.id.remoteEventsLv); @@ -453,7 +453,7 @@ public class LogManagerControlActivity extends AppCompatActivity { } return true; case R.id.start_stop_nda: - Log.i(TAG,"start/stop NDA"); + Log.i(TAG, "start/stop NDA"); if (mConnection.mSdServer.mLogNDA) { new AlertDialog.Builder(this) .setTitle(R.string.stop_nda_logging_dialog_title) @@ -475,7 +475,6 @@ public class LogManagerControlActivity extends AppCompatActivity { }) .setNegativeButton(android.R.string.no, null) .show(); - } else { new AlertDialog.Builder(this) .setTitle(R.string.start_nda_logging_dialog_title) @@ -499,7 +498,6 @@ public class LogManagerControlActivity extends AppCompatActivity { .show(); } - return true; case R.id.action_mark_unknown: Log.i(TAG, "action_mark_unknown"); @@ -527,6 +525,18 @@ public class LogManagerControlActivity extends AppCompatActivity { }) .setNegativeButton(android.R.string.no, null) .show(); + case R.id.export_data_menuitem: + Log.i(TAG, "export data menu item"); + try { + Intent i = new Intent( + getApplicationContext(), + ExportDataActivity.class); + this.startActivity(i); + } catch (Exception ex) { + Log.i(TAG, "exception starting export data activity " + ex.toString()); + } + return true; + default: return super.onOptionsItemSelected(item); } @@ -551,7 +561,7 @@ public class LogManagerControlActivity extends AppCompatActivity { // Confirmation dialog based on: https://stackoverflow.com/a/12213536/2104584 AlertDialog.Builder builder = new AlertDialog.Builder(mContext); builder.setTitle(R.string.prune_database_title); - builder.setMessage(String.format(getString(R.string.prune_database_dialog_msg) , mLm.mDataRetentionPeriod)); + builder.setMessage(String.format(getString(R.string.prune_database_dialog_msg), mLm.mDataRetentionPeriod)); builder.setPositiveButton(R.string.yes_button_title, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java index b06e473..4751fff 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java @@ -617,6 +617,12 @@ public abstract class SdDataSource { mSdData.alarmState = 2; } } + } else { + mSdData.mHRFaultStanding = false; + mSdData.mHRAlarmStanding = false; + mSdData.mAdaptiveHRAlarmStanding = false; + mSdData.mAverageHRAlarmStanding = false; + } } @@ -646,6 +652,9 @@ public abstract class SdDataSource { mSdData.mO2SatFaultStanding = false; mSdData.mO2SatAlarmStanding = false; } + } else { + mSdData.mO2SatFaultStanding = false; + mSdData.mO2SatAlarmStanding = false; } } diff --git a/app/src/main/res/layout/activity_dbquery.xml b/app/src/main/res/layout/activity_dbquery.xml index 3ab16be..a073818 100644 --- a/app/src/main/res/layout/activity_dbquery.xml +++ b/app/src/main/res/layout/activity_dbquery.xml @@ -3,6 +3,12 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> + - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 256d49f..6bcf5e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ OpenSeizureDetector "\n - \nV4.1.4 - Fixed bug in notifications on Android 12 + \nV4.1.4 - Fixed bug in notifications on Android 12, added an Export Data function to save heart rate and accelerometer data to a file. \nV4.1.3 - Fixed display of O2 Saturation in Network Data Source, and added Polish Translations by Jacek Błoniarz-Łuczak. Fixed crash when displaying main activity during alarm on some devices. Added adaptive Heart Rate alarms \nV4.1.2 - Added Machine Learning (Artificial Intelligence) Detection Algorithm Option (CNN V0.24) and 'Normal Daily Activity (NDA) logging function " @@ -499,4 +499,7 @@ Window size (in seconds) for Average Heart Rate Calculation - must be a factor of 5 seconds. Adaptive Heart Rate Alarm Settings Average Heart Rate Alarm Settings + Export Data + Data Exported OK + *** ERROR Exporting Data *** diff --git a/build.gradle b/build.gradle index d99e13c..ea68539 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ buildscript { name 'Google' } google() + mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.3.1'