diff --git a/.gitignore b/.gitignore index 326ee92..48d2b95 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,10 @@ build .idea local.properties *.iml -app/app-release.apk +app/release/app-release.apk app/build app/app.iml +app/release/output-metadata.json +app/google-services.json +*# diff --git a/CHANGELOG.md b/CHANGELOG.md index e1eec5c..ca432e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ OpenSeizureDetector Android App - Change Log ============================================ + V4.0.6 - fixed issue with O2sat data not being recorded to database + V4.0.5 - Added support for 3D data logging + - Fixed issue with seizure reporting crashing if quotation marks included in text. + V4.0.4 - Added support for Data Sharing system V3.6.2 - Nov 2020 - Extended Polish translation further - Improved system log output to help with answering queries from users. diff --git a/CREDITS.md b/CREDITS.md index a5d7c0a..0041b24 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -7,3 +7,4 @@ Thank you to the following people who have contributed to this app: * Andreas & Marie Ekstrom - Swedish Translation * Cesareo Mario Guajardo Murillo - Spanish Translation * Jolanta Kowalska and Artur Chlebek - Polish Translation + * Jozefus Gaarthuis - high resolution star of life icon. diff --git a/QA/QA_checksheet_pre-release_blank.xlsx b/QA/QA_checksheet_pre-release_blank.xlsx new file mode 100644 index 0000000..5d5e203 Binary files /dev/null and b/QA/QA_checksheet_pre-release_blank.xlsx differ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..4c9e8ca --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,13 @@ +OpenSeizureDetector Android App - RELEASE NOTES +============================================== + +Version 4.0.0 + - Logs all seizure detector data to local database + - Adds 'Data Sharing' functionality to upload data to remote database and edit events to say if they are false alarms or genuine seizures + - Settings screens tidied up (removed some unnecessary options to simplify settings) + - Added check of whether the App is being 'Optimised' for battery usage by the Android System + - Fixed problem where the web server receiving data will send it to the analysis routines, even if the data source is not set to Garmin. + - for example if you set the data source to phone, but ran OSD on a garmin watch, the data would oscillate between phone and watch + data + - + diff --git a/app/build.gradle b/app/build.gradle index 8b648be..05b0690 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' - +apply plugin: 'com.google.gms.google-services' android { - compileSdkVersion 28 + compileSdkVersion 31 useLibrary 'org.apache.http.legacy' defaultConfig { applicationId "uk.org.openseizuredetector" - minSdkVersion 21 - targetSdkVersion 29 + minSdkVersion 23 + targetSdkVersion 30 multiDexEnabled true } @@ -24,30 +24,41 @@ android { includeAndroidResources = true } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { - implementation 'com.android.support:multidex:1.0.3' + implementation 'androidx.multidex:multidex:2.0.1' implementation files('libs/mpandroidchartlibrary-2-0-7.jar') implementation 'com.getpebble:pebblekit:3.1.0@aar' // Unit testing dependencies - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - testImplementation 'junit:junit:4.12' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'com.google.android.material:material:1.4.0' + implementation 'com.google.firebase:firebase-auth:19.2.0' + implementation 'androidx.test:core:1.4.0' + testImplementation 'junit:junit:4.13.2' // Set this dependency if you want to use Mockito - testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'org.mockito:mockito-core:4.3.1' // Set this dependency if you want to use Hamcrest matching - testImplementation 'org.hamcrest:hamcrest-library:1.1' - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support:support-v4:28.0.0' - //compile files('libs/JTransforms-3.1-with-dependencies.jar') + testImplementation 'org.hamcrest:hamcrest-library:2.2' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'org.apache.commons:commons-math3:3.6.1' // google play services used for location finding for SMS alerts. - implementation 'com.google.android.gms:play-services:10.0.1' - implementation 'com.github.wendykierp:JTransforms:3.0' - implementation 'com.google.android.gms:play-services-location:10.0.0' + //implementation 'com.google.android.gms:play-services:+' + implementation 'com.google.android.gms:play-services-wearable:+' + implementation 'com.github.wendykierp:JTransforms:3.1' + implementation 'com.google.android.gms:play-services-location:+' //implementation 'com.github.RohitSurwase.UCE-Handler:uce_handler:1.3' - testImplementation 'org.robolectric:robolectric:4.3' - + testImplementation 'org.robolectric:robolectric:4.7.3' + implementation 'com.android.volley:volley:1.2.1' + implementation platform('com.google.firebase:firebase-bom:29.2.0') + implementation 'com.google.firebase:firebase-analytics' + implementation 'com.firebaseui:firebase-ui-auth:7.2.0' + implementation 'com.google.firebase:firebase-firestore' } repositories { diff --git a/app/release/app-release-4.0.6.apk b/app/release/app-release-4.0.6.apk new file mode 100644 index 0000000..dc4e325 Binary files /dev/null and b/app/release/app-release-4.0.6.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json deleted file mode 100644 index e96a6b2..0000000 --- a/app/release/output-metadata.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": 2, - "artifactType": { - "type": "APK", - "kind": "Directory" - }, - "applicationId": "uk.org.openseizuredetector", - "variantName": "processReleaseResources", - "elements": [ - { - "type": "SINGLE", - "filters": [], - "versionCode": 87, - "versionName": "3.6.2", - "outputFile": "app-release.apk" - } - ] -} diff --git a/app/release/output.json b/app/release/output.json deleted file mode 100644 index 6468f2d..0000000 --- a/app/release/output.json +++ /dev/null @@ -1 +0,0 @@ -[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":72,"versionName":"3.5.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release","dirName":""},"path":"app-release.apk","properties":{}}] \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5c78f79..d25abd9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,21 +2,23 @@ + android:versionCode="102" + android:versionName="4.0.6"> + + - + @@ -24,16 +26,19 @@ + - - - + android:theme="@style/AppTheme"> + + + + @@ -57,7 +62,10 @@ android:name=".SdServer" android:exported="false" /> - + + + + - + \ No newline at end of file diff --git a/app/src/main/java/com/rohitss/uceh/UCEDefaultActivity.java b/app/src/main/java/com/rohitss/uceh/UCEDefaultActivity.java index 3375c5f..eb66878 100644 --- a/app/src/main/java/com/rohitss/uceh/UCEDefaultActivity.java +++ b/app/src/main/java/com/rohitss/uceh/UCEDefaultActivity.java @@ -18,8 +18,6 @@ package com.rohitss.uceh; import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -28,7 +26,6 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; @@ -39,6 +36,9 @@ import android.view.View; import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -54,7 +54,7 @@ import uk.org.openseizuredetector.R; *

This class is used to

* Created by Rohit. */ -public final class UCEDefaultActivity extends Activity { +public final class UCEDefaultActivity extends AppCompatActivity { private File txtFile; private String strCurrentErrorLog; private String TAG = "UCEDefaultActivity"; diff --git a/app/src/main/java/com/rohitss/uceh/UCEHandler.java b/app/src/main/java/com/rohitss/uceh/UCEHandler.java index 9970e55..b63e6a0 100644 --- a/app/src/main/java/com/rohitss/uceh/UCEHandler.java +++ b/app/src/main/java/com/rohitss/uceh/UCEHandler.java @@ -25,6 +25,8 @@ import android.content.Intent; import android.os.Bundle; import android.util.Log; +import androidx.appcompat.app.AppCompatActivity; + import java.io.PrintWriter; import java.io.StringWriter; import java.lang.ref.WeakReference; @@ -228,7 +230,7 @@ public final class UCEHandler { return context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).getLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, -1); } - static void closeApplication(Activity activity) { + static void closeApplication(AppCompatActivity activity) { activity.finish(); killCurrentProcess(); } diff --git a/app/src/main/java/uk/org/openseizuredetector/AccelData.java b/app/src/main/java/uk/org/openseizuredetector/AccelData.java deleted file mode 100644 index 2eabed3..0000000 --- a/app/src/main/java/uk/org/openseizuredetector/AccelData.java +++ /dev/null @@ -1,85 +0,0 @@ -package uk.org.openseizuredetector; - -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.TimeZone; - -/** - * Created by graham on 27/06/16. - */ -/* From https://github.com/kramimus/pebble-accel-analyzer */ -public class AccelData { - private final String TAG = AccelData.class.getSimpleName(); - - final private int x; - final private int y; - final private int z; - - private long timestamp = 0; - final private boolean didVibrate; - - public AccelData(byte[] data) { - x = (data[0] & 0xff) | (data[1] << 8); - y = (data[2] & 0xff) | (data[3] << 8); - z = (data[4] & 0xff) | (data[5] << 8); - didVibrate = data[6] != 0; - - for (int i = 0; i < 8; i++) { - timestamp |= ((long)(data[i+7] & 0xff)) << (i * 8); - } - } - - public JSONObject toJson() { - JSONObject json = new JSONObject(); - try { - json.put("x", x); - json.put("y", y); - json.put("z", z); - json.put("ts", timestamp); - json.put("v", didVibrate); - return json; - } catch (JSONException e) { - Log.w(TAG, "problem constructing accel data, skipping " + e); - } - return null; - } - - public static List fromDataArray(byte[] data) { - List accels = new ArrayList(); - for (int i = 0; i < data.length; i += 15) { - accels.add(new AccelData(Arrays.copyOfRange(data, i, i + 15))); - } - return accels; - } - - public long getTimestamp() { - return timestamp; - } - - public int getX() { - return x; - } - - public int getY() { - return y; - } - - public int getZ() { - return z; - } - - public int getMagnitude() { - return (int)Math.sqrt(x*x + y*y + z*z); - } - - public void applyTimezone(TimeZone tz) { - timestamp -= tz.getOffset(timestamp); - } -} - diff --git a/app/src/main/java/uk/org/openseizuredetector/AuthDialog.java b/app/src/main/java/uk/org/openseizuredetector/AuthDialog.java deleted file mode 100644 index ed4d645..0000000 --- a/app/src/main/java/uk/org/openseizuredetector/AuthDialog.java +++ /dev/null @@ -1,78 +0,0 @@ -package uk.org.openseizuredetector; - - -import android.app.Activity; -import android.content.Context; -import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; - -public class AuthDialog extends DialogFragment { - private String TAG = "AuthDialog"; - private AuthDialogInterface mListener; - private Context mContext; - private EditText mUnameEt; - private EditText mPasswdEt; - - @Override - public View onCreateView( - LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState) { - Log.v(TAG, "onCreateView()"); - View v = inflater.inflate(R.layout.dialog_authenticate, - container, false); - Button cancelBtn = - (Button) v.findViewById(R.id.cancelBtn); - cancelBtn.setOnClickListener(onCancel); - Button OKBtn = (Button) v.findViewById(R.id.OKBtn); - OKBtn.setOnClickListener(onOK); - - mUnameEt = (EditText) v.findViewById(R.id.username); - mPasswdEt = (EditText) v.findViewById(R.id.password); - - return v; - - } - - View.OnClickListener onCancel = - new View.OnClickListener() { - @Override - public void onClick(View view) { - Log.v(TAG, "onCancel"); - //m_status=false; - mListener.onDialogDone(false); - dismiss(); - } - }; - - View.OnClickListener onOK = - new View.OnClickListener() { - @Override - public void onClick(View view) { - //m_status=true; - Log.v(TAG, "onOK()"); - String uname = mUnameEt.getText().toString(); - String passwd = mPasswdEt.getText().toString(); - Log.v(TAG,"onOK() - uname="+uname+", passwd="+passwd); - mListener.onDialogDone(true); - dismiss(); - } - }; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - try { - mListener = (AuthDialogInterface) context; - } catch (ClassCastException e) { - throw new ClassCastException(context.toString() - + " must implement dialogDoneistener"); - } - } -} diff --git a/app/src/main/java/uk/org/openseizuredetector/AuthDialogInterface.java b/app/src/main/java/uk/org/openseizuredetector/AuthDialogInterface.java deleted file mode 100644 index f59b062..0000000 --- a/app/src/main/java/uk/org/openseizuredetector/AuthDialogInterface.java +++ /dev/null @@ -1,5 +0,0 @@ -package uk.org.openseizuredetector; - -public interface AuthDialogInterface { - void onDialogDone(boolean state); -} \ No newline at end of file diff --git a/app/src/main/java/uk/org/openseizuredetector/AuthenticateActivity.java b/app/src/main/java/uk/org/openseizuredetector/AuthenticateActivity.java new file mode 100644 index 0000000..be74ee6 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/AuthenticateActivity.java @@ -0,0 +1,343 @@ +package uk.org.openseizuredetector; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Handler; +import android.preference.PreferenceManager; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.firebase.ui.auth.AuthUI; +import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.auth.FirebaseAuth; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; + +public class AuthenticateActivity extends AppCompatActivity { + private String TAG = "AuthenticateActivity"; + private OsdUtil mUtil; + private EditText mUnameEt; + private EditText mPasswdEt; + private SdServiceConnection mConnection; + final Handler serverStatusHandler = new Handler(); + private WebApiConnection mWac; + private LogManager mLm; + private static final String TOKEN_ID = "webApiAuthToken"; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_authenticate); + + mUtil = new OsdUtil(getApplicationContext(), serverStatusHandler); + if (!mUtil.isServerRunning()) { + mUtil.showToast(getString(R.string.error_server_not_running)); + finish(); + return; + } + + Button cancelBtn = + (Button) findViewById(R.id.cancelBtn); + cancelBtn.setOnClickListener(onCancel); + Button loginBtn = (Button) findViewById(R.id.loginBtn); + loginBtn.setOnClickListener(onLogin); + Button logoutCancelBtn = + (Button) findViewById(R.id.logoutCancelBtn); + logoutCancelBtn.setOnClickListener(onCancel); + Button logoutBtn = (Button) findViewById(R.id.logoutBtn); + logoutBtn.setOnClickListener(onLogout); + + // Components required only for osdapi backend + if (LogManager.USE_FIREBASE_BACKEND) { } + else { + mConnection = new SdServiceConnection(getApplicationContext()); + + Button registerBtn = (Button) findViewById(R.id.RegisterBtn); + registerBtn.setOnClickListener(onRegister); + Button resetPasswordBtn = (Button) findViewById(R.id.ResetPasswordBtn); + resetPasswordBtn.setOnClickListener(onResetPassword); + + mUnameEt = (EditText) findViewById(R.id.username); + mPasswdEt = (EditText) findViewById(R.id.password); + } + + Button aboutDataSharingBtn = (Button) findViewById(R.id.aboutDataSharingBtn); + aboutDataSharingBtn.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG,"aboutDataSharingBtn.onClick()"); + String url = OsdUtil.DATA_SHARING_URL; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + } + } + ); + Button privacyPolicyBtn = (Button) findViewById(R.id.privacyPolicyBtn); + privacyPolicyBtn.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG,"privacyPolicyBtn.onClick()"); + String url = OsdUtil.PRIVACY_POLICY_URL; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + } + } + ); + + } + + @Override + protected void onStart() { + Log.d(TAG, "onStart()"); + super.onStart(); + if (LogManager.USE_FIREBASE_BACKEND) { + updateUi(); + } else { + mUtil.bindToServer(getApplicationContext(), mConnection); + waitForConnection(); + } + } + + @Override + protected void onStop() { + Log.d(TAG, "onStop()"); + super.onStop(); + if (LogManager.USE_FIREBASE_BACKEND) { + + } else { + 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); + } + } + + private void initialiseServiceConnection() { + Log.v(TAG,"initialiseServiceConnection()"); + mLm = mConnection.mSdServer.mLm; + mWac = mConnection.mSdServer.mLm.mWac; + updateUi(); + } + + + + // Called after the Firebase Auth UI has completed + private ActivityResultLauncher signInLauncher = registerForActivityResult( + new FirebaseAuthUIActivityResultContract(), + (result) -> { + Log.i(TAG, "FirebaseAuthUIActivityResult - " + result.toString()); + updateUi(); + }); + +// ... + + + private void updateUi() { + Log.v(TAG,"updateUi()"); + LinearLayout loginLl = (LinearLayout) findViewById(R.id.login_ui); + LinearLayout osdApiLoginLl = (LinearLayout) findViewById(R.id.login_osdapi_ui); + LinearLayout logoutLl = (LinearLayout) findViewById(R.id.logout_ui); + + if (mWac == null) { + Log.i(TAG,"mWac is null - not updating UI"); + return; + } + + if (mWac.isLoggedIn()) { + Log.v(TAG, "Already Logged in - showing Log Out prompt"); + loginLl.setVisibility(View.GONE); + logoutLl.setVisibility(View.VISIBLE); + if (!LogManager.USE_FIREBASE_BACKEND) { + osdApiLoginLl.setVisibility(View.GONE); + } + mWac.getUserProfile((JSONObject profileObj) -> { + try { + String userId = profileObj.getString("id"); + String userName = profileObj.getString("username"); + TextView tv2 = (TextView) findViewById(R.id.userIdTv); + tv2.setText(userId); + tv2 = (TextView) findViewById(R.id.usernameTv); + tv2.setText(userName); + } catch (JSONException e) { + Log.e(TAG, "Error Parsing profileObj: " + e.getMessage()); + mUtil.showToast("Error Parsing profileObj - this should not happen!!!"); + } + }); + } else { + Log.v(TAG,"updateUi() - not logged in.."); + loginLl.setVisibility(View.VISIBLE); + logoutLl.setVisibility(View.GONE); + if (!LogManager.USE_FIREBASE_BACKEND) { + osdApiLoginLl.setVisibility(View.VISIBLE); + } + + } + } + + View.OnClickListener onCancel = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onCancel"); + //m_status=false; + finish(); + } + }; + + View.OnClickListener onLogin = + new View.OnClickListener() { + @Override + public void onClick(View view) { + //m_status=true; + if (LogManager.USE_FIREBASE_BACKEND) { + Log.v(TAG, "onLogin() - using Firebase Login"); + Intent signInIntent = AuthUI.getInstance() + .createSignInIntentBuilder() + .setAvailableProviders(Arrays.asList( + new AuthUI.IdpConfig.GoogleBuilder().build(), + //new AuthUI.IdpConfig.FacebookBuilder().build(), + //new AuthUI.IdpConfig.TwitterBuilder().build(), + //new AuthUI.IdpConfig.MicrosoftBuilder().build(), + //new AuthUI.IdpConfig.YahooBuilder().build(), + //new AuthUI.IdpConfig.AppleBuilder().build(), + new AuthUI.IdpConfig.EmailBuilder().build() + //new AuthUI.IdpConfig.PhoneBuilder().build() + //new AuthUI.IdpConfig.AnonymousBuilder().build())) + )) + // ... options ... + .build(); + signInLauncher.launch(signInIntent); + } else { + // Use Username and password authentication for OSDAPI. + // FIXME - make this work with Google Authentication like we do for Firebase. + String uname = mUnameEt.getText().toString(); + String passwd = mPasswdEt.getText().toString(); + Log.v(TAG,"onOK() - uname="+uname+", passwd="+passwd); + mWac.authenticate(uname, passwd, new WebApiConnection.StringCallback() { + @Override + public void accept(String retVal) { + if (retVal != null) { + Log.d(TAG,"Authentication Success - token is "+retVal); + mUtil.showToast("Login Successful"); + saveAuthToken(retVal); + updateUi(); + } else { + Log.e(TAG,"onOk: Authentication failure for "+uname+", "+passwd); + mUtil.showToast("ERROR: Authentication Failed - Please Try Again"); + mUtil.writeToSysLogFile("AuthActivity - Authorisation failed for "+uname+", "+passwd); + } + } + }); + } + } + }; + + View.OnClickListener onLogout = new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onLogout"); + if (LogManager.USE_FIREBASE_BACKEND) { + AuthUI.getInstance() + .signOut(getApplicationContext()) + .addOnCompleteListener(new OnCompleteListener() { + public void onComplete(@NonNull Task task) { + // user is now signed out + updateUi(); + } + }); + } else { + if (mWac != null) { + mWac.logout(); + saveAuthToken(null); + } else { + Log.e(TAG,"logout() - mWac is null - not doing anything"); + } + } + updateUi(); + } + }; + + View.OnClickListener onRegister = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.d(TAG, "onRegisterBtn"); + //Intent i; + //i = new Intent(getApplicationContext(), RemoteDbActivity.class); + //i.putExtra("url", "https://osdapi.ddns.net/static/register.html"); + //startActivity(i); + String url = "https://osdapi.ddns.net/static/register.html"; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + } + }; + + View.OnClickListener onResetPassword = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.d(TAG, "onResetPasswordBtn"); + //Intent i; + //i = new Intent(getApplicationContext(), RemoteDbActivity.class); + //i.putExtra("url", "https://osdapi.ddns.net/static/register.html"); + //startActivity(i); + String url = "https://osdapi.ddns.net/static/request_password_reset.html"; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + } + }; + + + private void saveAuthToken(String tokenStr) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + prefs.edit().putString(TOKEN_ID, tokenStr).commit(); + mWac.setStoredToken(tokenStr); + } + + public String getAuthToken() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + String authToken = prefs.getString(TOKEN_ID, null); + return authToken; + } + + +} \ No newline at end of file diff --git a/app/src/main/java/uk/org/openseizuredetector/BLEScanActivity.java b/app/src/main/java/uk/org/openseizuredetector/BLEScanActivity.java index 7d2daf1..da99a4a 100644 --- a/app/src/main/java/uk/org/openseizuredetector/BLEScanActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/BLEScanActivity.java @@ -17,12 +17,17 @@ package uk.org.openseizuredetector; */ +import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED; + import android.Manifest; import android.app.Activity; import android.app.ListActivity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanResult; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -30,19 +35,23 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; -import android.support.v4.app.ActivityCompat; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.BaseAdapter; +import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.view.MenuItemCompat; + import java.util.ArrayList; /** @@ -51,16 +60,20 @@ import java.util.ArrayList; public class BLEScanActivity extends ListActivity { private LeDeviceListAdapter mLeDeviceListAdapter; private BluetoothAdapter mBluetoothAdapter; + private BluetoothLeScanner mBluetoothLeScanner; private boolean mScanning; private Handler mHandler; + private boolean bleAvailable = false; private boolean mPermissionsRequested = false; private final String TAG = "BLEScanActivity"; private final String[] REQUIRED_PERMISSIONS = { + Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, + //Manifest.permission.BLUETOOTH_PRIVILEGED, }; private static final int REQUEST_ENABLE_BT = 1; @@ -70,6 +83,7 @@ public class BLEScanActivity extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.ble_scan_activity); //this.getActionBar().setTitle(R.string.title_devices); this.setTitle(R.string.title_devices); mHandler = new Handler(); @@ -79,6 +93,8 @@ public class BLEScanActivity extends ListActivity { if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); finish(); + } else { + bleAvailable = true; } // Initializes a Bluetooth adapter. For API level 18 and above, get a reference to @@ -93,6 +109,8 @@ public class BLEScanActivity extends ListActivity { finish(); return; } + + mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); } @Override @@ -101,7 +119,7 @@ public class BLEScanActivity extends ListActivity { if (!mScanning) { menu.findItem(R.id.menu_stop).setVisible(false); menu.findItem(R.id.menu_scan).setVisible(true); - menu.findItem(R.id.menu_refresh).setActionView(null); + MenuItemCompat.setActionView(menu.findItem(R.id.menu_refresh), null); } else { menu.findItem(R.id.menu_stop).setVisible(true); menu.findItem(R.id.menu_scan).setVisible(false); @@ -125,17 +143,79 @@ public class BLEScanActivity extends ListActivity { return true; } + + public void onScanButtonClick(View v) { + scanLeDevice(true); + } + @Override protected void onResume() { super.onResume(); + SharedPreferences SP = PreferenceManager + .getDefaultSharedPreferences(this); + TextView tv = (TextView) findViewById(R.id.current_ble_device_tv); + try { + String bleAddr = SP.getString("BLE_Device_Addr", "none"); + String bleName = SP.getString("BLE_Device_Name", "none"); + tv.setText("Current Device=" + bleName + " (" + bleAddr + ")"); + } catch (Exception e) { + tv.setText("Current Device=" + "none" + " (" + "none" + ")"); + } + tv = (TextView) findViewById(R.id.ble_present_tv); + if (mBluetoothAdapter == null) { + tv.setText("ERROR - Bluetooth Adapter Not Present"); + } else { + tv.setText("Bluetooth Adapter Present - OK"); + } // Ensures Bluetooth is enabled on the device. If Bluetooth is not currently enabled, // fire an intent to display a dialog asking the user to grant permission to enable it. + tv = (TextView) findViewById(R.id.ble_adapter_tv); if (!mBluetoothAdapter.isEnabled()) { + tv.setText("ERROR - Bluetoot NOT Enabled"); Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); + } else { + tv.setText("Bluetooth Adapter Enabled OK"); } + requestBTPermissions(this); + + for (int i = 0; i < REQUIRED_PERMISSIONS.length; i++) { + if (ContextCompat.checkSelfPermission(this, REQUIRED_PERMISSIONS[i]) == PERMISSION_GRANTED) { + Log.i(TAG, "Permission " + REQUIRED_PERMISSIONS[i] + " OK"); + } else { + Log.e(TAG, "Permission " + REQUIRED_PERMISSIONS[i] + " NOT GRANTED"); + Toast.makeText(this, "ERROR - Permission " + REQUIRED_PERMISSIONS[i] + " not Granted - this will not work!!!!!", Toast.LENGTH_SHORT).show(); + } + } + + tv = (TextView) findViewById(R.id.ble_perm1_tv); + if (ContextCompat.checkSelfPermission(this, REQUIRED_PERMISSIONS[0]) == PERMISSION_GRANTED) { + tv.setText("Permission " + REQUIRED_PERMISSIONS[0] + " OK"); + } else { + tv.setText("ERROR: Permission " + REQUIRED_PERMISSIONS[0] + " NOT GRANTED"); + } + tv = (TextView) findViewById(R.id.ble_perm2_tv); + if (ContextCompat.checkSelfPermission(this, REQUIRED_PERMISSIONS[1]) == PERMISSION_GRANTED) { + tv.setText("Permission " + REQUIRED_PERMISSIONS[1] + " OK"); + } else { + tv.setText("ERROR: Permission " + REQUIRED_PERMISSIONS[1] + " NOT GRANTED"); + } + tv = (TextView) findViewById(R.id.ble_perm3_tv); + if (ContextCompat.checkSelfPermission(this, REQUIRED_PERMISSIONS[2]) == PERMISSION_GRANTED) { + tv.setText("Permission " + REQUIRED_PERMISSIONS[2] + " OK"); + } else { + tv.setText("ERROR: Permission " + REQUIRED_PERMISSIONS[2] + " NOT GRANTED"); + } + tv = (TextView) findViewById(R.id.ble_perm4_tv); + if (ContextCompat.checkSelfPermission(this, REQUIRED_PERMISSIONS[3]) == PERMISSION_GRANTED) { + tv.setText("Permission " + REQUIRED_PERMISSIONS[3] + " OK"); + } else { + tv.setText("ERROR: Permission " + REQUIRED_PERMISSIONS[3] + " NOT GRANTED"); + } + + // Initializes list view adapter. mLeDeviceListAdapter = new LeDeviceListAdapter(); setListAdapter(mLeDeviceListAdapter); @@ -166,10 +246,10 @@ public class BLEScanActivity extends ListActivity { if (device == null) return; Log.v(TAG, "onListItemClick: Device=" + device.getName() + ", Addr=" + device.getAddress()); if (mScanning) { - mBluetoothAdapter.stopLeScan(mLeScanCallback); + mBluetoothLeScanner.stopScan(mLeScanCallback); mScanning = false; } - Log.v(TAG,"Saving Device Details"); + Log.v(TAG, "Saving Device Details"); SharedPreferences.Editor SPE = PreferenceManager .getDefaultSharedPreferences(this).edit(); try { @@ -178,36 +258,70 @@ public class BLEScanActivity extends ListActivity { SPE.apply(); SPE.commit(); - Log.v(TAG, "Saved Device Name="+device.getName()+" and Address="+device.getAddress()); + Log.v(TAG, "Saved Device Name=" + device.getName() + " and Address=" + device.getAddress()); } catch (Exception ex) { - Log.e(TAG, "Error Saving Devie Name and Address!"); + Log.e(TAG, "Error Saving Device Name and Address!"); Toast toast = Toast.makeText(this, "Problem Saving Device Name and Address", Toast.LENGTH_SHORT); toast.show(); } SharedPreferences SP = PreferenceManager.getDefaultSharedPreferences((this)); - Log.v(TAG,"Check of saved values - Name="+SP.getString("BLE_Device_Name","NOT SET")+", Addr="+SP.getString("BLE_Device_Addr","NOT SET")); + Log.v(TAG, "Check of saved values - Name=" + SP.getString("BLE_Device_Name", "NOT SET") + ", Addr=" + SP.getString("BLE_Device_Addr", "NOT SET")); finish(); } + public void requestBTPermissions(Activity activity) { + if (mPermissionsRequested) { + Log.i(TAG, "requestPermissions() - request already sent - not doing anything"); + } else { + Log.i(TAG, "requestPermissions() - requesting permissions"); + for (int i = 0; i < REQUIRED_PERMISSIONS.length; i++) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, + REQUIRED_PERMISSIONS[i])) { + Log.i(TAG, "shouldShowRationale for permission" + REQUIRED_PERMISSIONS[i]); + } + } + ActivityCompat.requestPermissions(activity, + REQUIRED_PERMISSIONS, + 42); + mPermissionsRequested = true; + } + } + + private void scanLeDevice(final boolean enable) { - requestPermissions(this); + TextView tv; + if (enable) { // Stops scanning after a pre-defined scan period. mHandler.postDelayed(new Runnable() { @Override public void run() { mScanning = false; - mBluetoothAdapter.stopLeScan(mLeScanCallback); + mBluetoothLeScanner.stopScan(mLeScanCallback); invalidateOptionsMenu(); + TextView tv = (TextView) (findViewById(R.id.ble_scan_status_tv)); + tv.setText("Stopped"); + Button b = (Button) findViewById(R.id.startScanButton); + b.setEnabled(true); + } }, SCAN_PERIOD); mScanning = true; - mBluetoothAdapter.startLeScan(mLeScanCallback); + mBluetoothLeScanner.startScan(mLeScanCallback); + tv = (TextView) (findViewById(R.id.ble_scan_status_tv)); + tv.setText("Scanning"); + Button b = (Button) findViewById(R.id.startScanButton); + b.setEnabled(false); + } else { mScanning = false; - mBluetoothAdapter.stopLeScan(mLeScanCallback); + mBluetoothLeScanner.stopScan(mLeScanCallback); + tv = (TextView) (findViewById(R.id.ble_scan_status_tv)); + tv.setText("Stopped"); + Button b = (Button) findViewById(R.id.startScanButton); + b.setEnabled(true); } invalidateOptionsMenu(); } @@ -225,7 +339,7 @@ public class BLEScanActivity extends ListActivity { public void addDevice(BluetoothDevice device) { if (!mLeDevices.contains(device)) { - Log.v(TAG,"addDevice - "+device.getName()); + Log.v(TAG, "addDevice - " + device.getName()); mLeDevices.add(device); } } @@ -256,7 +370,7 @@ public class BLEScanActivity extends ListActivity { @Override public View getView(int i, View view, ViewGroup viewGroup) { ViewHolder viewHolder; - Log.v(TAG,"scanner getView i="+i); + Log.v(TAG, "scanner getView i=" + i); // General ListView optimization code. if (view == null) { view = mInflator.inflate(R.layout.ble_list_item_device, null); @@ -281,19 +395,14 @@ public class BLEScanActivity extends ListActivity { } // Device scan callback. - private BluetoothAdapter.LeScanCallback mLeScanCallback = - new BluetoothAdapter.LeScanCallback() { - + private ScanCallback mLeScanCallback = + new ScanCallback() { @Override - public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { - Log.v(TAG,"LEScanCallback - device="+device.getName()); - runOnUiThread(new Runnable() { - @Override - public void run() { - mLeDeviceListAdapter.addDevice(device); - mLeDeviceListAdapter.notifyDataSetChanged(); - } - }); + public void onScanResult(int callbackType, ScanResult result) { + //super.onScanResult(callbackType, result); + Log.v(TAG, "ScanCallback - " + result.getDevice().getName()); + mLeDeviceListAdapter.addDevice(result.getDevice()); + mLeDeviceListAdapter.notifyDataSetChanged(); } }; @@ -303,21 +412,4 @@ public class BLEScanActivity extends ListActivity { } - public void requestPermissions(Activity activity) { - if (mPermissionsRequested) { - Log.i(TAG, "requestPermissions() - request already sent - not doing anything"); - } else { - Log.i(TAG, "requestPermissions() - requesting permissions"); - for (int i = 0; i < REQUIRED_PERMISSIONS.length; i++) { - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, - REQUIRED_PERMISSIONS[i])) { - Log.i(TAG, "shouldShowRationale for permission" + REQUIRED_PERMISSIONS[i]); - } - } - ActivityCompat.requestPermissions(activity, - REQUIRED_PERMISSIONS, - 42); - mPermissionsRequested = true; - } - } } diff --git a/app/src/main/java/uk/org/openseizuredetector/CircularArrayList.java b/app/src/main/java/uk/org/openseizuredetector/CircularArrayList.java deleted file mode 100644 index 08d7d10..0000000 --- a/app/src/main/java/uk/org/openseizuredetector/CircularArrayList.java +++ /dev/null @@ -1,107 +0,0 @@ -package uk.org.openseizuredetector; - -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.RandomAccess; - -/** - * Created by graham on 28/06/16. - */ -public class CircularArrayList - extends AbstractList implements RandomAccess { - /** - * If you use this code, please consider notifying isak at du-preez dot com - * with a brief description of your application. - * - * This is free and unencumbered software released into the public domain. - * Anyone is free to copy, modify, publish, use, compile, sell, or - * distribute this software, either in source code form or as a compiled - * binary, for any purpose, commercial or non-commercial, and by any - * means. - */ - - private final int n; // buffer length - private final List buf; // a List implementing RandomAccess - private int head = 0; - private int tail = 0; - - public CircularArrayList(int capacity) { - n = capacity + 1; - buf = new ArrayList(Collections.nCopies(n, (E) null)); - } - - public int capacity() { - return n - 1; - } - - private int wrapIndex(int i) { - int m = i % n; - if (m < 0) { // java modulus can be negative - m += n; - } - return m; - } - - // This method is O(n) but will never be called if the - // CircularArrayList is used in its typical/intended role. - private void shiftBlock(int startIndex, int endIndex) { - assert (endIndex > startIndex); - for (int i = endIndex - 1; i >= startIndex; i--) { - set(i + 1, get(i)); - } - } - - @Override - public int size() { - return tail - head + (tail < head ? n : 0); - } - - @Override - public E get(int i) { - if (i < 0 || i >= size()) { - throw new IndexOutOfBoundsException(); - } - return buf.get(wrapIndex(head + i)); - } - - @Override - public E set(int i, E e) { - if (i < 0 || i >= size()) { - throw new IndexOutOfBoundsException(); - } - return buf.set(wrapIndex(head + i), e); - } - - @Override - public void add(int i, E e) { - int s = size(); - if (s == n - 1) { - throw new IllegalStateException("Cannot add element." - + " CircularArrayList is filled to capacity."); - } - if (i < 0 || i > s) { - throw new IndexOutOfBoundsException(); - } - tail = wrapIndex(tail + 1); - if (i < s) { - shiftBlock(i, s); - } - set(i, e); - } - - @Override - public E remove(int i) { - int s = size(); - if (i < 0 || i >= s) { - throw new IndexOutOfBoundsException(); - } - E e = get(i); - if (i > 0) { - shiftBlock(0, i); - } - head = wrapIndex(head + 1); - return e; - } -} diff --git a/app/src/main/java/uk/org/openseizuredetector/EditEventActivity.java b/app/src/main/java/uk/org/openseizuredetector/EditEventActivity.java new file mode 100644 index 0000000..ccdc01e --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/EditEventActivity.java @@ -0,0 +1,360 @@ +package uk.org.openseizuredetector; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +public class EditEventActivity extends AppCompatActivity { + private String TAG = "EditEventActivity"; + private Context mContext; + private WebApiConnection mWac; + private LogManager mLm; + private SdServiceConnection mConnection; + final Handler serverStatusHandler = new Handler(); + private OsdUtil mUtil; + private List mEventTypesList = null; + private HashMap> mEventSubTypesHashMap = null; + private String mEventTypeStr = null; + private String mEventSubTypeStr = null; + private String mEventId; + private String mEventNotes = ""; + //private Date mEventDateTime; + private RadioGroup mEventTypeRg; + private boolean mEventTypesListChanged = false; + private RadioGroup mEventSubTypeRg; + private boolean mEventSubTypesListChanged = false; + private JSONObject mEventObj; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_event); + mUtil = new OsdUtil(getApplicationContext(), serverStatusHandler); + mConnection = new SdServiceConnection(getApplicationContext()); + + //mWac = new WebApiConnection(this, this, this, this); + //mLm = new LogManager(this); + + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + String eventId = extras.getString("eventId"); + mEventId = eventId; + Log.v(TAG, "onCreate - mEventId=" + mEventId); + } + + + Button cancelBtn = + (Button) findViewById(R.id.cancelBtn); + cancelBtn.setOnClickListener(onCancel); + Button OKBtn = (Button) findViewById(R.id.loginBtn); + OKBtn.setOnClickListener(onOK); + + mEventTypeRg = findViewById(R.id.eventTypeRg); + mEventTypeRg.setOnCheckedChangeListener(onEventTypeChange); + mEventSubTypeRg = findViewById(R.id.eventSubTypeRg); + mEventSubTypeRg.setOnCheckedChangeListener(onEventSubTypeChange); + + + } + + @Override + protected void onStart() { + super.onStart(); + Log.i(TAG, "onStart()"); + mUtil.bindToServer(getApplicationContext(), mConnection); + waitForConnection(); + + updateUi(); + } + + @Override + protected void onStop() { + super.onStop(); + Log.i(TAG, "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); + } + } + + private void initialiseServiceConnection() { + mLm = mConnection.mSdServer.mLm; + mWac = mConnection.mSdServer.mLm.mWac; + + // Retrieve the JSONObject containing the standard event types. + // Note this obscure syntax is to avoid having to create another interface, so it is worth it :) + // See https://medium.com/@pra4mesh/callback-function-in-java-20fa48b27797 + mWac.getEventTypes(new WebApiConnection.JSONObjectCallback() { + @Override + public void accept(JSONObject eventTypesObj) { + Log.v(TAG, "initialiseServiceConnection().onEventTypesReceived"); + if (eventTypesObj == null) { + Log.e(TAG, "initialiseServiceConnection().getEventTypes Callback: Error Retrieving event types"); + mUtil.showToast("Error Retrieving Event Types from Server - Please Try Again Later!"); + } else { + Iterator keys = eventTypesObj.keys(); + mEventTypesList = new ArrayList(); + mEventSubTypesHashMap = new HashMap>(); + while (keys.hasNext()) { + String key = keys.next(); + Log.v(TAG, "initialiseServiceConnection().getEventTypes Callback: key=" + key); + mEventTypesList.add(key); + try { + JSONArray eventSubTypes = eventTypesObj.getJSONArray(key); + ArrayList eventSubtypesList = new ArrayList(); + for (int i = 0; i < eventSubTypes.length(); i++) { + eventSubtypesList.add(eventSubTypes.getString(i)); + } + mEventSubTypesHashMap.put(key, eventSubtypesList); + mEventTypesListChanged = true; + } catch (JSONException e) { + Log.e(TAG, "initialiseServiceConnection().getEventTypes Callback: Error parsing JSONObject" + e.getMessage() + e.toString()); + } + } + updateUi(); + } + } + }); + + // Retrieve the event data to edit + try { + mWac.getEvent(mEventId, new WebApiConnection.JSONObjectCallback() { + @Override + public void accept(JSONObject eventObj) { + Log.v(TAG, "initialiseServiceConnection.getEvent"); + if (eventObj != null) { + mEventObj = eventObj; + Log.v(TAG, "initialiseServiceConnection.getEvent: eventObj=" + eventObj.toString()); + updateUi(); + // FIXME: modify updateUi to use mEventObj + } else { + mUtil.showToast("Failed to Retrieve Event from Remote Database"); + finish(); + } + } + }); + } catch (Exception e) { + Log.e(TAG, "ERROR:" + e.getMessage()); + e.printStackTrace(); + } + } + + private void updateUi() { + Log.v(TAG, "updateUI"); + TextView tv; + RadioButton b; + + // Populate event type button group if necessary + if (mEventTypesList != null && mEventTypesListChanged) { + Log.v(TAG, "updateUi: " + mEventTypesList.toString()); + mEventTypeRg.removeAllViews(); + for (String eventTypeStr : mEventTypesList) { + b = new RadioButton(this); + b.setText(eventTypeStr); + mEventTypeRg.addView(b); + } + mEventTypesListChanged = false; + } + + + try { + if (mEventObj != null) { + tv = (TextView) findViewById(R.id.eventIdTv); + tv.setText(mEventId); + tv = (TextView) findViewById(R.id.eventAlarmStateTv); + String alarmStateStr = mEventObj.getString("osdAlarmState"); + try { + int alarmStateVal = Integer.parseInt(alarmStateStr); + alarmStateStr = mUtil.alarmStatusToString(alarmStateVal); + } catch (Exception e) { + Log.v(TAG,"updateUi: alarmState does not parse to int so displaying it as string: " +alarmStateStr); + } + tv.setText(alarmStateStr); + tv = (TextView) findViewById(R.id.eventNotsTv); + tv.setText(mEventObj.getString("desc")); + + + tv = (TextView) findViewById(R.id.eventDateTv); + try { + String dateStr = mEventObj.getString("dataTime"); + Date dataTime = mUtil.string2date(dateStr); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + tv.setText(dateFormat.format(dataTime)); + } catch (Exception e) { + Log.e(TAG,"updateUI: Error Parsing dataDate "+e.getLocalizedMessage()); + tv.setText("---"); + } + + // Check the correct seizure type button in the event type group + for (int index = 0; index < mEventTypeRg.getChildCount(); index++) { + b = (RadioButton) mEventTypeRg.getChildAt(index); + String buttonText = b.getText().toString(); + if (buttonText.equals(mEventObj.getString("type"))) { + Log.v(TAG, "updateUi - selecting button " + mEventObj.getString("type")); + b.setChecked(true); + } + } + + // Populate the event sub-types radio button list. + Log.v(TAG,"updateUi() - meventsubtypeshashmap="+mEventSubTypesHashMap+", mEventSubtypesListChanged="+mEventSubTypesListChanged); + if (mEventSubTypesHashMap != null && mEventSubTypesListChanged) { + Log.v(TAG,"UpdateUi() - populating event sub types list"); + if (mEventObj.getString("type") != null) { + // based on https://androidexample.com/create-a-simple-listview + ArrayList subtypesArrayList = mEventSubTypesHashMap.get(mEventObj.getString("type")); + Log.v(TAG, "updateUi() - eventType=" + mEventObj.getString("type") + ", subtypes=" + subtypesArrayList); + mEventSubTypeRg.removeAllViews(); + for (String eventSubTypeStr : subtypesArrayList) { + b = new RadioButton(this); + b.setText(eventSubTypeStr); + mEventSubTypeRg.addView(b); + } + mEventSubTypesListChanged = false; + } + } + + + // And show the correct sub-type selected. + for (int index = 0; index < mEventSubTypeRg.getChildCount(); index++) { + b = (RadioButton) mEventSubTypeRg.getChildAt(index); + String buttonText = b.getText().toString(); + if (buttonText.equals(mEventObj.getString("subType"))) { + Log.v(TAG, "updateUi - selecting button " + mEventObj.getString("subType")); + b.setChecked(true); + } + } + + + } + } catch (JSONException e) { + Log.e(TAG,"Error Parsing mEventObj: "+e.getMessage()); + } + + + + } // updateUi() + + View.OnClickListener onCancel = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onCancel"); + //m_status=false; + finish(); + } + }; + + View.OnClickListener onOK = + new View.OnClickListener() { + @Override + public void onClick(View view) { + //m_status=true; + TextView tv = (TextView)findViewById(R.id.eventNotsTv); + try { + mEventObj.put("desc",tv.getText()); + mEventObj.put("id",mEventId); // Add event Id to event object manually because firestore does not include it by default. + } catch (JSONException e) { + Log.e(TAG,"Error writing mEventObj: "+e.getMessage()); + } + Log.v(TAG, "onOK() - eventObj="+mEventObj.toString()); + + try { + mWac.updateEvent(mEventObj, new WebApiConnection.JSONObjectCallback() { + @Override + public void accept(JSONObject eventObj) { + Log.v(TAG, "onOk.updateEvent"); + //mEventObj = eventObj; + if (eventObj != null) { + Log.v(TAG, "onOk.getEvent: eventObj=" + eventObj.toString()); + mUtil.showToast("Event Updated OK"); + finish(); + } else { + Log.e(TAG, "onOk.updateEvent - Error - returned NULL"); + mUtil.showToast("Error Updating Event"); + updateUi(); + } + } + }); + } catch (Exception e) { + Log.e(TAG,"onOK() - ERROR: "+e.getMessage()+" : " +e.toString()); + e.printStackTrace(); + mUtil.showToast("Error Updating Event"); + updateUi(); + } + } + }; + + + RadioGroup.OnCheckedChangeListener onEventTypeChange = + new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + Log.v(TAG,"onEventTypeChange() - id="+checkedId); + RadioButton b = (RadioButton)findViewById(group.getCheckedRadioButtonId()); + String selectedEventType = b.getText().toString(); + try { + mEventObj.put("type", selectedEventType); + } catch (JSONException e) { + Log.e(TAG,"Error setting mEventObj.type: "+e.getMessage()); + } + mEventSubTypesListChanged = true; + Log.v(TAG,"onEventTypeChange() - mEventSubTypesListChanged="+mEventSubTypesListChanged); + updateUi(); + } + }; + RadioGroup.OnCheckedChangeListener onEventSubTypeChange = + new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + Log.v(TAG,"onEventSubTypeChange() - id="+checkedId); + RadioButton b = (RadioButton)findViewById(group.getCheckedRadioButtonId()); + String selectedEventSubType = b.getText().toString(); + try { + mEventObj.put("subType", selectedEventSubType); + } catch (JSONException e) { + Log.e(TAG,"Error setting mEventObj.type: "+e.getMessage()); + } + updateUi(); + } + }; + + +} \ No newline at end of file diff --git a/app/src/main/java/uk/org/openseizuredetector/EventLogManager/EventLogListAdapter.java b/app/src/main/java/uk/org/openseizuredetector/EventLogManager/EventLogListAdapter.java deleted file mode 100644 index 91db4ef..0000000 --- a/app/src/main/java/uk/org/openseizuredetector/EventLogManager/EventLogListAdapter.java +++ /dev/null @@ -1,98 +0,0 @@ -package uk.org.openseizuredetector.EventLogManager; - -import java.util.ArrayList; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import uk.org.openseizuredetector.R; - -public class EventLogListAdapter extends BaseAdapter { - EventLogManager dm; - ArrayList logEntryModelList; - LayoutInflater inflater; - Context _context; - - public EventLogListAdapter(Context context) { - - logEntryModelList = new ArrayList(); - _context = context; - inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - dm = new EventLogManager(_context); - logEntryModelList = dm.getAllData(); - - } - - @Override - public void notifyDataSetChanged() { - super.notifyDataSetChanged(); - //refetching the new data from database - logEntryModelList = dm.getAllData(); - - } - - public void delRow(int delPosition) { - - dm.deleteRow(logEntryModelList.get(delPosition).getId()); - logEntryModelList.remove(delPosition); - - } - - @Override - public int getCount() { - return logEntryModelList.size(); - } - - @Override - public Object getItem(int position) { - return logEntryModelList.get(position); - } - - @Override - public long getItemId(int position) { - return 0; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - ViewHolder vHolder; - if (convertView == null) { - convertView = inflater.inflate(R.layout.log_entry_layout, null); - vHolder = new ViewHolder(); - - vHolder.date = (TextView) convertView - .findViewById(R.id.event_date); - vHolder.alarmState = (TextView) convertView - .findViewById(R.id.event_alarmState); - vHolder.note = (TextView) convertView - .findViewById(R.id.event_note); - vHolder.dataJSON = (TextView) convertView - .findViewById(R.id.event_dataJSON); - convertView.setTag(vHolder); - } else { - vHolder = (ViewHolder) convertView.getTag(); - } - - LogEntryModel eventObj = logEntryModelList.get(position); - - //vHolder.date.setText(eventObj.getDate().toString()); - vHolder.alarmState.setText(eventObj.getAlarmState()); - vHolder.note.setText(eventObj.getNote()); - vHolder.dataJSON.setText(eventObj.getDataJSON()); - - return convertView; - } - - class ViewHolder { - TextView date,alarmState,note,dataJSON; - } - -} diff --git a/app/src/main/java/uk/org/openseizuredetector/EventLogManager/EventLogManager.java b/app/src/main/java/uk/org/openseizuredetector/EventLogManager/EventLogManager.java deleted file mode 100644 index daebfca..0000000 --- a/app/src/main/java/uk/org/openseizuredetector/EventLogManager/EventLogManager.java +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Database manager for logging events and associated seizure detector data. - */ -package uk.org.openseizuredetector.EventLogManager; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.Locale; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; - -public class EventLogManager { - final static String TAG = "EventLogManager"; - private SQLiteDatabase db; // a reference to the database manager class. - private static final String DB_NAME = "eventlog"; // the name of our database - private static final int DB_VERSION = 1; // the version of the database - - private static final String TABLE_NAME = "events";// table name - - // the names for our database columns - private static final String TABLE_ROW_ID = "_id"; - private static final String TABLE_ROW_DATE = "event_date"; - private static final String TABLE_ROW_ALARM_STATE = "alarm_state"; - private static final String TABLE_ROW_DATA_JSON = "data_json"; - private static final String TABLE_ROW_NOTE = "note"; - private Context context; - - public EventLogManager(Context context) { - this.context = context; - - // create or open the database - CustomSQLiteOpenHelper helper = new CustomSQLiteOpenHelper(context); - this.db = helper.getWritableDatabase(); - - helper.onCreate(this.db); - } - - // the beginnings our SQLiteOpenHelper class - private class CustomSQLiteOpenHelper extends SQLiteOpenHelper { - - public CustomSQLiteOpenHelper(Context context) { - super(context, DB_NAME, null, DB_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - // the SQLite query string that will create our column database - // table. - String newTableQueryString = "create table " + TABLE_NAME + " (" - + TABLE_ROW_ID - + " integer primary key autoincrement not null," - + TABLE_ROW_DATE + " timestamp not null," + TABLE_ROW_ALARM_STATE - + " integer not null," + TABLE_ROW_NOTE + " text not null," - + TABLE_ROW_DATA_JSON + " text not null" + ");"; - - // execute the query string to the database. - db.execSQL(newTableQueryString); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - - // LATER, WE WOULD SPECIFIY HOW TO UPGRADE THE DATABASE - // FROM OLDER VERSIONS. - String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; - db.execSQL(DROP_TABLE); - onCreate(db); - - } - - } - - public void addRow(LogEntryModel eventObj) { - ContentValues values = prepareData(eventObj); - // ask the database object to insert the new data - try { - db.insert(TABLE_NAME, null, values); - } catch (Exception e) { - Log.e("DB ERROR", e.toString()); // prints the error message to - // the log - e.printStackTrace(); // prints the stack trace to the log - } - } - - private String getDateTime(Date date) { - SimpleDateFormat dateFormat = new SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - if (date==null) - return ""; - else - return dateFormat.format(date); - } - - private ContentValues prepareData(LogEntryModel eventObj) { - - ContentValues values = new ContentValues(); - values.put(TABLE_ROW_ALARM_STATE, eventObj.getAlarmState()); - values.put(TABLE_ROW_DATE, getDateTime(eventObj.getDate())); - values.put(TABLE_ROW_NOTE, eventObj.getNote()); - values.put(TABLE_ROW_DATA_JSON, eventObj.getDataJSON()); - return values; - } - - // Returns row data in form of LogEntryModel object - public LogEntryModel getRowAsObject(int rowID) { - - LogEntryModel rowContactObj = new LogEntryModel(); - Cursor cursor; - - try { - - cursor = db.query(TABLE_NAME, new String[] { TABLE_ROW_ID, - TABLE_ROW_ALARM_STATE, TABLE_ROW_DATE, TABLE_ROW_NOTE, - TABLE_ROW_DATA_JSON }, TABLE_ROW_ID + "=" + rowID, null, - null, null, null, null); - - cursor.moveToFirst(); - prepareSendObject(rowContactObj, cursor); - - } catch (SQLException e) { - Log.e("DB ERROR", e.toString()); - e.printStackTrace(); - } - - return rowContactObj; - } - - // Returns all the rows data in form of LogEntryModel object list - - public ArrayList getAllData() { - - ArrayList allRowsObj = new ArrayList(); - Cursor cursor; - LogEntryModel rowContactObj; - - String[] columns = new String[] { TABLE_ROW_ID, TABLE_ROW_ALARM_STATE, - TABLE_ROW_DATE, TABLE_ROW_NOTE, TABLE_ROW_DATA_JSON }; - - try { - - cursor = db - .query(TABLE_NAME, columns, null, null, null, null, null); - cursor.moveToFirst(); - - if (!cursor.isAfterLast()) { - do { - rowContactObj = new LogEntryModel(); - rowContactObj.setId(cursor.getInt(0)); - prepareSendObject(rowContactObj, cursor); - allRowsObj.add(rowContactObj); - - } while (cursor.moveToNext()); // try to move the cursor's - // pointer forward one position. - } - } catch (SQLException e) { - Log.e("DB ERROR", e.toString()); - e.printStackTrace(); - } - - return allRowsObj; - - } - - private void prepareSendObject(LogEntryModel rowObj, Cursor cursor) { - rowObj.setId(cursor.getInt(cursor.getColumnIndexOrThrow(TABLE_ROW_ID))); - rowObj.setAlarmState(cursor.getInt(cursor - .getColumnIndexOrThrow(TABLE_ROW_ALARM_STATE))); - String dateTimeStr = cursor.getString(cursor - .getColumnIndexOrThrow(TABLE_ROW_DATE)); - Log.v(TAG,"dateTimeStr = "+dateTimeStr); - Date dateVal; - try { dateVal = new Date(dateTimeStr); } - catch (IllegalArgumentException e) { dateVal = null; } - rowObj.setDate(dateVal); - rowObj.setNote(cursor.getString(cursor - .getColumnIndexOrThrow(TABLE_ROW_NOTE))); - rowObj.setDataJSON(cursor.getString(cursor - .getColumnIndexOrThrow(TABLE_ROW_DATA_JSON))); - } - - public void deleteRow(int rowID) { - // ask the database manager to delete the row of given id - try { - db.delete(TABLE_NAME, TABLE_ROW_ID + "=" + rowID, null); - } catch (Exception e) { - Log.e("DB ERROR", e.toString()); - e.printStackTrace(); - } - } - - public void updateRow(int rowId, LogEntryModel contactObj) { - - ContentValues values = prepareData(contactObj); - - String whereClause = TABLE_ROW_ID + "=?"; - String whereArgs[] = new String[] { String.valueOf(rowId) }; - - db.update(TABLE_NAME, values, whereClause, whereArgs); - - } - - -} diff --git a/app/src/main/java/uk/org/openseizuredetector/EventLogManager/LogEntryModel.java b/app/src/main/java/uk/org/openseizuredetector/EventLogManager/LogEntryModel.java deleted file mode 100644 index 9502239..0000000 --- a/app/src/main/java/uk/org/openseizuredetector/EventLogManager/LogEntryModel.java +++ /dev/null @@ -1,53 +0,0 @@ -package uk.org.openseizuredetector.EventLogManager; - -import java.util.Date; - -/** - * Our LogEntryModel class which will have fields like id, name, contact number - * and email and corresponding getter and setter methods. - * **/ -public class LogEntryModel { - - private int id; - private Date date; - private int alarmState; - private String dataJSON; - private String note; - - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public int getAlarmState() { - return alarmState; - } - - public void setAlarmState(int alarmState) { - this.alarmState = alarmState; - } - - public String getNote() { - return note; - } - - public void setNote(String note) { - this.note = note; - } - - public Date getDate() { - return date; - } - - public void setDate(Date date) { - this.date = date; - } - - public String getDataJSON() { return dataJSON; } - - public void setDataJSON(String dataJSON) { this.dataJSON = dataJSON; } -} diff --git a/app/src/main/java/uk/org/openseizuredetector/DBQueryActivity.java b/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java similarity index 95% rename from app/src/main/java/uk/org/openseizuredetector/DBQueryActivity.java rename to app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java index d653e6b..d8b82fe 100644 --- a/app/src/main/java/uk/org/openseizuredetector/DBQueryActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/ExportDataActivity.java @@ -2,9 +2,8 @@ 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 androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; @@ -15,9 +14,9 @@ import android.widget.TimePicker; import java.util.Calendar; -public class DBQueryActivity extends AppCompatActivity +public class ExportDataActivity extends AppCompatActivity implements View.OnClickListener { - String TAG = "DBQueryActivity"; + String TAG = "ExportDataActivity"; Button mDateBtn; Button mTimeBtn; Button mExportBtn; diff --git a/app/src/main/java/uk/org/openseizuredetector/LogManager.java b/app/src/main/java/uk/org/openseizuredetector/LogManager.java index 38c1d20..8d5c72f 100644 --- a/app/src/main/java/uk/org/openseizuredetector/LogManager.java +++ b/app/src/main/java/uk/org/openseizuredetector/LogManager.java @@ -2,7 +2,7 @@ Android_SD - Android host for Garmin or Pebble watch based seizure detectors. See http://openseizuredetector.org for more information. - Copyright Graham Jones, 2019. + Copyright Graham Jones, 2019, 2021. This file is part of Android_SD. @@ -22,6 +22,7 @@ */ package uk.org.openseizuredetector; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; @@ -31,78 +32,245 @@ 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.JSONArray; +import org.json.JSONException; 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 java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; -import static android.database.sqlite.SQLiteDatabase.openOrCreateDatabase; +//import static android.database.sqlite.SQLiteDatabase.openOrCreateDatabase; /** * LogManager is a class to handle all aspects of Data Logging within OpenSeizureDetector. + * It performs several functions: + * - It will store seizure detector data to a local database on demand (it is called by the SdServer background service) + * - It will store system log data to the local database on demand (called by any part of OSD via the osdUtil functions) + * - It will periodically attempt to upload the oldest logged data to the osdApi remote database - the interface to the + * remote database is handled by the WebApiConnection class. It only tries to do one transaction with the external database + * at a time - if the periodic timer times out and an upload is in progress it will not do anything and wait for the next timeout.* + *

+ * The data upload process is as follows: + * - Select the oldest non-uploaded datapoint that is marked as an alarm or warning state. + * - Create an Event in the remote database based on that datapoint date and alarm type, and note the Event ID. + * - 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. */ public class LogManager { - private String TAG = "LogManager"; - private String mDbName = "osdData"; - private String mDbTableName = "datapoints"; + static final private String TAG = "LogManager"; + //private String mDbName = "osdData"; + final static private String mDpTableName = "datapoints"; + final static private String mEventsTableName = "events"; private boolean mLogRemote; private boolean mLogRemoteMobile; - private String mOSDUrl = "https://https://osd.dynu.net/"; - private String mApiToken; - private OsdDbHelper mOSDDb; + private String mAuthToken; + static private SQLiteDatabase mOsdDb = null; // SQLite Database for data and log entries. private RemoteLogTimer mRemoteLogTimer; - private Context mContext; + private static Context mContext; private OsdUtil mUtil; + public static WebApiConnection mWac; + public static final boolean USE_FIREBASE_BACKEND = false; + private boolean mUploadInProgress; + private long mEventDuration = 120; // event duration in seconds - uploads datapoints that cover this time range centred on the event time. + public long mDataRetentionPeriod = 1; // Prunes the local db so it only retains data younger than this duration (in days) + private long mRemoteLogPeriod = 60; // Period in seconds between uploads to the remote server. + private ArrayList mDatapointsToUploadList; + private String mCurrentEventRemoteId; + private long mCurrentEventLocalId = -1; + private int mCurrentDatapointId; + private long mAutoPrunePeriod = 3600; // Prune the database every hour + private boolean mAutoPruneDb; + private AutoPruneTimer mAutoPruneTimer; - public LogManager(Context context) { - mLogRemote = false; - mLogRemoteMobile = false; - mOSDUrl = null; - mContext = context; - - Handler handler = new Handler(); - mUtil = new OsdUtil(mContext, handler); - - startRemoteLogTimer(); + public interface CursorCallback { + void accept(Cursor retVal); } - private boolean openDb() { - try { - mOSDDb = new OsdDbHelper(mDbTableName, mContext); - if (!checkTableExists(mOSDDb, mDbTableName)) { - Log.e(TAG,"ERROR - Table does not exist"); - return false; + public interface ArrayListCallback { + void accept(ArrayList> retVal); + } + + public LogManager(Context context, + boolean logRemote, boolean logRemoteMobile, String authToken, + long eventDuration, long remoteLogPeriod, + boolean autoPruneDb, long dataRetentionPeriod) { + Log.d(TAG, "LogManger Constructor"); + mContext = context; + Handler handler = new Handler(); + + mLogRemote = logRemote; + mLogRemoteMobile = logRemoteMobile; + mAuthToken = authToken; + mEventDuration = eventDuration; + mAutoPruneDb = autoPruneDb; + mDataRetentionPeriod = dataRetentionPeriod; + mRemoteLogPeriod = remoteLogPeriod; + Log.v(TAG, "mLogRemote=" + mLogRemote); + Log.v(TAG, "mLogRemoteMobile=" + mLogRemoteMobile); + Log.v(TAG, "mEventDuration=" + mEventDuration); + Log.v(TAG, "mAutoPruneDb=" + mAutoPruneDb); + Log.v(TAG, "mDataRetentionPeriod=" + mDataRetentionPeriod); + Log.v(TAG, "mRemoteLogPeriod=" + mRemoteLogPeriod); + + mUtil = new OsdUtil(mContext, handler); + openDb(); + Log.i(TAG, "Starting Remote Database Interface"); + if (USE_FIREBASE_BACKEND) { + mWac = new WebApiConnection_firebase(mContext); + } else { + mWac = new WebApiConnection_osdapi(mContext); + } + + mWac.setStoredToken(mAuthToken); + + if (mLogRemote) { + Log.i(TAG, "Starting Remote Log Timer"); + startRemoteLogTimer(); + } else { + Log.i(TAG, "mLogRemote is false - not starting remote log timer"); + } + + if (mAutoPruneDb) { + Log.i(TAG, "Starting Auto Prune Timer"); + startAutoPruneTimer(); + } else { + Log.i(TAG, "AutoPruneDB is not set - not starting Auto Prune Timer"); + } + + } + + /** + * Returns a JSON String representing an array of datapoints that are selected from sqlite cursor c. + * + * @param c sqlite cursor pointing to datapoints query result. + * @return JSON String. + * from https://stackoverflow.com/a/20488153/2104584 + */ + private String cursor2Json(Cursor c) { + StringBuilder cNames = new StringBuilder(); + for (String n : c.getColumnNames()) { + cNames.append(", ").append(n); + } + //Log.v(TAG,"cursor2Json() - c="+c.toString()+", columns="+cNames+", number of rows="+c.getCount()); + c.moveToFirst(); + //JSONObject Root = new JSONObject(); + JSONArray dataPointArray = new JSONArray(); + int i = 0; + while (!c.isAfterLast()) { + JSONObject datapoint = new JSONObject(); + try { + datapoint.put("id", c.getString(c.getColumnIndex("id"))); + datapoint.put("dataTime", c.getString(c.getColumnIndex("dataTime"))); + datapoint.put("status", c.getString(c.getColumnIndex("status"))); + datapoint.put("dataJSON", c.getString(c.getColumnIndex("dataJSON"))); + datapoint.put("uploaded", c.getString(c.getColumnIndex("uploaded"))); + //Log.v(TAG,"cursor2json() - datapoint="+datapoint.toString()); + c.moveToNext(); + dataPointArray.put(i, datapoint); + i++; + } catch (JSONException e) { + Log.e(TAG, "cursor2Json(): error creating JSON Object"); + e.printStackTrace(); + } + } + return dataPointArray.toString(); + } + + /** + * Returns a JSON String representing an array of events that are selected from sqlite cursor c. + * + * @param c sqlite cursor pointing to events query result. + * @return JSON String. + * from https://stackoverflow.com/a/20488153/2104584 + */ + private String eventCursor2Json(Cursor c) { + StringBuilder cNames = new StringBuilder(); + for (String n : c.getColumnNames()) { + cNames.append(", ").append(n); + } + c.moveToFirst(); + Log.v(TAG, "eventCursor2Json: size of cursor=" + c.getCount()); + JSONArray eventsArray = new JSONArray(); + int i = 0; + while (!c.isAfterLast()) { + JSONObject event = new JSONObject(); + try { + 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 ); + val = c.getString(c.getColumnIndex("dataTime")); + event.put("dataTime", val==null ? "" : val); + val = c.getString(c.getColumnIndex("status")); + event.put("status", val==null ? "" : val); + val = c.getString(c.getColumnIndex("type")); + event.put("type", val==null ? "" : val); + val = c.getString(c.getColumnIndex("subType")); + event.put("subType", val==null ? "" : val); + val = c.getString(c.getColumnIndex("notes")); + event.put("desc", val==null ? "" : val); + val = c.getString(c.getColumnIndex("dataJSON")); + event.put("dataJSON", val==null ? "" : val); + val = c.getString(c.getColumnIndex("uploaded")); + event.put("uploaded", val==null ? "" : val); + c.moveToNext(); + eventsArray.put(i, event); + i++; + } catch (JSONException e) { + Log.e(TAG, "eventCursor2Json(): error creating JSON Object"); + e.printStackTrace(); + } + } + Log.v(TAG, "eventCursor2JSON(): returning " + eventsArray.toString()); + return eventsArray.toString(); + } + + + private static boolean openDb() { + Log.d(TAG, "openDb"); + try { + if (mOsdDb == null) { + Log.i(TAG, "openDb: mOsdDb is null - initialising"); + mOsdDb = new OsdDbHelper(mContext).getWritableDatabase(); + } else { + Log.i(TAG, "openDb: mOsdDb has been initialised already so not doing anything"); + } + String[] tableNames = new String[]{mDpTableName, mEventsTableName}; + for (String tableName : tableNames) { + if (!checkTableExists(mOsdDb, tableName)) { + Log.e(TAG, "ERROR - Table " + tableName + " does not exist"); + return false; + } else { + Log.d(TAG, "table " + tableName + " exists ok"); + } } - return true; } catch (SQLException e) { Log.e(TAG, "Failed to open Database: " + e.toString()); - mOSDDb = null; return false; } + return true; } - private boolean checkTableExists(OsdDbHelper osdDb, String osdTableName) { + private static boolean checkTableExists(SQLiteDatabase osdDb, String osdTableName) { Cursor c = null; boolean tableExists = false; + Log.d(TAG, "checkTableExists()"); try { - c = osdDb.getWritableDatabase().query(osdTableName, null, + c = osdDb.query(osdTableName, null, null, null, null, null, null); tableExists = true; - } - catch (Exception e) { - Log.d(TAG, osdTableName+" doesn't exist :((("); + c.close(); + } catch (Exception e) { + Log.d(TAG, osdTableName + " doesn't exist :((("); } return tableExists; } @@ -112,261 +280,845 @@ public class LogManager { * Write data to local database * FIXME - I am sure we should not be using raw SQL Srings to do this! */ - public void writeToLocalDb(SdData sdData) { - Log.v(TAG, "writeToLocalDb()"); - Time tnow = new Time(Time.getCurrentTimezone()); - tnow.setToNow(); - String dateStr = tnow.format("%Y-%m-%d"); + public void writeDatapointToLocalDb(SdData sdData) { + //Log.v(TAG, "writeDatapointToLocalDb()"); + Date curDate = new Date(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + String dateStr = dateFormat.format(curDate); String SQLStr = "SQLStr"; - try { - SQLStr = "INSERT INTO "+ mDbTableName - + "(dataTime, wearer_id, BattPC, specPow, roiRatio, avAcc, sdAcc, hr, status, dataJSON, uploaded)" - + " VALUES(" - +"CURRENT_TIMESTAMP," - + -1 + "," - + sdData.batteryPc + "," - + sdData.specPower + "," - + 10. * sdData.roiPower / sdData.specPower + "," - + sdData.getAvAcc() + "," - + sdData.getSdAcc() + "," - + sdData.mHR + "," - + sdData.alarmState + "," - + DatabaseUtils.sqlEscapeString(sdData.toCSVString(true)) + "," - + 0 - +")"; - mOSDDb.getWritableDatabase().execSQL(SQLStr); - - } catch (SQLException e) { - Log.e(TAG,"writeToLocalDb(): Error Writing Data: " + e.toString()); - Log.e(TAG,"SQLStr was "+SQLStr); + if (mOsdDb == null) { + Log.e(TAG, "writeDatapointToLocalDb(): mOsdDb is null - doing nothing"); + return; } + try { + // Write Datapoint to database + SQLStr = "INSERT INTO " + mDpTableName + + "(dataTime, status, dataJSON, uploaded)" + + " VALUES(" + + "'" + dateStr + "'," + + sdData.alarmState + "," + + DatabaseUtils.sqlEscapeString(sdData.toDatapointJSON()) + "," + + 0 + + ")"; + mOsdDb.execSQL(SQLStr); + Log.v(TAG, "writeDatapointToLocalDb(): datapoint written to database"); + if (sdData.alarmState != 0) { + Log.i(TAG, "writeDatapointToLocalDb(): adding event to local DB"); + createLocalEvent(dateStr,sdData.alarmState,null, null, null, sdData.toSettingsJSON()); + } + } catch (SQLException e) { + Log.e(TAG, "writeToLocalDb(): Error Writing Data: " + e.toString()); + Log.e(TAG, "SQLStr was " + SQLStr); + } catch (NullPointerException e) { + Log.e(TAG, "writeToLocalDb(): Null Pointer Exception: " + e.toString()); + } } + public boolean createLocalEvent(String dataTime, long status) { + return (createLocalEvent(dataTime, status, null, null, null, null)); + } + + 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); + // Write Event to database + //String SQLStr = "INSERT INTO " + mEventsTableName + // + "(dataTime, status, type, subtype, notes, dataJSON)" + // + " VALUES(" + // + "'" + dataTime + "'," + // + status + "," + // + "'" + type + "'," + // + "'" + subType + "'," + // + "'" + desc + "'," + // + "'" + dataJSON + "'" + // + ")"; + //mOsdDb.execSQL(SQLStr); + 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("dataJSON", dataJSON); + + long newRowId = mOsdDb.insert(mEventsTableName, null, values); + Log.d(TAG, "Created Row ID"+newRowId); + return true; + } + + /** + * Returns a json representation of locally stored event 'id'. + * + * @param id event id to return + * @return JSON representation of requested event (single element JSON array) + */ + public String getLocalEventById(long id) { + Log.d(TAG, "getLocalEventById() - id=" + id); + Cursor c; + String retVal; + try { + String selectStr = "select * from " + mEventsTableName + " where id=" + id + ";"; + c = mOsdDb.rawQuery(selectStr, null); + retVal = eventCursor2Json(c); + } catch (Exception e) { + Log.d(TAG, "getLocalEventById(): Error Querying Database: " + e.getLocalizedMessage()); + retVal = null; + } + Log.d(TAG, "getLocalEventById() - returning " + retVal); + return (retVal); + } + + + /** + * Returns a json representation of datapoint 'id'. + * + * @param id datapoint id to return + * @return JSON representation of requested datapoint (single element JSON array) + */ + public String getDatapointById(long id) { + Log.d(TAG, "getDatapointById() - id=" + id); + Cursor c; + String retVal; + try { + String selectStr = "select * from " + mDpTableName + " where id=" + id + ";"; + c = mOsdDb.rawQuery(selectStr, null); + retVal = cursor2Json(c); + } catch (Exception e) { + Log.d(TAG, "getDatapointById(): Error Querying Database: " + e.getLocalizedMessage()); + retVal = null; + } + return (retVal); + } + + /** + * setDatapointToUploaded + * + * @param id - datapoint ID to change + * @param eventId - the eventId associated with the uploaded datapoint - the 'uploaded' field is set to this value. + * @return True on success or False on failure. + */ + public boolean setDatapointToUploaded(int id, String eventId) { + Log.d(TAG, "setDatapointToUploaded() - id=" + id); + if (mOsdDb == null) { + Log.e(TAG, "setDatapointToUploaded() - mOsdDb is null - not doing anything"); + return false; + } + ContentValues cv = new ContentValues(); + cv.put("uploaded", eventId); + int nRowsUpdated = mOsdDb.update(mDpTableName, cv, "id = ?", + new String[]{String.format("%d", id)}); + return (nRowsUpdated == 1); + } + + /** + * setDatapointStatus() - Update the status of data point id. + * + * @param id datapont id + * @param statusVal the status to set for the datapoint. + * @return true on success or false on failure + */ + public boolean setDatapointStatus(Long id, int statusVal) { + Log.d(TAG, "setDatapointStatus() - id=" + id + ", statusVal=" + statusVal); + //Cursor c = null; + ContentValues cv = new ContentValues(); + cv.put("status", statusVal); + int nRowsUpdated = mOsdDb.update(mDpTableName, cv, "id = ?", + new String[]{String.format("%d", id)}); + + return (nRowsUpdated == 1); + } + + + /** + * Return a JSON string representing all the datapoints between startDate and endDate + * + * @return True on successful start or false if call fails. + */ + public boolean getDatapointsByDate(String startDateStr, String endDateStr, WebApiConnection.StringCallback callback) { + Log.d(TAG, "getDatapointsbyDate() - startDateStr=" + startDateStr + ", endDateStr=" + endDateStr); + String[] columns = {"*"}; + String whereClause = "DataTime>? AND DataTime { + Log.v(TAG, "getDataPointsByDate - returned " + cursor); + if (cursor != null) { + callback.accept(cursor2Json(cursor)); + } else { + callback.accept(null); + } + }).execute(); + return (true); + } + + + /** + * Return an array list of objects representing the events in the database by calling the specified callback function. + * + * @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) { + Log.v(TAG, "getEventsList - includeWarnings=" + includeWarnings); + ArrayList> eventsList = new ArrayList<>(); + + String[] whereArgs = getEventWhereArgs(includeWarnings); + String whereClause = getEventWhereClause(includeWarnings); + //sqlStr = "SELECT * from " + mDbTableName + " where Status in (" + statusListStr + ") order by dataTime desc;"; + String[] columns = {"*"}; + new SelectQueryTask(mEventsTableName, columns, whereClause, whereArgs, + null, null, "dataTime DESC", (Cursor cursor) -> { + Log.v(TAG, "getEventsList - returned " + cursor); + if (cursor != null) { + Log.v(TAG, "getEventsList - returned " + cursor.getCount() + " records"); + while (!cursor.isAfterLast()) { + HashMap event = new HashMap<>(); + //event.put("id", cursor.getString(cursor.getColumnIndex("id"))); + event.put("dataTime", cursor.getString(cursor.getColumnIndex("dataTime"))); + int status = cursor.getInt(cursor.getColumnIndex("status")); + String statusStr = mUtil.alarmStatusToString(status); + event.put("status", statusStr); + event.put("uploaded", cursor.getString(cursor.getColumnIndex("uploaded"))); + //event.put("dataJSON", cursor.getString(cursor.getColumnIndex("dataJSON"))); + eventsList.add(event); + cursor.moveToNext(); + } + } + callback.accept(eventsList); + }).execute(); + return (true); + } + + + /** + * pruneLocalDb() removes data that is older than mLocalDbMaxAgeDays days + */ + public int pruneLocalDb() { + Log.d(TAG, "pruneLocalDb()"); + int retVal = 0; + long currentDateMillis = new Date().getTime(); + long endDateMillis = currentDateMillis - 24 * 3600 * 1000 * mDataRetentionPeriod; + //long endDateMillis = currentDateMillis - 3600*1000* mDataRetentionPeriod; // Using hours rather than days for testing + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String endDateStr = dateFormat.format(new Date(endDateMillis)); + String[] tableNames = new String[]{mDpTableName, mEventsTableName}; + for (String tableName : tableNames) { + Log.i(TAG, "pruneLocalDb - pruning table " + tableName); + try { + String selectStr = "DataTime<=?"; + String[] selectArgs = {endDateStr}; + retVal = mOsdDb.delete(tableName, selectStr, selectArgs); + } catch (Exception e) { + Log.d(TAG, "Error deleting data " + e.toString()); + retVal = 0; + } + Log.d(TAG, String.format("pruneLocalDb() - deleted %d records from table %s", retVal, tableName)); + } + return (retVal); + } + + /** + * setEventToUploaded + * + * @param localEventId - local Event ID to change + * @param remoteEventId - the remote eventId associated with the uploaded datapoint - the 'uploaded' field is set to this value. + * @return True on success or False on failure. + */ + public boolean setEventToUploaded(long localEventId, String remoteEventId) { + Log.d(TAG, "setEventToUploaded() - local id=" + localEventId + " remote id=" + remoteEventId); + if (mOsdDb == null) { + Log.e(TAG, "setEventToUploaded() - mOsdDb is null - not doing anything"); + return false; + } + ContentValues cv = new ContentValues(); + cv.put("uploaded", remoteEventId); + int nRowsUpdated = mOsdDb.update(mEventsTableName, cv, "id = ?", + new String[]{String.format("%d", localEventId)}); + return (nRowsUpdated == 1); + } + + + /** + * Return the ID of the next event (alarm, warning, fall etc that needs to be uploaded (alarm or warning condition and has not yet been uploaded. + * + * @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) { + Log.v(TAG, "getNextEventToUpload - includeWarnings=" + includeWarnings); + + String[] whereArgsStatus = getEventWhereArgs(includeWarnings); + String whereClauseStatus = getEventWhereClause(includeWarnings); + String[] columns = {"*"}; + + // Do not try to upload very recent events so that we have chance to record the post-event data before uploading it. + long currentDateMillis = new Date().getTime(); + long endDateMillis = currentDateMillis - 1000 * mEventDuration; + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String endDateStr = dateFormat.format(new Date(endDateMillis)); + String whereClauseUploaded = "uploaded is null"; + String whereClauseDate = "DataTime { + Long recordId = new Long(-1); + if (cursor != null) { + Log.v(TAG, "getNextEventToUpload - returned " + cursor.getCount() + " records"); + cursor.moveToFirst(); + if (cursor.getCount() == 0) { + Log.v(TAG, "getNextEventToUpload() - no events to Upload - exiting"); + recordId = new Long(-1); + } else { + recordId = cursor.getLong(0); + Log.d(TAG, "getNextEventToUpload(): id=" + recordId); + } + } + callback.accept(recordId); + }).execute(); + return (true); + } + + + /** + * Return the ID of the datapoint that is closest to date/time string dateStr + * Based on https://stackoverflow.com/questions/45749046/sql-get-nearest-date-record + * + * @return True on successful start or false if call fails. + */ + 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;"; + String orderByStr = "ABS(ddiff) asc"; + new SelectQueryTask(mDpTableName, columns, null, null, + null, null, orderByStr, (Cursor cursor) -> { + Log.v(TAG, "getEventsNearestDatapointToDate - returned " + cursor); + Long recordId = new Long(-1); + if (cursor != null) { + Log.v(TAG, "getNearestDatapointToDate - returned " + cursor.getCount() + " records"); + cursor.moveToFirst(); + if (cursor.getCount() == 0) { + Log.v(TAG, "getNearestDatapointToDate() - no events to Upload - exiting"); + recordId = new Long(-1); + } else { + String recordStr = cursor.getString(3); + recordId = cursor.getLong(0); + Log.d(TAG, "getNearestDatapointToDate(): id=" + recordId + ", recordStr=" + recordStr); + } + } + callback.accept(recordId); + }).execute(); + return (true); + } + + + /** + * Return the number of events stored in the local database (via a callback). + * + * @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) { + //Log.v(TAG, "getLocalEventsCount- includeWarnings=" + includeWarnings); + String[] whereArgs = getEventWhereArgs(includeWarnings); + String whereClause = getEventWhereClause(includeWarnings); + String[] columns = {"*"}; + new SelectQueryTask(mEventsTableName, columns, whereClause, whereArgs, + null, null, null, (Cursor cursor) -> { + //Log.v(TAG, "getLocalEventsCount - returned " + cursor); + Long eventCount = Long.valueOf(0); + if (cursor != null) { + eventCount = Long.valueOf(cursor.getCount()); + Log.v(TAG, "getLocalEventsCount - returned " + eventCount + " records"); + } + callback.accept(eventCount); + }).execute(); + return (true); + } + + /** + * Return the number of datapoints stored in the local database (via a callback). + * + * @return True on successful start or false if call fails. + */ + public boolean getLocalDatapointsCount(WebApiConnection.LongCallback callback) { + //Log.v(TAG, "getLocalDatapointsCount"); + String[] whereArgs = null; + String whereClause = null; + String[] columns = {"*"}; + new SelectQueryTask(mDpTableName, columns, whereClause, whereArgs, + null, null, null, (Cursor cursor) -> { + //Log.v(TAG, "getLocalDatapointsCount - returned " + cursor); + Long eventCount = Long.valueOf(0); + if (cursor != null) { + eventCount = Long.valueOf(cursor.getCount()); + Log.v(TAG, "getLocalDatapointsCount - returned " + eventCount + " records"); + } + callback.accept(eventCount); + }).execute(); + return (true); + } + + + /** + * Executes the sqlite query (=SELECT statement) + * Use as new SelectQueryTask(xxx,xxx,xx,xxxx).execute() + */ + static private class SelectQueryTask extends AsyncTask { + // Based on https://stackoverflow.com/a/21120199/2104584 + String mTable; + String[] mColumns; + String mSelection; + String[] mSelectionArgs; + String mGroupBy; + String mHaving; + String mOrderBy; + CursorCallback mCallback; + + //query(String table, String[] columns, String selection, String[] selectionArgs, + // String groupBy, String having, String orderBy) + SelectQueryTask(String table, String[] columns, String selection, String[] selectionArgs, + String groupBy, String having, String orderBy, CursorCallback callback) { + // list all the parameters like in normal class define + this.mTable = table; + this.mColumns = columns; + this.mSelection = selection; + this.mSelectionArgs = selectionArgs; + this.mGroupBy = groupBy; + this.mHaving = having; + this.mOrderBy = orderBy; + this.mCallback = callback; + + } + + @Override + protected Cursor doInBackground(Void... params) { + //Log.v(TAG, "runSelect.doInBackground()"); + Log.v(TAG, "SelectQueryTask.doInBackground: mTable=" + mTable + ", mColumns=" + Arrays.toString(mColumns) + + ", mSelection=" + mSelection + ", mSelectionArgs=" + Arrays.toString(mSelectionArgs) + ", mGroupBy=" + mGroupBy + + ", mHaving =" + mHaving + ", mOrderBy=" + mOrderBy); + + try { + Cursor resultSet = mOsdDb.query(mTable, mColumns, mSelection, + mSelectionArgs, mGroupBy, mHaving, mOrderBy); + resultSet.moveToFirst(); + return (resultSet); + } catch (SQLException e) { + Log.e(TAG, "SelectQueryTask.doInBackground(): Error selecting Data: " + e.toString()); + return (null); + } catch (IllegalArgumentException e) { + Log.e(TAG, "SelectQueryTask.doInBackground(): Illegal Argument Exception: " + e.toString()); + return (null); + } catch (NullPointerException e) { + Log.e(TAG, "SelectQueryTask.doInBackground(): Null Pointer Exception: " + e.toString()); + return (null); + } + } + + @Override + protected void onPostExecute(final Cursor result) { + mCallback.accept(result); + } + } + + + private String getEventWhereClause(boolean includeWarnings) { + String whereClause; + if (includeWarnings) { + whereClause = "Status in (?, ?, ?, ?)"; + } else { + whereClause = "Status in (?, ?, ?)"; + } + return (whereClause); + } + + private String[] getEventWhereArgs(boolean includeWarnings) { + String[] whereArgs; + if (includeWarnings) { + whereArgs = new String[]{"1", "2", "3", "5"}; + } else { + whereArgs = new String[]{"2", "3", "5"}; + } + return (whereArgs); + } + + + /*************************************************************************************** + * Remote Database Part + */ public void writeToRemoteServer() { - Log.v(TAG,"writeToRemoteServer()"); + Log.v(TAG, "writeToRemoteServer()"); if (!mLogRemote) { - Log.v(TAG,"mLogRemote not set, not doing anything"); + Log.v(TAG, "writeToRemoteServer(): 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"); + Log.v(TAG, "writeToRemoteServer(): Using mobile data, so not doing anything"); return; } } if (!mUtil.isNetworkConnected()) { - Log.v(TAG,"No network connection - doing nothing"); + Log.v(TAG, "writeToRemoteServer(): No network connection - doing nothing"); return; } - Log.v(TAG,"Requirements for remote logging met!"); + if (mUploadInProgress) { + Log.v(TAG, "writeToRemoteServer(): Upload already in progress, not starting another upload"); + return; + } + + Log.d(TAG, "writeToRemoteServer(): calling UploadSdData()"); uploadSdData(); } - /** - * Authenticate using the WebAPI to obtain a token for future API requests. - * @param uname - user name - * @param passwd - password - */ - public void authenticate(String uname, String passwd) { - Log.v(TAG, "authenticate()"); - // FIXME - this does not work!!!! - String dataStr = "{'login':"+uname+", 'password':"+passwd+"}"; - //new PostDataTask().execute("http://" + mOSDUrl + ":8080/data", dataStr, mOSDUname, mOSDPasswd); - String urlStr = mOSDUrl+"/api/accounts/login/"; - Log.v(TAG,"authenticate: url="+urlStr+", data="+dataStr); - new PostDataTask().execute( - urlStr, dataStr); - } - /** * 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. + * Uses the webApiConnection class to upload the data in the background. + * It searches the local database for the oldest event that has not been uploaded and uploads it. + * eventCallback is called when the event is created. */ 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); + //int eventId = -1; + //Log.v(TAG, "uploadSdData()"); + // First try uploading full alarms, and only if we do not have any of those, upload warnings. + //boolean warningsArr[] = {false, true}; + // Upload everything - alarms and warnings - we can sort it out in post-processing the data! + boolean warningsArr[] = {true}; + for (int n = 0; n < warningsArr.length; n++) { + boolean warningsVal = warningsArr[n]; + Log.i(TAG, "uploadSdData(): warningsVal=" + warningsVal); + if (mUploadInProgress) { + Log.d(TAG, "uploadSdData - upload already in progress - not doing anything"); + return; + } + mUploadInProgress = true; + getNextEventToUpload(warningsVal, (Long eventId) -> { + if (eventId != -1) { + Log.i(TAG, "uploadSdData() - next Event to Upload eventId=" + eventId); + String eventJsonStr = getLocalEventById(eventId); + Log.v(TAG, "uploadSdData() - event to upload eventJsonStr=" + eventJsonStr); + //int eventType; + JSONObject eventObj; + int eventAlarmStatus; + String eventDateStr; + Date eventDate; + String eventType; + String eventSubType; + String eventDesc; + String eventDataJSON; + try { + JSONArray datapointJsonArr = new JSONArray(eventJsonStr); + eventObj = datapointJsonArr.getJSONObject(0); // We only look at the first (and hopefully only) item in the array. + eventAlarmStatus = Integer.parseInt(eventObj.getString("status")); + eventDateStr = eventObj.getString("dataTime"); + eventType = eventObj.getString("type"); + eventSubType = eventObj.getString("subType"); + if (eventObj.has("desc")) + eventDesc = eventObj.getString("desc"); + else + eventDesc = ""; + eventDataJSON = eventObj.getString("dataJSON"); + Log.d(TAG, "uploadSdData - data from local DB is:" + eventJsonStr + ", eventAlarmStatus=" + + eventAlarmStatus + ", eventDateStr=" + eventDateStr); + } catch (JSONException e) { + Log.e(TAG, "uploadSdData(): ERROR parsing event JSON Data" + eventJsonStr); + e.printStackTrace(); + mUploadInProgress = false; + return; + } catch (NullPointerException e) { + Log.e(TAG, "uploadSdData(): ERROR null pointer exception parsing event JSON Data: " + eventJsonStr); + e.printStackTrace(); + mUploadInProgress = false; + return; + } + try { + eventDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(eventDateStr); + } catch (ParseException e) { + Log.e(TAG, "UploadSdData(): Error parsing date " + eventDateStr); + mUploadInProgress = false; + return; + } + + Log.i(TAG, "uploadSdData - calling mWac.createEvent"); + mCurrentEventLocalId = eventId; + mWac.createEvent(eventAlarmStatus, eventDate, eventType, eventSubType, eventDesc, eventDataJSON, this::createEventCallback); + } else { + Log.v(TAG, "uploadSdData - no data to upload "); //(warnings="+warningsVal+")"); + mUploadInProgress = false; + } + }); + } } - 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 (not used) - // params[3] is the password (not used) - int MAXLEN = 500; // Maximum length of response that we will accept (bytes) - InputStream is = null; - String urlStr = params[0]; - String dataStr = params[1]; - String resultStr = "Not Initialised"; - Log.v(TAG,"doInBackgound(): url="+urlStr+" data="+dataStr); - 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); + // Mark the relevant member variables to show we are not cuurrently doing an upload, so a new one can be + // started if necessary. + public void finishUpload() { + mCurrentEventRemoteId = null; + mCurrentEventLocalId = -1; + mCurrentDatapointId = -1; + mDatapointsToUploadList = null; + mUploadInProgress = false; + } - // 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) { + // Called by WebApiConnection when a new event record is created. + // Once the event is created it queries the local database to find the datapoints associated with the event + // and uploads those as a batch of data points. + public void createEventCallback(String eventId) { + Log.v(TAG, "createEventCallback(): " + eventId); + Log.v(TAG, "createEventCallback(): Retrieving remote event details"); + mWac.getEvent(eventId, new WebApiConnection.JSONObjectCallback() { + @Override + public void accept(JSONObject eventObj) { + if (eventObj == null) { + Log.e(TAG, "createEventCallback() - eventObj is null - failed to create event"); + mUtil.showToast("Error Creating Remote Event"); + } else { + Log.v(TAG, "createEventCallback() - eventObj=" + eventObj.toString()); + Date eventDate; + String eventDateStr = ""; try { - is.close(); - } catch (IOException e) { - Log.v(TAG,"doInBackground(): IOException - "+e.toString()); - resultStr = "Error"+e.toString(); + String dateStr = eventObj.getString("dataTime"); + eventDate = mUtil.string2date(dateStr); + } catch (JSONException e) { + Log.e(TAG, "createEventCallback() - Error parsing JSONObject: " + eventObj.toString()); + finishUpload(); + return; + } + if (eventDate != null) { + Log.v(TAG, "createEventCallback() EventId=" + eventId + ", eventDateStr=" + eventDateStr + ", eventDate=" + eventDate); + mUploadInProgress = true; + long eventDateMillis = eventDate.getTime(); + long startDateMillis = eventDateMillis - 1000 * mEventDuration / 2; + long endDateMillis = eventDateMillis + 1000 * mEventDuration / 2; + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + getDatapointsByDate( + dateFormat.format(new Date(startDateMillis)), + dateFormat.format(new Date(endDateMillis)), + (String datapointsJsonStr) -> { + //Log.v(TAG, "createEventCallback() - datapointsJsonStr=" + datapointsJsonStr); + JSONArray dataObj; + mDatapointsToUploadList = new ArrayList(); + try { + //DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dataObj = new JSONArray(datapointsJsonStr); + Log.v(TAG, "createEventCallback() - datapointsObj length=" + dataObj.length()); + for (int i = 0; i < dataObj.length(); i++) { + mDatapointsToUploadList.add(dataObj.getJSONObject(i)); + } + } catch (JSONException e) { + Log.v(TAG, "createEventCallback(): Error Creating JSON Object from string " + datapointsJsonStr); + dataObj = null; + finishUpload(); + } + // This starts the process of uploading the datapoints, one at a time. + mCurrentEventRemoteId = eventId; + Log.v(TAG, "createEventCallback() - starting datapoints upload with eventId " + mCurrentEventRemoteId + + " Uploading " + mDatapointsToUploadList.size() + " datapoints"); + uploadNextDatapoint(); + + }); + } else { + Log.e(TAG, "createEventCallback() - Error - event date is null - not doing anything"); + mUtil.showToast("Error uploading event - date is null"); + finishUpload(); } } } + }); + } + + // takes the next datapoint of the list mDatapointsToUploadList and uploads it to the remote server. + // datapointCallback is called when the upload is complete. + public void uploadNextDatapoint() { + //Log.v(TAG, "uploadNextDatapoint()"); + if (mDatapointsToUploadList != null) { + if (mDatapointsToUploadList.size() > 0) { + mUploadInProgress = true; + try { + mCurrentDatapointId = mDatapointsToUploadList.get(0).getInt("id"); + } catch (JSONException e) { + Log.e(TAG, "uploadNextDatapoint(): Error reading currentDatapointID from mDatapointsToUploadList[0]" + e.getMessage()); + Log.e(TAG, "uploadNextDatapoint(): Removing mDatapointsToUploadList[0] and trying the next datapoint"); + mDatapointsToUploadList.remove(0); + uploadNextDatapoint(); + } + + Log.v(TAG, "uploadNextDatapoint() - " + mDatapointsToUploadList.size() + " datapoints to upload. Uploading datapoint ID:" + mCurrentDatapointId); + mWac.createDatapoint(mDatapointsToUploadList.get(0), mCurrentEventRemoteId, this::datapointCallback); - if (resultStr.startsWith("Unable to retrieve web page")) { - Log.v(TAG,"doInBackground() - Unable to retrieve data"); } else { - Log.v(TAG,"doInBackground(): result = "+resultStr); + Log.i(TAG, "uploadNextDatapoint() - All datapoints uploaded!"); + setEventToUploaded(mCurrentEventLocalId, mCurrentEventRemoteId); + finishUpload(); } - return (resultStr); - - } - // onPostExecute displays the results of the AsyncTask. - @Override - protected void onPostExecute(String result) { - Log.v(TAG,"onPostExecute() - result = "+result); + } else { + Log.w(TAG, "uploadNextDatapoint - mDatapointsToUploadList is null - I don't thin this should have happened!"); } } + // Called by WebApiConnection when a new datapoint is created. It assumes that we have just created + // a datapoint based on mDatapointsToUploadList(0) so removes that from the list and calls UploadDatapoint() + // to upload the next one. + public void datapointCallback(String datapointStr) { + Log.v(TAG, "datapointCallback() dataPointId=" + mCurrentDatapointId + " remote datapointID=" + datapointStr + ", mCurrentEventId=" + mCurrentEventRemoteId); + if (mDatapointsToUploadList != null) { + if (mDatapointsToUploadList.size() > 0) { + mDatapointsToUploadList.remove(0); + } + } else { + Log.w(TAG, "datapointCallback - mDatapointsToUploadList is null - I don't thin this should have happened!"); + } + setDatapointToUploaded(mCurrentDatapointId, mCurrentEventRemoteId); + uploadNextDatapoint(); + } + /** + * close() - shut down the logging system + * WARNING - this should only be called by the final destructor of the app (not individual class destructors) + * because it will close the DB for all instances of LogManger, not just the one on which it is called. + * FIXME: If I was keen I would keep a count of how many instances of LogManager there are, and have this function do nothing + * unless it was the last instance. + */ + public static void close() { + mOsdDb.close(); + mOsdDb = null; + if (mWac != null) { + Log.i(TAG, "Stopping Remote Database Interface"); + mWac.close(); + } + } - public void close() { - mOSDDb.close(); + public void stop() { + // Stop the timers and shutdown the remote API connection. 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 { - // If you change the database schema, you must increment the database version. - public static final int DATABASE_VERSION = 1; - public static final String DATABASE_NAME = "OsdData.db"; - private String mOsdTableName; - private String TAG = "OsdDbHelper"; - - public OsdDbHelper(String osdTableName, Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - mOsdTableName = osdTableName; - } - public void onCreate(SQLiteDatabase db) { - Log.v(TAG,"onCreate - TableName="+mOsdTableName); - String SQLStr = "CREATE TABLE IF NOT EXISTS "+mOsdTableName+"(" - + "id INT AUTO_INCREMENT PRIMARY KEY," - + "dataTime DATETIME," - + "wearer_id INT NOT NULL," - + "BattPC FLOAT," - + "specPow FLOAT," - + "roiRatio FLOAT," - + "avAcc FLOAT," - + "sdAcc FLOAT," - + "HR FLOAT," - + "Status INT," - + "dataJSON TEXT," - + "uploaded INT" - + ");"; - - db.execSQL(SQLStr); - } - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // This database is only a cache for online data, so its upgrade policy is - // to simply to discard the data and start over - db.execSQL("Drop table if exists " + mOsdTableName + ";"); - onCreate(db); - } - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - onUpgrade(db, oldVersion, newVersion); - } + stopAutoPruneTimer(); } /* - * Start the timer that will send and SMS alert after a given period. + * Start the timer that will upload data to the remote server after a given period. */ private void startRemoteLogTimer() { if (mRemoteLogTimer != null) { - Log.v(TAG, "startRemoteLogTimer -timer already running - cancelling it"); + Log.i(TAG, "startRemoteLogTimer -timer already running - cancelling it"); mRemoteLogTimer.cancel(); mRemoteLogTimer = null; } - Log.v(TAG, "startRemoteLogTimer() - starting RemoteLogTimer"); + Log.i(TAG, "startRemoteLogTimer() - starting RemoteLogTimer"); mRemoteLogTimer = - new RemoteLogTimer(10 * 1000, 1000); + new RemoteLogTimer(mRemoteLogPeriod * 1000, 1000); mRemoteLogTimer.start(); } /* - * Cancel the SMS timer to prevent the SMS message being sent.. + * Cancel the remote logging timer to prevent attempts to upload to remote database. */ public void stopRemoteLogTimer() { if (mRemoteLogTimer != null) { - Log.v(TAG, "stopRemoteLogTimer(): cancelling Remote Log timer"); + Log.i(TAG, "stopRemoteLogTimer(): cancelling Remote Log timer"); mRemoteLogTimer.cancel(); mRemoteLogTimer = null; } } + + + /* + * Start the timer that will Auto Prune the database + */ + private void startAutoPruneTimer() { + if (mAutoPruneTimer != null) { + Log.i(TAG, "startAutoPruneTimer -timer already running - cancelling it"); + mAutoPruneTimer.cancel(); + mAutoPruneTimer = null; + } + Log.i(TAG, "startAutoPruneTimer() - starting AutoPruneTimer"); + mAutoPruneTimer = + new AutoPruneTimer(mAutoPrunePeriod * 1000, 1000); + mAutoPruneTimer.start(); + } + + + /* + * Cancel the auto prune timer to prevent attempts to upload to remote database. + */ + public void stopAutoPruneTimer() { + if (mAutoPruneTimer != null) { + Log.i(TAG, "stopAutoPruneTimer(): cancelling Auto Prune timer"); + mAutoPruneTimer.cancel(); + mAutoPruneTimer = null; + } + } + + + public static class OsdDbHelper extends SQLiteOpenHelper { + // If you change the database schema, you must increment the database version. + public static final int DATABASE_VERSION = 1; + public static final String DATABASE_NAME = "OsdData.db"; + private static final String TAG = "LogManager.OsdDbHelper"; + + public OsdDbHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + Log.d(TAG, "OsdDbHelper constructor"); + } + + public void onCreate(SQLiteDatabase db) { + Log.i(TAG, "onCreate - TableName=" + mDpTableName); + String SQLStr = "CREATE TABLE IF NOT EXISTS " + mDpTableName + "(" + + "id INTEGER PRIMARY KEY," + + "dataTime DATETIME," + + "status INT," + + "dataJSON TEXT," + + "uploaded TEXT" // Stores the ID of the datapoint in the remote database if uploaded, otherwise empty + + ");"; + db.execSQL(SQLStr); + Log.i(TAG, "onCreate - TableName=" + mEventsTableName); + SQLStr = "CREATE TABLE IF NOT EXISTS " + mEventsTableName + "(" + + "id INTEGER PRIMARY KEY," + + "dataTime DATETIME," + + "status INT," + + "type TEXT," + + "subType TEXT," + + "notes TEXT," // avoiding using 'desc' as that is an sql name. + + "dataJSON TEXT," + + "uploaded TEXT" // stores the id of the event in the remote dabase if uploaded, otherwise empty + + ");"; + db.execSQL(SQLStr); + } + + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // This database is only a cache for online data, so its upgrade policy is + // to simply to discard the data and start over + Log.i(TAG, "onUpgrade()"); + db.execSQL("Drop table if exists " + mDpTableName + ";"); + onCreate(db); + } + + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.i(TAG, "onDowngrade()"); + onUpgrade(db, oldVersion, newVersion); + } + } + + /** - * Inhibit fault alarm initiation for a period to avoid spurious warning - * beeps caused by short term network interruptions. + * Upload recorded data to the remote database periodically. */ private class RemoteLogTimer extends CountDownTimer { public RemoteLogTimer(long startTime, long interval) { @@ -380,12 +1132,35 @@ public class LogManager { @Override public void onFinish() { - //FIXME - make this do something! - //Log.v(TAG, "mRemoteLogTimer - onFinish"); - //writeToRemoteServer(); + Log.d(TAG, "mRemoteLogTimer - onFinish - uploading data to remote database"); + writeToRemoteServer(); + // Restart this timer. start(); } } + /** + * Prune the database periodically. + */ + private class AutoPruneTimer extends CountDownTimer { + public AutoPruneTimer(long startTime, long interval) { + super(startTime, interval); + } + + @Override + public void onTick(long l) { + } + + @Override + public void onFinish() { + Log.d(TAG, "mAutoPruneTimer - onFinish - Pruning Local Database"); + pruneLocalDb(); + // Restart this timer. + start(); + } + + } + + } diff --git a/app/src/main/java/uk/org/openseizuredetector/LogManagerActivity.java b/app/src/main/java/uk/org/openseizuredetector/LogManagerActivity.java deleted file mode 100644 index c45888c..0000000 --- a/app/src/main/java/uk/org/openseizuredetector/LogManagerActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -package uk.org.openseizuredetector; - -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.ListView; - -import uk.org.openseizuredetector.EventLogManager.EventLogListAdapter; -import uk.org.openseizuredetector.EventLogManager.EventLogManager; -import uk.org.openseizuredetector.EventLogManager.LogEntryModel; - - -public class LogManagerActivity extends FragmentActivity -implements AuthDialogInterface { - private String TAG = "LogManagerActivity"; - private EventLogListAdapter mEventLogListAdapter; - private ListView mEventLogListView; - private EventLogManager mElm; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_log_manager); - - LogEntryModel lem = new LogEntryModel(); - //lem.setDate(new Date()); - lem.setNote("Test Entry"); - lem.setDataJSON("[]"); - lem.setAlarmState(1); - - //mElm = new EventLogManager(this); - //mElm.addRow(lem); - - //mEventLogListAdapter = new EventLogListAdapter(this); - //mEventLogListView = (ListView) findViewById(R.id.eventLogListView); - //mEventLogListView.setAdapter(mEventLogListAdapter); - - Button b; - - b = (Button) findViewById(R.id.authenticate_button); - b.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Log.v(TAG, "authenticate button clicked"); - AuthDialog authDialog = new AuthDialog(); - authDialog.show(getSupportFragmentManager(),"authDialog"); - } - }); - } - - public void updateUi() { - Log.v(TAG, "updateUi"); - } - - public void onDialogDone(boolean State) { - - Log.v(TAG,"onDialogDone()"); - LogManager lm = new LogManager(this); - lm.authenticate("test", "testpw"); - - } - -} \ No newline at end of file diff --git a/app/src/main/java/uk/org/openseizuredetector/LogManagerControlActivity.java b/app/src/main/java/uk/org/openseizuredetector/LogManagerControlActivity.java new file mode 100644 index 0000000..82a25f1 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/LogManagerControlActivity.java @@ -0,0 +1,653 @@ +package uk.org.openseizuredetector; + +//import androidx.appcompat.app.AppCompatActivity; + +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.Handler; +import android.os.IBinder; + +import androidx.core.view.MenuCompat; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.RadioButton; +import android.widget.SimpleAdapter; +import android.widget.TextView; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Field; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LogManagerControlActivity extends AppCompatActivity { + private String TAG = "LogManagerControlActivity"; + private LogManager mLm; + private Context mContext; + private UiTimer mUiTimer; + private ArrayList> mEventsList; + private ArrayList> mRemoteEventsList; + private ArrayList> mSysLogList; + private SdServiceConnection mConnection; + private OsdUtil mUtil; + final Handler serverStatusHandler = new Handler(); + private Integer mUiTimerPeriodFast = 2000; // 2 seconds - we use fast updating while UI is blank and we are waiting for first data + private Integer mUiTimerPeriodSlow = 60000; // 60 seconds - once data has been received and UI populated we only update once per minute. + private boolean mUpdateSysLog = true; + //private Integer UI_MODE_LOCAL = 0; + //private Integer UI_MODE_SHARED = 1; + //private Integer mUiMode = UI_MODE_SHARED; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + mContext = this; + mUtil = new OsdUtil(getApplicationContext(), serverStatusHandler); + + if (!mUtil.isServerRunning()) { + mUtil.showToast(getString(R.string.error_server_not_running)); + finish(); + return; + } + + mConnection = new SdServiceConnection(getApplicationContext()); + + setContentView(R.layout.activity_log_manager_control); + + /* Force display of overflow menu - from stackoverflow + * "how to force use of..." + */ + try { + Log.v(TAG, "trying menubar fiddle..."); + ViewConfiguration config = ViewConfiguration.get(this); + Field menuKeyField = + ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); + if (menuKeyField != null) { + Log.v(TAG, "menuKeyField is not null - configuring...."); + menuKeyField.setAccessible(true); + menuKeyField.setBoolean(config, false); + } else { + Log.v(TAG, "menuKeyField is null - doing nothing..."); + } + } catch (Exception e) { + Log.v(TAG, "menubar fiddle exception: " + e.toString()); + } + + Button authBtn = + (Button) findViewById(R.id.auth_button); + authBtn.setOnClickListener(onAuth); + //Button pruneBtn = + // (Button) findViewById(R.id.pruneDatabaseBtn); + //pruneBtn.setOnClickListener(onPruneBtn); + //Button reportSeizureBtn = + // (Button) findViewById(R.id.reportSeizureBtn); + //reportSeizureBtn.setOnClickListener(onReportSeizureBtn); + Button remoteDbBtn = + (Button) findViewById(R.id.refresh_button); + remoteDbBtn.setOnClickListener(onRefreshBtn); + + ListView lv = (ListView) findViewById(R.id.eventLogListView); + lv.setOnItemClickListener(onEventListClick); + + lv = (ListView) findViewById(R.id.remoteEventsLv); + lv.setOnItemClickListener(onRemoteEventListClick); + } + + /** + * Create Action Bar + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + Log.i(TAG, "onCreateOptionsMenu()"); + getMenuInflater().inflate(R.menu.log_manager_activity_menu, menu); + MenuCompat.setGroupDividerEnabled(menu, true); + return true; + } + + + @Override + protected void onStart() { + Log.v(TAG, "onStart()"); + super.onStart(); + mUtil.bindToServer(getApplicationContext(), mConnection); + waitForConnection(); + startUiTimer(mUiTimerPeriodFast); + } + + @Override + protected void onStop() { + Log.v(TAG, "onStop()"); + super.onStop(); + stopUiTimer(); + mUtil.unbindFromServer(getApplicationContext(), mConnection); + } + + @Override + protected void onPause() { + Log.v(TAG, "onPause()"); + super.onPause(); + //stopUiTimer(); + } + + @Override + protected void onResume() { + Log.v(TAG, "onResume()"); + super.onResume(); + //startUiTimer(); + } + + 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() { + mLm = mConnection.mSdServer.mLm; + startUiTimer(mUiTimerPeriodFast); + getRemoteEvents(); + // Populate events list - we only do it once when the activity is created because the query might slow down the UI. + // We could try this code in updateUI() and see though. + // Based on https://www.tutlane.com/tutorial/android/android-sqlite-listview-with-examples + mLm.getEventsList(true, (ArrayList> eventsList) -> { + mEventsList = eventsList; + Log.v(TAG, "initialiseServiceConnection() - set mEventsList - Updating UI"); + updateUi(); + }); + mUtil.getSysLogList((ArrayList> syslogList) -> { + mSysLogList = syslogList; + Log.v(TAG, "initialiseServiceConnection() - set mSysLogList - Updating UI"); + updateUi(); + }); + } + + + private void getRemoteEvents() { + // Retrieve events from remote database + mLm.mWac.getEvents((JSONObject remoteEventsObj) -> { + Log.v(TAG, "getRemoteEvents()"); + if (remoteEventsObj == null) { + Log.e(TAG, "getRemoteEvents Callback: Error Retrieving events"); + mUtil.showToast("Error Retrieving Remote Events from Server - Please Try Again Later!"); + } else { + //Log.v(TAG, "remoteEventsObj = " + remoteEventsObj.toString()); + try { + JSONArray eventsArray = remoteEventsObj.getJSONArray("events"); + mRemoteEventsList = new ArrayList>(); + // A bit of a hack to display in reverse chronological order + for (int i = eventsArray.length() - 1; i >= 0; i--) { + JSONObject eventObj = eventsArray.getJSONObject(i); + Log.v(TAG, "getRemoteEvents() - " + eventObj.toString()); + String id = null; + if (!eventObj.isNull("id")) { + id = eventObj.getString("id"); + } + int osdAlarmState = -1; + if (!eventObj.isNull("osdAlarmState")) { + osdAlarmState = eventObj.getInt("osdAlarmState"); + } + String dataTime = "null"; + if (!eventObj.isNull("dataTime")) { + dataTime = eventObj.getString("dataTime"); + Log.v(TAG, "getRemoteEvents() - dataTime=" + dataTime); + } + String typeStr = "null"; + if (!eventObj.isNull("type")) { + typeStr = eventObj.getString("type"); + } + String subType = "null"; + if (!eventObj.isNull("subType")) { + subType = eventObj.getString("subType"); + } + String desc = "null"; + if (!eventObj.isNull("desc")) { + desc = eventObj.getString("desc"); + } + HashMap eventHashMap = new HashMap(); + eventHashMap.put("id", id); + eventHashMap.put("osdAlarmState", String.valueOf(osdAlarmState)); + eventHashMap.put("osdAlarmStateStr", mUtil.alarmStatusToString(osdAlarmState)); + eventHashMap.put("dataTime", dataTime); + eventHashMap.put("type", typeStr); + eventHashMap.put("subType", subType); + eventHashMap.put("desc", desc); + mRemoteEventsList.add(eventHashMap); + } + Log.v(TAG, "getRemoteEvents() - set mRemoteEventsList(). Updating UI"); + updateUi(); + } catch (JSONException e) { + Log.e(TAG, "getRemoteEvents(): Error Parsing remoteEventsObj: " + e.getMessage()); + mUtil.showToast("Error Parsing remoteEventsObj - this should not happen!!!"); + mRemoteEventsList = null; + } + //Log.v(TAG, "getRemoteEvents(): mRemoteEventsList = " + mRemoteEventsList.toString()); + } + }); + } + + + private void updateUi() { + Log.i(TAG, "updateUi()"); + boolean stopUpdating = true; + TextView tv; + Button btn; + // Local Database Information + if (mLm != null) { + mLm.getLocalEventsCount(true, (Long eventCount) -> { + TextView tv1 = (TextView) findViewById(R.id.num_local_events_tv); + tv1.setText(String.format("%d", eventCount)); + }); + mLm.getLocalDatapointsCount((Long datapointsCount) -> { + TextView tv2 = (TextView) findViewById(R.id.num_local_datapoints_tv); + tv2.setText(String.format("%d", datapointsCount)); + }); + } else { + stopUpdating = false; + } + // Local Database ListView + if (mEventsList != null) { + ListView lv = (ListView) findViewById(R.id.eventLogListView); + ListAdapter adapter = new SimpleAdapter(LogManagerControlActivity.this, mEventsList, R.layout.log_entry_layout, + new String[]{"dataTime", "status", "uploaded"}, + new int[]{R.id.event_date, R.id.event_alarmState, R.id.event_uploaded}); + lv.setAdapter(adapter); + //Log.v(TAG,"eventsList="+mEventsList); + } else { + stopUpdating = false; + } + // SysLog ListView + if (mSysLogList != null && mUpdateSysLog) { + ListView lv = (ListView) findViewById(R.id.sysLogListView); + ListAdapter adapter = new SimpleAdapter(LogManagerControlActivity.this, mSysLogList, R.layout.syslog_entry_layout, + new String[]{"dataTime", "logLevel", "dataJSON"}, + new int[]{R.id.syslog_entry_date_tv, R.id.syslog_level_tv, R.id.syslog_entry_text_tv}); + lv.setAdapter(adapter); + //Log.v(TAG,"eventsList="+mEventsList); + mUpdateSysLog = false; + } + // Remote Database List View + if (mRemoteEventsList != null) { + ListView lv = (ListView) findViewById(R.id.remoteEventsLv); + ListAdapter adapter = new RemoteEventsAdapter(LogManagerControlActivity.this, mRemoteEventsList, R.layout.log_entry_layout_remote, + new String[]{"id", "dataTime", "type", "subType", "osdAlarmStateStr", "desc"}, + new int[]{R.id.event_id_remote_tv, R.id.event_date_remote_tv, R.id.event_type_remote_tv, R.id.event_subtype_remote_tv, + R.id.event_alarmState_remote_tv, R.id.event_notes_remote_tv}); + lv.setAdapter(adapter); + //Log.i(TAG,"adapter[0]="+adapter.getItem(0)); + //Log.i(TAG,"adapter[3]="+adapter.getItem(3)); + } else { + //mUtil.showToast("No Remote Events"); + Log.i(TAG, "UpdateUi: No Remote Events"); + stopUpdating = false; + } + + + // Remote Database Information + if (mLm != null) { + tv = (TextView) findViewById(R.id.authStatusTv); + btn = (Button) findViewById(R.id.auth_button); + if (mLm.mWac.isLoggedIn()) { + tv.setText(getString(R.string.logged_in_with_token)); + btn.setText(getString(R.string.logout)); + } else { + tv.setText(getString(R.string.not_authenticated)); + btn.setText(getString(R.string.login)); + } + } else { + stopUpdating = false; + } + + // Note we do not really stop updating the UI, just change from the fast update period to the slow update period + // to save hammering the databases once the UI has been populated once. + if (stopUpdating) { + stopUiTimer(); + startUiTimer(mUiTimerPeriodSlow); + } + } //updateUi(); + + public void onRadioButtonClicked(View view) { + LinearLayout localDataLl = (LinearLayout) findViewById(R.id.local_data_ll); + LinearLayout sharedDataLl = (LinearLayout) findViewById(R.id.shared_data_ll); + LinearLayout syslogLl = (LinearLayout) findViewById(R.id.syslog_ll); + // Is the button now checked? + boolean checked = ((RadioButton) view).isChecked(); + + // Check which radio button was clicked + switch (view.getId()) { + case R.id.local_data_rb: + if (checked) { + // Switch to the local data view + localDataLl.setVisibility(View.VISIBLE); + sharedDataLl.setVisibility(View.GONE); + syslogLl.setVisibility(View.GONE); + } + break; + case R.id.shared_data_rb: + if (checked) { + // Switch to the local data view + localDataLl.setVisibility(View.GONE); + sharedDataLl.setVisibility(View.VISIBLE); + syslogLl.setVisibility(View.GONE); + } + break; + case R.id.syslog_rb: + if (checked) { + // Switch to the local data view + localDataLl.setVisibility(View.GONE); + sharedDataLl.setVisibility(View.GONE); + syslogLl.setVisibility(View.VISIBLE); + } + break; + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Log.i(TAG, "onOptionsItemSelected() : " + item.getItemId() + " selected"); + switch (item.getItemId()) { + case R.id.action_authenticate_api: + Log.i(TAG, "action_autheticate_api"); + try { + Intent i = new Intent( + getApplicationContext(), + AuthenticateActivity.class); + this.startActivity(i); + } catch (Exception ex) { + Log.i(TAG, "exception starting export activity " + ex.toString()); + } + return true; + case R.id.pruneDatabaseMenuItem: + Log.i(TAG, "action_pruneDatabase"); + onPruneBtn.onClick(null); + return true; + case R.id.action_report_seizure: + Log.i(TAG, "action_report_seizure"); + try { + Intent intent = new Intent( + getApplicationContext(), + ReportSeizureActivity.class); + this.startActivity(intent); + } catch (Exception ex) { + Log.i(TAG, "exception starting Report Seizure activity " + ex.toString()); + } + return true; + case R.id.action_settings: + Log.i(TAG, "action_settings"); + try { + Intent prefsIntent = new Intent( + getApplicationContext(), + PrefActivity.class); + this.startActivity(prefsIntent); + } catch (Exception ex) { + Log.i(TAG, "exception starting settings activity " + ex.toString()); + } + return true; + case R.id.action_mark_unknown: + Log.i(TAG, "action_mark_unknown"); + new AlertDialog.Builder(this) + .setTitle(R.string.mark_unverified_events_unknown_dialog_title) + .setMessage(R.string.mark_unverified_events_unknown_dialog_message) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + mLm.mWac.markUnverifiedEventsAsUnknown(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + default: + return super.onOptionsItemSelected(item); + } + } + + + View.OnClickListener onAuth = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onAuth"); + Intent i; + i = new Intent(mContext, AuthenticateActivity.class); + startActivity(i); + } + }; + View.OnClickListener onPruneBtn = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onPruneBtn"); + // Confirmation dialog based on: https://stackoverflow.com/a/12213536/2104584 + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle("Prune Database"); + builder.setMessage(String.format("This will remove all data from the database that is more than %d days old." + + "\nThis can NOT be undone.\nAre you sure?", mLm.mDataRetentionPeriod)); + builder.setPositiveButton("YES", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mLm.pruneLocalDb(); + dialog.dismiss(); + } + }); + builder.setNegativeButton("NO", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + AlertDialog alert = builder.create(); + alert.show(); + } + }; + + View.OnClickListener onReportSeizureBtn = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onReportSeizureBtn"); + Intent i; + i = new Intent(mContext, ReportSeizureActivity.class); + startActivity(i); + } + }; + + View.OnClickListener onRemoteDbBtn = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onRemoteDbBtn"); + Intent i; + i = new Intent(mContext, RemoteDbActivity.class); + startActivity(i); + } + }; + + View.OnClickListener onRefreshBtn = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onRefreshBtn"); + initialiseServiceConnection(); + } + }; + + + AdapterView.OnItemClickListener onEventListClick = + new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView adapter, View v, int position, long id) { + Log.v(TAG, "onItemClicKListener() - Position=" + position + ", id=" + id);// Confirmation dialog based on: https://stackoverflow.com/a/12213536/2104584 + HashMap eventObj = (HashMap) adapter.getItemAtPosition(position); + String eventId = eventObj.get("uploaded"); + Log.d(TAG, "onItemClickListener(): eventId=" + eventId + ", eventObj=" + eventObj); + if (eventId != null) { + Intent i = new Intent(getApplicationContext(), EditEventActivity.class); + i.putExtra("eventId", eventId); + startActivity(i); + } else { + mUtil.showToast("You Must Wait for Event to Upload before Editing it"); + } + } + }; + + AdapterView.OnItemClickListener onRemoteEventListClick = + new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView adapter, View v, int position, long id) { + Log.v(TAG, "onRemoteEventList Click() - Position=" + position + ", id=" + id);// Confirmation dialog based on: https://stackoverflow.com/a/12213536/2104584 + HashMap eventObj = (HashMap) adapter.getItemAtPosition(position); + String eventId = eventObj.get("id"); + Log.d(TAG, "onItemClickListener(): eventId=" + eventId + ", eventObj=" + eventObj); + Intent i = new Intent(getApplicationContext(), EditEventActivity.class); + i.putExtra("eventId", eventId); + startActivity(i); + } + }; + + + /* + * Start the timer that will update the user interface every 5 seconds.. + */ + private void startUiTimer(Integer uiTimerPeriod) { + if (mUiTimer != null) { + Log.v(TAG, "startUiTimer -timer already running - cancelling it"); + mUiTimer.cancel(); + mUiTimer = null; + } + Log.v(TAG, "startUiTimer() - starting UiTimer"); + mUiTimer = + new UiTimer(uiTimerPeriod, 1000); + mUiTimer.start(); + } + + + /* + * Cancel the remote logging timer to prevent attempts to upload to remote database. + */ + public void stopUiTimer() { + if (mUiTimer != null) { + Log.v(TAG, "stopUiTimer(): cancelling UI timer"); + mUiTimer.cancel(); + mUiTimer = null; + } + } + + /** + * Upload recorded data to the remote database periodically. + */ + private class UiTimer extends CountDownTimer { + public UiTimer(long startTime, long interval) { + super(startTime, interval); + } + + @Override + public void onTick(long l) { + // Do Nothing + } + + @Override + public void onFinish() { + //Log.v(TAG, "UiTimer - onFinish - Updating UI"); + updateUi(); + // Restart this timer. + if (mUiTimer != null) + start(); + } + + } + + + private class RemoteEventsAdapter extends SimpleAdapter { + + /** + * Constructor + * + * @param context The context where the View associated with this SimpleAdapter is running + * @param data A List of Maps. Each entry in the List corresponds to one row in the list. The + * Maps contain the data for each row, and should include all the entries specified in + * "from" + * @param resource Resource identifier of a view layout that defines the views for this list + * item. The layout file should include at least those named views defined in "to" + * @param from A list of column names that will be added to the Map associated with each + * item. + * @param to The views that should display column in the "from" parameter. These should all be + * TextViews. The first N views in this list are given the values of the first N columns + */ + public RemoteEventsAdapter(Context context, List> data, int resource, String[] from, int[] to) { + super(context, data, resource, from, to); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = super.getView(position, convertView, parent); + Map dataItem = (Map) getItem(position); + Log.v(TAG, "getView() " + dataItem.toString()); + switch (dataItem.get("type").toString()) { + case "null": + case "": + v.setBackgroundColor(Color.parseColor("#ffaaaa")); + break; + case "Seizure": + v.setBackgroundColor(Color.parseColor("#ff6060")); + break; + default: + v.setBackgroundColor(Color.TRANSPARENT); + } + + // Convert date format to something more readable. + TextView tv = (TextView) v.findViewById(R.id.event_date_remote_tv); + Date dataTime = null; + String dateStr = (String) dataItem.get("dataTime"); + dataTime = mUtil.string2date(dateStr); + if (dataTime != null) { + SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); + tv.setText(dateFormat.format(dataTime)); + } else { + tv.setText("---"); + } + return (v); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/org/openseizuredetector/MainActivity.java b/app/src/main/java/uk/org/openseizuredetector/MainActivity.java index 04bd053..bb3a5a9 100644 --- a/app/src/main/java/uk/org/openseizuredetector/MainActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/MainActivity.java @@ -25,7 +25,8 @@ package uk.org.openseizuredetector; -import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; @@ -36,24 +37,20 @@ import android.os.Handler; import android.os.Message; import android.os.Messenger; import android.preference.PreferenceManager; -import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; +import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; -import android.widget.Button; -import java.lang.reflect.Field; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Timer; -import java.util.TimerTask; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.MenuCompat; -//MPAndroidChart import com.github.mikephil.charting.charts.BarChart; import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; @@ -63,6 +60,14 @@ import com.github.mikephil.charting.data.BarEntry; import com.github.mikephil.charting.utils.ValueFormatter; import com.rohitss.uceh.UCEHandler; +import java.lang.reflect.Field; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Timer; +import java.util.TimerTask; + +//MPAndroidChart + public class MainActivity extends AppCompatActivity { static final String TAG = "MainActivity"; private int okColour = Color.BLUE; @@ -80,6 +85,7 @@ public class MainActivity extends AppCompatActivity { final Handler serverStatusHandler = new Handler(); Messenger messenger = new Messenger(new ResponseHandler()); Timer mUiTimer; + private Context mContext; /** * Called when the activity is first created. @@ -87,7 +93,7 @@ public class MainActivity extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Log.i(TAG,"onCreate()"); + Log.i(TAG, "onCreate()"); // Set our custom uncaught exception handler to report issues. //Thread.setDefaultUncaughtExceptionHandler(new OsdUncaughtExceptionHandler(MainActivity.this)); @@ -96,18 +102,19 @@ public class MainActivity extends AppCompatActivity { .build(); //int i = 5/0; // Force exception to test handler. - mUtil = new OsdUtil(this,serverStatusHandler); - mConnection = new SdServiceConnection(this); + mUtil = new OsdUtil(getApplicationContext(), serverStatusHandler); + mConnection = new SdServiceConnection(getApplicationContext()); mUtil.writeToSysLogFile(""); mUtil.writeToSysLogFile("* MainActivity Started *"); mUtil.writeToSysLogFile("MainActivity.onCreate()"); + mContext = this; // Initialise the User Interface setContentView(R.layout.main); - /* Force display of overflow menu - from stackoverflow - * "how to force use of..." - */ + /* Force display of overflow menu - from stackoverflow + * "how to force use of..." + */ try { Log.v(TAG, "trying menubar fiddle..."); ViewConfiguration config = ViewConfiguration.get(this); @@ -134,15 +141,14 @@ public class MainActivity extends AppCompatActivity { Log.v(TAG, "acceptAlarmButton.onClick()"); if (mConnection.mBound) { if ((mConnection.mSdServer.mSmsTimer != null) - && (mConnection.mSdServer.mSmsTimer.mTimeLeft > 0 )){ - Log.v(TAG, "acceptAlarmButton.onClick() - Stopping SMS Timer"); + && (mConnection.mSdServer.mSmsTimer.mTimeLeft > 0)) { + Log.i(TAG, "acceptAlarmButton.onClick() - Stopping SMS Timer"); mUtil.showToast(getString(R.string.SMSAlarmCancelledMsg)); mConnection.mSdServer.stopSmsTimer(); - } - else { + } else { Log.v(TAG, "acceptAlarmButton.onClick() - Accepting Alarm"); mConnection.mSdServer.acceptAlarm(); - } + } } } }); @@ -158,7 +164,74 @@ public class MainActivity extends AppCompatActivity { } }); + // Deal with the 'Raise Alarm' + button = (Button) findViewById(R.id.manualAlarmButton); + button.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Log.v(TAG, "manualAlarmButton.onClick()"); + // Confirmation dialog based on: https://stackoverflow.com/a/12213536/2104584 + //AlertDialog.Builder builder = new AlertDialog.Builder(getBaseContext()); + //builder.setTitle("Raise Alarm"); + //builder.setMessage(String.format("Raise a Seizure Detected Alarm NOW?")); + //builder.setPositiveButton("YES", new DialogInterface.OnClickListener() { + // @Override + // public void onClick(DialogInterface dialog, int which) { + if (mConnection.mBound) { + mConnection.mSdServer.raiseManualAlarm(); + } + // dialog.dismiss(); + // } + //}); + //builder.setNegativeButton("NO", new DialogInterface.OnClickListener() { + // @Override + // public void onClick(DialogInterface dialog, int which) { + // dialog.dismiss(); + // } + //}); + //AlertDialog alert = builder.create(); + //if (!(this).isFinishing()) { + // alert.show(); + //} + + } + }); + // The background service might ask us to show the data sharing dialog if data sharing is not working correctly + String actionStr = getIntent().getAction(); + if (actionStr != null) { + Log.i(TAG, "onCreate() - action=" + actionStr); + if (actionStr.equals("showDataSharingDialog")) { + showDataSharingDialog(); + } + } else { + Log.i(TAG, "onCreate - action is null - starting normally"); + } + } + + + @Override + protected void onNewIntent(Intent intent) { + String actionStr; + Log.i(TAG, "onNewIntent"); + Bundle extras = intent.getExtras(); + // The background service might ask us to show the data sharing dialog if data sharing is not working correctly + actionStr = getIntent().getAction(); + if (actionStr != null) { + Log.i(TAG, "onNewIntent() - action=" + actionStr); + if (actionStr.equals("showDataSharingDialog")) { + showDataSharingDialog(); + } + } else { + if (extras != null) { + actionStr = extras.getString("action"); + if (actionStr.equals("showDataSharingDialog")) { + showDataSharingDialog(); + } + Log.i(TAG, "onNewIntent - extra actionstr is "+actionStr); + } else { + Log.i(TAG, "onNewIntent - extra actionstr is null - starting normally"); + } + } } /** @@ -168,6 +241,7 @@ public class MainActivity extends AppCompatActivity { public boolean onCreateOptionsMenu(Menu menu) { Log.i(TAG, "onCreateOptionsMenu()"); getMenuInflater().inflate(R.menu.main_activity_actions, menu); + MenuCompat.setGroupDividerEnabled(menu, true); //mOptionsMenu = menu; //if (mConnection.mSdServer.mSdDataSourceName != "Pebble") { // Log.v(TAG,"Disabling Pebble Specific Menu Items"); @@ -191,7 +265,7 @@ public class MainActivity extends AppCompatActivity { mConnection.mSdServer.mSdDataSource.installWatchApp(); return true; - case R.id.action_accept_alarm: + case R.id.action_accept_alarm: Log.i(TAG, "action_accept_alarm"); if (mConnection.mBound) { mConnection.mSdServer.acceptAlarm(); @@ -202,14 +276,14 @@ public class MainActivity extends AppCompatActivity { Log.i(TAG, "action_sart_stop"); if (mConnection.mBound) { Log.i(TAG, "Stopping Server"); - mUtil.unbindFromServer(this, mConnection); + mUtil.unbindFromServer(getApplicationContext(), mConnection); stopServer(); } else { Log.i(TAG, "Starting Server"); startServer(); // and bind to it so we can see its data Log.i(TAG, "Binding to Server"); - mUtil.bindToServer(this, mConnection); + mUtil.bindToServer(getApplicationContext(), mConnection); } return true; /* fault beep test does not work with fault timer, so disable test option. @@ -238,24 +312,44 @@ public class MainActivity extends AppCompatActivity { mConnection.mSdServer.sendSMSAlarm(); } return true; - case R.id.action_test_phone_alarm: + + /*case R.id.action_test_phone_alarm: Log.i(TAG, "action_test_phone_alarm"); if (mConnection.mBound) { mConnection.mSdServer.sendPhoneAlarm(); } return true; - case R.id.action_export: - Log.i(TAG, "action_export"); + */ + + case R.id.action_authenticate_api: + Log.i(TAG, "action_autheticate_api"); try { Intent i = new Intent( MainActivity.this, - DBQueryActivity.class); + AuthenticateActivity.class); this.startActivity(i); } catch (Exception ex) { Log.i(TAG, "exception starting export activity " + ex.toString()); } return true; - case R.id.action_logs: + case R.id.action_about_datasharing: + Log.i(TAG, "action_about_datasharing"); + showDataSharingDialog(); + return true; + /* + case R.id.action_export: + Log.i(TAG, "action_export"); + try { + Intent i = new Intent( + MainActivity.this, + ExportDataActivity.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 { String url = "http://" @@ -272,17 +366,29 @@ public class MainActivity extends AppCompatActivity { Log.i(TAG, "exception starting log manager activity " + ex.toString()); } return true; + */ case R.id.action_logmanager: Log.i(TAG, "action_logmanager"); try { Intent intent = new Intent( MainActivity.this, - LogManagerActivity.class); + LogManagerControlActivity.class); this.startActivity(intent); } catch (Exception ex) { Log.i(TAG, "exception starting log manager activity " + ex.toString()); } return true; + case R.id.action_report_seizure: + Log.i(TAG, "action_report_seizure"); + try { + Intent intent = new Intent( + MainActivity.this, + ReportSeizureActivity.class); + this.startActivity(intent); + } catch (Exception ex) { + Log.i(TAG, "exception starting Report Seizure activity " + ex.toString()); + } + return true; case R.id.action_settings: Log.i(TAG, "action_settings"); try { @@ -307,7 +413,7 @@ public class MainActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); - Log.i(TAG,"onStart()"); + Log.i(TAG, "onStart()"); mUtil.writeToSysLogFile("MainActivity.onStart()"); SharedPreferences SP = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); @@ -321,9 +427,9 @@ public class MainActivity extends AppCompatActivity { if (mUtil.isServerRunning()) { mUtil.writeToSysLogFile("MainActivity.onStart - Binding to Server"); - mUtil.bindToServer(this, mConnection); + mUtil.bindToServer(getApplicationContext(), mConnection); } else { - Log.i(TAG,"onStart() - Server Not Running"); + Log.i(TAG, "onStart() - Server Not Running"); mUtil.writeToSysLogFile("MainActivity.onStart - Server Not Running"); } // start timer to refresh user interface every second. @@ -341,9 +447,9 @@ public class MainActivity extends AppCompatActivity { @Override protected void onStop() { super.onStop(); - Log.i(TAG,"onStop() - unbinding from server"); + Log.i(TAG, "onStop() - unbinding from server"); mUtil.writeToSysLogFile("MainActivity.onStop()"); - mUtil.unbindFromServer(this, mConnection); + mUtil.unbindFromServer(getApplicationContext(), mConnection); mUiTimer.cancel(); } @@ -402,7 +508,7 @@ public class MainActivity extends AppCompatActivity { tv.setBackgroundColor(okColour); tv.setTextColor(okTextColour); tv = (TextView) findViewById(R.id.serverIpTv); - tv.setText(getString(R.string.AccessServerAt)+" http://" + tv.setText(getString(R.string.AccessServerAt) + " http://" + mUtil.getLocalIpAddress() + ":8080"); tv.setBackgroundColor(okColour); @@ -460,11 +566,17 @@ public class MainActivity extends AppCompatActivity { // Pebble Connected Phrase - use for HR if active instead. tv = (TextView) findViewById(R.id.pebbleTv); if (mConnection.mSdServer.mSdData.mHRAlarmActive) { - tv.setText(getString(R.string.HR_Equals) + mConnection.mSdServer.mSdData.mHR); - if (mConnection.mSdServer.mSdData.mHRAlarmStanding) { + if (mConnection.mSdServer.mSdData.mO2Sat>0) { + tv.setText(getString(R.string.HR_Equals) + mConnection.mSdServer.mSdData.mHR + " bpm\n" + + "O2 Sat = " + mConnection.mSdServer.mSdData.mO2Sat + "%"); + } else { + tv.setText(getString(R.string.HR_Equals) + mConnection.mSdServer.mSdData.mHR + " bpm\n" + + "O2 Sat = ---%"); + } + if (mConnection.mSdServer.mSdData.mHRAlarmStanding || mConnection.mSdServer.mSdData.mO2SatAlarmStanding) { tv.setBackgroundColor(alarmColour); tv.setTextColor(alarmTextColour); - } else if (mConnection.mSdServer.mSdData.mHRFaultStanding) { + } else if (mConnection.mSdServer.mSdData.mHRFaultStanding || mConnection.mSdServer.mSdData.mO2SatFaultStanding) { tv.setBackgroundColor(warnColour); tv.setTextColor(warnTextColour); } else { @@ -507,6 +619,65 @@ public class MainActivity extends AppCompatActivity { tv.setBackgroundColor(okColour); tv.setTextColor(okTextColour); } + + //////////////////////////////////////////////////////////// + // Populate the Data Sharing Status Box + // We start off with it set to OK, then check for several different abnormal conditions + // in turn - the last one that is active is the one that is displayed. + tv = (TextView) findViewById(R.id.remoteDbTv); + tv.setText(getString(R.string.data_sharing_status) + + ": " + + getString(R.string.data_sharing_setup_ok)); + tv.setBackgroundColor(okColour); + tv.setTextColor(okTextColour); + + if (!mConnection.mSdServer.mLm.mWac.checkServerConnection()) { + // Problem connecting to server + tv = (TextView) findViewById(R.id.remoteDbTv); + tv.setText(getString(R.string.data_sharing_status) + + ": " + + getString(R.string.error_connecting_to_server)); + tv.setBackgroundColor(warnColour); + tv.setTextColor(warnTextColour); + } + + if (!mConnection.mSdServer.mLogDataRemoteMobile && mUtil.isMobileDataActive()) { + // We are on mobile internet but we are set to not upload over mobile data. + tv.setText(getString(R.string.data_sharing_status) + + ": " + + getString(R.string.not_updating_mobile)); + tv.setBackgroundColor(warnColour); + tv.setTextColor(warnTextColour); + } + + if (!mUtil.isNetworkConnected()) { + // No network connection + tv.setText(getString(R.string.data_sharing_status) + + ": " + + getString(R.string.not_updating_no_network)); + tv.setBackgroundColor(warnColour); + tv.setTextColor(warnTextColour); + } + + if (!mConnection.mSdServer.mLm.mWac.isLoggedIn()) { + // Not Logged In + tv.setText(getString(R.string.data_sharing_status) + + ": " + + getString(R.string.not_logged_in)); + tv.setBackgroundColor(warnColour); + tv.setTextColor(warnTextColour); + } + + if (!mConnection.mSdServer.mLogData) { + // Not set to share data + tv.setText(getString(R.string.data_sharing_status) + + ": " + + getString(R.string.not_sharing_logged_data)); + tv.setBackgroundColor(warnColour); + tv.setTextColor(warnTextColour); + } + + ///////////////////////////////////////////////////// // Set ProgressBars to show margin to alarm. long powerPc; if (mConnection.mSdServer.mSdData.alarmThresh != 0) @@ -532,9 +703,9 @@ public class MainActivity extends AppCompatActivity { specRatio = 0; ((TextView) findViewById(R.id.powerTv)).setText(getString(R.string.PowerEquals) + mConnection.mSdServer.mSdData.roiPower + - " ("+ getString(R.string.Threshold) + "=" + mConnection.mSdServer.mSdData.alarmThresh + ")"); + " (" + getString(R.string.Threshold) + "=" + mConnection.mSdServer.mSdData.alarmThresh + ")"); ((TextView) findViewById(R.id.spectrumTv)).setText(getString(R.string.SpectrumRatioEquals) + specRatio + - " ("+ getString(R.string.Threshold) + "=" + mConnection.mSdServer.mSdData.alarmRatioThresh + ")"); + " (" + getString(R.string.Threshold) + "=" + mConnection.mSdServer.mSdData.alarmRatioThresh + ")"); ProgressBar pb; Drawable pbDrawable; @@ -589,21 +760,20 @@ public class MainActivity extends AppCompatActivity { tv.setTextColor(warnTextColour); tv = (TextView) findViewById(R.id.pebbleTv); - tv.setText(getString(R.string.HR_Equals)+"---"); + tv.setText(getString(R.string.HR_Equals) + " --- bpm\nO2 Sat = --- %"); tv.setBackgroundColor(warnColour); tv.setTextColor(warnTextColour); tv = (TextView) findViewById(R.id.appTv); - tv.setText(getString(R.string.WatchApp)+" ----"); + tv.setText(getString(R.string.WatchApp) + " ----"); tv.setBackgroundColor(warnColour); tv.setTextColor(warnTextColour); tv = (TextView) findViewById(R.id.battTv); - tv.setText(getString(R.string.WatchBatteryEquals)+" ---%"); + tv.setText(getString(R.string.WatchBatteryEquals) + " ---%"); tv.setBackgroundColor(warnColour); tv.setTextColor(warnTextColour); } - } else { // Not bound to server tv = (TextView) findViewById(R.id.alarmTv); tv.setText(R.string.Dashes); @@ -620,17 +790,22 @@ public class MainActivity extends AppCompatActivity { tv.setTextColor(warnTextColour); tv = (TextView) findViewById(R.id.pebbleTv); - tv.setText(getString(R.string.HR_Equals)+"---"); + tv.setText(getString(R.string.HR_Equals) + "---"); tv.setBackgroundColor(warnColour); tv.setTextColor(warnTextColour); tv = (TextView) findViewById(R.id.appTv); - tv.setText(getString(R.string.WatchApp)+" -----"); + tv.setText(getString(R.string.WatchApp) + " -----"); tv.setBackgroundColor(warnColour); tv.setTextColor(warnTextColour); tv = (TextView) findViewById(R.id.battTv); - tv.setText(getString(R.string.WatchBatteryEquals)+" ---%"); + tv.setText(getString(R.string.WatchBatteryEquals) + " ---%"); + tv.setBackgroundColor(warnColour); + tv.setTextColor(warnTextColour); + + tv = (TextView) findViewById(R.id.remoteDbTv); + tv.setText("---"); tv.setBackgroundColor(warnColour); tv.setTextColor(warnTextColour); } @@ -647,12 +822,12 @@ public class MainActivity extends AppCompatActivity { && (mConnection.mSdServer.mSmsTimer.mTimeLeft > 0)) { acceptAlarmButton.setText(getString(R.string.SMSWillBeSentIn) + " " + mConnection.mSdServer.mSmsTimer.mTimeLeft / 1000 + - " s - "+getString(R.string.Cancel)); + " s - " + getString(R.string.Cancel)); acceptAlarmButton.setBackgroundColor(alarmColour); acceptAlarmButton.setEnabled(true); } else { acceptAlarmButton.setText(R.string.AcceptAlarm); - acceptAlarmButton.setBackgroundColor(Color.DKGRAY); + acceptAlarmButton.setBackgroundColor(Color.GRAY); if (mConnection.mBound) if ((mConnection.mSdServer.isLatchAlarms()) || mConnection.mSdServer.mSdData.mFallActive) { @@ -673,15 +848,17 @@ public class MainActivity extends AppCompatActivity { if (mConnection.mBound) if (mConnection.mSdServer.isAudibleCancelled()) { cancelAudibleButton.setText(getString(R.string.AudibleAlarmsCancelledFor) - + mConnection.mSdServer. + + " " + mConnection.mSdServer. cancelAudibleTimeRemaining() - + " sec." - + getString(R.string.PressToReEnable)); + + " sec"); + cancelAudibleButton.setEnabled(true); } else { if (mConnection.mSdServer.mAudibleAlarm) { cancelAudibleButton.setText(R.string.CancelAudibleAlarms); + cancelAudibleButton.setEnabled(true); } else { cancelAudibleButton.setText(R.string.AudibleAlarmsOff); + cancelAudibleButton.setEnabled(false); } } @@ -696,17 +873,16 @@ public class MainActivity extends AppCompatActivity { ArrayList xVals = new ArrayList(); ArrayList yBarVals = new ArrayList(); for (int i = 0; i < 10; i++) { - xVals.add(i+"-"+(i+1)+" Hz"); + xVals.add(i + "-" + (i + 1) + " Hz"); if (mConnection.mSdServer != null) { yBarVals.add(new BarEntry(mConnection.mSdServer.mSdData.simpleSpec[i], i)); - } - else { - yBarVals.add(new BarEntry(i,i)); + } else { + yBarVals.add(new BarEntry(i, i)); } } // create a dataset and give it a type - BarDataSet barDataSet = new BarDataSet(yBarVals,"Spectrum"); + BarDataSet barDataSet = new BarDataSet(yBarVals, "Spectrum"); try { int[] barColours = new int[10]; for (int i = 0; i < 10; i++) { @@ -718,20 +894,20 @@ public class MainActivity extends AppCompatActivity { } } barDataSet.setColors(barColours); - } catch (NullPointerException e){ - Log.e(TAG,"Null pointer exception setting bar colours"); + } catch (NullPointerException e) { + Log.e(TAG, "Null pointer exception setting bar colours"); } barDataSet.setBarSpacePercent(20f); barDataSet.setBarShadowColor(Color.WHITE); - BarData barData = new BarData(xVals,barDataSet); + BarData barData = new BarData(xVals, barDataSet); barData.setValueFormatter(new ValueFormatter() { - @Override - public String getFormattedValue(float v) { - DecimalFormat format = new DecimalFormat("####"); - return format.format(v); - } - }); - mChart.setData(barData); + @Override + public String getFormattedValue(float v) { + DecimalFormat format = new DecimalFormat("####"); + return format.format(v); + } + }); + mChart.setData(barData); // format the axes XAxis xAxis = mChart.getXAxis(); @@ -764,7 +940,7 @@ public class MainActivity extends AppCompatActivity { try { mChart.getLegend().setEnabled(false); } catch (NullPointerException e) { - Log.e(TAG,"Null Pointer Exception setting legend"); + Log.e(TAG, "Null Pointer Exception setting legend"); } mChart.invalidate(); @@ -775,14 +951,14 @@ public class MainActivity extends AppCompatActivity { @Override protected void onPause() { super.onPause(); - Log.i(TAG,"onPause()"); + Log.i(TAG, "onPause()"); mUtil.writeToSysLogFile("MainActivity.onPause()"); } @Override protected void onResume() { super.onResume(); - Log.i(TAG,"onResume()"); + Log.i(TAG, "onResume()"); mUtil.writeToSysLogFile("MainActivity.onResume()"); } @@ -795,16 +971,69 @@ public class MainActivity extends AppCompatActivity { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setIcon(R.drawable.icon_24x24); builder.setTitle("OpenSeizureDetector V" + versionName); + builder.setNeutralButton(getString(R.string.closeBtnTxt), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + builder.setPositiveButton("Privacy Policy", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + String url = OsdUtil.PRIVACY_POLICY_URL; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + dialog.cancel(); + } + }); + builder.setNegativeButton("Data Sharing", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + String url = OsdUtil.DATA_SHARING_URL; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + dialog.cancel(); + } + }); builder.setView(aboutView); builder.create(); builder.show(); } + private void showDataSharingDialog() { + mUtil.writeToSysLogFile("MainActivity.showDataSharingDialog()"); + View aboutView = getLayoutInflater().inflate(R.layout.data_sharing_dialog_layout, null, false); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setIcon(R.drawable.datasharing_fault_24x24); + builder.setTitle("OpenSeizureDetector Data Sharing"); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.login), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.i(TAG, "dataSharingDialog.positiveButton.onClick()"); + try { + Intent i = new Intent( + MainActivity.this, + AuthenticateActivity.class); + mContext.startActivity(i); + } catch (Exception ex) { + Log.i(TAG, "exception starting activity " + ex.toString()); + } + + } + }); + builder.setView(aboutView); + builder.create(); + builder.show(); + } + + static class ResponseHandler extends Handler { @Override public void handleMessage(Message message) { Log.i(TAG, "Message=" + message.toString()); } - } + } } diff --git a/app/src/main/java/uk/org/openseizuredetector/OsdUncaughtExceptionHandler.java b/app/src/main/java/uk/org/openseizuredetector/OsdUncaughtExceptionHandler.java index 1a5f866..7d4cfed 100644 --- a/app/src/main/java/uk/org/openseizuredetector/OsdUncaughtExceptionHandler.java +++ b/app/src/main/java/uk/org/openseizuredetector/OsdUncaughtExceptionHandler.java @@ -1,6 +1,5 @@ package uk.org.openseizuredetector; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; @@ -11,7 +10,8 @@ import android.os.Environment; import android.os.Looper; import android.os.StatFs; import android.util.Log; -import android.view.WindowManager; + +import androidx.appcompat.app.AlertDialog; import java.io.File; import java.io.PrintWriter; diff --git a/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java b/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java index 6520b20..972e053 100644 --- a/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java +++ b/app/src/main/java/uk/org/openseizuredetector/OsdUtil.java @@ -27,93 +27,75 @@ package uk.org.openseizuredetector; import android.Manifest; import android.app.Activity; import android.app.ActivityManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.FeatureInfo; -import android.content.pm.InstrumentationInfo; import android.content.pm.PackageInfo; -import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; -import android.content.pm.PermissionGroupInfo; -import android.content.pm.PermissionInfo; -import android.content.pm.ProviderInfo; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.res.Resources; -import android.content.res.XmlResourceParser; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Environment; import android.os.Handler; -import android.os.IBinder; -import android.os.UserHandle; import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; import android.text.format.Time; import android.util.Log; -import android.view.MenuItem; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + import org.apache.http.conn.util.InetAddressUtils; import java.io.File; import java.io.FileWriter; -import java.io.IOException; import java.net.InetAddress; import java.net.NetworkInterface; -import java.util.AbstractList; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; +import java.util.Date; import java.util.Enumeration; -import java.util.List; -import java.util.RandomAccess; -import java.util.concurrent.RunnableFuture; +import java.util.HashMap; +import java.util.function.Consumer; /** * OsdUtil - OpenSeizureDetector Utilities * Deals with starting and stopping the background service and binding to it to receive data. */ -public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallback { +public class OsdUtil { + public final static String PRIVACY_POLICY_URL = "https://www.openseizuredetector.org.uk/?page_id=1415"; + public final static String DATA_SHARING_URL = "https://www.openseizuredetector.org.uk/?page_id=1818"; + private final String SYSLOG = "SysLog"; private final String ALARMLOG = "AlarmLog"; private final String DATALOG = "DataLog"; - private final String[] REQUIRED_PERMISSIONS = { - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.WAKE_LOCK, - }; - - private final String[] SMS_PERMISSIONS = { - Manifest.permission.SEND_SMS, - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.READ_PHONE_STATE, - }; - - /** * Based on http://stackoverflow.com/questions/7440473/android-how-to-check-if-the-intent-service-is-still-running-or-has-stopped-running */ - private Context mContext; + private static Context mContext; private Handler mHandler; - private String TAG = "OsdUtil"; + private static String TAG = "OsdUtil"; private boolean mLogAlarms = true; private boolean mLogSystem = true; private boolean mLogData = true; private boolean mPermissionsRequested = false; private boolean mSMSPermissionsRequested = false; + private static final String mSysLogTableName = "SysLog"; + //private LogManager mLm; + static private SQLiteDatabase mSysLogDb = null; // SQLite Database for data and log entries. + private final static Long mMinPruneInterval = new Long(5 * 60 * 1000); // minimum time between syslog pruning is 5 minutes + private static Long mLastPruneMillis = new Long(0); // Record of the last time we pruned the syslog db. private static int mNbound = 0; @@ -121,6 +103,9 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac mContext = context; mHandler = handler; updatePrefs(); + //Log.i(TAG,"Creating Log Manager instance"); + //mLm = new LogManager(mContext,false,false,null,0,0,false,0); + openDb(); writeToSysLogFile("OsdUtil() - initialised"); } @@ -136,8 +121,8 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac try { mLogAlarms = SP.getBoolean("LogAlarms", true); Log.v(TAG, "updatePrefs() - mLogAlarms = " + mLogAlarms); - mLogData = SP.getBoolean("LogData", false); - Log.v(TAG, "updatePrefs() - mLogData = " + mLogData); + mLogData = SP.getBoolean("LogData", true); + Log.v(TAG, "OsdUtil.updatePrefs() - mLogData = " + mLogData); mLogSystem = SP.getBoolean("LogSystem", true); Log.v(TAG, "updatePrefs() - mLogSystem = " + mLogSystem); @@ -170,7 +155,7 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac } } if (nServers != 0) { - Log.v(TAG, "isServerRunning() - " + nServers + " instances are running"); + //Log.v(TAG, "isServerRunning() - " + nServers + " instances are running"); return true; } else return false; @@ -199,7 +184,7 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac * Stop the SdServer service */ public void stopServer() { - Log.d(TAG, "OsdUtil.stopServer() - stopping Server... - mNbound=" + mNbound); + Log.i(TAG, "OsdUtil.stopServer() - stopping Server... - mNbound=" + mNbound); writeToSysLogFile("stopserver() - stopping server"); // then send an Intent to stop the service. @@ -213,7 +198,7 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac /** * bind an activity to to an already running server. */ - public void bindToServer(Activity activity, SdServiceConnection sdServiceConnection) { + public void bindToServer(Context activity, SdServiceConnection sdServiceConnection) { Log.i(TAG, "OsdUtil.bindToServer() - binding to SdServer"); writeToSysLogFile("bindToServer() - binding to SdServer"); Intent intent = new Intent(sdServiceConnection.mContext, SdServer.class); @@ -225,7 +210,7 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac /** * unbind an activity from server */ - public void unbindFromServer(Activity activity, SdServiceConnection sdServiceConnection) { + public void unbindFromServer(Context activity, SdServiceConnection sdServiceConnection) { // unbind this activity from the service if it is bound. if (sdServiceConnection.mBound) { Log.i(TAG, "unbindFromServer() - unbinding"); @@ -301,6 +286,7 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac // 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 == null) return false; if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) { return true; } else { @@ -312,7 +298,11 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac // 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()); + if (activeNetwork != null) { + return (activeNetwork.isConnected()); + } else { + return (false); + } } /** @@ -331,16 +321,18 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac /** - * Write a message to the system log file, provided mLogSystem is true. + * Write a message to the system log database. * * @param msgStr */ - public void writeToSysLogFile(String msgStr) { - if (mLogSystem) - writeToLogFile(SYSLOG, msgStr); - else - Log.v(TAG, "writeToSysLogFile - mLogSystem False so not writing"); + public void writeToSysLogFile(String msgStr,String logType) { + writeLogEntryToLocalDb(msgStr,logType); } + public void writeToSysLogFile(String msgStr) { + writeLogEntryToLocalDb(msgStr,"v"); + } + + /** * Write a message to the alarm log file, provided mLogAlarms is true. @@ -462,82 +454,289 @@ public class OsdUtil implements ActivityCompat.OnRequestPermissionsResultCallbac } } - public boolean arePermissionsOK() { - boolean allOk = true; - Log.v(TAG, "arePermissionsOK"); - for (int i = 0; i < REQUIRED_PERMISSIONS.length; i++) { - if (ContextCompat.checkSelfPermission(mContext, REQUIRED_PERMISSIONS[i]) - != PackageManager.PERMISSION_GRANTED) { - Log.i(TAG, REQUIRED_PERMISSIONS[i] + " Permission Not Granted"); - allOk = false; + /** + * string2date - returns a Date object represented by string dateStr + * It first attempts to parse it as a long integer, in which case it is assumed to + * be a unix timestamp. + * If that fails it attempts to parse it as yyyy-MM-dd'T'HH:mm:ss'Z' format. + * @param dateStr String reprenting a date + * @return Date object or null if parsing fails. + */ + public Date string2date(String dateStr) { + Date dataTime = null; + try { + Long tstamp = Long.parseLong(dateStr); + dataTime = new Date(tstamp); + } catch (NumberFormatException e) { + Log.v(TAG, "remoteEventsAdapter.getView: Error Parsing dataDate as Long: " + e.getLocalizedMessage()+" trying as string"); + try { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dataTime = dateFormat.parse(dateStr); + } catch (ParseException e2) { + Log.e(TAG, "remoteEventsAdapter.getView: Error Parsing dataDate " + e2.getLocalizedMessage()); + dataTime = null; } } - return allOk; - } - - public boolean areSMSPermissionsOK() { - boolean allOk = true; - Log.v(TAG, "areSMSPermissionsOK()"); - for (int i = 0; i < SMS_PERMISSIONS.length; i++) { - if (ContextCompat.checkSelfPermission(mContext, SMS_PERMISSIONS[i]) - != PackageManager.PERMISSION_GRANTED) { - Log.i(TAG, SMS_PERMISSIONS[i] + " Permission Not Granted"); - allOk = false; - } - } - return allOk; + return(dataTime); } - public void requestPermissions(Activity activity) { - if (mPermissionsRequested) { - Log.i(TAG, "requestPermissions() - request already sent - not doing anything"); - } else { - Log.i(TAG, "requestPermissions() - requesting permissions"); - for (int i = 0; i < REQUIRED_PERMISSIONS.length; i++) { - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, - REQUIRED_PERMISSIONS[i])) { - Log.i(TAG, "shouldShowRationale for permission" + REQUIRED_PERMISSIONS[i]); + public final int ALARM_STATUS_WARNING = 1; + public final int ALARM_STATUS_ALARM = 2; + public final int ALARM_STATUS_FALL = 3; + public final int ALARM_STATUS_MANUAL = 5; + + public String alarmStatusToString(int eventAlarmStatus) { + String retVal = "Unknown"; + switch (eventAlarmStatus) { + case ALARM_STATUS_WARNING: // Warning + retVal = "WARNING"; + break; + case ALARM_STATUS_ALARM: // alarm + retVal = "ALARM"; + break; + case ALARM_STATUS_FALL: // fall + retVal = "FALL"; + break; + case ALARM_STATUS_MANUAL: // Manual alarm + retVal = "MANUAL ALARM"; + break; + + } + return(retVal); + } + + private static boolean openDb() { + Log.d(TAG, "openDb"); + try { + if (mSysLogDb == null) { + Log.i(TAG,"openDb: mSysLogDb is null - initialising"); + mSysLogDb = new OsdSysLogHelper(mContext).getWritableDatabase(); + } else { + Log.i(TAG,"openDb: mSysLogDb has been initialised already so not doing anything"); + } + if (!checkTableExists(mSysLogDb, mSysLogTableName)) { + Log.e(TAG, "ERROR - Table "+mSysLogTableName+" does not exist"); + return false; + } else { + Log.d(TAG, "table " + mSysLogTableName + " exists ok"); + } + } catch (SQLException e) { + Log.e(TAG, "Failed to open Database: " + e.toString()); + return false; + } + return true; + } + + private static boolean checkTableExists(SQLiteDatabase osdDb, String osdTableName) { + Cursor c = null; + boolean tableExists = false; + Log.d(TAG, "checkTableExists()"); + try { + c = osdDb.query(osdTableName, null, + null, null, null, null, null); + tableExists = true; + c.close(); + } catch (Exception e) { + Log.d(TAG, osdTableName + " doesn't exist :((("); + } + return tableExists; + } + + /** + * Write syslog string to local database + * FIXME - I am sure we should not be using raw SQL Srings to do this! + */ + public void writeLogEntryToLocalDb(String logText, String statusVal) { + Log.v(TAG, "writeLogEntryToLocalDb()"); + Date curDate = new Date(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + String dateStr = dateFormat.format(curDate); + String SQLStr = "SQLStr"; + + try { + SQLStr = "INSERT INTO " + mSysLogTableName + + "(dataTime, logLevel, dataJSON, uploaded)" + + " VALUES(" + + "'" + dateStr + "'," + + DatabaseUtils.sqlEscapeString(statusVal) + "," + + DatabaseUtils.sqlEscapeString(logText) + "," + + 0 + + ")"; + mSysLogDb.execSQL(SQLStr); + Log.v(TAG, "syslog entry written to database: "+logText); + pruneSysLogDb(); + + } catch (SQLException e) { + Log.e(TAG, "writeLogEngryToLocalDb(): Error Writing Data: " + e.toString()); + Log.e(TAG, "SQLStr was " + SQLStr); + } + + } + + /** + * Return an array list of objects representing the syslog entries in the database by calling the specified callback function. + * + * @return True on successful start or false if call fails. + */ + public boolean getSysLogList(Consumer>> callback) { + Log.v(TAG, "getSysLogList"); + ArrayList> eventsList = new ArrayList<>(); + + String whereClause = ""; + String[] whereArgs = {}; + String[] columns = {"*"}; + new SelectQueryTask(mSysLogTableName, columns, null, null, + null, null, "dataTime DESC", (Cursor cursor) -> { + Log.v(TAG, "getSysLogList - returned " + cursor); + if (cursor != null) { + Log.v(TAG, "getSysLogList - returned " + cursor.getCount() + " records"); + while (!cursor.isAfterLast()) { + HashMap event = new HashMap<>(); + //event.put("id", cursor.getString(cursor.getColumnIndex("id"))); + event.put("dataTime", cursor.getString(cursor.getColumnIndex("dataTime"))); + String loglevel = cursor.getString(cursor.getColumnIndex("logLevel")); + event.put("loglevel", loglevel); + event.put("dataJSON", cursor.getString(cursor.getColumnIndex("dataJSON"))); + //event.put("dataJSON", cursor.getString(cursor.getColumnIndex("dataJSON"))); + eventsList.add(event); + cursor.moveToNext(); } } - ActivityCompat.requestPermissions(activity, - REQUIRED_PERMISSIONS, - 42); - mPermissionsRequested = true; + callback.accept(eventsList); + }).execute(); + return (true); + } + + /** + * Executes the sqlite query (=SELECT statement) + * Use as new SelectQueryTask(xxx,xxx,xx,xxxx).execute() + * + */ + static private class SelectQueryTask extends AsyncTask { + // Based on https://stackoverflow.com/a/21120199/2104584 + String mTable; + String[] mColumns; + String mSelection; + String[] mSelectionArgs; + String mGroupBy; + String mHaving; + String mOrderBy; + Consumer mCallback; + + //query(String table, String[] columns, String selection, String[] selectionArgs, + // String groupBy, String having, String orderBy) + SelectQueryTask(String table, String[] columns, String selection, String[] selectionArgs, + String groupBy, String having, String orderBy, Consumer callback) { + // list all the parameters like in normal class define + this.mTable = table; + this.mColumns = columns; + this.mSelection = selection; + this.mSelectionArgs = selectionArgs; + this.mGroupBy = groupBy; + this.mHaving = having; + this.mOrderBy = orderBy; + this.mCallback = callback; + + } + + @Override + protected Cursor doInBackground(Void... params) { + Log.v(TAG, "runSelect.doInBackground()"); + Log.v(TAG, "SelectQueryTask.doInBackground: mTable=" + mTable + ", mColumns=" + Arrays.toString(mColumns) + + ", mSelection=" + mSelection + ", mSelectionArgs=" + Arrays.toString(mSelectionArgs) + ", mGroupBy=" + mGroupBy + + ", mHaving =" + mHaving + ", mOrderBy=" + mOrderBy); + + try { + Cursor resultSet = mSysLogDb.query(mTable, mColumns, mSelection, + mSelectionArgs, mGroupBy, mHaving, mOrderBy); + resultSet.moveToFirst(); + return (resultSet); + } catch (SQLException e) { + Log.e(TAG, "SelectQueryTask.doInBackground(): Error selecting Data: " + e.toString()); + return (null); + } catch (IllegalArgumentException e) { + Log.e(TAG, "SelectQueryTask.doInBackground(): Illegal Argument Exception: " + e.toString()); + return (null); + } + } + + @Override + protected void onPostExecute(final Cursor result) { + mCallback.accept(result); } } - public void requestSMSPermissions(Activity activity) { - if (mSMSPermissionsRequested) { - Log.i(TAG, "requestSMSPermissions() - request already sent - not doing anything"); + + + /** + * pruneSysLogDb() removes data that is older than 7 days + */ + public int pruneSysLogDb() { + //Log.v(TAG, "pruneSysLogDb()"); + int retVal; + long currentDateMillis = new Date().getTime(); + if (currentDateMillis > mLastPruneMillis + mMinPruneInterval) { + mLastPruneMillis = currentDateMillis; + // FIXME - change this to something sensible like 7 days after testing + long endDateMillis = currentDateMillis - 5 * 60 * 1000; + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String endDateStr = dateFormat.format(new Date(endDateMillis)); + Log.v(TAG, "pruneSysLogDb - endDateStr=" + endDateStr); + try { + String selectStr = "DataTime<=?"; + String[] selectArgs = {endDateStr}; + retVal = mSysLogDb.delete(mSysLogTableName, selectStr, selectArgs); + } catch (Exception e) { + Log.e(TAG, "Error deleting log entries" + e.toString()); + retVal = 0; + } + if (retVal > 0) { + Log.v(TAG, String.format("pruneSysLogDb() - deleted %d records", retVal)); + } + return (retVal); } else { - Log.i(TAG, "requestSMSPermissions() - requesting permissions"); - for (int i = 0; i < SMS_PERMISSIONS.length; i++) { - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, - SMS_PERMISSIONS[i])) { - Log.i(TAG, "shouldShowRationale for permission" + SMS_PERMISSIONS[i]); - } - } - ActivityCompat.requestPermissions(activity, - SMS_PERMISSIONS, - 43); - mSMSPermissionsRequested = true; + return (0); } } - @Override - public void onRequestPermissionsResult(int requestCode, - String permissions[], int[] grantResults) { - Log.i(TAG, "onRequestPermissionsResult - Permission" + permissions + " = " + grantResults); - showToast(mContext.getString(R.string.RestartingServerMsg)); - stopServer(); - // Wait 0.1 second to give the server chance to shutdown, then re-start it - mHandler.postDelayed(new Runnable() { - public void run() { - startServer(); - } - }, 100); + public static class OsdSysLogHelper extends SQLiteOpenHelper { + // If you change the database schema, you must increment the database version. + public static final int DATABASE_VERSION = 1; + public static final String DATABASE_NAME = "OsdSysLog.db"; + private static final String TAG = "LogManager.OsdSysLogHelper"; + public OsdSysLogHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + Log.d(TAG, "OsdSysLogHelper constructor"); + } + + public void onCreate(SQLiteDatabase db) { + Log.i(TAG, "onCreate - TableName=" + mSysLogTableName); + String SQLStr = "CREATE TABLE IF NOT EXISTS " + mSysLogTableName + "(" + + "id INTEGER PRIMARY KEY," + + "dataTime DATETIME," + + "logLevel TEXT," + + "dataJSON TEXT," + + "uploaded INT" + + ");"; + db.execSQL(SQLStr); + } + + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // This database is only a cache for online data, so its upgrade policy is + // to simply to discard the data and start over + Log.i(TAG,"onUpgrade()"); + db.execSQL("Drop table if exists " + mSysLogTableName + ";"); + onCreate(db); + } + + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.i(TAG,"onDowngrade()"); + onUpgrade(db, oldVersion, newVersion); + } } + } diff --git a/app/src/main/java/uk/org/openseizuredetector/PrefActivity.java b/app/src/main/java/uk/org/openseizuredetector/PrefActivity.java index 9620c7c..8d0227f 100644 --- a/app/src/main/java/uk/org/openseizuredetector/PrefActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/PrefActivity.java @@ -75,7 +75,8 @@ public class PrefActivity extends PreferenceActivity implements SharedPreference .getDefaultSharedPreferences(this.getApplicationContext()); String dataSourceStr = SP.getString("DataSource", "Pebble"); Log.i(TAG, "onBuildHeaders DataSource = " + dataSourceStr); - Boolean advancedMode = SP.getBoolean("advancedMode", false); + //Boolean advancedMode = SP.getBoolean("advancedMode", false); + Boolean advancedMode = true; Log.i(TAG, "onBuildHeaders advancedMode = " + advancedMode); if (advancedMode) { @@ -120,7 +121,7 @@ public class PrefActivity extends PreferenceActivity implements SharedPreference } else { titleStr = getResources().getString(h.titleRes); } - Log.v(TAG, "i=" + i + ": found - " + titleStr + " looking for "+ getString(R.string.basic_settings_title)); + Log.v(TAG, "i=" + i + ": found - " + titleStr + " looking for " + getString(R.string.basic_settings_title)); if (!titleStr.equals(getString(R.string.basic_settings_title))) { Log.v(TAG, "an Advanced Mode Header, so removing it...."); target.remove(i); @@ -143,18 +144,22 @@ public class PrefActivity extends PreferenceActivity implements SharedPreference public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { Log.i(TAG, "SharedPreference " + s + " Changed."); + // if we have enabled the SMS alarm, we may need extra permissions approving. This is handled in + // StartUpActivity, so we exit this activity and start start-up activity. if (s.equals("SMSAlarm")) { if (sharedPreferences.getBoolean("SMSAlarm", false) == true) { - if (mUtil.areSMSPermissionsOK() == false) { - Log.i(TAG, "onSharedPreferenceChanged(): SMS Alarm Enabled - Requesting Permissions"); - mUtil.requestSMSPermissions(this); - } else { - Log.i(TAG, "OnSharedPreferenceCHanged(): SMS Permissions already granted, doing nothing"); - } + Log.i(TAG, "onSharedPreferenceChanged(): SMS Alarm Enabled - Restarting start-up activity to check permissions"); + Intent i; + i = new Intent(this, StartupActivity.class); + startActivity(i); + Log.i(TAG,"onSharedPreferenceChanged() - finishing PrefActivity"); + finish(); + return; } else { Log.i(TAG, "OnSharedPreferenceChanged(): SMS Alarm disabled so do not need permissions"); } } + // For all other preference changes we just restart SdServer so it is not as alarming for the user! //mUtil.showToast("Setting " + s + " Changed - restarting server"); mPrefChanged = true; mUtil.stopServer(); @@ -213,6 +218,7 @@ public class PrefActivity extends PreferenceActivity implements SharedPreference protected void onStop() { super.onStop(); mUtil.writeToSysLogFile("PrefActvity.onStop()"); + Log.i(TAG,"onStop()"); } /** @@ -276,6 +282,17 @@ public class PrefActivity extends PreferenceActivity implements SharedPreference } } + public static class LoggingPrefsFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.logging_prefs); + } + } + + public static class SeizureDetectorPrefsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/uk/org/openseizuredetector/RemoteDbActivity.java b/app/src/main/java/uk/org/openseizuredetector/RemoteDbActivity.java new file mode 100644 index 0000000..0060bbc --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/RemoteDbActivity.java @@ -0,0 +1,226 @@ +package uk.org.openseizuredetector; + +//import androidx.appcompat.app.AppCompatActivity; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.Handler; +import android.preference.PreferenceManager; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.Button; +import android.widget.TextView; + +import java.util.HashMap; + +public class RemoteDbActivity extends AppCompatActivity { + private String TAG = "RemoteDbActivity"; + private Context mContext; + private UiTimer mUiTimer; + private LogManager mLm; + private WebView mWebView; + private SdServiceConnection mConnection; + private OsdUtil mUtil; + final Handler serverStatusHandler = new Handler(); + private String TOKEN_ID = "webApiAuthToken"; + private String mRemtoteUrl = "https://osdapi.ddns.net/"; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + mContext = this; + setContentView(R.layout.activity_remote_db); + mUtil = new OsdUtil(getApplicationContext(), serverStatusHandler); + mConnection = new SdServiceConnection(getApplicationContext()); + mUtil.bindToServer(getApplicationContext(), mConnection); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + String remoteUrl = extras.getString("url"); + mRemtoteUrl = remoteUrl; + Log.d(TAG, "onCreate - mRemoteUrl=" + mRemtoteUrl); + } + + waitForConnection(); + + //mLm= new LogManager(mContext); + + Button authBtn = + (Button) findViewById(R.id.auth_button); + authBtn.setOnClickListener(onAuth); + //Button pruneBtn = + // (Button) findViewById(R.id.pruneDatabaseBtn); + //pruneBtn.setOnClickListener(onPruneBtn); + + mWebView = (WebView) findViewById(R.id.remote_db_webview); + WebSettings webSettings = mWebView.getSettings(); + webSettings.setJavaScriptEnabled(true); + + } + + 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.d(TAG, "waitForConnection - Bound!"); + initialiseServiceConnection(); + } else { + Log.v(TAG, "waitForConnection - waiting..."); + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + waitForConnection(); + } + }, 100); + } + } + + private void initialiseServiceConnection() { + mLm = mConnection.mSdServer.mLm; + mWebView.loadUrl(mRemtoteUrl, getAuthHeaders()); + //mWac = mConnection.mSdServer.mLm.mWac; + } + + + @Override + protected void onStart() { + super.onStart(); + waitForConnection(); + updateUi(); + //startUiTimer(); + } + + @Override + protected void onPause() { + super.onPause(); + stopUiTimer(); + } + + @Override + protected void onResume() { + super.onResume(); + startUiTimer(); + } + + private HashMap getAuthHeaders() { + HashMap headersMap = new HashMap<>(); + String authToken = getAuthToken(); + headersMap.put("Authorization", "Token "+authToken); + return (headersMap); + } + + public String getAuthToken() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + String authToken = prefs.getString(TOKEN_ID, null); + return authToken; + } + + private void updateUi() { + Log.v(TAG,"updateUi()"); + TextView tv; + Button btn; + // Local Database Information + //tv = (TextView)findViewById(R.id.num_local_events_tv); + //int eventCount = 0; + //tv.setText(String.format("%d",eventCount)); + //tv = (TextView)findViewById(R.id.num_local_datapoints_tv); + //int datapointsCount = 0; + //tv.setText(String.format("%d",datapointsCount)); + + + + // Remote Database Information + tv = (TextView)findViewById(R.id.authStatusTv); + btn = (Button)findViewById(R.id.auth_button); + if (mLm != null) { + if (mLm.mWac.isLoggedIn()) { + tv.setText("Authenticated"); + btn.setText("Log Out"); + } else { + tv.setText("NOT AUTHENTICATED"); + btn.setText("Log In"); + } + } + } + + View.OnClickListener onAuth = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onAuth"); + Intent i; + i =new Intent(mContext, AuthenticateActivity.class); + startActivity(i); + } + }; + View.OnClickListener onPruneBtn = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onPruneBtn"); + mLm.pruneLocalDb(); + } + }; + + + /* + * Start the timer that will upload data to the remote server after a given period. + */ + private void startUiTimer() { + if (mUiTimer != null) { + Log.v(TAG, "startRemoteLogTimer -timer already running - cancelling it"); + mUiTimer.cancel(); + mUiTimer = null; + } + Log.v(TAG, "startRemoteLogTimer() - starting RemoteLogTimer"); + mUiTimer = + new UiTimer(1000, 1000); + mUiTimer.start(); + } + + + /* + * Cancel the remote logging timer to prevent attempts to upload to remote database. + */ + public void stopUiTimer() { + if (mUiTimer != null) { + Log.v(TAG, "stopRemoteLogTimer(): cancelling Remote Log timer"); + mUiTimer.cancel(); + mUiTimer = null; + } + } + + /** + * Upload recorded data to the remote database periodically. + */ + private class UiTimer extends CountDownTimer { + public UiTimer(long startTime, long interval) { + super(startTime, interval); + } + + @Override + public void onTick(long l) { + // Do Nothing + } + + @Override + public void onFinish() { + Log.v(TAG, "UiTimer - onFinish - Updating UI"); + updateUi(); + // Restart this timer. + start(); + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/uk/org/openseizuredetector/ReportSeizureActivity.java b/app/src/main/java/uk/org/openseizuredetector/ReportSeizureActivity.java new file mode 100644 index 0000000..c626114 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/ReportSeizureActivity.java @@ -0,0 +1,427 @@ +package uk.org.openseizuredetector; + +//import androidx.appcompat.app.AppCompatActivity; + +import android.app.DatePickerDialog; +import android.app.TimePickerDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.Handler; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.DatePicker; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.TimePicker; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +/** + * ReportSeizureActivity - Allows the user to report a seizure manually, which is saved in the database for + * future analysis - particularlly useful if OpenSeizureDetector did not detect the seizure automatically as this + * will ensure the data for the missed seizure is saved. + * Based on: https://www.journaldev.com/9976/android-date-time-picker-dialog + */ +public class ReportSeizureActivity extends AppCompatActivity { + private String TAG = "ReportSeizureActivity"; + private Context mContext; + private UiTimer mUiTimer; + private LogManager mLm; + private WebApiConnection mWac; + + private int mYear, mMonth, mDay, mHour, mMinute; + private String mMsg = "Messages"; + private SdServiceConnection mConnection; + private OsdUtil mUtil; + final Handler serverStatusHandler = new Handler(); + private List mEventTypesList = null; + private HashMap> mEventSubTypesHashMap = null; + private String mEventTypeStr = null; + private String mEventSubTypeStr = null; + private String mEventNotes = ""; + private RadioGroup mEventTypeRg; + private boolean mRedrawEventSubTypesList = false; + private boolean mRedrawEventTypesList = false; + private RadioGroup mEventSubTypeRg; + private boolean mEventSubTypesListChanged = false; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + mContext = this; + mUtil = new OsdUtil(this, serverStatusHandler); + if (!mUtil.isServerRunning()) { + mUtil.showToast(getString(R.string.error_server_not_running)); + finish(); + return; + } + mContext = this; + mConnection = new SdServiceConnection(getApplicationContext()); + + setContentView(R.layout.activity_report_seizure); + + mEventTypeRg = findViewById(R.id.eventTypeRg); + mEventTypeRg.setOnCheckedChangeListener(onEventTypeChange); + mEventSubTypeRg = findViewById(R.id.eventSubTypeRg); + mEventSubTypeRg.setOnCheckedChangeListener(onEventSubTypeChange); + + Button okBtn = + (Button) findViewById(R.id.loginBtn); + okBtn.setOnClickListener(onOk); + + Button cancelBtn = + (Button) findViewById(R.id.cancelBtn); + cancelBtn.setOnClickListener(onCancel); + + Button setDateBtn = + (Button) findViewById(R.id.select_date_button); + setDateBtn.setOnClickListener(onSelectDate); + + Button setTimeBtn = + (Button) findViewById(R.id.select_time_button); + setTimeBtn.setOnClickListener(onSelectTime); + + // 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); + } + + @Override + protected void onStart() { + super.onStart(); + mUtil.bindToServer(getApplicationContext(), mConnection); + waitForConnection(); + } + + @Override + protected void onPause() { + super.onPause(); + stopUiTimer(); + } + + @Override + protected void onResume() { + super.onResume(); + //startUiTimer(); + } + + @Override + protected void 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); + } + } + + private void initialiseServiceConnection() { + mLm = mConnection.mSdServer.mLm; + mWac = mConnection.mSdServer.mLm.mWac; + + if (mWac.isLoggedIn()) { + + // Retrieve the JSONObject containing the standard event types. + // Note this obscure syntax is to avoid having to create another interface, so it is worth it :) + // See https://medium.com/@pra4mesh/callback-function-in-java-20fa48b27797 + mWac.getEventTypes(new WebApiConnection.JSONObjectCallback() { + @Override + public void accept(JSONObject eventTypesObj) { + Log.v(TAG, "initialiseServiceConnection().onEventTypesReceived"); + if (eventTypesObj == null) { + Log.e(TAG, "initialiseServiceConnection().getEventTypes Callback: Error Retrieving event types"); + mUtil.showToast("Error Retrieving Event Types from Server - Please Try Again Later!"); + } else { + Iterator keys = eventTypesObj.keys(); + mEventTypesList = new ArrayList(); + mEventSubTypesHashMap = new HashMap>(); + while (keys.hasNext()) { + String key = keys.next(); + Log.v(TAG, "initialiseServiceConnection().getEventTypes Callback: key=" + key); + mEventTypesList.add(key); + try { + JSONArray eventSubTypes = eventTypesObj.getJSONArray(key); + ArrayList eventSubtypesList = new ArrayList(); + for (int i = 0; i < eventSubTypes.length(); i++) { + eventSubtypesList.add(eventSubTypes.getString(i)); + } + mEventSubTypesHashMap.put(key, eventSubtypesList); + mRedrawEventSubTypesList = true; + } catch (JSONException e) { + Log.e(TAG, "initialiseServiceConnection().getEventTypes Callback: Error parsing JSONObject" + e.getMessage() + e.toString()); + } + } + mRedrawEventTypesList = true; + updateUi(); + } + } + }); + } else { + new AlertDialog.Builder(mContext) + .setTitle(R.string.not_logged_in_dialog_title) + .setMessage(R.string.not_logged_in_dialog_message) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + finish(); + } + }) + .show(); + + } + } + + + + private void updateUi() { + //Log.v(TAG,"updateUi()"); + TextView tv; + Button btn; + RadioButton b; + + tv = (TextView)findViewById(R.id.date_day_tv); + tv.setText(String.format("%02d",mDay)); + tv = (TextView)findViewById(R.id.date_mon_tv); + tv.setText(String.format("%02d",mMonth+1)); // Month counted from zero + tv = (TextView)findViewById(R.id.date_year_tv); + tv.setText(String.format("%04d",mYear)); + tv = (TextView)findViewById(R.id.time_hh_tv); + tv.setText(String.format("%02d",mHour)); + tv = (TextView)findViewById(R.id.time_mm_tv); + tv.setText(String.format("%02d",mMinute)); + tv = (TextView)findViewById(R.id.msg_tv); + tv.setText(mMsg); + + // Populate event type button group if necessary + if (mEventTypesList != null && mRedrawEventTypesList) { + Log.v(TAG, "updateUi: " + mEventTypesList.toString()); + mEventTypeRg.removeAllViews(); + for (String eventTypeStr : mEventTypesList) { + b = new RadioButton(this); + b.setText(eventTypeStr); + mEventTypeRg.addView(b); + } + mRedrawEventTypesList = false; + } + + + String seizureTypeStr = null; + // Find which seizure type is selected + int checkedRadioButtonId = mEventTypeRg.getCheckedRadioButtonId(); + //Log.i(TAG,"updateUi(): checkedRadioButtonId="+checkedRadioButtonId); + b = (RadioButton) findViewById(checkedRadioButtonId); + if (b != null) { + seizureTypeStr = b.getText().toString(); + } + Log.i(TAG,"updateUi - SeizureType="+seizureTypeStr); + + // Populate the event sub-types radio button list. + Log.v(TAG,"updateUi() - meventsubtypeshashmap="+mEventSubTypesHashMap+", mEventSubtypesListChanged="+mEventSubTypesListChanged); + if (mEventSubTypesHashMap != null && mRedrawEventSubTypesList) { + Log.v(TAG,"UpdateUi() - populating event sub types list"); + if (seizureTypeStr != null) { + // based on https://androidexample.com/create-a-simple-listview + ArrayList subtypesArrayList = mEventSubTypesHashMap.get(seizureTypeStr); + Log.v(TAG, "updateUi() - eventType=" + seizureTypeStr + ", subtypes=" + subtypesArrayList); + mEventSubTypeRg.removeAllViews(); + for (String eventSubTypeStr : subtypesArrayList) { + b = new RadioButton(this); + b.setText(eventSubTypeStr); + mEventSubTypeRg.addView(b); + } + mRedrawEventSubTypesList = false; + } + } + } + + View.OnClickListener onOk = + new View.OnClickListener() { + @Override + public void onClick(View view) { + RadioButton b; + String seizureTypeStr = null; + String seizureSubTypeStr = null; + String notesStr = null; + Log.v(TAG, "onOk"); + //SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String dateStr=String.format("%4d-%02d-%02d %02d:%02d:30",mYear,mMonth+1,mDay, mHour, mMinute); + Log.v(TAG, "onOk() - dateSTr="+dateStr); + + // Read seizure type from radio buttons + int checkedRadioButtonId = mEventTypeRg.getCheckedRadioButtonId(); + b = (RadioButton) findViewById(checkedRadioButtonId); + if (b != null) { + seizureTypeStr = b.getText().toString(); + } + Log.i(TAG,"onOk() - SeizureType="+seizureTypeStr); + + checkedRadioButtonId = mEventSubTypeRg.getCheckedRadioButtonId(); + b = (RadioButton) findViewById(checkedRadioButtonId); + if (b != null) { + seizureSubTypeStr = b.getText().toString(); + } + Log.i(TAG,"onOk() - SeizureSubType="+seizureSubTypeStr); + + TextView tv = (TextView)findViewById(R.id.eventNotesTv); + notesStr = tv.getText().toString(); + + mLm.createLocalEvent(dateStr,5,seizureTypeStr, seizureSubTypeStr, notesStr, + mConnection.mSdServer.mSdData.toSettingsJSON()); + mUtil.showToast("Seizure Event Created"); + finish(); + } + }; + View.OnClickListener onCancel = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onCancel"); + finish(); + } + }; + View.OnClickListener onSelectDate = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onSelectDate()"); + DatePickerDialog datePickerDialog = new DatePickerDialog(mContext, + new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePicker view, int year, + int monthOfYear, int dayOfMonth) { + + mYear = year; + mMonth = monthOfYear; + mDay = dayOfMonth; + } + }, mYear, mMonth, mDay); + datePickerDialog.show(); + } + }; + + View.OnClickListener onSelectTime = + new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.v(TAG, "onSelectTime()"); + TimePickerDialog timePickerDialog = new TimePickerDialog(mContext, + new TimePickerDialog.OnTimeSetListener() { + + @Override + public void onTimeSet(TimePicker view, int hourOfDay, + int minute) { + + mHour = hourOfDay; + mMinute = minute; + } + }, mHour, mMinute, true); + timePickerDialog.show(); + } + }; + + + RadioGroup.OnCheckedChangeListener onEventTypeChange = + new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + mRedrawEventSubTypesList = true; + updateUi(); + } + }; + RadioGroup.OnCheckedChangeListener onEventSubTypeChange = + new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + updateUi(); + } + }; + + + /* + * Start the timer that will upload data to the remote server after a given period. + */ + private void startUiTimer() { + if (mUiTimer != null) { + Log.v(TAG, "startUiTimer -timer already running - cancelling it"); + mUiTimer.cancel(); + mUiTimer = null; + } + Log.v(TAG, "startUiTimer() - starting UiTimer"); + mUiTimer = + new UiTimer(1000, 1000); + mUiTimer.start(); + } + + /* + * Cancel the remote logging timer to prevent attempts to upload to remote database. + */ + public void stopUiTimer() { + if (mUiTimer != null) { + Log.v(TAG, "stopUiTimer(): cancelling Ui timer"); + mUiTimer.cancel(); + mUiTimer = null; + } + } + + /** + * Update User Interface Periodically + */ + private class UiTimer extends CountDownTimer { + public UiTimer(long startTime, long interval) { + super(startTime, interval); + } + + @Override + public void onTick(long l) { + // Do Nothing + } + + @Override + public void onFinish() { + //Log.v(TAG, "UiTimer - onFinish - Updating UI"); + updateUi(); + // Restart this timer. + start(); + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/uk/org/openseizuredetector/SdData.java b/app/src/main/java/uk/org/openseizuredetector/SdData.java index f24f7b0..fd73a29 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdData.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdData.java @@ -38,6 +38,7 @@ public class SdData implements Parcelable { private final static String TAG = "SdData"; private final static int N_RAW_DATA = 500; // 5 seconds at 100 Hz. /* Analysis settings */ + public String phoneAppVersion = ""; public boolean haveSettings = false; // flag to say if we have received settings or not. public boolean haveData = false; // flag to say we have received data. public short mDataUpdatePeriod; @@ -65,7 +66,22 @@ public class SdData implements Parcelable { public boolean mHRNullAsAlarm = false; public double mHRThreshMin = 40.0; public double mHRThreshMax = 150.0; + + /* Oxygen Saturation Alarm Settings */ + public boolean mO2SatAlarmActive = false; + public boolean mO2SatNullAsAlarm = false; + public double mO2SatThreshMin = 80.0; + + /* Watch App Settings */ + public String dataSourceName = ""; + public String watchPartNo = ""; + public String watchFwVersion = ""; + public String watchSdVersion = ""; + public String watchSdName = ""; + + public double rawData[]; + public double rawData3D[]; int mNsamp = 0; /* Analysis results */ @@ -87,14 +103,21 @@ public class SdData implements Parcelable { public boolean mHRFaultStanding = false; public double mHR = 0; + public boolean mO2SatAlarmStanding = false; + public boolean mO2SatFaultStanding = false; + public double mO2Sat = 0; + + public SdData() { simpleSpec = new int[10]; rawData = new double[N_RAW_DATA]; + rawData3D = new double[N_RAW_DATA * 3]; dataTime = new Time(Time.getCurrentTimezone()); } /* * Intialise this SdData object from a JSON String + * FIXME - add O2saturation with checking in case it is not included in the data */ public boolean fromJSON(String jsonStr) { Log.v(TAG, "fromJSON() - parsing jsonString - " + jsonStr); @@ -120,12 +143,12 @@ public class SdData implements Parcelable { alarmPhrase = jo.optString("alarmPhrase"); alarmThresh = jo.optInt("alarmThresh"); alarmRatioThresh = jo.optInt("alarmRatioThresh"); - mHRAlarmActive=jo.optBoolean("hrAlarmActive"); + mHRAlarmActive = jo.optBoolean("hrAlarmActive"); mHRAlarmStanding = jo.optBoolean("hrAlarmStanding"); mHRThreshMin = jo.optDouble("hrThreshMin"); mHRThreshMax = jo.optDouble("hrThreshMax"); mHR = jo.optDouble("hr"); - if (mHR>=0.0) { + if (mHR >= 0.0) { mHRAlarmActive = true; } JSONArray specArr = jo.optJSONArray("simpleSpec"); @@ -136,7 +159,7 @@ public class SdData implements Parcelable { Log.v(TAG, "fromJSON(): sdData = " + this.toString()); return true; } catch (Exception e) { - Log.v(TAG, "fromJSON() - error parsing result"+e.toString()); + Log.v(TAG, "fromJSON() - error parsing result" + e.toString()); haveData = false; return false; } @@ -147,6 +170,106 @@ public class SdData implements Parcelable { return toDataString(false); } + public String toJSON(boolean includeRawData) { + return toDataString(includeRawData); + } + + public String toDatapointJSON() { + String retval; + retval = "SdData.toDatapointJSON() Output"; + try { + JSONObject jsonObj = new JSONObject(); + if (dataTime != null) { + jsonObj.put("dataTime", dataTime.format("%d-%m-%Y %H:%M:%S")); + jsonObj.put("dataTimeStr", dataTime.format("%Y%m%dT%H%M%S")); + } else { + jsonObj.put("dataTimeStr", "00000000T000000"); + jsonObj.put("dataTime", "00-00-00 00:00:00"); + } + Log.v(TAG, "mSdData.dataTime = " + dataTime); + jsonObj.put("maxVal", maxVal); + jsonObj.put("maxFreq", maxFreq); + jsonObj.put("specPower", specPower); + jsonObj.put("roiPower", roiPower); + jsonObj.put("roiRatio", 10 * roiPower / specPower); + jsonObj.put("alarmState", alarmState); + jsonObj.put("alarmPhrase", alarmPhrase); + jsonObj.put("hr", mHR); + jsonObj.put("o2Sat", mO2Sat); + JSONArray arr = new JSONArray(); + for (int i = 0; i < simpleSpec.length; i++) { + arr.put(simpleSpec[i]); + } + jsonObj.put("simpleSpec", arr); + JSONArray rawArr = new JSONArray(); + for (int i = 0; i < rawData.length; i++) { + rawArr.put(rawData[i]); + } + //Log.v(TAG,"rawData[0]="+rawData[0]+", rawArr[0]="+rawArr.getDouble(0)); + jsonObj.put("rawData", rawArr); + + JSONArray raw3DArr = new JSONArray(); + for (int i = 0; i < rawData3D.length; i++) { + raw3DArr.put(rawData3D[i]); + } + jsonObj.put("rawData3D", raw3DArr); + + retval = jsonObj.toString(); + Log.v(TAG,"retval rawData="+retval); + } catch (Exception ex) { + Log.v(TAG, "Error Creating Data Object - " + ex.toString()); + retval = "Error Creating Data Object - " + ex.toString(); + } + + return (retval); + } + + + public String toSettingsJSON() { + String retval; + retval = "SdData.toSettingsJSON() Output"; + try { + JSONObject jsonObj = new JSONObject(); + if (dataTime != null) { + jsonObj.put("dataTime", dataTime.format("%d-%m-%Y %H:%M:%S")); + jsonObj.put("dataTimeStr", dataTime.format("%Y%m%dT%H%M%S")); + } else { + jsonObj.put("dataTimeStr", "00000000T000000"); + jsonObj.put("dataTime", "00-00-00 00:00:00"); + } + jsonObj.put("batteryPc", batteryPc); + jsonObj.put("alarmState", alarmState); + jsonObj.put("alarmPhrase", alarmPhrase); + jsonObj.put("sdMode", mSdMode); + jsonObj.put("sampleFreq", mSampleFreq); + jsonObj.put("analysisPeriod", analysisPeriod); + jsonObj.put("alarmFreqMin", alarmFreqMin); + jsonObj.put("alarmFreqMax", alarmFreqMax); + jsonObj.put("alarmThresh", alarmThresh); + jsonObj.put("alarmRatioThresh", alarmRatioThresh); + jsonObj.put("hrAlarmActive", mHRAlarmActive); + jsonObj.put("hrAlarmStanding", mHRAlarmStanding); + jsonObj.put("hrThreshMin", mHRThreshMin); + jsonObj.put("hrThreshMax", mHRThreshMax); + jsonObj.put("o2SatAlarmActive", mO2SatAlarmActive); + jsonObj.put("o2SatAlarmStanding", mO2SatAlarmStanding); + jsonObj.put("o2SatThreshMin", mO2SatThreshMin); + jsonObj.put("dataSourceName", dataSourceName); + Log.v(TAG,"phoneAppVersion="+phoneAppVersion); + jsonObj.put("phoneAppVersion", phoneAppVersion); + jsonObj.put("watchPartNo", watchPartNo); + jsonObj.put("watchSdName", watchSdName); + jsonObj.put("watchFwVersion", watchFwVersion); + jsonObj.put("watchSdVersion", watchSdVersion); + + retval = jsonObj.toString(); + } catch (Exception ex) { + Log.e(TAG, "toSettingsJSON(): Error Creating Data Object - " + ex.toString()); + retval = "Error Creating Data Object - " + ex.toString(); + } + return (retval); + } + public String toDataString(boolean includeRawData) { String retval; retval = "SdData.toDataString() Output"; @@ -155,7 +278,7 @@ public class SdData implements Parcelable { if (dataTime != null) { jsonObj.put("dataTime", dataTime.format("%d-%m-%Y %H:%M:%S")); jsonObj.put("dataTimeStr", dataTime.format("%Y%m%dT%H%M%S")); - }else{ + } else { jsonObj.put("dataTimeStr", "00000000T000000"); jsonObj.put("dataTime", "00-00-00 00:00:00"); } @@ -170,18 +293,22 @@ public class SdData implements Parcelable { jsonObj.put("haveSettings", haveSettings); jsonObj.put("alarmState", alarmState); jsonObj.put("alarmPhrase", alarmPhrase); - jsonObj.put("sdMode",mSdMode); - jsonObj.put("sampleFreq",mSampleFreq); - jsonObj.put("analysisPeriod",analysisPeriod); - jsonObj.put("alarmFreqMin",alarmFreqMin); - jsonObj.put("alarmFreqMax",alarmFreqMax); + jsonObj.put("sdMode", mSdMode); + jsonObj.put("sampleFreq", mSampleFreq); + jsonObj.put("analysisPeriod", analysisPeriod); + jsonObj.put("alarmFreqMin", alarmFreqMin); + jsonObj.put("alarmFreqMax", alarmFreqMax); jsonObj.put("alarmThresh", alarmThresh); jsonObj.put("alarmRatioThresh", alarmRatioThresh); jsonObj.put("hrAlarmActive", mHRAlarmActive); jsonObj.put("hrAlarmStanding", mHRAlarmStanding); - jsonObj.put("hrThreshMin",mHRThreshMin); + jsonObj.put("hrThreshMin", mHRThreshMin); jsonObj.put("hrThreshMax", mHRThreshMax); - jsonObj.put("hr",mHR); + jsonObj.put("hr", mHR); + jsonObj.put("o2SatAlarmActive", mO2SatAlarmActive); + jsonObj.put("o2SatAlarmStanding", mO2SatAlarmStanding); + jsonObj.put("o2SatThreshMin", mO2SatThreshMin); + jsonObj.put("o2Sat", mO2Sat); JSONArray arr = new JSONArray(); for (int i = 0; i < simpleSpec.length; i++) { arr.put(simpleSpec[i]); @@ -189,9 +316,17 @@ public class SdData implements Parcelable { jsonObj.put("simpleSpec", arr); if (includeRawData) { JSONArray rawArr = new JSONArray(); - for (int i = 0; i< rawData.length;i++) { + for (int i = 0; i < rawData.length; i++) { rawArr.put(rawData[i]); } + jsonObj.put("rawData", rawArr); + + JSONArray raw3DArr = new JSONArray(); + for (int i = 0; i < rawData3D.length; i++) { + raw3DArr.put(rawData3D[i]); + } + jsonObj.put("rawData3D", raw3DArr); + } retval = jsonObj.toString(); @@ -209,7 +344,7 @@ public class SdData implements Parcelable { retval = ""; if (dataTime != null) { retval = dataTime.format("%d-%m-%Y %H:%M:%S"); - }else{ + } else { retval = "00-00-00 00:00:00"; } for (int i = 0; i < simpleSpec.length; i++) { @@ -220,32 +355,37 @@ public class SdData implements Parcelable { retval = retval + ", " + mSampleFreq; retval = retval + ", " + alarmPhrase; retval = retval + ", " + mHR; + retval = retval + ", " + mO2Sat; if (includeRawData) { - for (int i = 0; i< mNsamp;i++) { + for (int i = 0; i < mNsamp; i++) { retval = retval + ", " + rawData[i]; } } - return(retval); + return (retval); } - /** Return the average acceleration value in the dataset */ + /** + * Return the average acceleration value in the dataset + */ public double getAvAcc() { double sumAcc = 0.0; - for (int i = 0; i< mNsamp;i++) { + for (int i = 0; i < mNsamp; i++) { sumAcc += rawData[i]; } - return(sumAcc/mNsamp); + return (sumAcc / mNsamp); } - /** Return the standard deviation of the acceleration values */ + /** + * Return the standard deviation of the acceleration values + */ public double getSdAcc() { double avAcc = 0.0; double varAcc = 0.0; avAcc = getAvAcc(); - for (int i = 0; i< mNsamp;i++) { - varAcc += Math.pow(rawData[i]-avAcc,2); + for (int i = 0; i < mNsamp; i++) { + varAcc += Math.pow(rawData[i] - avAcc, 2); } - return(Math.sqrt(varAcc/(mNsamp-1))); + return (Math.sqrt(varAcc / (mNsamp - 1))); } diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java index 1218ee8..2b9a1ae 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java @@ -254,6 +254,7 @@ public abstract class SdDataSource { String sdVersion; String sdName; JSONArray accelVals = null; + JSONArray accelVals3D = null; Log.v(TAG, "updateFromJSON - " + jsonStr); try { @@ -270,6 +271,12 @@ public abstract class SdDataSource { // if we get 'null' HR (For example if the heart rate is not working) mSdData.mHR = -1; } + try { + mSdData.mO2Sat = dataObject.getDouble("O2sat"); + } catch (JSONException e) { + // if we get 'null' O2 Saturation (For example if the oxygen sensor is not working) + mSdData.mO2Sat = -1; + } try { mMute = dataObject.getInt("Mute"); } catch (JSONException e) { @@ -284,12 +291,31 @@ public abstract class SdDataSource { } int i; for (i = 0; i < accelVals.length(); i++) { - mSdData.rawData[i] = accelVals.getInt(i); + mSdData.rawData[i] = accelVals.getDouble(i); } mSdData.mNsamp = accelVals.length(); - //mNSamp = accelVals.length(); + //Log.d(TAG,"accelVals[0]="+accelVals.getDouble(0)+", mSdData.rawData[0]="+mSdData.rawData[0]); + try { + accelVals3D = dataObject.getJSONArray("data3D"); + Log.v(TAG, "Received " + accelVals3D.length() + " acceleration 3D values, rawData Length is " + mSdData.rawData3D.length); + if (accelVals3D.length() > mSdData.rawData3D.length) { + mUtil.writeToSysLogFile("ERROR: Received " + accelVals3D.length() + " 3D acceleration values, but rawData3D storage length is " + + mSdData.rawData3D.length); + } + for (i = 0; i < accelVals3D.length(); i++) { + mSdData.rawData3D[i] = accelVals3D.getDouble(i); + } + } catch (JSONException e) { + // If we get an error, just set rawData3D to zero + Log.i(TAG,"updateFromJSON - error parsing 3D data - setting it to zero"); + for (i = 0; i < mSdData.rawData3D.length; i++) { + mSdData.rawData3D[i] = 0.; + } + } + mWatchAppRunningCheck = true; doAnalysis(); + if (mSdData.haveSettings == false) { retVal = "sendSettings"; } else { @@ -312,6 +338,10 @@ public abstract class SdDataSource { sdName = dataObject.getString("sdName"); mUtil.writeToSysLogFile(" * sdName = " + sdName + " version " + sdVersion); mUtil.writeToSysLogFile(" * watchPartNo = " + watchPartNo + " fwVersion " + watchFwVersion); + mSdData.watchPartNo = watchPartNo; + mSdData.watchFwVersion = watchFwVersion; + mSdData.watchSdVersion = sdVersion; + mSdData.watchSdName = sdName; } catch (Exception e) { Log.e(TAG, "updateFromJSON - Error Parsing V3.2 JSON String - " + e.toString()); mUtil.writeToSysLogFile("updateFromJSON - Error Parsing V3.2 JSON String - " + jsonStr + " - " + e.toString()); @@ -455,8 +485,10 @@ public abstract class SdDataSource { // Check this data to see if it represents an alarm state. alarmCheck(); hrCheck(); + o2SatCheck(); fallCheck(); muteCheck(); + Log.v(TAG,"after fallCheck, mSdData.fallAlarmStanding="+mSdData.fallAlarmStanding); mSdDataReceiver.onSdDataReceived(mSdData); // and tell SdServer we have received data. } @@ -470,9 +502,12 @@ public abstract class SdDataSource { */ private void alarmCheck() { boolean inAlarm; - Log.v(TAG, "alarmCheck()"); + // Avoid potential divide by zero issue + if (mSdData.specPower == 0) + mSdData.specPower = 1; + Log.v(TAG, "alarmCheck() - roiPower="+mSdData.roiPower+" specPower="+ mSdData.specPower+" ratio="+10*mSdData.roiPower/ mSdData.specPower); // Is the current set of data representing an alarm state? - if ((mSdData.roiPower > mAlarmThresh) && (10 * (mSdData.roiPower / mSdData.specPower) > mAlarmRatioThresh)) { + if ((mSdData.roiPower > mAlarmThresh) && ((10 * mSdData.roiPower / mSdData.specPower) > mAlarmRatioThresh)) { inAlarm = true; } else { inAlarm = false; @@ -502,7 +537,7 @@ public abstract class SdDataSource { } } - Log.v(TAG, "alarmCheck(): inAlarm=" + inAlarm + ", alarmState = " + mSdData.alarmState + " alarmCount=" + mAlarmCount + " mAlarmTime=" + mAlarmTime); + Log.v(TAG, "alarmCheck(): inAlarm=" + inAlarm + ", alarmState = " + mSdData.alarmState + " alarmCount=" + mAlarmCount + " mWarnTime=" + mWarnTime+ " mAlarmTime=" + mAlarmTime); } @@ -543,9 +578,39 @@ public abstract class SdDataSource { mSdData.mHRAlarmStanding = false; } } + } + + /** + * hrCheck - check the Heart rate data in mSdData to see if it represents an alarm condition. + * Sets mSdData.mHRAlarmStanding + */ + public void o2SatCheck() { + Log.v(TAG, "o2SatCheck()"); + /* Check Oxygen Saturation against alarm settings */ + if (mSdData.mO2SatAlarmActive) { + if (mSdData.mO2Sat < 0) { + if (mSdData.mO2SatNullAsAlarm) { + Log.i(TAG, "Oxygen Saturation Null - Alarming"); + mSdData.mO2SatFaultStanding = false; + mSdData.mO2SatAlarmStanding = true; + } else { + Log.i(TAG, "Oxygen Saturation Fault (O2Sat<0)"); + mSdData.mO2SatFaultStanding = true; + mSdData.mO2SatAlarmStanding = false; + } + } else if (mSdData.mO2Sat < mSdData.mO2SatThreshMin) { + Log.i(TAG, "Oxygen Saturation Abnormal - " + mSdData.mO2Sat + " %"); + mSdData.mO2SatFaultStanding = false; + mSdData.mO2SatAlarmStanding = true; + } else { + mSdData.mO2SatFaultStanding = false; + mSdData.mO2SatAlarmStanding = false; + } + } } + /**************************************************************** * Simple threshold analysis to chech for fall. * Called from clock_tick_handler() @@ -570,8 +635,9 @@ public abstract class SdDataSource { if (mSdData.rawData[i + j] < minAcc) minAcc = mSdData.rawData[i + j]; if (mSdData.rawData[i + j] > maxAcc) maxAcc = mSdData.rawData[i + j]; } + Log.d(TAG, "check_fall() - minAcc=" + minAcc +" (mFallThreshMin="+mFallThreshMin+ "), maxAcc=" + maxAcc+" (mFallThreshMax="+mFallThreshMax+")") ; if ((minAcc < mFallThreshMin) && (maxAcc > mFallThreshMax)) { - Log.d(TAG, "check_fall() - minAcc=" + minAcc + ", maxAcc=" + maxAcc); + Log.d(TAG, "check_fall() ****FALL DETECTED***** minAcc=" + minAcc + ", maxAcc=" + maxAcc); Log.d(TAG, "check_fall() - ****FALL DETECTED****"); mSdData.fallAlarmStanding = true; return; @@ -649,11 +715,11 @@ public abstract class SdDataSource { // get time since the last data was received from the watch. tdiff = (tnow.toMillis(false) - mDataStatusTime.toMillis(false)); - Log.v(TAG, "faultCheck() - tdiff=" + tdiff + ", mDataUpatePeriod=" + mDataUpdatePeriod + ", mAppRestartTimeout=" + mAppRestartTimeout - + ", combined = " + (mDataUpdatePeriod + mAppRestartTimeout) * 1000); + //Log.v(TAG, "faultCheck() - tdiff=" + tdiff + ", mDataUpatePeriod=" + mDataUpdatePeriod + ", mAppRestartTimeout=" + mAppRestartTimeout + // + ", combined = " + (mDataUpdatePeriod + mAppRestartTimeout) * 1000); if (!mWatchAppRunningCheck && (tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) { - Log.v(TAG, "faultCheck() - watch app not running so not doing anything"); + //Log.v(TAG, "faultCheck() - watch app not running so not doing anything"); mAlarmCount = 0; } } @@ -814,6 +880,19 @@ public abstract class SdDataSource { Log.v(TAG, "updatePrefs() HRThreshMax = " + mSdData.mHRThreshMax); mUtil.writeToSysLogFile( "updatePrefs() HRThreshMax = " + mSdData.mHRThreshMax); + mSdData.mO2SatAlarmActive = SP.getBoolean("O2SatAlarmActive", false); + Log.v(TAG, "updatePrefs() O2SatAlarmActive = " + mSdData.mO2SatAlarmActive); + mUtil.writeToSysLogFile( "updatePrefs() O2SatAlarmActive = " + mSdData.mO2SatAlarmActive); + + mSdData.mO2SatNullAsAlarm = SP.getBoolean("O2SatNullAsAlarm", false); + Log.v(TAG, "updatePrefs() O2SatNullAsAlarm = " + mSdData.mO2SatNullAsAlarm); + mUtil.writeToSysLogFile( "updatePrefs() O2SatNullAsAlarm = " + mSdData.mO2SatNullAsAlarm); + + prefStr = SP.getString("O2SatThreshMin", "SET_FROM_XML"); + mSdData.mO2SatThreshMin = (short) Integer.parseInt(prefStr); + Log.v(TAG, "updatePrefs() O2SatThreshMin = " + mSdData.mO2SatThreshMin); + mUtil.writeToSysLogFile( "updatePrefs() O2SatThreshMin = " + mSdData.mO2SatThreshMin); + } else { Log.v(TAG, "updatePrefs() - prefStr is null - WHY????"); mUtil.writeToSysLogFile("SDDataSource.updatePrefs() - prefStr is null - WHY??"); diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceAw.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceAw.java index 60e1dde..e028c24 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceAw.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceAw.java @@ -40,7 +40,7 @@ import java.nio.IntBuffer; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; -import com.google.android.gms.wearable.MessageEvent; +import com.google.android.gms.wearable .MessageEvent; import com.google.android.gms.wearable.WearableListenerService; diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceBLE.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceBLE.java index b96d672..a56f059 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceBLE.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceBLE.java @@ -63,6 +63,7 @@ public class SdDataSourceBLE extends SdDataSource { private int nRawData = 0; private double[] rawData = new double[MAX_RAW_DATA]; + private boolean waitForDescriptorWrite = false; private static final int STATE_DISCONNECTED = 0; private static final int STATE_CONNECTING = 1; @@ -82,13 +83,12 @@ public class SdDataSourceBLE extends SdDataSource { public static String SERV_DEV_INFO = "0000180a-0000-1000-8000-00805f9b34fb"; public static String SERV_HEART_RATE = "0000180d-0000-1000-8000-00805f9b34fb"; - public static String SERV_OSD = "a19585e9-0001-39d0-015f-b3e2b9a0c854"; + public static String SERV_OSD = "000085e9-0000-1000-8000-00805f9b34fb"; public static String CHAR_HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb"; public static String CHAR_MANUF_NAME = "00002a29-0000-1000-8000-00805f9b34fb"; public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"; - public static String CHAR_OSD_ACC_DATA = "a19585e9-0002-39d0-015f-b3e2b9a0c854"; - public static String CHAR_OSD_BATT_DATA = "a19585e9-0004-39d0-015f-b3e2b9a0c854"; - + public static String CHAR_OSD_ACC_DATA = "000085ea-0000-1000-8000-00805f9b34fb"; + public static String CHAR_OSD_BATT_DATA = "000085eb-0000-1000-8000-00805f9b34fb"; public final static UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(CHAR_HEART_RATE_MEASUREMENT); private BluetoothGatt mGatt; @@ -264,7 +264,8 @@ public class SdDataSourceBLE extends SdDataSource { } else if (charUuidStr.equals(CHAR_OSD_BATT_DATA)) { Log.v(TAG,"Saving battery characteristic for later"); - mBattChar = gattCharacteristic; + Log.v(TAG, "Subscribing to battery change Notifications"); + setCharacteristicNotification(gattCharacteristic,true); } } } @@ -287,6 +288,8 @@ public class SdDataSourceBLE extends SdDataSource { } public void onDataReceived(BluetoothGattCharacteristic characteristic) { + Log.v(TAG, "onDataReceived uuid" + characteristic.getUuid().toString()); + // FIXME - collect data until we have enough to do analysis, then use onDataReceived to process it. //Log.v(TAG,"onDataReceived: Characteristic="+characteristic.getUuid().toString()); if (characteristic.getUuid().toString().equals(CHAR_HEART_RATE_MEASUREMENT)) { @@ -294,12 +297,13 @@ public class SdDataSourceBLE extends SdDataSource { int format = -1; if ((flag & 0x01) != 0) { format = BluetoothGattCharacteristic.FORMAT_UINT16; - //Log.d(TAG, "Heart rate format UINT16."); + Log.d(TAG, "Heart rate format UINT16."); } else { format = BluetoothGattCharacteristic.FORMAT_UINT8; - //Log.d(TAG, "Heart rate format UINT8."); + Log.d(TAG, "Heart rate format UINT8."); } final int heartRate = characteristic.getIntValue(format, 1); + mSdData.mHR = (double) heartRate; Log.d(TAG, String.format("Received heart rate: %d", heartRate)); } else if (characteristic.getUuid().toString().equals(CHAR_OSD_ACC_DATA)) { @@ -322,21 +326,15 @@ public class SdDataSourceBLE extends SdDataSource { //mNSamp = accelVals.length(); mWatchAppRunningCheck = true; mDataStatusTime = new Time(Time.getCurrentTimezone()); - - if (mSdData.haveSettings == false) { - Log.v(TAG,"Requesting Battery Data"); - mGatt.readCharacteristic(mBattChar); - } - doAnalysis(); - nRawData = 0; } } } else if (characteristic.getUuid().toString().equals(CHAR_OSD_BATT_DATA)) { - mSdData.batteryPc = characteristic.getValue()[0]; - Log.v(TAG,"Received Battery Data"); + byte batteryPc = characteristic.getValue()[0]; + mSdData.batteryPc = batteryPc; + Log.v(TAG,"Received Battery Data" + String.format("%d", batteryPc)); mSdData.haveSettings = true; } else { @@ -361,8 +359,13 @@ public class SdDataSourceBLE extends SdDataSource { Log.v(TAG,"onCharacteristicChanged(): Characteristic "+characteristic.getUuid()+" changed"); onDataReceived(characteristic); } - }; + @Override + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + Log.v(TAG,"onDescriptorWrite(): Characteristic " + descriptor.getUuid() + " changed"); + waitForDescriptorWrite = false; + } + }; /** * Enables or disables notification on a give characteristic. @@ -370,12 +373,27 @@ public class SdDataSourceBLE extends SdDataSource { * @param characteristic Characteristic to act on. * @param enabled If true, enable notification. False otherwise. */ - public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, - boolean enabled) { + public void setCharacteristicNotification(final BluetoothGattCharacteristic characteristic, final boolean enabled) { + Log.w(TAG, "setCharacteristicNotification " + characteristic.getUuid()); + if (mBluetoothAdapter == null || mBluetoothGatt == null) { Log.w(TAG, "BluetoothAdapter not initialized"); return; } + + if (waitForDescriptorWrite) { + // Apparently if you try to write multiple descriptors too quickly then only + // one is processed, hence why this waiting logic is necessary + Log.w(TAG, "waitForDescriptor " + characteristic.getUuid()); + mHandler.postDelayed(new Runnable() { + public void run() { + Log.w(TAG, "delayed"); + setCharacteristicNotification(characteristic, enabled); + } + }, 500); + return; + } + if (enabled) { Log.v(TAG, "setCharacteristicNotification - Requesting notifications"); mBluetoothGatt.setCharacteristicNotification(characteristic, true); @@ -396,8 +414,9 @@ public class SdDataSourceBLE extends SdDataSource { UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); - } + + waitForDescriptorWrite = true; } /** @@ -414,12 +433,3 @@ public class SdDataSourceBLE extends SdDataSource { } - - - - - - - - - diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePhone.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePhone.java index 625a7c1..48abb03 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePhone.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourcePhone.java @@ -129,6 +129,9 @@ public class SdDataSourcePhone extends SdDataSource implements SensorEventListen float z = event.values[2]; //Log.v(TAG,"Accelerometer Data Received: x="+x+", y="+y+", z="+z); mSdData.rawData[mSdData.mNsamp] = sqrt(x*x + y*y + z*z); + mSdData.rawData3D[3*mSdData.mNsamp] = x; + mSdData.rawData3D[3*mSdData.mNsamp+1] = y; + mSdData.rawData3D[3*mSdData.mNsamp+2] = z; mSdData.mNsamp++; if (mSdData.mNsamp==NSAMP) { // Calculate the sample frequency for this sample, but do not change mSampleFreq, which is used for @@ -142,6 +145,9 @@ public class SdDataSourcePhone extends SdDataSource implements SensorEventListen // FIXME - we should really do this properly rather than assume we are really receiving data at 50Hz. for (int i=0; i= 26) { NotificationChannel channel = new NotificationChannel(mNotChId, mNotChName, @@ -289,15 +301,16 @@ public class SdServer extends Service implements SdDataReceiver { Log.v(TAG, "showing Notification and calling startForeground (Android 8 and higher)"); mUtil.writeToSysLogFile("SdServer.onStartCommand() - showing Notification and calling startForeground (Android 8 and higher)"); showNotification(0); - startForeground(NOTIFICATION_ID,mNotification); + startForeground(NOTIFICATION_ID, mNotification); } else { Log.v(TAG, "showing Notification"); mUtil.writeToSysLogFile("SdServer.onStartCommand() - showing Notification"); showNotification(0); } // Record last time we sent an SMS so we can limit rate of SMS - // sending to one per minute. + // sending to one per minute. We set it to one minute ago (60000 milliseconds) mSMSTime = new Time(Time.getCurrentTimezone()); + mSMSTime.set(mSMSTime.toMillis(false) - 60000); // Start timer to log data regularly.. @@ -318,6 +331,10 @@ public class SdServer extends Service implements SdDataReceiver { mUtil.writeToSysLogFile("SdServer.onStartCommand() - dataLog timer already running???"); } + if (mLogDataRemote) { + startEventsTimer(); + } + // Start the web server mUtil.writeToSysLogFile("SdServer.onStartCommand() - starting web server"); @@ -333,6 +350,8 @@ public class SdServer extends Service implements SdDataReceiver { mUtil.writeToSysLogFile("SdServer.onStartCommand() - mWakeLock is not null - this shouldn't happen???"); } + checkEvents(); + return START_STICKY; } @@ -345,7 +364,7 @@ public class SdServer extends Service implements SdDataReceiver { if (mWakeLock != null) { try { mWakeLock.release(); - Log.v(TAG, "Released Wake Lock to allow device to sleep."); + Log.d(TAG, "Released Wake Lock to allow device to sleep."); } catch (Exception e) { Log.e(TAG, "Error Releasing Wakelock - " + e.toString()); mUtil.writeToSysLogFile("SdServer.onDestroy() - Error releasing wakelock."); @@ -357,7 +376,7 @@ public class SdServer extends Service implements SdDataReceiver { } if (mSdDataSource != null) { - Log.i(TAG, "stopping mSdDataSource"); + Log.d(TAG, "stopping mSdDataSource"); mUtil.writeToSysLogFile("SdServer.onDestroy() - stopping mSdDataSource"); mSdDataSource.stop(); } else { @@ -367,7 +386,7 @@ public class SdServer extends Service implements SdDataReceiver { // Stop the Cancel Audible timer if (mCancelAudibleTimer != null) { - Log.v(TAG, "onDestroy(): cancelling Cancel_Audible timer"); + Log.d(TAG, "onDestroy(): cancelling Cancel_Audible timer"); mCancelAudibleTimer.cancel(); //mCancelAudibleTimer.purge(); mCancelAudibleTimer = null; @@ -376,17 +395,25 @@ public class SdServer extends Service implements SdDataReceiver { // Stop the Fault timer if (mFaultTimer != null) { - Log.v(TAG, "onDestroy(): cancelling fault timer"); + Log.d(TAG, "onDestroy(): cancelling fault timer"); mFaultTimer.cancel(); mFaultTimer = null; } + // Stop the Event timer + if (mEventsTimer != null) { + Log.d(TAG, "onDestroy(): Cancelling events timer"); + stopEventsTimer(); + } + // Stop the Cancel Alarm Latch timer + Log.d(TAG, "onDestroy(): stopping alarm latch timer"); stopLatchTimer(); // Stop the location finder. if (mLocationFinder != null) { + Log.d(TAG, "onDestroy(): stopping Location Finder"); mLocationFinder.destroy(); mLocationFinder = null; } @@ -394,7 +421,7 @@ public class SdServer extends Service implements SdDataReceiver { try { // Stop web server - Log.v(TAG, "onDestroy(): stopping web server"); + Log.d(TAG, "onDestroy(): stopping web server"); mUtil.writeToSysLogFile("SdServer.onDestroy() - stopping Web Server"); stopWebServer(); @@ -404,20 +431,29 @@ public class SdServer extends Service implements SdDataReceiver { this.stopForeground(true); // Cancel the notification. - Log.v(TAG, "onDestroy(): cancelling notification"); + Log.d(TAG, "onDestroy(): cancelling notification"); mUtil.writeToSysLogFile("SdServer.onDestroy - cancelling notification"); mNM.cancel(NOTIFICATION_ID); + mNM.cancel(EVENT_NOTIFICATION_ID); + mNM.cancel(DATASHARE_NOTIFICATION_ID); + // stop this service. - Log.v(TAG, "onDestroy(): calling stopSelf()"); + Log.d(TAG, "onDestroy(): calling stopSelf()"); mUtil.writeToSysLogFile("SdServer.onDestroy() - stopping self"); stopSelf(); } catch (Exception e) { - Log.v(TAG, "Error in onDestroy() - " + e.toString()); + Log.e(TAG, "Error in onDestroy() - " + e.toString()); mUtil.writeToSysLogFile("SdServer.onDestroy() -error " + e.toString()); } + if (mLm != null) { + Log.d(TAG, "Closing Down Log Manager"); + mLm.stop(); + mLm.close(); + } + super.onDestroy(); } @@ -427,7 +463,7 @@ public class SdServer extends Service implements SdDataReceiver { * Show a notification while this service is running. */ private void showNotification(int alarmLevel) { - Log.v(TAG, "showNotification() - alarmLevel="+alarmLevel); + Log.v(TAG, "showNotification() - alarmLevel=" + alarmLevel); int iconId; String titleStr; Uri soundUri = null; @@ -462,7 +498,7 @@ public class SdServer extends Service implements SdDataReceiver { } if (mCancelAudible) { - Log.v(TAG,"ShowNotification - Not beeping because mCancelAudible set"); + Log.v(TAG, "ShowNotification - Not beeping because mCancelAudible set"); soundUri = null; } @@ -491,7 +527,7 @@ public class SdServer extends Service implements SdDataReceiver { .build(); if (mMp3Alarm) { if (soundUri != null) { - Log.v(TAG, "showNotification - setting Notification Sound to "+soundUri.toString()); + Log.v(TAG, "showNotification - setting Notification Sound to " + soundUri.toString()); mNotificationBuilder.setSound(soundUri); } } @@ -521,6 +557,13 @@ public class SdServer extends Service implements SdDataReceiver { } } + public void raiseManualAlarm() { + Log.d(TAG, "raiseManualAlarm()"); + SdData sdData = mSdData; + sdData.alarmState = 5; + onSdDataReceived(sdData); + } + /** * Process the data received from the SdData source. On exit, the mSdData structure is populated with * the appropriate data. @@ -529,6 +572,8 @@ public class SdServer extends Service implements SdDataReceiver { */ public void onSdDataReceived(SdData sdData) { Log.v(TAG, "onSdDataReceived() - " + sdData.toString()); + Log.v(TAG, "onSdDataReceived(), sdData.fallAlarmStanding=" + sdData.fallAlarmStanding); + if (sdData.alarmState == 0) { if ((!mLatchAlarms) || (mLatchAlarms && @@ -557,7 +602,7 @@ public class SdServer extends Service implements SdDataReceiver { } if (mLogAlarms) { Log.v(TAG, "WARNING - Logging to SD Card"); - writeAlarmToSD(); + //writeAlarmToSD(); } else { Log.v(TAG, "WARNING"); } @@ -570,7 +615,7 @@ public class SdServer extends Service implements SdDataReceiver { sdData.alarmStanding = true; if (mLogAlarms) { Log.v(TAG, "***ALARM*** - Logging to SD Card"); - writeAlarmToSD(); + //writeAlarmToSD(); } else { Log.v(TAG, "***ALARM***"); } @@ -602,12 +647,13 @@ public class SdServer extends Service implements SdDataReceiver { startLatchTimer(); } // Handle fall alarm + Log.v(TAG, "sdData.fallAlarmStanding=" + sdData.fallAlarmStanding); if ((sdData.alarmState == 3) || (sdData.fallAlarmStanding)) { sdData.alarmPhrase = "FALL"; sdData.fallAlarmStanding = true; if (mLogAlarms) { Log.v(TAG, "***FALL*** - Logging to SD Card"); - writeAlarmToSD(); + //writeAlarmToSD(); showNotification(2); } else { Log.v(TAG, "***FALL***"); @@ -641,7 +687,7 @@ public class SdServer extends Service implements SdDataReceiver { sdData.alarmPhrase = "HR ABNORMAL"; if (mLogAlarms) { Log.v(TAG, "***HEART RATE*** - Logging to SD Card"); - writeAlarmToSD(); + //writeAlarmToSD(); } else { Log.v(TAG, "***HEART RATE***"); } @@ -668,19 +714,56 @@ public class SdServer extends Service implements SdDataReceiver { mUtil.showToast(getString(R.string.SMSAlarmDisabledNotSendingMsg)); Log.v(TAG, "mSMSAlarm is false - not sending"); } - } + // Handle Oxygen Saturation alarm + if ((sdData.mO2SatAlarmActive) && (sdData.mO2SatAlarmStanding)) { + sdData.alarmPhrase = "Oxygen Saturation ABNORMAL"; + if (mLogAlarms) { + Log.v(TAG, "***OXYGEN SATURATION*** - Logging to SD Card"); + //writeAlarmToSD(); + } else { + Log.v(TAG, "***OXYGEN SATURATION***"); + } + // Make alarm beep tone + alarmBeep(); + showNotification(2); + // Display MainActvity + showMainActivity(); + // Send SMS Alarm. + if (mSMSAlarm) { + Time tnow = new Time(Time.getCurrentTimezone()); + tnow.setToNow(); + // limit SMS alarms to one per minute + if ((tnow.toMillis(false) + - mSMSTime.toMillis(false)) + > 60000) { + sendSMSAlarm(); + mSMSTime = tnow; + } else { + mUtil.showToast("SMS Alarm already sent - not re-sending"); + Log.v(TAG, "SMS Alarm already sent - not re-sending"); + } + } else { + mUtil.showToast(getString(R.string.SMSAlarmDisabledNotSendingMsg)); + Log.v(TAG, "mSMSAlarm is false - not sending"); + } + } + + // Fault if ((sdData.alarmState) == 4 || (sdData.alarmState == 7) || (sdData.mHRFaultStanding)) { sdData.alarmPhrase = "FAULT"; - writeAlarmToSD(); + //writeAlarmToSD(); faultWarningBeep(); showNotification(-1); } else { stopFaultTimer(); } mSdData = sdData; + mSdData.dataSourceName = mSdDataSourceName; + mSdData.phoneAppVersion = mUtil.getAppVersionName(); + if (webServer != null) webServer.setSdData(mSdData); Log.v(TAG, "onSdDataReceived() - setting mSdData to " + mSdData.toString()); @@ -713,7 +796,7 @@ public class SdServer extends Service implements SdDataReceiver { mUtil.writeToSysLogFile("onSdDataFault() - starting Fault Timer"); } - showNotification(-1); + showNotification(-1); } /* from http://stackoverflow.com/questions/12154940/how-to-make-a-beep-in-android */ @@ -737,8 +820,8 @@ public class SdServer extends Service implements SdDataReceiver { */ public void faultWarningBeep() { if (mCancelAudible) { - Log.v(TAG, "faultWarningBeep() - CancelAudible Active - silent beep..."); - } else { + Log.v(TAG, "faultWarningBeep() - CancelAudible Active - silent beep..."); + } else { if (mAudibleFaultWarning) { if (mMp3Alarm) { Log.v(TAG, "Not making MP3 fault beep - handled by notification"); @@ -807,24 +890,14 @@ public class SdServer extends Service implements SdDataReceiver { AlertDialog ad; if (mSMSAlarm) { if (!mCancelAudible) { - if (!mUtil.areSMSPermissionsOK()) { - mUtil.showToast(getString(R.string.SMSPermissionsDeniedMsg)); - Log.e(TAG, "ERROR - Permission for SMS or Location Denied - Not Sending SMS"); - } else { - //mSMSAlertDialog = new AlertDialog.Builder(this); - //mSMSAlertDialog.setMessage("SMS Will be Sent in 10 Seconds, unless you press the Cancel Button") - // .setPositiveButton("Send", smsCancelClickListener) - // .setNegativeButton("Cancel", smsCancelClickListener); - //ad = mSMSAlertDialog.show(); - startSmsTimer(); - } + startSmsTimer(); } else { Log.i(TAG, "sendSMSAlarm() - Cancel Audible Active - not sending SMS"); mUtil.showToast(getString(R.string.cancel_audible_not_sending_sms)); } } else { Log.i(TAG, "sendSMSAlarm() - SMS Alarms Disabled - not doing anything!"); - mUtil.showToast(getString(R.string.sms_alarm_disabled)); + mUtil.showToast(getString(R.string.sms_alarms_disabled)); } if (mPhoneAlarm) { if (!mCancelAudible) { @@ -848,14 +921,14 @@ public class SdServer extends Service implements SdDataReceiver { DialogInterface.OnClickListener smsCancelClickListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - switch (which){ + switch (which) { case DialogInterface.BUTTON_POSITIVE: - Log.v(TAG,"smsCancelClickListener - Positive button"); + Log.v(TAG, "smsCancelClickListener - Positive button"); //Yes button clicked break; case DialogInterface.BUTTON_NEGATIVE: - Log.v(TAG,"smsCancelClickListener - Negative button"); + Log.v(TAG, "smsCancelClickListener - Negative button"); //No button clicked break; } @@ -863,7 +936,6 @@ public class SdServer extends Service implements SdDataReceiver { }; - /* * Start the timer that will send and SMS alert after a given period. */ @@ -896,10 +968,8 @@ public class SdServer extends Service implements SdDataReceiver { } - - /* - * Start the timer that will automatically re-set a latched alarm after a given period. + * Start the timer that will automatically re-set a latched alarm after a given period. */ private void startLatchTimer() { if (mLatchAlarms) { @@ -1011,7 +1081,7 @@ public class SdServer extends Service implements SdDataReceiver { } else { if (webServer.isAlive()) { Log.w(TAG, "stopWebServer() - server still alive???"); - mUtil.writeToSysLogFile( "stopWebServer() - server still alive???"); + mUtil.writeToSysLogFile("stopWebServer() - server still alive???"); } else { mUtil.writeToSysLogFile("stopWebServer() - server died ok"); Log.v(TAG, "stopWebServer() - server died ok"); @@ -1069,9 +1139,13 @@ public class SdServer extends Service implements SdDataReceiver { */ public void logData() { if (mLogData) { - Log.v(TAG, "logData() - writing data to SD Card"); - writeToSD(); - //mLm.writeToLocalDb(mSdData); + if (mLm != null) { + Log.v(TAG, "logData() - writing data to Database"); + //writeToSD(); + mLm.writeDatapointToLocalDb(mSdData); + } else { + Log.e(TAG, "logData() - mLm is null - this should not happen"); + } } } @@ -1101,7 +1175,7 @@ public class SdServer extends Service implements SdDataReceiver { mUtil.writeToSysLogFile("updatePrefs() - mLatchAlarmTimerPeriod = " + mLatchAlarmPeriod); } catch (Exception ex) { Log.v(TAG, "updatePrefs() - Problem with LatchAlarmTimerPeriod preference!"); - mUtil.writeToSysLogFile( "updatePrefs() - Problem with LatchAlarmTimerPeriod preference!"); + mUtil.writeToSysLogFile("updatePrefs() - Problem with LatchAlarmTimerPeriod preference!"); mUtil.showToast(getString(R.string.problem_parsing_preferences)); } mAudibleFaultWarning = SP.getBoolean("AudibleFaultWarning", true); @@ -1127,42 +1201,65 @@ public class SdServer extends Service implements SdDataReceiver { mUtil.writeToSysLogFile("updatePrefs() - mAuidbleWarning = " + mAudibleWarning); mMp3Alarm = SP.getBoolean("UseMp3Alarm", false); Log.v(TAG, "updatePrefs() - mMp3Alarm = " + mMp3Alarm); - mUtil.writeToSysLogFile( "updatePrefs() - mMp3Alarm = " + mMp3Alarm); + mUtil.writeToSysLogFile("updatePrefs() - mMp3Alarm = " + mMp3Alarm); mSMSAlarm = SP.getBoolean("SMSAlarm", false); Log.v(TAG, "updatePrefs() - mSMSAlarm = " + mSMSAlarm); - mUtil.writeToSysLogFile( "updatePrefs() - mSMSAlarm = " + mSMSAlarm); + mUtil.writeToSysLogFile("updatePrefs() - mSMSAlarm = " + mSMSAlarm); mPhoneAlarm = SP.getBoolean("PhoneCallAlarm", false); Log.v(TAG, "updatePrefs() - mSMSAlarm = " + mSMSAlarm); - mUtil.writeToSysLogFile( "updatePrefs() - mSMSAlarm = " + mSMSAlarm); + mUtil.writeToSysLogFile("updatePrefs() - mSMSAlarm = " + mSMSAlarm); String SMSNumberStr = SP.getString("SMSNumbers", ""); mSMSNumbers = SMSNumberStr.split(","); mSMSMsgStr = SP.getString("SMSMsg", "Seizure Detected!!!"); Log.v(TAG, "updatePrefs() - SMSNumberStr = " + SMSNumberStr); - mUtil.writeToSysLogFile( "updatePrefs() - SMSNumberStr = " + SMSNumberStr); + mUtil.writeToSysLogFile("updatePrefs() - SMSNumberStr = " + SMSNumberStr); Log.v(TAG, "updatePrefs() - mSMSNumbers = " + mSMSNumbers); - mUtil.writeToSysLogFile( "updatePrefs() - mSMSNumbers = " + mSMSNumbers); + mUtil.writeToSysLogFile("updatePrefs() - mSMSNumbers = " + mSMSNumbers); mLogAlarms = SP.getBoolean("LogAlarms", true); Log.v(TAG, "updatePrefs() - mLogAlarms = " + mLogAlarms); mUtil.writeToSysLogFile("updatePrefs() - mLogAlarms = " + mLogAlarms); - mLogData = SP.getBoolean("LogData", true); - Log.v(TAG, "updatePrefs() - mLogData = " + mLogData); - mUtil.writeToSysLogFile( "updatePrefs() - mLogData = " + mLogData); - mLogDataRemote = SP.getBoolean("LogDataRemote", false); + //mLogData = SP.getBoolean("LogData", true); + mLogData = true; + Log.v(TAG, "SdServer.updatePrefs() - mLogData = " + mLogData); + mUtil.writeToSysLogFile("updatePrefs() - mLogData = " + mLogData); + //mLogDataRemote = SP.getBoolean("LogDataRemote", false); + mLogDataRemote = true; Log.v(TAG, "updatePrefs() - mLogDataRemote = " + mLogDataRemote); - mUtil.writeToSysLogFile( "updatePrefs() - mLogDataRemote = " + mLogDataRemote); + mUtil.writeToSysLogFile("updatePrefs() - mLogDataRemote = " + mLogDataRemote); mLogDataRemoteMobile = SP.getBoolean("LogDataRemoteMobile", false); Log.v(TAG, "updatePrefs() - mLogDataRemoteMobile = " + mLogDataRemoteMobile); mUtil.writeToSysLogFile("updatePrefs() - mLogDataRemoteMobile = " + mLogDataRemoteMobile); - mOSDUname = SP.getString("OSDUname", ""); - Log.v(TAG, "updatePrefs() - mOSDUname = " + mOSDUname); - mOSDPasswd = SP.getString("OSDPasswd", ""); - Log.v(TAG, "updatePrefs() - mOSDPasswd = " + mOSDPasswd); - mOSDWearerId = Integer.parseInt(SP.getString("OSDWearerId", "0")); - Log.v(TAG, "updatePrefs() - mOSDWearerId = " + mOSDWearerId); + mAuthToken = SP.getString("webApiAuthToken", null); + Log.v(TAG, "updatePrefs() - mAuthToken = " + mAuthToken); + mUtil.writeToSysLogFile("updatePrefs() - mAuthToken = " + mAuthToken); + + String prefVal; + prefVal = SP.getString("EventDurationSec", "300"); + mEventDuration = Integer.parseInt(prefVal); + Log.v(TAG, "mEventDuration=" + mEventDuration); + + mAutoPruneDb = SP.getBoolean("AutoPruneDb", true); + Log.v(TAG, "mAutoPruneDb=" + mAutoPruneDb); + + prefVal = SP.getString("DataRetentionPeriod", "28"); + mDataRetentionPeriod = Integer.parseInt(prefVal); + Log.v(TAG, "mDataRetentionPeriod=" + mDataRetentionPeriod); + + //prefVal = SP.getString("RemoteLogPeriod", "60"); + //mRemoteLogPeriod = Integer.parseInt(prefVal); + //mRemoteLogPeriod = 60; + Log.v(TAG, "mRemoteLogPeriod=" + mRemoteLogPeriod); + + //mOSDUname = SP.getString("OSDUname", ""); + //Log.v(TAG, "updatePrefs() - mOSDUname = " + mOSDUname); + //mOSDPasswd = SP.getString("OSDPasswd", ""); + //Log.v(TAG, "updatePrefs() - mOSDPasswd = " + mOSDPasswd); + //mOSDWearerId = Integer.parseInt(SP.getString("OSDWearerId", "0")); + //Log.v(TAG, "updatePrefs() - mOSDWearerId = " + mOSDWearerId); mOSDUrl = SP.getString("OSDUrl", "http://openseizuredetector.org.uk/webApi"); Log.v(TAG, "updatePrefs() - mOSDUrl = " + mOSDUrl); - mUtil.writeToSysLogFile( "updatePrefs() - mOSDUrl = " + mOSDUrl); + mUtil.writeToSysLogFile("updatePrefs() - mOSDUrl = " + mOSDUrl); } catch (Exception ex) { Log.v(TAG, "updatePrefs() - Problem parsing preferences!"); mUtil.writeToSysLogFile("SdServer.updatePrefs() - Error " + ex.toString()); @@ -1171,70 +1268,14 @@ public class SdServer extends Service implements SdDataReceiver { } - /** - * Write data to SD card alarm log - */ - public void writeAlarmToSD() { - writeToSD(true); - } - - /** - * Write to data log file on SD Card - */ - public void writeToSD() { - writeToSD(false); - } - - /** - * Write data to SD card - writes to data log file unless alarm=true, - * in which case writes to alarm log file. - */ - public void writeToSD(boolean alarm) { - //Log.v(TAG, "writeToSD(" + alarm + ")"); - Time tnow = new Time(Time.getCurrentTimezone()); - tnow.setToNow(); - String dateStr = tnow.format("%Y-%m-%d"); - - // Select filename depending on 'alarm' parameter. - String fname; - if (alarm) - fname = "AlarmLog"; - else - fname = "DataLog"; - - fname = fname + "_" + dateStr + ".txt"; - // Open output directory on SD Card. - if (mUtil.isExternalStorageWritable()) { - try { - FileWriter of = new FileWriter(getExternalFilesDir(null).toString() - + "/" + fname, true); - if (mSdData != null) { - if (alarm) { - //Log.v(TAG, "writeToSD() - logging mSdData.toString()"); - of.append(mSdData.toString() + "\n"); - } else { - //Log.v(TAG, "writeToSD() - logging mSdData.toCSVString()"); - of.append(mSdData.toCSVString(true) + "\n"); - } - } - of.close(); - } catch (Exception ex) { - Log.e(TAG, "writeAlarmToSD - error " + ex.toString()); - } - } else { - Log.e(TAG, "ERROR - Can not Write to External Folder"); - } - } - - public void sendPhoneAlarm() { /** * Use the separate OpenSeizureDetector Dialler app to generate a phone call alarm to the numbers selected for SMS Alarms. */ - Log.v(TAG,"sendPhoneAlarm() - sending broadcast intent"); + Log.v(TAG, "sendPhoneAlarm() - sending broadcast intent"); Intent intent = new Intent(); intent.setAction("uk.org.openseizuredetector.dialler.ALARM"); - intent.putExtra("NUMBERS",mSMSNumbers); + intent.putExtra("NUMBERS", mSMSNumbers); sendBroadcast(intent); } @@ -1245,6 +1286,7 @@ public class SdServer extends Service implements SdDataReceiver { */ public class SmsTimer extends CountDownTimer implements SdLocationReceiver { public long mTimeLeft = -1; + public SmsTimer(long startTime, long interval) { super(startTime, interval); } @@ -1254,21 +1296,26 @@ public class SdServer extends Service implements SdDataReceiver { public void onFinish() { Log.v(TAG, "SmsTimer.onFinish()"); mTimeLeft = 0; - mLocationFinder.getLocation(this); - Location loc = mLocationFinder.getLastLocation(); - if (loc != null) { - mUtil.showToast(getString(R.string.send_sms_last_location) - + loc.getLongitude() + "," - + loc.getLatitude()); + if (mLocationFinder != null) { + mLocationFinder.getLocation(this); + Location loc = mLocationFinder.getLastLocation(); + if (loc != null) { + mUtil.showToast(getString(R.string.send_sms_last_location) + + loc.getLongitude() + "," + + loc.getLatitude()); + } else { + Log.i(TAG, "SmsTimer.onFinish() - Last Location is Null so sending first SMS without location."); + } } else { - Log.i(TAG, "SmsTimer.onFinish() - Last Location is Null so sending first SMS without location."); + Log.e(TAG,"SmsTimer.onFinish - mLocationFinder is null - this should not happen!"); + mUtil.showToast("SmsTimer.onFinish - mLocationFinder is null - this should not happen! - Please report this issue!"); } Log.i(TAG, "SmsTimer.onFinish() - Sending to " + mSMSNumbers.length + " Numbers"); mUtil.writeToSysLogFile("SdServer.SmsTimer.onFinish()"); Time tnow = new Time(Time.getCurrentTimezone()); tnow.setToNow(); String dateStr = tnow.format("%H:%M:%S %d/%m/%Y"); - String shortUuidStr = mUuidStr.substring(mUuidStr.length()-6); + String shortUuidStr = mUuidStr.substring(mUuidStr.length() - 6); // SmsManager sm = SmsManager.getDefault(); for (int i = 0; i < mSMSNumbers.length; i++) { @@ -1289,7 +1336,6 @@ public class SdServer extends Service implements SdDataReceiver { /** * onSdLocationReceived - called with the best estimate location after mLocationReceiver times out. * - * @param ll - location (may be null if no location found) */ private void sendSMS(String phoneNo, String msgStr) { Log.i(TAG, "sendSMS() - Sending to " + phoneNo); @@ -1341,7 +1387,7 @@ public class SdServer extends Service implements SdDataReceiver { + ";u=" + df.format(ll.getAccuracy()) + "'>here"; String googleUrl = "https://www.google.com/maps/place?q=" + ll.getLatitude() + "%2C" + ll.getLongitude(); - String shortUuidStr = mUuidStr.substring(mUuidStr.length()-6); + String shortUuidStr = mUuidStr.substring(mUuidStr.length() - 6); String messageStr = mSMSMsgStr + " - " + dateStr + " - " + googleUrl + " " + shortUuidStr; @@ -1360,14 +1406,13 @@ public class SdServer extends Service implements SdDataReceiver { } - } /* - * Latch alarm in alarm state for a given period (mLatchAlarmPeriod seconds) after the alarm is raised. - * This is to ensure multiple Alarm annunciations even if only a single Alarm signal is received. - */ + * Latch alarm in alarm state for a given period (mLatchAlarmPeriod seconds) after the alarm is raised. + * This is to ensure multiple Alarm annunciations even if only a single Alarm signal is received. + */ private class LatchAlarmTimer extends CountDownTimer { public LatchAlarmTimer(long startTime, long interval) { super(startTime, interval); @@ -1390,9 +1435,9 @@ public class SdServer extends Service implements SdDataReceiver { } /* - * Temporary cancel audible alarms, for the period specified by the - * CancelAudiblePeriod setting. - */ + * Temporary cancel audible alarms, for the period specified by the + * CancelAudiblePeriod setting. + */ private class CancelAudibleTimer extends CountDownTimer { public CancelAudibleTimer(long startTime, long interval) { super(startTime, interval); @@ -1438,13 +1483,13 @@ public class SdServer extends Service implements SdDataReceiver { public void stopFaultTimer() { if (mFaultTimer != null) { - Log.v(TAG, "stopFaultTimer(): fault timer already running - cancelling it."); + //Log.v(TAG, "stopFaultTimer(): fault timer already running - cancelling it."); mUtil.writeToSysLogFile("stopFaultTimer() - stopping fault timer"); mFaultTimer.cancel(); mFaultTimer = null; mFaultTimerCompleted = false; } else { - Log.v(TAG, "stopFaultTimer(): fault timer not running - not doing anything."); + //Log.v(TAG, "stopFaultTimer(): fault timer not running - not doing anything."); //mUtil.writeToSysLogFile("stopFaultTimer() - fault timer not running"); } } @@ -1476,4 +1521,211 @@ public class SdServer extends Service implements SdDataReceiver { } + + /** + * Start the events timer. + */ + public void startEventsTimer() { + if (mEventsTimer != null) { + Log.v(TAG, "startEventsTimer(): timer already running - not doing anything."); + mUtil.writeToSysLogFile("startEventsTimer() - timer already running"); + } else { + Log.v(TAG, "startEventsTimer(): starting timer."); + mUtil.writeToSysLogFile("startEventsTimer() - starting timer"); + runOnUiThread(new Runnable() { + public void run() { + mEventsTimer = + // Run every 10 sec (convert to ms.) + new CheckEventsTimer(mEventsTimerPeriod * 1000, 1000); + mEventsTimer.mIsRunning = true; + mEventsTimer.start(); + } + }); + } + } + + public void stopEventsTimer() { + if (mEventsTimer != null) { + Log.v(TAG, "stopEventsTimer(): timer already running - cancelling it."); + mUtil.writeToSysLogFile("stopEventsTimer() - stopping timer, setting mIsRunning to false"); + mEventsTimer.mIsRunning = false; + mEventsTimer.cancel(); + //mEventsTimer = null; + } else { + Log.v(TAG, "stopEventsTimer(): timer not running - not doing anything."); + } + } + + + private void checkEvents() { + // Retrieve events from remote database + if (mLm.mWac.getEvents((JSONObject remoteEventsObj) -> { + Log.v(TAG, "CheckEvents.getEvents.Callback()"); + Boolean haveUnvalidatedEvent = false; + if (remoteEventsObj == null) { + Log.e(TAG, "CheckEvents.Callback: Error Retrieving events"); + } else { + try { + JSONArray eventsArray = remoteEventsObj.getJSONArray("events"); + // A bit of a hack to display in reverse chronological order + for (int i = eventsArray.length() - 1; i >= 0; i--) { + JSONObject eventObj = eventsArray.getJSONObject(i); + String typeStr = eventObj.getString("type"); + //Log.v(TAG,"CheckEventsTimer: id="+id+", typeStr="+typeStr); + if (typeStr.equals("null") || typeStr.equals("")) { + haveUnvalidatedEvent = true; + //Log.v(TAG,"CheckEventsTimer:setting firstUnvalidatedEvent to "+firstUnvalidatedEvent); + } + } + Log.v(TAG, "CheckEventsTimer.onFinish.callback - haveUnvalidatedEvent = " + + haveUnvalidatedEvent); + if (haveUnvalidatedEvent) { + showEventNotification(); + mNM.cancel(DATASHARE_NOTIFICATION_ID); + } else { + mNM.cancel(EVENT_NOTIFICATION_ID); + mNM.cancel(DATASHARE_NOTIFICATION_ID); + } + } catch (JSONException e) { + Log.e(TAG, "CheckEventsTimer.onFinish(): Error Parsing remoteEventsObj: " + e.getMessage()); + //mUtil.showToast("Error Parsing remoteEventsObj - this should not happen!!!"); + } + } + })) { + Log.v(TAG, "CheckEventsTimer() - requested events"); + } else { + Log.v(TAG, "CheckEventsTimer() - Not Logged In"); + mNM.cancel(EVENT_NOTIFICATION_ID); + showDatashareNotification(); + } + + } + + /** + * Periodically check if we have unvalidated events in the remote database. + * Show a notification if we do. + */ + private class CheckEventsTimer extends CountDownTimer { + public boolean mIsRunning = true; + + public CheckEventsTimer(long startTime, long interval) { + super(startTime, interval); + } + + @Override + public void onFinish() { + Log.v(TAG, "CheckEventsTimer.onFinish()"); + checkEvents(); + if (mIsRunning) { + // Restart this timer. + Log.v(TAG, "CheckEventsTimer.onFinish() - mIsRunning is true, so re-starting timer"); + start(); + } + } + + @Override + public void onTick(long msRemaining) { + } + } + + /** + * Show a notification to tell the user that we have unvalidated events. + */ + private void showEventNotification() { + Log.v(TAG, "showEventNotification()"); + int iconId; + String titleStr; + Uri soundUri = null; + + // Initialise Notification channel for API level 26 and over + // from https://stackoverflow.com/questions/44443690/notificationcompat-with-api-26 + NotificationManager nM = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getApplicationContext(), mEventNotChId); + if (Build.VERSION.SDK_INT >= 26) { + NotificationChannel channel = new NotificationChannel(mEventNotChId, + mEventNotChName, + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(mEventNotChDesc); + nM.createNotificationChannel(channel); + } + + iconId = R.drawable.datasharing_query_24x24; + titleStr = getString(R.string.unvalidatedEventsTitle); + + Intent i = new Intent(getApplicationContext(), LogManagerControlActivity.class); + i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + i.setAction("None"); + PendingIntent contentIntent = + PendingIntent.getActivity(getApplicationContext(), + 0, i, PendingIntent.FLAG_UPDATE_CURRENT); + String contentStr = getString(R.string.please_confirm_seizure_events); + + Notification notification = notificationBuilder.setContentIntent(contentIntent) + .setSmallIcon(iconId) + .setColor(0x00ffffff) + .setAutoCancel(false) + .setContentTitle(titleStr) + .setContentText(contentStr) + .setOnlyAlertOnce(true) + .build(); + nM.notify(EVENT_NOTIFICATION_ID, notification); + } + + + /** + * Show a notification asking the user to set-up data sharing. + */ + private void showDatashareNotification() { + Log.v(TAG, "showDatashareNotification()"); + int iconId; + String titleStr; + Uri soundUri = null; + + // Initialise Notification channel for API level 26 and over + // from https://stackoverflow.com/questions/44443690/notificationcompat-with-api-26 + NotificationManager nM = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getApplicationContext(), mEventNotChId); + if (Build.VERSION.SDK_INT >= 26) { + NotificationChannel channel = new NotificationChannel(mEventNotChId, + mEventNotChName, + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(mEventNotChDesc); + nM.createNotificationChannel(channel); + } + + iconId = R.drawable.datasharing_fault_24x24; + titleStr = getString(R.string.datasharing_notification_title); + + Intent i = new Intent(getApplicationContext(), MainActivity.class); + i.putExtra("action", "showDataSharingDialog"); + i.setAction("showDataSharingDialog"); + i.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent contentIntent = + PendingIntent.getActivity(getApplicationContext(), + 0, i, PendingIntent.FLAG_UPDATE_CURRENT); + Intent loginIntent = new Intent(getApplicationContext(), AuthenticateActivity.class); + loginIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + PendingIntent loginPendingIntent = + PendingIntent.getActivity(getApplicationContext(), + 0, loginIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + String contentStr = getString(R.string.datasharing_notification_text); + Notification notification = notificationBuilder + .setContentIntent(contentIntent) + .setSmallIcon(iconId) + .setColor(0x00ffffff) + .setAutoCancel(false) + .setContentTitle(titleStr) + .setContentText(contentStr) + .setOnlyAlertOnce(true) + .addAction(R.drawable.common_google_signin_btn_icon_dark, getString(R.string.login), loginPendingIntent) + .setPriority(0) + .build(); + nM.notify(DATASHARE_NOTIFICATION_ID, notification); + } + + } + + + diff --git a/app/src/main/java/uk/org/openseizuredetector/SdWebServer.java b/app/src/main/java/uk/org/openseizuredetector/SdWebServer.java index 2b0e182..eadebc1 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdWebServer.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdWebServer.java @@ -104,13 +104,18 @@ public class SdWebServer extends NanoHTTPD { Log.v(TAG, " files=" + files.toString()); String postData = files.get("postData"); Log.v(TAG, " postData=" + postData); - // Send the data to the SdDataSource so the app can pick it up. - if (parameters.get("dataObj") != null) { - Log.v(TAG,"passing parameters to data source"); - answer = mSdServer.mSdDataSource.updateFromJSON(parameters.get("dataObj").toString()); + if (mSdServer.mSdDataSourceName.equals("Garmin")) { + // Send the data to the SdDataSource so the app can pick it up. + if (parameters.get("dataObj") != null) { + Log.v(TAG, "passing parameters to data source"); + answer = mSdServer.mSdDataSource.updateFromJSON(parameters.get("dataObj").toString()); + } else { + Log.v(TAG, "Passing postData to data source"); + answer = mSdServer.mSdDataSource.updateFromJSON(files.get("postData")); + } } else { - Log.v(TAG,"Passing postData to data source"); - answer = mSdServer.mSdDataSource.updateFromJSON(files.get("postData")); + Log.i(TAG,"Web server received data, but datasource is not set to 'Garmin' - Ignoring"); + mUtil.showToast("Web server received data, but datasource is not set to 'Garmin' - Ignoring"); } break; default: @@ -202,7 +207,7 @@ public class SdWebServer extends NanoHTTPD { } else { Log.v(TAG, "WebServer.serve() - Unknown uri -" + uri); - answer = "{'msg' : 'Unknown URI: '}"; + answer = "{'msg' : 'Unknown URI: "+uri+"'}"; } } res = new NanoHTTPD.Response(answer); diff --git a/app/src/main/java/uk/org/openseizuredetector/StartupActivity.java b/app/src/main/java/uk/org/openseizuredetector/StartupActivity.java index 4c86409..78eba5b 100644 --- a/app/src/main/java/uk/org/openseizuredetector/StartupActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/StartupActivity.java @@ -24,20 +24,22 @@ */ package uk.org.openseizuredetector; -import android.app.Activity; -import android.app.AlertDialog; +import android.Manifest; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.PowerManager; import android.preference.PreferenceManager; +import android.text.Html; import android.text.SpannableString; -import android.text.SpannedString; import android.text.util.Linkify; import android.util.Log; import android.view.View; @@ -46,6 +48,12 @@ import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.text.HtmlCompat; + import com.rohitss.uceh.UCEHandler; import java.util.Timer; @@ -56,7 +64,7 @@ import java.util.TimerTask; * for it to start and to receive data and settings from the seizure detector before exiting and * starting the main activity. */ -public class StartupActivity extends Activity { +public class StartupActivity extends AppCompatActivity { private static String TAG = "StartupActivity"; private int okColour = Color.BLUE; private int warnColour = Color.MAGENTA; @@ -74,11 +82,42 @@ public class StartupActivity extends Activity { private Handler mHandler = new Handler(); // used to update ui from mUiTimer private boolean mUsingPebbleDataSource = true; private String mPebbleAppPackageName = null; + private boolean mBatteryOptDialogDisplayed = false; + private AlertDialog mBatteryOptDialog; + private boolean mLocationPermissions1Requested; + private boolean mLocationPermissions2Requested; + private boolean mSmsPermissionsRequested; + private boolean mPermissionsRequested; + + public final String[] REQUIRED_PERMISSIONS = { + //Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.WAKE_LOCK, + }; + + public final String[] SMS_PERMISSIONS_1 = { + Manifest.permission.SEND_SMS, + Manifest.permission.READ_PHONE_STATE, + }; + + public final String[] LOCATION_PERMISSIONS_1 = { + Manifest.permission.SEND_SMS, + Manifest.permission.ACCESS_FINE_LOCATION, + //Manifest.permission.ACCESS_BACKGROUND_LOCATION, + Manifest.permission.READ_PHONE_STATE, + }; + + public final String[] LOCATION_PERMISSIONS_2 = { + Manifest.permission.ACCESS_BACKGROUND_LOCATION, + }; + + + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Log.i(TAG,"onCreate()"); + Log.i(TAG, "onCreate()"); + setContentView(R.layout.startup_activity); // Set our custom uncaught exception handler to report issues. //Thread.setDefaultUncaughtExceptionHandler(new OsdUncaughtExceptionHandler(StartupActivity.this)); @@ -86,20 +125,6 @@ public class StartupActivity extends Activity { .addCommaSeparatedEmailAddresses("crashreports@openseizuredetector.org.uk,") .build(); - - mHandler = new Handler(); - mUtil = new OsdUtil(this, mHandler); - mUtil.writeToSysLogFile(""); - mUtil.writeToSysLogFile("*******************************"); - mUtil.writeToSysLogFile("* StartUpActivity Started *"); - mUtil.writeToSysLogFile("*******************************"); - - // Force the screen to stay on when the app is running - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - - setContentView(R.layout.startup_activity); - // Read the default settings from the xml preferences files, so we do // not have to use the hard coded ones in the java files. PreferenceManager.setDefaultValues(this, R.xml.alarm_prefs, true); @@ -108,6 +133,17 @@ public class StartupActivity extends Activity { PreferenceManager.setDefaultValues(this, R.xml.pebble_datasource_prefs, true); PreferenceManager.setDefaultValues(this, R.xml.seizure_detector_prefs, true); PreferenceManager.setDefaultValues(this, R.xml.network_passive_datasource_prefs, true); + PreferenceManager.setDefaultValues(this, R.xml.logging_prefs, true); + + mHandler = new Handler(); + mUtil = new OsdUtil(getApplicationContext(), mHandler); + mUtil.writeToSysLogFile(""); + mUtil.writeToSysLogFile("*******************************"); + mUtil.writeToSysLogFile("* StartUpActivity Started *"); + mUtil.writeToSysLogFile("*******************************"); + + // Force the screen to stay on when the app is running + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Button b; @@ -142,24 +178,16 @@ public class StartupActivity extends Activity { } }); - mConnection = new SdServiceConnection(this); + mConnection = new SdServiceConnection(getApplicationContext()); } @Override protected void onStart() { super.onStart(); - Log.i(TAG,"onStart()"); + Log.i(TAG, "onStart()"); mUtil.writeToSysLogFile("StartupActivity.onStart()"); TextView tv; - if (mUtil.arePermissionsOK()) { - Log.i(TAG,"onStart() - Permissions OK"); - } else { - Log.i(TAG,"onStart() - Permissions Not OK - requesting them"); - mUtil.requestPermissions(this); - } - - String versionName = mUtil.getAppVersionName(); tv = (TextView) findViewById(R.id.appNameTv); tv.setText("OpenSeizureDetector V" + versionName); @@ -174,18 +202,36 @@ public class StartupActivity extends Activity { if (mUtil.isServerRunning()) { - Log.i(TAG, "onStart() - server running - stopping it"); + Log.i(TAG, "onStart() - server running - stopping it - isServerRunning="+mUtil.isServerRunning()); mUtil.writeToSysLogFile("StartupActivity.onStart() - server already running - stopping it."); mUtil.stopServer(); + } else { + Log.i(TAG, "onStart() - server not running - isServerRunning="+mUtil.isServerRunning()); + } + // Wait 0.1 second to give the server chance to shutdown in case we have just shut it down below, then start it + mHandler.postDelayed(new Runnable() { + public void run() { + mUtil.writeToSysLogFile("StartupActivity.onStart() - starting server after delay - isServerRunning="+mUtil.isServerRunning()); + Log.i(TAG, "onStart() - starting server after delay -isServerRunning="+mUtil.isServerRunning()); + mUtil.startServer(); + // Bind to the service. + Log.i(TAG, "onStart() - binding to server"); + mUtil.writeToSysLogFile("StartupActivity.onStart() - binding to server"); + mUtil.bindToServer(getApplicationContext(), mConnection); + } + }, 100); + + // Check power management settings + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager.isIgnoringBatteryOptimizations(getPackageName())) { + Log.i(TAG, "Power Management OK - we are ignoring Battery Optimizations"); + mBatteryOptDialogDisplayed = false; + } else { + Log.e(TAG, "Power Management Problem - not ignoring Battery Optimisations"); + //mUtil.showToast("WARNING - Phone is Optimising OpenSeizureDetector Battery Usage - this is likely to prevent it working correctly when running on battery!"); + if (!mBatteryOptDialogDisplayed) showBatteryOptimisationWarningDialog(); } - mUtil.writeToSysLogFile("StartupActivity.onStart() - starting server"); - Log.i(TAG,"onStart() - starting server"); - mUtil.startServer(); - // Bind to the service. - Log.i(TAG,"onStart() - binding to server"); - mUtil.writeToSysLogFile("StartupActivity.onStart() - binding to server"); - mUtil.bindToServer(this, mConnection); // Check to see if this is the first time the app has been run, and display welcome dialog if it is. checkFirstRun(); @@ -198,7 +244,7 @@ public class StartupActivity extends Activity { mHandler.post(serverStatusRunnable); //updateServerStatus(); } - }, 0, 1000); + }, 0, 2000); } @@ -208,17 +254,17 @@ public class StartupActivity extends Activity { super.onStop(); Log.i(TAG, "onStop() - unbinding from server"); mUtil.writeToSysLogFile("StartupActivity.onStop() - unbinding from server"); - mUtil.unbindFromServer(this, mConnection); + mUtil.unbindFromServer(getApplicationContext(), mConnection); mUiTimer.cancel(); } /* - * serverStatusRunnable - called by updateServerStatus - updates the - * user interface to reflect the current status received from the server. - * If everything is ok, we close this activity and open the main user interface - * activity. - */ + * serverStatusRunnable - called by updateServerStatus - updates the + * user interface to reflect the current status received from the server. + * If everything is ok, we close this activity and open the main user interface + * activity. + */ final Runnable serverStatusRunnable = new Runnable() { public void run() { Boolean allOk = true; @@ -227,23 +273,49 @@ public class StartupActivity extends Activity { boolean smsAlarmsActive = true; boolean phoneAlarmsActive = true; - Log.v(TAG,"serverStatusRunnable()"); + Log.v(TAG, "serverStatusRunnable()"); SharedPreferences SP = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); smsAlarmsActive = SP.getBoolean("SMSAlarm", false); phoneAlarmsActive = SP.getBoolean("PhoneCallAlarm", false); + // Check power management settings + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager.isIgnoringBatteryOptimizations(getPackageName())) { + Log.i(TAG, "Power Management OK - we are ignoring Battery Optimizations"); + if (mBatteryOptDialogDisplayed) { + mBatteryOptDialog.cancel(); + mBatteryOptDialogDisplayed = false; + } + } + // Settings ok tv = (TextView) findViewById(R.id.textItem1); pb = (ProgressBar) findViewById(R.id.progressBar1); - if (mUtil.arePermissionsOK()) { - if (smsAlarmsActive && !mUtil.areSMSPermissionsOK()) { + if (arePermissionsOK()) { + if (smsAlarmsActive && !areSMSPermissions1OK()) { + Log.i(TAG,"SMS permissions NOT OK"); tv.setText(getString(R.string.SmsPermissionWarning)); - tv.setBackgroundColor(okColour); - tv.setTextColor(okTextColour); - pb.setIndeterminateDrawable(getResources().getDrawable(R.drawable.start_server)); - pb.setProgressDrawable(getResources().getDrawable(R.drawable.start_server)); - mUtil.requestSMSPermissions(StartupActivity.this); + tv.setBackgroundColor(alarmColour); + tv.setTextColor(alarmTextColour); + //pb.setIndeterminateDrawable(getResources().getDrawable(R.drawable.start_server)); + //pb.setProgressDrawable(getResources().getDrawable(R.drawable.start_server)); + requestSMSPermissions(); + allOk = false; + } else if (smsAlarmsActive && !areLocationPermissions1OK()) { + Log.i(TAG,"Location permissions NOT OK"); + tv.setText(getString(R.string.SmsPermissionWarning)); + tv.setBackgroundColor(alarmColour); + tv.setTextColor(alarmTextColour); + requestLocationPermissions1(); + allOk = false; + } else if (smsAlarmsActive && !areLocationPermissions2OK()) { + Log.i(TAG,"SMS permissions2 NOT OK"); + tv.setText(getString(R.string.SmsPermissionWarning)); + tv.setBackgroundColor(alarmColour); + tv.setTextColor(alarmTextColour); + requestLocationPermissions2(); + allOk = false; } else { tv.setText(getString(R.string.AppPermissionsOk)); tv.setBackgroundColor(okColour); @@ -257,7 +329,7 @@ public class StartupActivity extends Activity { tv.setTextColor(alarmTextColour); pb.setIndeterminate(true); allOk = false; - mUtil.requestPermissions(StartupActivity.this); + requestPermissions(StartupActivity.this); } // If phone alarms are selected, we need to have the uk.org.openseizuredetector.dialler package installed to do the actual dialling. @@ -305,7 +377,6 @@ public class StartupActivity extends Activity { } - // Do we have seizure detector data? tv = (TextView) findViewById(R.id.textItem5); pb = (ProgressBar) findViewById(R.id.progressBar5); @@ -342,11 +413,10 @@ public class StartupActivity extends Activity { } - // If all the parameters are ok, close this activity and open the main // user interface activity instead. if (allOk) { - if (!mDialogDisplayed) { + if (!mDialogDisplayed && !mBatteryOptDialogDisplayed) { if (!mStartedMainActivity) { Log.i(TAG, "serverStatusRunnable() - starting main activity..."); mUtil.writeToSysLogFile("StartupActivity.serverStatusRunnable - all checks ok - starting main activity."); @@ -393,42 +463,65 @@ public class StartupActivity extends Activity { } } - /** - * checkFirstRun - checks to see if this is the first run of the app after installation or upgrade. - * if it is, the relevant dialog message is displayed. If not, the routine just exists so start-up can continue. - */ + /** + * checkFirstRun - checks to see if this is the first run of the app after installation or upgrade. + * if it is, the relevant dialog message is displayed. If not, the routine just exists so start-up can continue. + */ public void checkFirstRun() { String storedVersionName = ""; String versionName; AlertDialog UpdateDialog; AlertDialog FirstRunDialog; SharedPreferences prefs; - Log.i(TAG,"checkFirstRun()"); + Log.i(TAG, "checkFirstRun()"); versionName = this.getVersionName(this, StartupActivity.class); prefs = PreferenceManager.getDefaultSharedPreferences(this); storedVersionName = (prefs.getString("AppVersionName", null)); - Log.v(TAG,"storedVersionName="+storedVersionName+", versionName="+versionName); + Log.v(TAG, "storedVersionName=" + storedVersionName + ", versionName=" + versionName); // CHeck for new installation + //storedVersionName = null; // FIXME Force first run dialog for easier testing **************************** if (storedVersionName == null || storedVersionName.length() == 0) { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder( this); - final SpannableString s = new SpannableString( - getString(R.string.FirstRunDlgMsg)+getString(R.string.changelog) - ); - // This makes the links display as links, but they do not respond to clicks for some reason... - Linkify.addLinks(s, Linkify.ALL); + final String s = new String( + getString(R.string.FirstRunDlgMsg)); alertDialogBuilder .setTitle(getString(R.string.FirstRunDlgTitle)) - .setMessage(s) + .setMessage(Html.fromHtml(s)) .setCancelable(false) - .setPositiveButton(getString(R.string.okBtnTxt), new DialogInterface.OnClickListener() { + .setNeutralButton(getString(R.string.closeBtnTxt), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); mDialogDisplayed = false; //MainActivity.this.finish(); } - }); + }) + .setPositiveButton("Privacy Policy", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + mDialogDisplayed = false; + String url = OsdUtil.PRIVACY_POLICY_URL; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + dialog.cancel(); + mDialogDisplayed = false; + } + }) + .setNegativeButton("Data Sharing", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + mDialogDisplayed = false; + String url = OsdUtil.DATA_SHARING_URL; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + dialog.cancel(); + mDialogDisplayed = false; + } + }) + ; FirstRunDialog = alertDialogBuilder.create(); Log.i(TAG, "Displaying First Run Dialog"); FirstRunDialog.show(); @@ -437,19 +530,43 @@ public class StartupActivity extends Activity { // Check for update of installed application AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder( this); - final SpannableString s = new SpannableString( - getString(R.string.UpgradeMsg)+getString(R.string.changelog) + final String s = new String( + getString(R.string.UpgradeMsg) + getString(R.string.changelog) ); - // This makes the links display as links, but they do not respond to clicks for some reason... - Linkify.addLinks(s, Linkify.ALL); + alertDialogBuilder .setTitle(getString(R.string.UpdateDialogTitleTxt)) - .setMessage(s) + .setMessage(Html.fromHtml(s)) .setCancelable(false) - .setPositiveButton(getString(R.string.okBtnTxt), new DialogInterface.OnClickListener() { + .setNeutralButton(getString(R.string.closeBtnTxt), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); mDialogDisplayed = false; + //MainActivity.this.finish(); + } + }) + .setPositiveButton("Privacy Policy", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + mDialogDisplayed = false; + String url = OsdUtil.PRIVACY_POLICY_URL; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + dialog.cancel(); + mDialogDisplayed = false; + } + }) + .setNegativeButton("Data Sharing", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + mDialogDisplayed = false; + String url = OsdUtil.DATA_SHARING_URL; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + startActivity(i); + dialog.cancel(); + mDialogDisplayed = false; } }); UpdateDialog = alertDialogBuilder.create(); @@ -463,4 +580,202 @@ public class StartupActivity extends Activity { prefs.edit().putString("AppVersionName", versionName).commit(); } + private void showBatteryOptimisationWarningDialog() { + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder( + this); + final SpannableString s = new SpannableString( + getString(R.string.battery_usage_optimisation_dialog_text) + ); + // This makes the links display as links, but they do not respond to clicks for some reason... + Linkify.addLinks(s, Linkify.ALL); + alertDialogBuilder + .setTitle(R.string.battery_usage_optimisation_dialog_title) + .setMessage(s) + .setCancelable(false) + .setPositiveButton(getString(R.string.okBtnTxt), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + mBatteryOptDialogDisplayed = false; + } + }); + mBatteryOptDialog = alertDialogBuilder.create(); + Log.i(TAG, "Displaying Update Dialog"); + mBatteryOptDialog.show(); + mBatteryOptDialogDisplayed = true; + } + + /*****************************************************************************/ + public boolean arePermissionsOK() { + boolean allOk = true; + Log.v(TAG, "arePermissionsOK"); + for (int i = 0; i < REQUIRED_PERMISSIONS.length; i++) { + if (ContextCompat.checkSelfPermission(this, REQUIRED_PERMISSIONS[i]) + != PackageManager.PERMISSION_GRANTED) { + Log.i(TAG, REQUIRED_PERMISSIONS[i] + " Permission Not Granted"); + allOk = false; + } + } + return allOk; + } + + public boolean areSMSPermissions1OK() { + boolean allOk = true; + Log.v(TAG, "areSMSPermissions1 OK()"); + for (int i = 0; i < SMS_PERMISSIONS_1.length; i++) { + if (ContextCompat.checkSelfPermission(this, SMS_PERMISSIONS_1[i]) + != PackageManager.PERMISSION_GRANTED) { + Log.i(TAG, "areSMSPermissions1OK: "+SMS_PERMISSIONS_1[i] + " Permission Not Granted"); + allOk = false; + } + } + return allOk; + } + + + public boolean areLocationPermissions1OK() { + boolean allOk = true; + Log.v(TAG, "areLocationPermissions1 OK()"); + for (int i = 0; i < LOCATION_PERMISSIONS_1.length; i++) { + if (ContextCompat.checkSelfPermission(this, LOCATION_PERMISSIONS_1[i]) + != PackageManager.PERMISSION_GRANTED) { + Log.i(TAG, LOCATION_PERMISSIONS_1[i] + " Permission Not Granted"); + allOk = false; + } + } + return allOk; + } + + public boolean areLocationPermissions2OK() { + boolean allOk = true; + Log.v(TAG, "areSMSPermissions2OK()"); + for (int i = 0; i < LOCATION_PERMISSIONS_2.length; i++) { + if (ContextCompat.checkSelfPermission(this, LOCATION_PERMISSIONS_2[i]) + != PackageManager.PERMISSION_GRANTED) { + Log.i(TAG, LOCATION_PERMISSIONS_2[i] + " Permission Not Granted"); + allOk = false; + } + } + return allOk; + } + + public void requestPermissions(AppCompatActivity activity) { + if (mPermissionsRequested) { + Log.i(TAG, "requestPermissions() - request already sent - not doing anything"); + } else { + Log.i(TAG, "requestPermissions() - requesting permissions"); + for (int i = 0; i < REQUIRED_PERMISSIONS.length; i++) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, + REQUIRED_PERMISSIONS[i])) { + Log.i(TAG, "shouldShowRationale for permission" + REQUIRED_PERMISSIONS[i]); + } + } + ActivityCompat.requestPermissions(activity, + REQUIRED_PERMISSIONS, + 42); + mPermissionsRequested = true; + } + } + + public void requestSMSPermissions() { + if (mSmsPermissionsRequested) { + Log.i(TAG, "requestSMSPermissions() - request already sent - not doing anything"); + } else { + Log.i(TAG, "requestSMSPermissions() - requesting permissions"); + mSmsPermissionsRequested = true; + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder( + this); + alertDialogBuilder + .setTitle(R.string.permissions_required) + .setMessage(R.string.sms_permissions_rationale_1) + .setCancelable(false) + .setPositiveButton(getString(R.string.okBtnTxt), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + Log.i(TAG,"requestSMSPermissions(): Launching ActivityCompat.requestPermissions()"); + ActivityCompat.requestPermissions(StartupActivity.this, + SMS_PERMISSIONS_1, + 45); + } + }) + .setNegativeButton(getString(R.string.cancelBtnTxt), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }).create().show(); + } + } + + + public void requestLocationPermissions1() { + if (mLocationPermissions1Requested) { + Log.i(TAG, "requestLocationPermissions1() - request already sent - not doing anything"); + } else { + Log.i(TAG, "requestLocationPermissions1() - requesting permissions"); + mLocationPermissions1Requested = true; + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder( + this); + alertDialogBuilder + .setTitle(R.string.permissions_required) + .setMessage(R.string.location_permissions_rationale_1) + .setCancelable(false) + .setPositiveButton(getString(R.string.okBtnTxt), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + Log.i(TAG, "requestLocationPermissions1(): Launching ActivityCompat.requestPermissions()"); + ActivityCompat.requestPermissions(StartupActivity.this, + LOCATION_PERMISSIONS_1, + 43); + } + }) + .setNegativeButton(getString(R.string.cancelBtnTxt), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }) + .create().show(); + } + } + + public void requestLocationPermissions2() { + if (mLocationPermissions2Requested) { + Log.i(TAG, "requestSMSPermissions2() - request already sent - not doing anything"); + } else { + Log.i(TAG, "requestSMSPermissions2() - requesting permissions"); + mLocationPermissions2Requested = true; + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder( + this); + alertDialogBuilder + .setTitle(R.string.permissions_required) + .setMessage(R.string.location_permissions_2_rationale) + .setCancelable(false) + .setPositiveButton(getString(R.string.okBtnTxt), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + Log.i(TAG,"requestSMSPermissions(): Launching ActivityCompat.requestPermissions()"); + ActivityCompat.requestPermissions(StartupActivity.this, + LOCATION_PERMISSIONS_2, + 44); + } + }) + .setNegativeButton(getString(R.string.cancelBtnTxt), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }).create().show(); + } + } + + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + Log.i(TAG, "onRequestPermissionsResult - Permission" + permissions + " = " + grantResults); + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + for (int i = 0; i < permissions.length; i++) { + Log.i(TAG, "Permission " + permissions[i] + " = " + grantResults[i]); + } + } + + } diff --git a/app/src/main/java/uk/org/openseizuredetector/WebApiConnection.java b/app/src/main/java/uk/org/openseizuredetector/WebApiConnection.java new file mode 100644 index 0000000..b261bec --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/WebApiConnection.java @@ -0,0 +1,234 @@ +package uk.org.openseizuredetector; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.VolleyLog; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.toolbox.Volley; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QueryDocumentSnapshot; +import com.google.firebase.firestore.QuerySnapshot; +import com.google.firebase.firestore.core.OrderBy; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.UnsupportedEncodingException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +// This class is intended to handle all interactions with the OSD WebAPI +public abstract class WebApiConnection { + protected Context mContext; + protected OsdUtil mUtil; + private String TAG = "WebApiConnection"; + private String mAuthToken; + + + public interface JSONObjectCallback { + public void accept(JSONObject retValObj); + } + + public interface StringCallback { + public void accept(String retValStr); + } + + public interface LongCallback { + public void accept(Long retVal); + } + + public WebApiConnection(Context context) { + mContext = context; + mUtil = new OsdUtil(mContext, new Handler()); + } + + public void close() { + Log.i(TAG, "stop()"); + } + + public abstract boolean isLoggedIn(); + + + // Create a new event in the remote database, based on the provided parameters. + // passes the newly created documentId to function callback on successful completion, or null on error. + public abstract boolean createEvent(final int osdAlarmState, final Date eventDate, final String type, final String subType, + final String eventDesc, final String dataJSON, StringCallback callback); + + // calls function callback with a JSONObject representation of the event with id 'eventId' + public abstract boolean getEvent(String eventId, JSONObjectCallback callback); + + + /** + * Retrieve all events accessible to the logged in user, and pass them to the callback function as a JSONObject + * + * @param callback + * @return true on success or false on failure to initiate the request. + */ + public abstract boolean getEvents(JSONObjectCallback callback); + + public abstract boolean updateEvent(final JSONObject eventObj, JSONObjectCallback callback); + + public abstract boolean createDatapoint(JSONObject dataObj, String eventId, StringCallback callback); + + /** + * Retrieve the file containing the standard event types from the server. + * Calls the specified callback function, passing a JSONObject as a parameter when the data has been received and parsed. + * + * @return true if request sent successfully or else false. + */ + public abstract boolean getEventTypes(JSONObjectCallback callback); + + + /** + * Retrieve a trivial file from the server to check we have a good server connection. + * sets mServerConnectionOk. + * + * @return true if request sent successfully or else false. + */ + public abstract boolean checkServerConnection(); + + public abstract boolean getUserProfile(JSONObjectCallback callback); + + + public boolean authenticate(final String uname, final String passwd, StringCallback callback) { + Log.e(TAG, "WebApiConnection.authenticate(username, password, callback) Not Implemented"); + return false; + } + + // Remove the stored token so future calls are not authenticated. + public void logout() { + Log.v(TAG, "logout()"); + setStoredToken(null); + } + + protected void setStoredToken(String authToken) { + mAuthToken = authToken; + } + + protected String getStoredToken() { + return (mAuthToken); + } + + + /** + * Mark all of the events with IDs contained in eventList as unknown type. + * @param eventList list of String IDs of the events to mark as unknown. + * @return true if request sent successfully or false. + */ + private boolean markEventsAsUnknown(ArrayListeventList) { + if (eventList.size()>0) { + Log.i(TAG,"markEventsAsUnknown - eventList.size()="+eventList.size()); + Log.i(TAG,"markEventsAsUnknown - eventList(0) = "+eventList.get(0)); + getEvent(eventList.get(0), new WebApiConnection.JSONObjectCallback() { + @Override + public void accept(JSONObject eventObj) { + Log.v(TAG, "markEventsAsUnknown.getEvent.callback: "+eventObj); + if (eventObj != null) { + Log.v(TAG, "markEventsAsUnknown.getEvent.callback: eventObj=" + eventObj.toString()); + try { + eventObj.put("type", "Unknown"); + String notesStr = eventObj.getString("desc"); + if (notesStr == null) notesStr = new String(""); + notesStr = notesStr + " Set to Unknown automatically by OSD Android App"; + eventObj.put("desc", notesStr); + updateEvent(eventObj,new WebApiConnection.JSONObjectCallback() { + @Override + public void accept(JSONObject eventObj) { + if (eventObj != null) { + Log.i(TAG, "markEventsAsUnknown.updateEvent.callback" + eventObj.toString()); + // Remove the first item from the list,then call this whole procedure again to modify the next one on the list. + eventList.remove(0); + markEventsAsUnknown(eventList); + } else { + Log.e(TAG, "markEventsAsUnknown.updateEvent.callback - eventObj is null"); + mUtil.showToast("markEventsAsUnknown.updateEvent.callback - eventObj is null"); + } + } + }); + } catch (JSONException e) { + Log.e(TAG,"markEventsAsUnknown.getEvent.callback: Error editing eventObj"); + mUtil.showToast("markEventsAsUnknown.getEvent.callback: Error editing eventObj"); + } + } else { + mUtil.showToast("Failed to Retrieve Event from Remote Database"); + return; + } + } + }); + } else { + Log.i(TAG,"markEventsAsUnknown(): No more events to Modify"); + mUtil.showToast("No more unvalidated events to modify."); + + } + return(true); + } + + /** + * Mark all unverified events in the remote database as unknown + * + * @return true if request is successful or false. + */ + public boolean markUnverifiedEventsAsUnknown() { + if (getEvents((JSONObject remoteEventsObj) -> { + Log.v(TAG, "markUnverifiedEventsAsUnknown.getEvents.Callback()"); + Boolean haveUnvalidatedEvent = false; + if (remoteEventsObj == null) { + Log.e(TAG, "markUnverifiedEventsAsUnknown.getEvents.Callback: Error Retrieving events"); + } else { + try { + JSONArray eventsArray = remoteEventsObj.getJSONArray("events"); + ArrayList unvalidatedEventsList = new ArrayList(); + for (int i = eventsArray.length() - 1; i >= 0; i--) { + JSONObject eventObj = eventsArray.getJSONObject(i); + String typeStr = eventObj.getString("type"); + if (typeStr.equals("null") || typeStr.equals("")) { + haveUnvalidatedEvent = true; + unvalidatedEventsList.add(eventObj.getString("id")); + } + } + Log.v(TAG, "markUnverifiedEventsAsUnknown.getEvents.onFinish.callback - haveUnvalidatedEvent = " + + haveUnvalidatedEvent); + markEventsAsUnknown(unvalidatedEventsList); + + } catch (JSONException e) { + Log.e(TAG, "markUnverifiedEventsAsUnknown.getEvents.onFinish(): Error Parsing remoteEventsObj: " + e.getMessage()); + //mUtil.showToast("Error Parsing remoteEventsObj - this should not happen!!!"); + } + } + })) { + Log.v(TAG, "markUnverifiedEventsAsUnknown.getEvents - requested events"); + } else { + Log.v(TAG, "markUnverifiedEventsAsUnknown.getEvents - Not Logged In"); + + } + + + return (true); + } + +} diff --git a/app/src/main/java/uk/org/openseizuredetector/WebApiConnection_firebase.java b/app/src/main/java/uk/org/openseizuredetector/WebApiConnection_firebase.java new file mode 100644 index 0000000..426f2ea --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/WebApiConnection_firebase.java @@ -0,0 +1,426 @@ +package uk.org.openseizuredetector; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.Volley; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QueryDocumentSnapshot; +import com.google.firebase.firestore.QuerySnapshot; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +// This class is intended to handle all interactions with the OSD WebAPI +public class WebApiConnection_firebase extends WebApiConnection { + public String retVal; + public int retCode; + public boolean mServerConnectionOk = false; + private String TAG = "WebApiConnection_firebase"; + private String mAuthToken; + private Context mContext; + private OsdUtil mUtil; + FirebaseFirestore mDb; + + RequestQueue mQueue; + + + public WebApiConnection_firebase(Context context) { + super(context); + loginToFirebase(); + } + + public void loginToFirebase() { + // Check if we are already logged in + FirebaseAuth auth = FirebaseAuth.getInstance(); + mDb = FirebaseFirestore.getInstance(); + if (auth != null) { + if (auth.getCurrentUser() != null) { + Log.i(TAG, "Firebase Logged in OK -" + auth.getCurrentUser().getDisplayName()); + } else { + Log.e(TAG, "Firebase not logged in - no current user"); + } + } else { + Log.e(TAG, "Firebase not logged in"); + } + } + + public void close() { + Log.i(TAG, "stop()"); + mQueue.stop(); + } + + public boolean isLoggedIn() { + FirebaseAuth auth = FirebaseAuth.getInstance(); + if (auth != null) { + if (auth.getCurrentUser() != null) { + //Log.v(TAG, "isLoggedIn(): Firebase Logged in OK"); + return (true); + } else { + //Log.v(TAG, "isLoggedIn(): Current user is null - Firebase not logged in"); + return (false); + } + } else { + //Log.v(TAG, "isLoggedIn(): Firebase not logged in"); + return (false); + } + } + + public boolean getUserProfile(JSONObjectCallback callback) { + Log.v(TAG, "getUserProfile()"); + FirebaseAuth auth = FirebaseAuth.getInstance(); + if (!isLoggedIn()) { + Log.v(TAG, "not logged in - doing nothing"); + return (false); + } else { + try { + JSONObject retObj = new JSONObject(); + retObj.put("id",auth.getCurrentUser().getUid()); + retObj.put("username", auth.getCurrentUser().getDisplayName()); + retObj.put("email", auth.getCurrentUser().getEmail()); + callback.accept(retObj); + } catch (JSONException e) { + Log.e(TAG, "Error Creating retObjObj: " + e.getMessage()); + mUtil.showToast("Error Creating retObj - this should not happen!!!"); + return (false); + } + } + return (true); + } + + public String getStoredToken() { + return null; + } + + public void setStoredToken(String s) { + return; + } + + + // Create a new event in the remote database, based on the provided parameters. + // passes the newly created documentId to function callback on successful completion, or null on error. + public boolean createEvent(final int osdAlarmState, final Date eventDate, final String type, final String subType, + final String eventDesc, final String dataJSON, StringCallback callback) { + // FIXME - save type, subtype, eventDesc and dataJSON + Log.v(TAG, "createEvent()"); + String userId = null; + + if (mDb == null) { + Log.w(TAG, "createEvent() - mDb is null - not doing anything"); + return false; + } + + if (FirebaseAuth.getInstance().getCurrentUser() == null) { + Log.e(TAG, "ERROR: createEvent() - not logged in"); + return false; + } else { + userId = FirebaseAuth.getInstance().getCurrentUser().getUid(); + } + Map event = new HashMap<>(); + event.put("dataTime", eventDate.getTime()); + event.put("osdAlarmState", osdAlarmState); + event.put("desc", eventDesc); + event.put("type", type); + event.put("subType", subType); + event.put("dataJSON", dataJSON); + event.put("userId", userId); + + mDb.collection("Events") + .add(event) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(DocumentReference documentReference) { + Log.d(TAG, "createEvent.onSuccess() - DocumentSnapshot added with ID: " + documentReference.getId()); + mServerConnectionOk = true; + callback.accept(documentReference.getId()); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.w(TAG, "createEvent.onFailure() - Error adding document", e); + callback.accept(null); + } + }); + return (true); + } + + // calls function callback with a JSONObject representation of the event with id 'eventId' + public boolean getEvent(String eventId, JSONObjectCallback callback) { + Log.v(TAG, "getEvent()"); + if (mDb == null) { + Log.w(TAG, "getEvent() - mDb is null - not doing anything"); + return false; + } + + DocumentReference docRef = mDb + .collection("Events").document(eventId); + docRef.get().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + DocumentSnapshot document = task.getResult(); + if (document.exists()) { + Log.d(TAG, "getEvent.onComplete(): DocumentSnapshot data: " + document.getData()); + if (document.getData() == null) { + callback.accept(null); + } else + callback.accept(new JSONObject(document.getData())); + } else { + Log.d(TAG, "No such document"); + callback.accept(null); + } + } else { + Log.d(TAG, "get failed with ", task.getException()); + callback.accept(null); + } + } + }); + + return true; + + } + + /** + * Retrieve all events accessible to the logged in user, and pass them to the callback function as a JSONObject + * + * @param callback + * @return true on success or false on failure to initiate the request. + */ + public boolean getEvents(JSONObjectCallback callback) { + //Long eventId=Long.valueOf(285); + Log.v(TAG, "getEvents()"); + if (mDb == null) { + Log.w(TAG, "getEvents() - mDb is null - not doing anything"); + return false; + } + + if (!isLoggedIn()) { + Log.w(TAG, "getEvents() - not logged in - not doing anything"); + return false; + } + String userId = FirebaseAuth.getInstance().getCurrentUser().getUid(); + mDb.collection("Events") //.where("userId", "==", userId) + .whereEqualTo("userId", userId) + .orderBy("dataTime", Query.Direction.ASCENDING) + .get() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + try { + JSONObject retObj = new JSONObject(); + JSONArray eventArray = new JSONArray(); + Log.d(TAG, "getEvents() - returned " + task.getResult().size()); + for (QueryDocumentSnapshot document : task.getResult()) { + Log.d(TAG, "getEvents() - " + document.getId() + " => " + document.getData()); + JSONObject eventObj = new JSONObject(document.getData()); + // Add the event id into the event data because firebase does not include it as part of the document. + eventObj.put("id", document.getId()); + eventArray.put(eventObj); + } + retObj.put("events", eventArray); + callback.accept(retObj); + } catch (JSONException e) { + Log.e(TAG, "getEvents.onResponse(): Error: " + e.getMessage() + "," + e.toString()); + callback.accept(null); + } + + } else { + Log.d(TAG, "Error getting documents: ", task.getException()); + callback.accept(null); + } + } + }); + return (true); + } + + public boolean updateEvent(final JSONObject eventObj, JSONObjectCallback callback) { + String eventId; + Log.v(TAG, "updateEvent()"); + if (mDb == null) { + Log.w(TAG, "updateEvent() - mDb is null - not doing anything"); + return false; + } + + try { + eventId = eventObj.getString("id"); + } catch (JSONException e) { + Log.e(TAG, "updateEvent(): Error reading id from eventObj"); + eventId = null; + return false; + } + final String dataStr = eventObj.toString(); + Log.v(TAG, "updateEvent - data=" + dataStr); + Map eventMap = new HashMap<>(); + try { + eventMap.put("dataTime", eventObj.getLong("dataTime")); + eventMap.put("osdAlarmState", eventObj.getInt("osdAlarmState")); + eventMap.put("desc", eventObj.getString("desc")); + eventMap.put("type", eventObj.getString("type")); + eventMap.put("subType", eventObj.getString("subType")); + eventMap.put("userId", eventObj.getString("userId")); + } catch (JSONException e) { + Log.e(TAG, "updateEvent(): Error data from eventObj." + e.toString()); + e.printStackTrace(); + return false; + } + Log.v(TAG, "updateEvent - map=" + eventMap.toString()); + + try { + DocumentReference docRef = mDb.collection("Events").document(eventId); + docRef.set(eventMap) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void aVoid) { + JSONObject retObj; + try { + retObj = new JSONObject("{\"status\":\"OK\"}"); + } catch (Exception e) { + retObj = null; + } + callback.accept(retObj); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.w(TAG, "Error updating document", e); + callback.accept(null); + } + }); + return (true); + } catch (Exception e) { + Log.e(TAG, "updateEvent() - ERROR: " + e.toString()); + e.printStackTrace(); + } + return (false); + } + + public boolean createDatapoint(JSONObject dataObj, String eventId, StringCallback callback) { + Log.v(TAG, "createDatapoint()"); + // Create a new event in the remote database, based on the provided parameters. + String userId = null; + if (FirebaseAuth.getInstance().getCurrentUser() == null) { + Log.e(TAG, "ERROR: createDatapoint() - not logged in"); + return false; + } else { + userId = FirebaseAuth.getInstance().getCurrentUser().getUid(); + } + String dataTime; + try { + dataTime = dataObj.getString("dataTime"); + } catch (JSONException e) { + dataTime = ""; + } + Map datapoint = new HashMap<>(); + datapoint.put("dataTime", dataTime); + datapoint.put("dataJSON", dataObj.toString()); + datapoint.put("userId", userId); + datapoint.put("eventId", userId); + + mDb.collection("Datapoints") + .add(datapoint) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(DocumentReference documentReference) { + Log.d(TAG, "createDatapoint.onSuccess() - DocumentSnapshot added with ID: " + documentReference.getId()); + mServerConnectionOk = true; + callback.accept(documentReference.getId()); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.w(TAG, "createDatapoint.onFailure() - Error adding document", e); + callback.accept(null); + } + }); + return (true); + } + + + /** + * Retrieve the file containing the standard event types from the server. + * Calls the specified callback function, passing a JSONObject as a parameter when the data has been received and parsed. + * + * @return true if request sent successfully or else false. + */ + public boolean getEventTypes(JSONObjectCallback callback) { + Log.v(TAG, "getEventTypes()"); + if (mDb == null) { + Log.w(TAG, "getEventTypes() - mDb is null - not doing anything"); + return false; + } + + mDb.collection("EventTypes") + .get() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + try { + JSONObject retObj = new JSONObject(); + for (QueryDocumentSnapshot document : task.getResult()) { + Log.d(TAG, "getEventTypes.onComplete(): " + document.getId() + " => " + document.getData()); + Log.v(TAG, "getEventTypes.onComplete() - subtypes=" + document.getData().get("subTypes")); + JSONArray subTypesArray = listToJSONArray((List) document.getData().get("subTypes")); + retObj.put(document.getData().get("type").toString(), subTypesArray); + } + Log.d(TAG, "getEventTypes.onComplete() - retObj=" + retObj.toString()); + callback.accept(retObj); + } catch (JSONException e) { + Log.e(TAG, "getEventTypes.onResponse(): Error: " + e.getMessage() + "," + e.toString()); + callback.accept(null); + } + } else { + Log.d(TAG, "Error getting documents: ", task.getException()); + callback.accept(null); + } + } + }); + return (true); + + } + + private JSONArray listToJSONArray(List list) { + JSONArray arr = new JSONArray(); + for (Object obj : list) { + arr.put(obj); + } + return arr; + } + + /** + * Retrieve a trivial file from the server to check we have a good server connection. + * sets mServerConnectionOk. + * + * @return true if request sent successfully or else false. + */ + public boolean checkServerConnection() { + //FIXME There must be a Firebase function for this? + mServerConnectionOk = true; + return mServerConnectionOk; + } + +} diff --git a/app/src/main/java/uk/org/openseizuredetector/WebApiConnection_osdapi.java b/app/src/main/java/uk/org/openseizuredetector/WebApiConnection_osdapi.java new file mode 100644 index 0000000..0e01565 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/WebApiConnection_osdapi.java @@ -0,0 +1,667 @@ +package uk.org.openseizuredetector; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.VolleyLog; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.toolbox.Volley; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.UnsupportedEncodingException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + + +// This class is intended to handle all interactions with the OSD WebAPI +public class WebApiConnection_osdapi extends WebApiConnection { + public String retVal; + public int retCode; + public boolean mServerConnectionOk = false; + private String mUrlBase = "https://osdApi.ddns.net"; + private String TAG = "WebApiConnection_osdapi"; + RequestQueue mQueue; + + public WebApiConnection_osdapi(Context context) { + super(context); + mQueue = Volley.newRequestQueue(context); + } + + public void close() { + super.close(); + Log.i(TAG,"stop()"); + mQueue.stop(); + } + + /** + * Attempt to authenticate with the web API using user name uname and password passwd. Calls function callback with either + * the authentication token on success or null on failure. + * + * @param uname - user name + * @param passwd - password + * @param callback - call back function callback(String retVal) + * @return true if request sent, or false if failed to send request. + */ + @Override + public boolean authenticate(final String uname, final String passwd, StringCallback callback) { + // NOTE: the 'final' keyword is necessary for uname and passwd to be accessible to getParams below - I don't know why! + // We know that this command works, so we just need the Java equivalent: + // curl -X POST -d 'login=graham4&password=testpwd1' https://osdapi.ddns.net/api/accounts/login/ + // sending the credentials as a JSONObject postData did not work, so try the method from: + // https://protocoderspoint.com/login-and-registration-form-in-android-using-volley-keeping-user-logged-in/#Login_Registration_form_in_android_using_volley_library + String urlStr = mUrlBase + "/api/accounts/login/"; + Log.v(TAG, "urlStr=" + urlStr); + + StringRequest req = new StringRequest(Request.Method.POST, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + String tokenStr = null; + Log.v(TAG, "Response is: " + response); + try { + JSONObject jo = new JSONObject(response); + tokenStr = jo.getString("token"); + mServerConnectionOk = true; + } catch (JSONException e) { + tokenStr = "Error Parsing Rsponse"; + } + setStoredToken(tokenStr); + callback.accept(tokenStr); + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + if (error != null) { + Log.e(TAG, "Login Error: " + error.toString() + ", message:" + error.getMessage()); + } else { + Log.e(TAG, "Login Error: Returned null response"); + } + mServerConnectionOk = false; + setStoredToken(null); + callback.accept(null); + } + }) { + // Note, this is overriding part of StringRequest, not one of the sub-classes above! + @Override + protected Map getParams() { + Map params = new HashMap<>(); + // params.put("name",sname); // passing parameters to server + params.put("login", uname); + params.put("password", passwd); + return params; + } + }; + + mQueue.add(req); + return (true); + } + + + + + public boolean isLoggedIn() { + String authToken = getStoredToken(); + Log.v(TAG, "isLoggedIn(): token=" + authToken); + if (authToken == null || authToken.length() == 0) { + Log.v(TAG, "isLogged in - not logged in"); + return (false); + } else { + Log.v(TAG,"isLoggedIn - logged in ok"); + return (true); + } + + } + + + // Create a new event in the remote database, based on the provided parameters. + public boolean createEvent(final int osdAlarmState, final Date eventDate, final String type, final String subType, + final String eventDesc, final String dataJSON, StringCallback callback) { + Log.v(TAG, "createEvent()"); + String urlStr = mUrlBase + "/api/events/"; + Log.v(TAG, "urlStr=" + urlStr); + final String authtoken = getStoredToken(); + + if (!isLoggedIn()) { + Log.v(TAG, "not logged in - doing nothing"); + return (false); + } + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("osdAlarmState", String.valueOf(osdAlarmState)); + jsonObject.put("dataTime", dateFormat.format(eventDate)); + jsonObject.put("type", type); + jsonObject.put("subType", subType); + jsonObject.put("desc", eventDesc); + jsonObject.put("dataJSON", dataJSON); + } catch (JSONException e) { + Log.e(TAG, "Error generating event JSON string"); + } + final String dataStr = jsonObject.toString(); + Log.v(TAG, "createEvent - data=" + dataStr); + + StringRequest req = new StringRequest(Request.Method.POST, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + Log.v(TAG, "createEvent.onResponse - Response is: " + response); + mServerConnectionOk = true; + // we return just the eventId to be consistent with the firebase version of WebApiConnection. + String retVal = null; + try { + JSONObject retObj = new JSONObject(response); + retVal = retObj.getString("id"); + } catch (JSONException e) { + Log.e(TAG, "createEvent.onResponse(): Error: " + e.getMessage() + "," + e.toString()); + retVal = null; + } + callback.accept(retVal); + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + mServerConnectionOk = false; + if (error != null) { + Log.e(TAG, "createEvent Error: " + error.toString() + ", message:" + error.getMessage()); + callback.accept(null); + } else { + Log.e(TAG, "createEvent Error - null response"); + callback.accept(null); + } + } + }) { + // Note, this is overriding part of StringRequest, not one of the sub-classes above! + @Override + protected Map getParams() { + Map params = new HashMap<>(); + // params.put("name",sname); // passing parameters to server + String authToken = getStoredToken(); + params.put("Authorization: Token " + authToken, authToken); + Log.v(TAG, "getParams: params=" + params.toString()); + return params; + } + + @Override + public Map getHeaders() throws AuthFailureError { + Map params = new HashMap(); + params.put("Content-Type", "application/json; charset=UTF-8"); + params.put("Authorization", "Token " + getStoredToken()); + return params; + } + + @Override + public byte[] getBody() throws AuthFailureError { + try { + return dataStr == null ? null : dataStr.getBytes("utf-8"); + } catch (UnsupportedEncodingException uee) { + VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s", dataStr, "utf-8"); + return null; + } + } + }; + + mQueue.add(req); + return (true); + } + + public boolean getEvent(String eventId, JSONObjectCallback callback) { + Log.v(TAG, "getEvent()"); + String urlStr = mUrlBase + "/api/events/" + eventId; + Log.v(TAG, "getEvent(): urlStr=" + urlStr); + final String authtoken = getStoredToken(); + + if (!isLoggedIn()) { + Log.v(TAG, "not logged in - doing nothing"); + return (false); + } + + StringRequest req = new StringRequest(Request.Method.GET, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + Log.v(TAG, "Response is: " + response); + try { + JSONObject retObj = new JSONObject(response); + retObj.put("alarmStateStr", mUtil.alarmStatusToString(retObj.getInt("osdAlarmState"))); + callback.accept(retObj); + } catch (JSONException e) { + Log.e(TAG, "getEventTypes.onRespons(): Error: " + e.getMessage() + "," + e.toString()); + callback.accept(null); + } + mServerConnectionOk = true; + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + if (error != null) { + Log.e(TAG, "Create Event Error: " + error.toString() + ", message:" + error.getMessage()); + } else { + Log.e(TAG, "Create Event Error: returned null response"); + } + mServerConnectionOk = false; + callback.accept(null); + } + }) { + + @Override + public Map getHeaders() throws AuthFailureError { + Map params = new HashMap(); + params.put("Content-Type", "application/json; charset=UTF-8"); + params.put("Authorization", "Token " + getStoredToken()); + return params; + } + }; + mQueue.add(req); + return (true); + } + + /** + * Retrieve all events accessible to the logged in user, and pass them to the callback function as a JSONObject + * + * @param callback + * @return true on success or false on failure to initiate the request. + */ + public boolean getEvents(JSONObjectCallback callback) { + Log.v(TAG, "getEvents()"); + String urlStr = mUrlBase + "/api/events/"; + Log.v(TAG, "getEvents(): urlStr=" + urlStr); + final String authtoken = getStoredToken(); + + if (!isLoggedIn()) { + Log.v(TAG, "not logged in - doing nothing"); + return (false); + } + + StringRequest req = new StringRequest(Request.Method.GET, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + Log.v(TAG, "Response is: " + response); + mServerConnectionOk = true; + try { + JSONObject retObj = new JSONObject(); + JSONArray eventArray = new JSONArray(response); + retObj.put("events", eventArray); + callback.accept(retObj); + } catch (JSONException e) { + Log.e(TAG, "getEventTypes.onRespons(): Error: " + e.getMessage() + "," + e.toString()); + callback.accept(null); + } + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + //if ((error != null) && (error.networkResponse != null) && (error.networkResponse.data != null)) {# + mServerConnectionOk = false; + if (error != null) { + if (error.networkResponse != null) { + Log.e(TAG, "getEvents(): Error: " + error.toString() + ", message:" + error.getMessage()); + } else { + Log.e(TAG, "getEvents(): Error: - request returned null networkResponse"); + } + } else{ + Log.e(TAG, "getEvents(): Error: - request returned null response"); + } + callback.accept(null); + } + }) { + + @Override + public Map getHeaders() throws AuthFailureError { + Map params = new HashMap(); + params.put("Content-Type", "application/json; charset=UTF-8"); + params.put("Authorization", "Token " + getStoredToken()); + return params; + } + }; + mQueue.add(req); + return (true); + } + + + public boolean updateEvent(final JSONObject eventObj, JSONObjectCallback callback) { + String eventId; + Log.v(TAG, "updateEvent()"); + final String authtoken = getStoredToken(); + + if (!isLoggedIn()) { + Log.v(TAG, "not logged in - doing nothing"); + return (false); + } + try { + eventId = eventObj.getString("id"); + } catch (JSONException e) { + Log.e(TAG, "updateEvent(): Error reading id from eventObj"); + eventId = null; + } + final String dataStr = eventObj.toString(); + Log.v(TAG, "updateEvent - data=" + dataStr); + + int reqMethod; + String urlStr; + if (eventId != null) { + Log.v(TAG, "updateEvent() - found eventId " + eventId + ", Updating event record"); + urlStr = mUrlBase + "/api/events/" + eventId + "/"; + Log.v(TAG, "urlStr=" + urlStr); + reqMethod = Request.Method.PUT; + } else { + Log.v(TAG, "updateEvent() - eventId not found - creating new event record"); + urlStr = mUrlBase + "/api/events/"; + Log.v(TAG, "urlStr=" + urlStr); + reqMethod = Request.Method.POST; + } + + StringRequest req = new StringRequest(reqMethod, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + Log.v(TAG, "Response is: " + response); + mServerConnectionOk = true; + try { + JSONObject retObj = new JSONObject(response); + callback.accept(retObj); + } catch (JSONException e) { + Log.e(TAG, "getEventTypes.onRespons(): Error: " + e.getMessage() + "," + e.toString()); + callback.accept(null); + } + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + mServerConnectionOk = false; + if (error != null) { + Log.e(TAG, "Create Event Error: " + error.toString() + ", message:" + error.getMessage()); + } else { + Log.e(TAG, "Create Event Error - returned null response"); + } + callback.accept(null); + } + }) { + // Note, this is overriding part of StringRequest, not one of the sub-classes above! + @Override + protected Map getParams() { + Map params = new HashMap<>(); + // params.put("name",sname); // passing parameters to server + String authToken = getStoredToken(); + params.put("Authorization: Token " + authToken, authToken); + Log.v(TAG, "getParams: params=" + params.toString()); + //params.put("eventType", String.valueOf(eventType)); + //params.put("dataTime", dateFormat.format(eventDate)); + //params.put("desc", eventDesc); + return params; + } + + @Override + public Map getHeaders() throws AuthFailureError { + Map params = new HashMap(); + params.put("Content-Type", "application/json; charset=UTF-8"); + params.put("Authorization", "Token " + getStoredToken()); + return params; + } + + @Override + public byte[] getBody() throws AuthFailureError { + try { + return dataStr == null ? null : dataStr.getBytes("utf-8"); + } catch (UnsupportedEncodingException uee) { + VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s", dataStr, "utf-8"); + return null; + } + } + }; + + mQueue.add(req); + return (true); + } + + + public boolean createDatapoint(JSONObject dataObj, String eventId, StringCallback callback) { + Log.v(TAG, "createDatapoint()"); + // Create a new event in the remote database, based on the provided parameters. + String urlStr = mUrlBase + "/api/datapoints/"; + Log.v(TAG, "urlStr=" + urlStr); + final String authtoken = getStoredToken(); + + if (!isLoggedIn()) { + Log.v(TAG, "not logged in - doing nothing"); + return (false); + } + + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + JSONObject jsonObject = new JSONObject(); + try { + //jsonObject.put("userId", -1); + jsonObject.put("eventId", String.valueOf(eventId)); + jsonObject.put("dataTime", dataObj.getString("dataTime")); + jsonObject.put("dataJSON", dataObj.toString()); + } catch (JSONException e) { + Log.e(TAG, "Error generating event JSON string"); + } + final String dataStr = jsonObject.toString(); + Log.v(TAG, "createDatapoint - dataStr=" + dataStr); + + + StringRequest req = new StringRequest(Request.Method.POST, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + Log.v(TAG, "Response is: " + response); + mServerConnectionOk = true; + callback.accept(response); + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + mServerConnectionOk = false; + if (error != null) { + Log.e(TAG, "Create Datapoint Error: " + error.toString() + ", message:" + error.getMessage()); + callback.accept(null); + } else { + Log.e(TAG, "Create Datapoint Error - returned null respones"); + callback.accept(null); + } + } + }) { + // Note, this is overriding part of StringRequest, not one of the sub-classes above! + @Override + protected Map getParams() { + Map params = new HashMap<>(); + // params.put("name",sname); // passing parameters to server + String authToken = getStoredToken(); + params.put("Authorization: Token " + authToken, authToken); + Log.v(TAG, "getParams: params=" + params.toString()); + return params; + } + + @Override + public Map getHeaders() throws AuthFailureError { + Map params = new HashMap(); + params.put("Content-Type", "application/json; charset=UTF-8"); + params.put("Authorization", "Token " + getStoredToken()); + return params; + } + + @Override + public byte[] getBody() throws AuthFailureError { + try { + return dataStr == null ? null : dataStr.getBytes("utf-8"); + } catch (UnsupportedEncodingException uee) { + VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s", dataStr, "utf-8"); + return null; + } + } + }; + + mQueue.add(req); + return (true); + + } + + /** + * Retieve the user profile of the authenticated user from the server, and return it to the callback function. + * @param callback - function to be called with a JSONObject as a parameter that contains the user profile data. + * @return true if request sent successfully, or else false. + */ + public boolean getUserProfile(JSONObjectCallback callback) { + Log.v(TAG, "getUserProfile()"); + String urlStr = mUrlBase + "/api/accounts/profile/"; + Log.v(TAG, "getUserProfile(): urlStr=" + urlStr); + final String authtoken = getStoredToken(); + + if (!isLoggedIn()) { + Log.v(TAG, "not logged in - doing nothing"); + return (false); + } + + StringRequest req = new StringRequest(Request.Method.GET, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + Log.v(TAG, "Response is: " + response); + try { + JSONObject retObj = new JSONObject(response); + callback.accept(retObj); + } catch (JSONException e) { + Log.e(TAG, "getUserProfile.onResponse(): Error: " + e.getMessage() + "," + e.toString()); + callback.accept(null); + } + mServerConnectionOk = true; + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + if (error != null) { + Log.e(TAG, "Create Event Error: " + error.toString() + ", message:" + error.getMessage()); + } else { + Log.e(TAG, "Create Event Error: returned null response"); + } + mServerConnectionOk = false; + callback.accept(null); + } + }) { + + @Override + public Map getHeaders() throws AuthFailureError { + Map params = new HashMap(); + params.put("Content-Type", "application/json; charset=UTF-8"); + params.put("Authorization", "Token " + getStoredToken()); + return params; + } + }; + mQueue.add(req); + return (true); + } + + + + + /** + * Retrieve the file containing the standard event types from the server. + * Calls the specified callback function, passing a JSONObject as a parameter when the data has been received and parsed. + * + * @return true if request sent successfully or else false. + */ + public boolean getEventTypes(JSONObjectCallback callback) { + Log.v(TAG, "getEventTypes()"); + String urlStr = mUrlBase + "/static/eventTypes.json"; + Log.v(TAG, "urlStr=" + urlStr); + final String authtoken = getStoredToken(); + + if (!isLoggedIn()) { + Log.v(TAG, "not logged in - doing nothing"); + return (false); + } + + StringRequest req = new StringRequest(Request.Method.GET, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + Log.v(TAG, "getEventTypes.onResponse(): Response is: " + response); + mServerConnectionOk = true; + try { + JSONObject retObj = new JSONObject(response); + callback.accept(retObj); + } catch (JSONException e) { + Log.e(TAG, "getEventTypes.onRespons(): Error: " + e.getMessage() + "," + e.toString()); + callback.accept(null); + } + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + mServerConnectionOk = false; + if (error != null) { + Log.e(TAG, "getEventTypes.onErrorResponse(): " + error.toString() + ", message:" + error.getMessage()); + } else { + Log.e(TAG, "getEventTypes.onErrorResponse() - returned null response"); + } + callback.accept(null); + } + }) { + // Note, this is overriding part of StringRequest, not one of the sub-classes above! + @Override + public Map getHeaders() throws AuthFailureError { + Map params = new HashMap(); + params.put("Content-Type", "application/json; charset=UTF-8"); + params.put("Authorization", "Token " + getStoredToken()); + return params; + } + }; + + mQueue.add(req); + return (true); + + } + + /** + * Retrieve a trivial file from the server to check we have a good server connection. + * sets mServerConnectionOk. + * @return true if request sent successfully or else false. + */ + public boolean checkServerConnection() { + Log.v(TAG, "checkServerConnection()"); + String urlStr = mUrlBase + "/static/test.txt"; + Log.v(TAG, "urlStr=" + urlStr); + + StringRequest req = new StringRequest(Request.Method.GET, urlStr, + new Response.Listener() { + @Override + public void onResponse(String response) { + Log.v(TAG, "checkServerConnection.onResponse(): Response is: " + response); + mServerConnectionOk = true; + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Log.v(TAG, "checkServerConnection.onErrorResponse"); + mServerConnectionOk = false; + } + }); + + mQueue.add(req); + return (true); + + } + +} diff --git a/app/src/main/res/drawable/datasharing_fault_24x24.png b/app/src/main/res/drawable/datasharing_fault_24x24.png new file mode 100644 index 0000000..84ef630 Binary files /dev/null and b/app/src/main/res/drawable/datasharing_fault_24x24.png differ diff --git a/app/src/main/res/drawable/datasharing_query_24x24.png b/app/src/main/res/drawable/datasharing_query_24x24.png new file mode 100644 index 0000000..d61279d Binary files /dev/null and b/app/src/main/res/drawable/datasharing_query_24x24.png differ diff --git a/app/src/main/res/drawable/star_of_life_query_24x24.png b/app/src/main/res/drawable/star_of_life_query_24x24.png new file mode 100644 index 0000000..b362570 Binary files /dev/null and b/app/src/main/res/drawable/star_of_life_query_24x24.png differ diff --git a/app/src/main/res/drawable/stat_sample.png b/app/src/main/res/drawable/stat_sample.png deleted file mode 100644 index 1733042..0000000 Binary files a/app/src/main/res/drawable/stat_sample.png and /dev/null differ diff --git a/app/src/main/res/layout/about_layout.xml b/app/src/main/res/layout/about_layout.xml index a1c3b06..69c8b0b 100644 --- a/app/src/main/res/layout/about_layout.xml +++ b/app/src/main/res/layout/about_layout.xml @@ -2,11 +2,11 @@ - + - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_authenticate.xml b/app/src/main/res/layout/activity_authenticate.xml new file mode 100644 index 0000000..3f051ab --- /dev/null +++ b/app/src/main/res/layout/activity_authenticate.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + +