Compare commits
11 Commits
9ac505fc07
...
1851fa76e6
| Author | SHA1 | Date | |
|---|---|---|---|
| 1851fa76e6 | |||
| ab2c2e69e6 | |||
| f24e9f2040 | |||
| 4212860dc1 | |||
| 4d9c3009ce | |||
| 0f55f105f8 | |||
| 9892aa43f4 | |||
| 24744abb80 | |||
| d2c1cb0ae3 | |||
| 5f8d6c0585 | |||
| f8c8ba6bf1 |
@@ -0,0 +1,378 @@
|
||||
# OpenSeizureDetector Android App Structure
|
||||
|
||||
This document gives new contributors a fast, high-level map of how the Android application is organised: the major Java classes, resource folders, the startup / shutdown lifecycle, and the data + alarm flow.
|
||||
|
||||
## 1. Top-Level Overview
|
||||
OpenSeizureDetector is an Android foreground-service based application that:
|
||||
- Collects motion (acceleration) and physiological (heart rate, optionally SpO₂) data from a wearable (Garmin, Pebble, BLE devices, phone sensors, etc.).
|
||||
- Analyses incoming samples to detect tonic–clonic seizure patterns.
|
||||
- Raises local (audible) and remote (SMS / phone call) alarms and optionally shares anonymised data with a central server.
|
||||
- Provides a swipe-based main UI (MainActivity2 + fragments) and a startup checklist screen (StartupActivity) to ensure prerequisites are satisfied before normal operation.
|
||||
|
||||
Core runtime logic lives in the `SdServer` foreground service; Activities and Fragments mostly visualize status and manipulate preferences.
|
||||
|
||||
## 2. Startup Lifecycle (Happy Path)
|
||||
1. User taps the launcher icon -> Android launches `StartupActivity` (declared with MAIN/LAUNCHER intent filter in `AndroidManifest.xml`).
|
||||
2. `StartupActivity`:
|
||||
- Applies default preference values from XML (alarm, general, datasource, logging, etc.).
|
||||
- Requests / validates required runtime permissions (notifications, SMS, location, Bluetooth, activity recognition, etc.).
|
||||
- Starts (or restarts) the foreground service `SdServer` via `OsdUtil.startServer()` if not already running.
|
||||
- Binds to the service through `SdServiceConnection` to monitor status (watch connection, settings received, data flowing).
|
||||
- Displays a checklist (ProgressBars + TextViews) updated by a periodic timer until all conditions are OK.
|
||||
- When all OK, transitions to either `MainActivity2` (new UI) or legacy `MainActivity` depending on the `UseNewUi` preference.
|
||||
3. `SdServer.onStartCommand()`:
|
||||
- Calls `updatePrefs()`; selects concrete `SdDataSource*` implementation based on `DataSource` preference.
|
||||
- Instantiates and starts the chosen data source (e.g., `SdDataSourceGarmin`, `SdDataSourceBLE`, `SdDataSourcePhone`, etc.).
|
||||
- Initialises logging (`LogManager`), location (`LocationFinder` if SMS alarms enabled), timers, embedded HTTP server (`SdWebServer`), wake lock, and notification channels; enters foreground (persistent notification).
|
||||
- Begins receiving data; populates `SdData` and runs analysis algorithms (e.g., seizure + heart rate) to update alarm state.
|
||||
4. `MainActivity2` binds to the already running `SdServer` to present live status via Fragments.
|
||||
|
||||
## 3. Shutdown Lifecycle
|
||||
There are several ways the service stops:
|
||||
- User selects an "Exit" / stop option (menu action triggers `OsdUtil.stopServer()`), which calls `stopService` for `SdServer`.
|
||||
- System kills the service (low memory or user force-stop) -> `SdServer.onDestroy()` releases wake lock, stops data source, timers, web server, and cleans up.
|
||||
- Device reboot triggers `BootBroadcastReceiver` which, if `AutoStart` preference is true, launches `StartupActivity` to restart.
|
||||
|
||||
## 4. Major Java Components
|
||||
### Activities
|
||||
- `StartupActivity`: Launcher activity; initial permission + readiness checklist; starts/binds the service; routes to main UI.
|
||||
- `MainActivity2`: Modern, swipe-based interface using `ViewPager2` + Fragments; shows system/algorithm status, data sharing, web server info, heart rate, ML algorithm, battery, watch signal etc.
|
||||
- `MainActivity`: Legacy UI retained for backward compatibility (optionally used if `UseNewUi` is false).
|
||||
- `PrefActivity`: Preferences editor (headers + fragments defined in `res/xml/*prefs.xml`).
|
||||
- `BLEScanActivity`: Discovers and selects BLE devices when using BLE/BLE2 data source.
|
||||
- `AuthenticateActivity`: Handles login for data sharing / remote API.
|
||||
- `LogManagerControlActivity`, `ExportDataActivity`, `RemoteDbActivity`: Data sharing, viewing, exporting, pruning local DB.
|
||||
- `ReportSeizureActivity`, `EditEventActivity`: Manual event reporting / editing.
|
||||
|
||||
### Foreground Service
|
||||
- `SdServer`: Heart of the application. Manages:
|
||||
- Data source selection and life-cycle.
|
||||
- Alarm evaluation (seizure, heart rate, fall, etc.).
|
||||
- Notification updates (different channels for service status, events, data sharing issues).
|
||||
- SMS & phone call alarm orchestration (timers to allow cancellation).
|
||||
- Logging & data sharing upload scheduling.
|
||||
- Embedded HTTP server (`SdWebServer`) for local access.
|
||||
- Wake lock to keep CPU active during monitoring.
|
||||
|
||||
### Data Sources (inherit from / similar pattern to `SdDataSource`)
|
||||
Each encapsulates a communication protocol + parsing logic:
|
||||
- `SdDataSourcePebble` – Legacy Pebble watch integration.
|
||||
- `SdDataSourceAw` – Android Wear devices.
|
||||
- `SdDataSourceGarmin` – Garmin watch app (acceleration + heart rate streams).
|
||||
- `SdDataSourceBLE` / `SdDataSourceBLE2` – Generic BLE device integrations (v1 / v2 protocols for devices like PineTime / BangleJS).
|
||||
- `SdDataSourceNetwork` – Pulls detector data over network from a remote instance.
|
||||
- `SdDataSourcePhone` – Uses phone onboard sensors as the detector.
|
||||
|
||||
### Algorithms & Data Structures
|
||||
- `SdData`: Aggregates current sample values, processing results, and metadata (data source name, versions, etc.).
|
||||
- `SdAlgHr`: Heart rate alarm algorithms (simple threshold, adaptive, average-based).
|
||||
- `SdAlgNn`: Neural network / machine learning based seizure detection (see `FragmentMlAlg` for UI).
|
||||
- `CircBuf`: Circular buffer used for windowed averaging and historical metrics.
|
||||
|
||||
#### Core Analysis Pipeline (`SdDataSource.doAnalysis()`)
|
||||
The principal seizure detection loop lives in the protected method `SdDataSource.doAnalysis()`, called each time a fresh window of accelerometer samples arrives (vector magnitude or derived from 3D data):
|
||||
1. Phone/watch battery percentages are updated and appended to rolling buffers.
|
||||
2. (Currently hard-coded) sample frequency set (e.g. 25 Hz) and frequency resolution derived from window length (`mNsamp`).
|
||||
3. FFT performed on the raw acceleration window using JTransforms `DoubleFFT_1D.realForward`.
|
||||
4. Overall spectrum "power" (`specPower`) accumulated up to a cutoff frequency (`FreqCutoff`), zeroing higher bins to reduce noise.
|
||||
5. Region of Interest (ROI) defined by preferences `AlarmFreqMin`/`AlarmFreqMax`; average ROI power (`roiPower`) and the ratio `roiRatio = 10 * roiPower / specPower` computed.
|
||||
6. Simplified spectrum (`simpleSpec[]`) built in 1 Hz bins for UI visualisation (scaled by `ACCEL_SCALE_FACTOR` to align with historic Pebble scaling).
|
||||
7. Populates fields in `mSdData` (timestamps, `specPower`, `roiPower`, thresholds, ROI freq bounds, simplified spectrum array, flags) and marks `haveData`.
|
||||
8. If the CNN alarm feature is enabled (`mCnnAlarmActive`) then `nnAnalysis()` updates `mPseizure` probability.
|
||||
9. Secondary narrow-band motion check via `flapCheck()` (arm flapping detection) producing a boolean fed into `alarmCheck()`.
|
||||
10. `alarmCheck()` applies thresholds (`AlarmThresh`, `AlarmRatioThresh`) and enabled algorithm flags (classic OSD, flap, CNN) to set alarm cause/state.
|
||||
11. Additional modalities processed: `hrCheck()` (heart rate alarms / frozen HR detection), `o2SatCheck()` (oxygen saturation), `fallCheck()` (fall detection), and `muteCheck()` (user-induced mute logic).
|
||||
12. Result dispatched upstream via `mSdDataReceiver.onSdDataReceived(mSdData)` (SdServer consumes to raise notifications / alarms).
|
||||
|
||||
Key preferences influencing `doAnalysis()`:
|
||||
- `AlarmFreqMin` / `AlarmFreqMax`: ROI frequency band.
|
||||
- `AlarmThresh` / `AlarmRatioThresh`: Power and ratio thresholds for alarm state.
|
||||
- Flap detection thresholds (`FlapThresh`, `FlapRatioThresh`, min/max flap band) when flap alarm active.
|
||||
- Flags enabling algorithms: `mOsdAlarmActive`, `mFlapAlarmActive`, `mCnnAlarmActive`.
|
||||
|
||||
Performance / Extension Notes:
|
||||
- Current implementation re-allocates FFT arrays each call; optimisation could reuse buffers.
|
||||
- Sample frequency is hard-coded (25 Hz) inside analysis; aligning it with dynamic settings from watch would improve fidelity.
|
||||
- Multiple ROIs could be generalised (current flapCheck duplicates spectral processing).
|
||||
- Scaling (`ACCEL_SCALE_FACTOR`) is applied post-hoc; future refactor could normalise early and adopt floating-point consistently for UI.
|
||||
|
||||
#### `doAnalysis()` Invocation & Alarm Propagation
|
||||
`doAnalysis()` is not called in a tight loop by the service; instead each concrete `SdDataSource` decides when a complete window of samples is ready and then invokes it:
|
||||
- BLE (`SdDataSourceBLE` / `SdDataSourceBLE2`): Acceleration notifications fill a raw buffer (`rawData` length 125 = 5s @25Hz). When full, the datasource copies buffered samples into `mSdData`, sets `mNsamp`, calls `doAnalysis()`, then sends a one–byte alarm state back to the device via a status GATT characteristic.
|
||||
- Phone (`SdDataSourcePhone`): Collects accelerometer sensor events, performs crude downsampling from ~50Hz to 25Hz. Once `rawData` is full it triggers `doAnalysis()`, resets counters and continues.
|
||||
- Pebble (`SdDataSourcePebble`): The watch app performs analysis on-device and sends already processed results (including `alarmState`, spectrum) – so `doAnalysis()` is NOT used for Pebble; received data directly calls `mSdDataReceiver.onSdDataReceived`.
|
||||
- Network (`SdDataSourceNetwork`): Fetches remote JSON; if successful passes parsed `SdData` upward. Remote faults set `alarmState` to NET FAULT (7). Local `doAnalysis()` not used.
|
||||
- Garmin (`SdDataSourceGarmin`): Similar pattern (buffer fill -> `doAnalysis()`).
|
||||
|
||||
After `doAnalysis()` completes in a source that uses it:
|
||||
1. Spectral metrics (`roiPower`, `specPower`, simplified spectrum) and timing fields are populated.
|
||||
2. `flapCheck()` optionally computes a narrow-band flap detection boolean.
|
||||
3. `alarmCheck(flapDetected)` applies power & ratio thresholds and accumulates time in alarm (`mAlarmCount += mSamplePeriod`) to transition through:
|
||||
- OK (0) -> WARNING (1) after `mWarnTime` seconds of continuous in-alarm condition.
|
||||
- WARNING (1) -> ALARM (2) after `mAlarmTime` seconds.
|
||||
- Recovery logic: leaving in-alarm state downgrades from ALARM (2) to WARNING (1) (simulating a just-entered warning), or from WARNING (1) to OK (0).
|
||||
4. Other modality checks may elevate alarmState:
|
||||
- `hrCheck()`: If any heart rate alarm stands (simple / adaptive / average) sets `alarmState = 2` and appends cause tags (`HR`, `HR_ADAPT`, `HR_AVG`). Null HR may either cause alarm or fault depending on `mHRNullAsAlarm`.
|
||||
- `o2SatCheck()`: Low or null oxygen saturation (with null-as-alarm enabled) sets standing flags and may escalate to ALARM.
|
||||
- `fallCheck()`: Sets `fallAlarmStanding` and may signal FALL alarm state (3).
|
||||
- `muteCheck()`: Watch/user mute sets `alarmState = 6` (MUTE) overriding other transient states.
|
||||
- Fault timers (`faultCheck()` elsewhere) may set FAULT (4) or NET FAULT (7).
|
||||
5. The datasource calls `mSdDataReceiver.onSdDataReceived(mSdData)` (implemented by `SdServer`).
|
||||
|
||||
##### Alarm State Codes (as observed in code)
|
||||
| Code | Meaning | Origin / Trigger |
|
||||
|------|---------|------------------|
|
||||
| 0 | OK | No current alarm condition or post-recovery. |
|
||||
| 1 | WARNING | Thresholds exceeded for > `warnTime` but < `alarmTime`. |
|
||||
| 2 | ALARM | Thresholds exceeded for > `alarmTime`, or HR/O₂/fall promoted, or HR adaptive/average thresholds stand. |
|
||||
| 3 | FALL | Fall detection logic sets `fallAlarmStanding` or explicit fall state. |
|
||||
| 4 | FAULT | Internal fault (e.g., missing data, HR sensor failure without null-as-alarm). |
|
||||
| 5 | MANUAL ALARM | Raised manually (e.g., `SdServer.raiseManualAlarm()`). |
|
||||
| 6 | MUTE | User/watch initiated mute; prevents audible alarm but maintains monitoring. |
|
||||
| 7 | NET FAULT | Network datasource error / fault condition (`SdDataSourceNetwork`). |
|
||||
|
||||
##### How `SdServer` Reacts (`onSdDataReceived`)
|
||||
`SdServer.onSdDataReceived(sdData)` interprets `alarmState` plus standing flags and performs side-effects:
|
||||
- OK (0): Clears `alarmStanding` unless latched (`mLatchAlarms`) from previous alarm or fall.
|
||||
- MUTE (6): Sets phrase "MUTE", suppresses alarms and notifications severity.
|
||||
- WARNING (1): Plays warning tone (`warningBeep()`), logs (if enabled), updates notification to warning channel/state.
|
||||
- ALARM (2) or MANUAL ALARM (5): Sets phrase "ALARM", raises `alarmStanding`, plays alarm tone (`alarmBeep()`), shows main UI, posts high-severity notification, initiates latch timer (`startLatchTimer()`), and sends SMS / phone alarms if enabled (rate-limited to one per minute).
|
||||
- FALL (3 or `fallAlarmStanding` true): Behaves similarly to ALARM but with phrase "FALL" (alarms + SMS sending). Fall may remain standing until cleared.
|
||||
- HR / O₂ / Adaptive HR / Average HR: These set `alarmState = 2` when standing; `alarmCause` accumulates tokens; downstream handling identical to ALARM.
|
||||
- FAULT (4, 7, HR fault, frozen HR fault): Plays fault warning beep (`faultWarningBeep()`), shows fault notification; may attempt datasource restart after timer (auto-restart currently disabled for BLE2 to prevent duplicate notifications).
|
||||
|
||||
##### Latching & Reset
|
||||
With `mLatchAlarms` enabled, returning to OK does not immediately clear previous ALARM/FALL states; user must manually accept/reset (e.g., via UI actions) or wait for latch timer expiry (`mLatchAlarmTimer`). Without latching, state machine freely transitions downwards.
|
||||
|
||||
##### Data Sharing & Logging Post-Analysis
|
||||
Upon each received dataset, `SdServer` updates internal `mSdData`, pushes it to `SdWebServer` for external viewing, and passes it to `LogManager` (`mLm.updateSdData(mSdData)`), which may create/append local events (especially on transitions into ALARM states) and schedule remote uploads.
|
||||
|
||||
##### Device Feedback
|
||||
BLE/BLE2 write a single-byte alarm state back to the watch/device after analysis (`executeWriteCharacteristic(mStatusChar, statusVal)` or peripheral write) enabling haptic / on-watch UI feedback.
|
||||
Pebble handles its own alarm transitions internally before sending results.
|
||||
|
||||
This separation lets wearable implementations stay lightweight (simple streaming) while centralizing threshold timing, multi-modal fusion, and alarm escalation logic on the phone (except for Pebble legacy analysis).
|
||||
|
||||
### UI Fragments (used in `MainActivity2` ViewPager)
|
||||
- `FragmentCommon`: Overall status & key indicators.
|
||||
- `FragmentOsdAlg`: Seizure algorithm metrics (spectrum ratio, thresholds, raw/processed values).
|
||||
- `FragmentHrAlg`: Heart rate algorithm status & thresholds.
|
||||
- `FragmentMlAlg`: ML model results / confidence scores.
|
||||
- `FragmentBatt`: Watch + phone battery status.
|
||||
- `FragmentSystem`: System info (permissions, service state, logging flags).
|
||||
- `FragmentWatchSig`: Signal quality / connectivity indicators.
|
||||
- `FragmentWebServer`: Local web server URL / status.
|
||||
- `FragmentDataSharing`: Data sharing setup state, counts of local vs remote events.
|
||||
|
||||
### Utilities & Helpers
|
||||
- `OsdUtil`: Starts/stops/binds the service; permission checks; logging helpers; system/environment utilities.
|
||||
- `LogManager`: Handles local + remote logging, event packaging, pruning, and upload scheduling.
|
||||
- `MlModelManager`: Manages loading / inference of ML models (if in use).
|
||||
- `LocationFinder`: Acquires GPS coordinates for SMS alarms.
|
||||
- `WebApiConnection` / `WebApiConnection_firebase` / `WebApiConnection_osdapi`: Remote data sharing / API integrations.
|
||||
- `SdServiceConnection`: Wraps service binding / connection callbacks, exposes convenience methods (`watchConnected()`, `hasSdData()`, `hasSdSettings()`).
|
||||
- `BootBroadcastReceiver`: Auto-start on device boot when preference enabled.
|
||||
- `GattAttributes`: BLE UUID constants and attribute names.
|
||||
- `OsdUncaughtExceptionHandler`: Crash reporting path (uses UCE Handler library).
|
||||
- `SdWebServer`: Lightweight embedded HTTP server (for local status / data access).
|
||||
|
||||
### Data Sharing Module
|
||||
Under `uk/org/openseizuredetector/data/...`:
|
||||
- Repository pattern for authentication: `LoginRepository`, `LoginDataSource`, `LoggedInUser`, `Result` (standard wrapper around success/error).
|
||||
|
||||
## 5. Resource Folder Structure
|
||||
```
|
||||
res/
|
||||
layout/ Activity & Fragment UI XML (e.g., startup_activity, activity_main2, fragment_*).
|
||||
menu/ Action bar & overflow menus (e.g., main_activity_actions.xml).
|
||||
values/ Strings (`strings.xml`), styles, colors, dimensions; base resources.
|
||||
values-XX/ Localized strings (de, es, pl, ru, sl, sv, etc.).
|
||||
drawable/ Icons and graphics (e.g., star_of_life_48x48). Might also include vector assets.
|
||||
xml/ Preference definition files and network security config:
|
||||
- alarm_prefs.xml
|
||||
- basic_prefs.xml
|
||||
- general_prefs.xml
|
||||
- logging_prefs.xml
|
||||
- pebble_datasource_prefs.xml
|
||||
- network_datasource_prefs.xml
|
||||
- network_passive_datasource_prefs.xml
|
||||
- seizure_detector_prefs.xml
|
||||
- preference_headers.xml (groups preferences)
|
||||
- network_security_config.xml
|
||||
```
|
||||
Other notable folders:
|
||||
- `assets/` (if present) – Additional static assets (not heavily used here).
|
||||
- `libs/` – Third-party JARs (e.g., FFT / chart libraries) bundled with the app.
|
||||
|
||||
## 6. Preferences Flow
|
||||
1. XML files under `res/xml` define keys and defaults.
|
||||
2. `StartupActivity.onCreate()` calls `PreferenceManager.setDefaultValues(...)` for each preference file (once per install/version).
|
||||
3. Classes such as `OsdUtil`, `SdServer`, `SdAlgHr` invoke `updatePrefs()` to read `SharedPreferences` (
|
||||
`PreferenceManager.getDefaultSharedPreferences(context)`), caching operational parameters (thresholds, window lengths, flags).
|
||||
4. Preference changes may trigger service restarts or algorithm behavior changes (e.g., enabling SMS alarms requires location permission and `LocationFinder`).
|
||||
|
||||
## 7. Data & Alarm Flow
|
||||
```
|
||||
Wearable / Phone Sensors --> Concrete SdDataSource --> SdServer (receives callbacks) -->
|
||||
Algorithms (SdAlgNn, SdAlgHr, fall detection, etc.) --> Alarm State Transitions -->
|
||||
Audible ToneGenerator / MP3 playback
|
||||
Foreground Notification Updates
|
||||
Timed SMS / Phone Call Alerts (with cancellation window)
|
||||
Data Logging (local DB) / Remote Sharing (LogManager, WebApiConnection*)
|
||||
Web Server exposure (SdWebServer)
|
||||
```
|
||||
Heart rate buffering uses `CircBuf` windows for simple/adaptive thresholding; seizure analysis (frequency spectrum, ratio thresholds) executed inside data source analysis routines (see respective `SdDataSource*` classes).
|
||||
|
||||
## 8. Foreground Service & Resilience
|
||||
- Service runs in foreground with a persistent notification (required for stable long-running monitoring on modern Android).
|
||||
- Wake lock prevents CPU sleep during monitoring sessions (battery intensive but improves reliability).
|
||||
- Timers manage periodic tasks (event validation checks, remote upload scheduling, alarm muting windows).
|
||||
- Boot auto-start via broadcast receiver ensures continuity if user opted in.
|
||||
|
||||
## 9. Adding a New Data Source (High-Level Guide)
|
||||
1. Create a new `SdDataSource<YourDevice>` class implementing the expected interface / callback pattern (see existing sources for template).
|
||||
2. Handle connection, authentication/handshake, data parsing, and call back into `SdServer` with new samples.
|
||||
3. Add a selection case in `SdServer.onStartCommand()` for your `DataSource` preference string.
|
||||
4. Provide any additional preferences XML (e.g., update period, device address) and add them to default initialization in `StartupActivity`.
|
||||
5. Update UI fragments if device supplies new metrics.
|
||||
|
||||
## 10. Where to Look for Key Logic
|
||||
- Service lifecycle & alarm orchestration: `SdServer.java` (`onStartCommand`, `onDestroy`, timers, notifications).
|
||||
- Startup readiness checklist: `StartupActivity.serverStatusRunnable`.
|
||||
- Data source selection: `SdServer.onStartCommand()` switch over `mSdDataSourceName`.
|
||||
- Heart rate algorithms: `SdAlgHr.java`.
|
||||
- ML / seizure algorithm UI: `FragmentMlAlg.java` + `SdAlgNn.java`.
|
||||
- Permission checks: `StartupActivity` & `OsdUtil` (Bluetooth, activity recognition, SMS, location).
|
||||
- Logging & data sharing: `LogManager.java`, `RemoteDbActivity.java`, `FragmentDataSharing.java`.
|
||||
- Embedded web server logic: `SdWebServer.java`.
|
||||
|
||||
## 11. Key Preference Examples (Selected)
|
||||
| Preference Key | Purpose |
|
||||
| -------------- | ------- |
|
||||
| `DataSource` | Selects which device source (Pebble, Garmin, BLE, Phone, Network). |
|
||||
| `AlarmThresh` / `AlarmRatioThresh` | Seizure detection thresholds (spectrum amplitude / ratio). |
|
||||
| `HRThreshMin` / `HRThreshMax` | Simple heart rate alarm bounds. |
|
||||
| `HRAdaptiveAlarmWindowSecs` | Window size for adaptive HR average buffering. |
|
||||
| `SMSAlarm` / `PhoneCallAlarm` | Enable remote alerts. |
|
||||
| `LogData` / `LogDataRemote` | Enable local logging vs remote data sharing. |
|
||||
| `UseNewUi` | Switch between legacy and modern main UI. |
|
||||
| `AutoStart` | Auto-launch on device boot. |
|
||||
|
||||
(See `res/xml/*_prefs.xml` for full list.)
|
||||
|
||||
## 12. Notifications & Timers
|
||||
- Multiple channels for service status and events (IDs inside `SdServer`).
|
||||
- Timers: `FaultTimer`, `CheckEventsTimer`, SMS countdown (`SmsTimer`), latch alarm timer, etc., each controlling asynchronous transitions.
|
||||
|
||||
## 13. Data Sharing Flow
|
||||
1. User authenticates (`AuthenticateActivity`) -> obtains token stored in preferences.
|
||||
2. `LogManager` packages events (timestamped, with retention pruning) and attempts periodic uploads (`remoteLogPeriod`).
|
||||
3. Unvalidated remote events prompt UI reminders (`FragmentDataSharing`).
|
||||
|
||||
## 14. Crash Handling
|
||||
`UCEHandler` integrated in Activities and Service to capture uncaught exceptions and offer sending logs via email.
|
||||
|
||||
## 15. Embedded Web Server
|
||||
`SdWebServer` exposes (read-only) status / logged data for local network access; started automatically by `SdServer` after data source initialisation.
|
||||
|
||||
## 16. Stopping / Restarting Service Programmatically
|
||||
- Stop: `OsdUtil.stopServer()` -> calls `stopService(Intent(SdServer))`.
|
||||
- Start: `OsdUtil.startServer()` -> `Context.startForegroundService(...)` (on modern Android) then service builds notification & begins monitoring.
|
||||
- Restart triggered implicitly if critical permissions change (logic can call stop/start to reinitialise components).
|
||||
|
||||
## 17. Extending Alarms / Algorithms
|
||||
To add new alarm logic (e.g., oxygen saturation):
|
||||
1. Introduce algorithm class (`SdAlgO2` style) storing buffers and thresholds.
|
||||
2. Update data source parsing to capture new metric.
|
||||
3. Integrate into `SdServer` evaluation loop; amend notification text generation.
|
||||
4. Provide preference keys + XML + UI Fragment display.
|
||||
|
||||
## 18. Glossary
|
||||
- "Latch Alarm": Alarm remains active until explicitly reset (even if underlying condition clears) for a configured duration.
|
||||
- "Adaptive HR Alarm": Builds a moving average; raises alarm when HR deviates beyond +/- configurable delta.
|
||||
- "Foreground Service": Long-lived component with persistent notification; less likely to be killed.
|
||||
- "Data Sharing": User-consented upload of anonymised seizure events / sensor data to central server for algorithm improvement.
|
||||
|
||||
## 19. Useful Entry Points for Debugging
|
||||
- Set breakpoints in `SdServer.onStartCommand()` to inspect data source initialisation.
|
||||
- Use logs emitted via `OsdUtil.writeToSysLogFile` to trace state transitions.
|
||||
- Inspect `StartupActivity.serverStatusRunnable` for readiness gating issues.
|
||||
|
||||
## 20. Related Documents
|
||||
- `README.md`: General project overview, build instructions.
|
||||
- `DEV_NOTES.txt`: Developer notes / historical comments.
|
||||
- `BLE_Datasource_Specification.md`: Protocol specifics for BLE devices.
|
||||
- PDFs under `doc/` for algorithm assessment and app structure diagrams.
|
||||
|
||||
## 21. End-to-End Data -> Alarm Flow Diagram
|
||||
Below are two complementary diagrams (ASCII and Mermaid) showing how a data window travels from the wearable to an alarm being raised.
|
||||
|
||||
PNG Version: See `FLOW_DIAGRAM.png` in the repository root for a downloadable image.
|
||||
|
||||
ASCII Flow
|
||||
```
|
||||
Wearable Sensors (Accel / HR / O₂)
|
||||
|
|
||||
v
|
||||
Watch Firmware / Device App
|
||||
| (Pebble: does analysis + sends results)
|
||||
| (BLE/Garmin/PineTime/etc.: streams raw samples)
|
||||
v
|
||||
SdDataSource (Buffer + Parse + Downsample/Scale)
|
||||
| (Collect ~125 samples = 5s @25Hz)
|
||||
v (window full)
|
||||
doAnalysis()
|
||||
|-> FFT (JTransforms) & Spectrum Power (specPower)
|
||||
|-> ROI Power & Ratio (roiPower / roiRatio)
|
||||
|-> Simplified Spectrum (simpleSpec[])
|
||||
|-> flapCheck() narrow band detection
|
||||
|-> nnAnalysis() (if CNN enabled)
|
||||
|-> hrCheck(), o2SatCheck(), fallCheck(), muteCheck()
|
||||
v
|
||||
Populate mSdData (specPower, roiPower, simpleSpec, alarmState, alarmCause, metrics)
|
||||
v
|
||||
mSdDataReceiver.onSdDataReceived(mSdData)
|
||||
v
|
||||
SdServer.onSdDataReceived()
|
||||
|-> Alarm state machine (OK/WARNING/ALARM/FALL/FAULT/MUTE)
|
||||
|-> Latching logic (startLatchTimer if ALARM)
|
||||
|-> Notifications (foreground + event channels)
|
||||
|-> Tones (warningBeep / alarmBeep / faultWarningBeep)
|
||||
|-> SMS / Phone Call (rate-limited, if enabled)
|
||||
|-> Logging & Data Sharing (LogManager update, remote upload scheduling)
|
||||
|-> Web Server update (SdWebServer.setSdData)
|
||||
|-> Write alarmState byte back to device (BLE/Garmin)
|
||||
v
|
||||
UI Fragments / Web Server / Remote API Consumers
|
||||
```
|
||||
|
||||
Mermaid Diagram (optional rendering if supported):
|
||||
```mermaid
|
||||
flowchart TD
|
||||
W[Wearable Sensors\n(Accel / HR / O₂)] --> WF[Watch Firmware / Device App]
|
||||
WF --> DS{SdDataSource\nBuffer & Parse}
|
||||
DS -->|Window Full| AN[doAnalysis()]
|
||||
AN --> FFT[FFT + Spectrum]
|
||||
FFT --> ROI[ROI Power & Ratio]
|
||||
ROI --> ALG[flapCheck / nnAnalysis / hrCheck / o2SatCheck / fallCheck / muteCheck]
|
||||
ALG --> SD[SdData Populated\n(alarmState, metrics)]
|
||||
SD --> RCV[mSdDataReceiver.onSdDataReceived]
|
||||
RCV --> SRV[SdServer.onSdDataReceived]
|
||||
SRV --> ACT[Alarm Actions\nNotification / Tone / SMS / Phone / Latch / Log]
|
||||
SRV --> FEED[Write alarmState\nback to Device]
|
||||
ACT --> UI[UI Fragments / Web Server / Data Sharing]
|
||||
WF -. Pebble path (analysis on watch) .-> SD
|
||||
```
|
||||
Notes:
|
||||
- Pebble path bypasses local `doAnalysis()`; analysis executes on the watch and sets `alarmState` before dispatch.
|
||||
- Network datasource substitutes "Buffer & Parse" with remote JSON fetch and may directly set NET FAULT (7) on failure.
|
||||
- Latching prevents immediate clearing of ALARM/FALL states until user intervention or timer expiry.
|
||||
|
||||
## 22. Related Documents
|
||||
- `README.md`: General project overview, build instructions.
|
||||
- `DEV_NOTES.txt`: Developer notes / historical comments.
|
||||
- `BLE_Datasource_Specification.md`: Protocol specifics for BLE devices.
|
||||
- PDFs under `doc/` for algorithm assessment and app structure diagrams.
|
||||
|
||||
---
|
||||
Questions / Improvements: Feel free to open issues or pull requests on GitHub.
|
||||
@@ -1,5 +1,11 @@
|
||||
OpenSeizureDetector Android App - Change Log
|
||||
============================================
|
||||
V4.3.1 - Fixed corrupted user interface issues on Android 15 and Android 16.
|
||||
V4.3.0 - 2025-07-16
|
||||
- Added support for Android 15 (API 35) to allow publishing on Play Store.
|
||||
- Improved the data sharing screen to show grouped events to reduce the number of events that need to be edited.
|
||||
V4.2.12 - Fixed crash when pressing 'Install Watch App' button by hiding the button if the Pebble data source is not selected
|
||||
- Added a 'Help' and 'Troubleshooting' button and menu item to draw users' attention to the web site instructions.
|
||||
V4.2.11 - Updated permissions handling to support Android 14 (needed to publish on Play Store)
|
||||
- added a crude 'flap' detector into OSD Algorithm
|
||||
- Added setting to change the delay before SMS alert is sent (Issue #202)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -5,6 +5,8 @@ This repository contains the source code for the main
|
||||
Android App, which is published on
|
||||
(Google Play Store)[https://play.google.com/store/apps/details?id=uk.org.openseizuredetector].
|
||||
|
||||
For a detailed architectural overview (activities, service, data flow, resources) see APP_STRUCTURE.md.
|
||||
|
||||
This seizure detector uses a Garmin smart watch to collect movement (acceleration) and heart rate data which is used to detect tonic-clonic epileptic seizures.
|
||||
See (the OpenSeizureDetector Web Site)[https://www.openseizuredetector.org.uk/] for more details.
|
||||
|
||||
|
||||
+3
-2
@@ -1,13 +1,14 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
android {
|
||||
compileSdk 34 // Android 14
|
||||
compileSdk 36
|
||||
// Android 14
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "uk.org.openseizuredetector"
|
||||
minSdkVersion 23 // Android 6
|
||||
targetSdkVersion 34 // Android 14 = 34
|
||||
targetSdkVersion 35 // Android 15 = 35
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="149"
|
||||
android:versionName="4.2.11e">
|
||||
android:versionCode="154"
|
||||
android:versionName="4.3.1">
|
||||
<!-- android:allowBackup="false" -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
@@ -38,6 +41,7 @@ public class EditEventActivity extends AppCompatActivity {
|
||||
private String mEventTypeStr = null;
|
||||
private String mEventSubTypeStr = null;
|
||||
private String mEventId;
|
||||
private ArrayList<String> mEventIds; // For group editing
|
||||
private String mEventNotes = "";
|
||||
//private Date mEventDateTime;
|
||||
private RadioGroup mEventTypeRg;
|
||||
@@ -52,6 +56,22 @@ public class EditEventActivity extends AppCompatActivity {
|
||||
Log.v(TAG, "onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit_event);
|
||||
// Handle system window insets for all API levels
|
||||
View rootView = findViewById(R.id.root_layout_edit_event);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
|
||||
// Get the system bar insets
|
||||
int top = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
|
||||
int bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||
|
||||
// Apply padding to your main content view
|
||||
LinearLayout content = findViewById(R.id.edit_event_content_layout);
|
||||
content.setPadding(0, top, 0, bottom);
|
||||
|
||||
// Return the insets so they keep propagating
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
|
||||
mUtil = new OsdUtil(getApplicationContext(), serverStatusHandler);
|
||||
mConnection = new SdServiceConnection(getApplicationContext());
|
||||
|
||||
@@ -61,8 +81,15 @@ public class EditEventActivity extends AppCompatActivity {
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
String eventId = extras.getString("eventId");
|
||||
mEventId = eventId;
|
||||
mEventIds = extras.getStringArrayList("eventIds");
|
||||
if (mEventIds != null && !mEventIds.isEmpty()) {
|
||||
Log.v(TAG, "onCreate - Group Edit - eventIds=" + mEventIds.toString());
|
||||
mEventId = mEventIds.get(0);
|
||||
} else {
|
||||
Log.v(TAG, "onCreate - Single Edit - eventId=" + extras.getString("eventId"));
|
||||
mEventId = extras.getString("eventId");
|
||||
mEventIds = null;
|
||||
}
|
||||
Log.v(TAG, "onCreate - mEventId=" + mEventId);
|
||||
}
|
||||
|
||||
@@ -297,6 +324,7 @@ public class EditEventActivity extends AppCompatActivity {
|
||||
}
|
||||
Log.v(TAG, "onOK() - eventObj=" + mEventObj.toString());
|
||||
|
||||
// First we just save the open event, irrespective of whether it is a group edit or not.
|
||||
try {
|
||||
mWac.updateEvent(mEventObj, new WebApiConnection.JSONObjectCallback() {
|
||||
@Override
|
||||
@@ -320,9 +348,58 @@ public class EditEventActivity extends AppCompatActivity {
|
||||
mUtil.showToast("Error Updating Event");
|
||||
updateUi();
|
||||
}
|
||||
|
||||
// If this is a group edit, we need to update the other events in the group.
|
||||
if (mEventIds != null && mEventIds.size() > 1) {
|
||||
Log.v(TAG, "onOK() - Group Edit - updating other events in group");
|
||||
updateGroupEventsSequentially(0);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private void updateGroupEventsSequentially(final int index) {
|
||||
if (mEventIds == null || index >= mEventIds.size()) {
|
||||
Log.v(TAG, "updateGroupEventsSequentially - All events updated");
|
||||
return;
|
||||
}
|
||||
final String eventId = mEventIds.get(index);
|
||||
mWac.getEvent(eventId, new WebApiConnection.JSONObjectCallback() {
|
||||
@Override
|
||||
public void accept(JSONObject eventObj) {
|
||||
if (eventObj == null) {
|
||||
Log.e(TAG, "updateGroupEventsSequentially - ERROR: could not retrieve event " + eventId);
|
||||
mUtil.showToast("Error Retrieving Event " + eventId);
|
||||
updateGroupEventsSequentially(index + 1);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
eventObj.put("id", eventId);
|
||||
eventObj.put("type", mEventObj.getString("type"));
|
||||
eventObj.put("subType", mEventObj.getString("subType"));
|
||||
eventObj.put("desc", mEventObj.getString("desc"));
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "updateGroupEventsSequentially - ERROR: " + e.getMessage());
|
||||
updateGroupEventsSequentially(index + 1);
|
||||
return;
|
||||
}
|
||||
mWac.updateEvent(eventObj, new WebApiConnection.JSONObjectCallback() {
|
||||
@Override
|
||||
public void accept(JSONObject updatedObj) {
|
||||
if (updatedObj == null) {
|
||||
Log.e(TAG, "updateGroupEventsSequentially - ERROR: update failed for " + eventId);
|
||||
mUtil.showToast("Error Updating Event " + eventId);
|
||||
} else {
|
||||
Log.v(TAG, "updateGroupEventsSequentially - Updated event " + eventId + " OK");
|
||||
mUtil.showToast("Event " + eventId + " Updated OK");
|
||||
}
|
||||
updateGroupEventsSequentially(index + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
RadioGroup.OnCheckedChangeListener onEventTypeChange =
|
||||
new RadioGroup.OnCheckedChangeListener() {
|
||||
|
||||
@@ -13,6 +13,8 @@ import android.os.AsyncTask;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
@@ -22,6 +24,7 @@ import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TimePicker;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
@@ -83,6 +86,21 @@ public class ExportDataActivity extends AppCompatActivity
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_dbquery);
|
||||
// Handle system window insets for all API levels
|
||||
View rootView = findViewById(R.id.root_layout_export_data);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
|
||||
// Get the system bar insets
|
||||
int top = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
|
||||
int bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||
|
||||
// Apply padding to your main content view
|
||||
LinearLayout content = findViewById(R.id.export_data_content_layout);
|
||||
content.setPadding(0, top, 0, bottom);
|
||||
|
||||
// Return the insets so they keep propagating
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
|
||||
mHandler = new Handler();
|
||||
mUtil = new OsdUtil(this, mHandler);
|
||||
|
||||
@@ -12,11 +12,15 @@ import android.os.CountDownTimer;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.MenuCompat;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@@ -42,18 +46,23 @@ import java.lang.reflect.Field;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class LogManagerControlActivity extends AppCompatActivity {
|
||||
private String TAG = "LogManagerControlActivity";
|
||||
private static final long GROUPING_WINDOW_MINUTES = 3;
|
||||
private static final long GROUPING_WINDOW_MS = GROUPING_WINDOW_MINUTES * 60 * 1000;
|
||||
private LogManager mLm;
|
||||
private Context mContext;
|
||||
private UiTimer mUiTimer;
|
||||
private ArrayList<HashMap<String, String>> mEventsList;
|
||||
private ArrayList<HashMap<String, String>> mRemoteEventsList;
|
||||
private ArrayList<ArrayList<HashMap<String, String>>> mGroupedRemoteEventsList; // Each item is a list of event objects, similar to mRemoteEventsList
|
||||
private ArrayList<HashMap<String, String>> mSysLogList;
|
||||
private SdServiceConnection mConnection;
|
||||
private OsdUtil mUtil;
|
||||
@@ -62,6 +71,7 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
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 Menu mMenu;
|
||||
private CheckBox mGroupEventsCb; // Declare the CheckBox member
|
||||
//private Integer UI_MODE_LOCAL = 0;
|
||||
//private Integer UI_MODE_SHARED = 1;
|
||||
//private Integer mUiMode = UI_MODE_SHARED;
|
||||
@@ -83,6 +93,20 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
|
||||
setContentView(R.layout.activity_log_manager_control);
|
||||
|
||||
// Handle system window insets for all API levels
|
||||
View rootView = findViewById(R.id.root_layout_log_manager_control);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
|
||||
// Get the system bar insets
|
||||
int top = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
|
||||
int bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||
|
||||
// Apply padding to your main content view
|
||||
LinearLayout content = findViewById(R.id.log_manager_control_content_layout); // Add this ID to your LinearLayout
|
||||
content.setPadding(0, top, 0, bottom);
|
||||
|
||||
// Return the insets so they keep propagating
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
/* Force display of overflow menu - from stackoverflow
|
||||
* "how to force use of..."
|
||||
*/
|
||||
@@ -117,6 +141,21 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
(CheckBox) findViewById(R.id.include_nda_cb);
|
||||
includeNDACb.setOnCheckedChangeListener(onIncludeNDACb);
|
||||
|
||||
mGroupEventsCb = findViewById(R.id.group_events_cb);
|
||||
mGroupEventsCb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
// When the checkbox state changes, re-process and update the UI
|
||||
if (mRemoteEventsList != null && !mRemoteEventsList.isEmpty()) {
|
||||
if (isChecked) {
|
||||
createGroupedEventsList();
|
||||
}
|
||||
updateUi(); // Update UI to reflect grouped or non-grouped list
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ListView lv = (ListView) findViewById(R.id.eventLogListView);
|
||||
lv.setOnItemClickListener(onEventListClick);
|
||||
|
||||
@@ -226,6 +265,7 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
|
||||
private void getRemoteEvents(boolean includeWarnings, boolean includeNDA) {
|
||||
mRemoteEventsList = null; // clear existing data
|
||||
mGroupedRemoteEventsList = null;
|
||||
// Retrieve events from remote database
|
||||
mLm.mWac.getEvents((JSONObject remoteEventsObj) -> {
|
||||
Log.v(TAG, "getRemoteEvents()");
|
||||
@@ -252,7 +292,7 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
String dataTime = "null";
|
||||
if (!eventObj.isNull("dataTime")) {
|
||||
dataTime = eventObj.getString("dataTime");
|
||||
Log.v(TAG, "getRemoteEvents() - dataTime=" + dataTime);
|
||||
//Log.v(TAG, "getRemoteEvents() - dataTime=" + dataTime);
|
||||
}
|
||||
String typeStr = "null";
|
||||
if (!eventObj.isNull("type")) {
|
||||
@@ -281,18 +321,142 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
Log.v(TAG, "getRemoteEvents - skipping warning or NDA record");
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "getRemoteEvents() - set mRemoteEventsList(). Updating UI");
|
||||
|
||||
// Sort the remote events list by date, descending (newest first)
|
||||
Log.v(TAG, "getRemoteEvents() - Sorting mRemoteEventsList by date");
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); // Adjust format if needed
|
||||
Collections.sort(mRemoteEventsList, (event1, event2) -> {
|
||||
try {
|
||||
String dt1Str = event1.get("dataTime");
|
||||
String dt2Str = event2.get("dataTime");
|
||||
if (
|
||||
dt1Str == null
|
||||
|| dt2Str == null
|
||||
|| dt1Str.equals("null")
|
||||
|| dt2Str.equals("null"))
|
||||
return 0;
|
||||
Date date1 = sdf.parse(dt1Str);
|
||||
Date date2 = sdf.parse(dt2Str);
|
||||
return date2.compareTo(date1); // Descending
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Error parsing date for sorting: " + e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
if (mGroupEventsCb.isChecked()) { // Check if grouping is enabled
|
||||
createGroupedEventsList();
|
||||
Log.v(TAG, "getRemoteEvents() - created grouped events. Updating UI");
|
||||
} else {
|
||||
mGroupedRemoteEventsList = null; // Ensure grouped list is cleared if not used
|
||||
Log.v(TAG, "getRemoteEvents() - grouping disabled. Updating UI with flat list.");
|
||||
}
|
||||
updateUi();
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "getRemoteEvents(): Error Parsing remoteEventsObj: " + e.getMessage());
|
||||
mUtil.showToast("Error Parsing remoteEventsObj - this should not happen!!!");
|
||||
mRemoteEventsList = null;
|
||||
mGroupedRemoteEventsList = null;
|
||||
updateUi(); // Update UI to show error state
|
||||
}
|
||||
//Log.v(TAG, "getRemoteEvents(): mRemoteEventsList = " + mRemoteEventsList.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* createGroupedEventsList()
|
||||
* Reads the complete list of remote events mRemoteEventsList and creates a new list mGroupedRemoteEventsList
|
||||
* where each item is a list of events that comprise a group based on time (all events within a 3 minute period are grouped together).
|
||||
*/
|
||||
private void createGroupedEventsList() {
|
||||
Log.i(TAG, "createGroupedEventsList()");
|
||||
/**
|
||||
* createGroupedEventsList()
|
||||
* Reads the complete list of remote events mRemoteEventsList (sorted newest first)
|
||||
* and creates a new list mGroupedRemoteEventsList
|
||||
* where each item is a list of events that comprise a group based on time.
|
||||
*/
|
||||
mGroupedRemoteEventsList = new ArrayList<>();
|
||||
if (mRemoteEventsList == null || mRemoteEventsList.isEmpty()) {
|
||||
Log.i(TAG, "createGroupedEventsList() - mRemoteEventsList is null or empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to parse date strings to long timestamps.
|
||||
// Adjust the SimpleDateFormat pattern to match your "dataTime" format.
|
||||
// If "dataTime" is already a timestamp (long), you can use it directly.
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); // Example format
|
||||
|
||||
ArrayList<HashMap<String, String>> currentGroup = null;
|
||||
long lastEventTimeInGroup = 0;
|
||||
|
||||
for (HashMap<String, String> event : mRemoteEventsList) {
|
||||
String dataTimeString = event.get("dataTime");
|
||||
if (dataTimeString == null || dataTimeString.equals("null")) {
|
||||
Log.w(TAG, "Event has null or invalid dataTime: " + event.get("id"));
|
||||
continue; // Skip events with no valid time
|
||||
}
|
||||
|
||||
long currentEventTime;
|
||||
try {
|
||||
Date eventDate = sdf.parse(dataTimeString);
|
||||
if (eventDate == null) {
|
||||
Log.w(TAG, "Could not parse dataTime: " + dataTimeString + " for event: " + event.get("id"));
|
||||
continue;
|
||||
}
|
||||
currentEventTime = eventDate.getTime();
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "Error parsing date string: " + dataTimeString + " - " + e.getMessage());
|
||||
continue; // Skip if date can't be parsed
|
||||
}
|
||||
|
||||
if (currentGroup == null || (lastEventTimeInGroup - currentEventTime) > GROUPING_WINDOW_MS) {
|
||||
// Start a new group
|
||||
if (currentGroup != null) {
|
||||
moveFirstAlarmToFront(currentGroup); // Move the first ALARM event to the front of the group)
|
||||
mGroupedRemoteEventsList.add(currentGroup);
|
||||
}
|
||||
currentGroup = new ArrayList<>();
|
||||
currentGroup.add(event);
|
||||
lastEventTimeInGroup = currentEventTime;
|
||||
} else {
|
||||
// Add to the current group
|
||||
currentGroup.add(event);
|
||||
// lastEventTimeInGroup remains the time of the first event added to this group (newest)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last group if it exists
|
||||
if (currentGroup != null && !currentGroup.isEmpty()) {
|
||||
moveFirstAlarmToFront(currentGroup); // Move the first ALARM event to the front of the group
|
||||
mGroupedRemoteEventsList.add(currentGroup);
|
||||
}
|
||||
|
||||
Log.i(TAG, "createGroupedEventsList() - Grouped " + mRemoteEventsList.size() +
|
||||
" events into " + mGroupedRemoteEventsList.size() + " groups.");
|
||||
}
|
||||
|
||||
/**
|
||||
* moveFirstAlarmToFront() - This method checks the group for the first
|
||||
* event with an ALARM state (osdAlarmState = 2) and makes that event the
|
||||
* first in the list.
|
||||
*
|
||||
* @param group An ArrayList of HashMaps representing a group of events.
|
||||
*/
|
||||
private void moveFirstAlarmToFront(ArrayList<HashMap<String, String>> group) {
|
||||
//Log.i(TAG, "moveFirstAlarmToFront() - Checking group of size: " + group.size());
|
||||
for (int i = 0; i < group.size(); i++) {
|
||||
HashMap<String, String> event = group.get(i);
|
||||
String alarmStateStr = event.get("osdAlarmState");
|
||||
if (alarmStateStr != null && alarmStateStr.equals("2")) { // ALARM is 2
|
||||
//Log.v(TAG," moveFirstAlarmToFront() - Found ALARM event at index: " + i);
|
||||
if (i != 0) {
|
||||
group.remove(i);
|
||||
group.add(0, event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUi() {
|
||||
Log.i(TAG, "updateUi()");
|
||||
@@ -342,13 +506,27 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
pb.setIndeterminate(false);
|
||||
pb.setVisibility(View.INVISIBLE);
|
||||
ListView lv = (ListView) findViewById(R.id.remoteEventsLv);
|
||||
|
||||
if (mGroupEventsCb.isChecked() && mGroupedRemoteEventsList != null) {
|
||||
// Show only the first event of each group
|
||||
ArrayList<HashMap<String, String>> displayList = new ArrayList<>();
|
||||
for (ArrayList<HashMap<String, String>> group : mGroupedRemoteEventsList) {
|
||||
displayList.add(group.get(0));
|
||||
}
|
||||
ListAdapter adapter = new RemoteEventsAdapter(LogManagerControlActivity.this, displayList, 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);
|
||||
} else if (mRemoteEventsList != null) {
|
||||
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 {
|
||||
Log.i(TAG, "UpdateUi: No Remote Events");
|
||||
}
|
||||
} else {
|
||||
//mUtil.showToast("No Remote Events");
|
||||
Log.i(TAG, "UpdateUi: No Remote Events");
|
||||
@@ -645,15 +823,26 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
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
|
||||
|
||||
if (mGroupEventsCb.isChecked() && mGroupedRemoteEventsList != null) {
|
||||
Log.v(TAG,"onItemClickListener() - Creating Grouped Events List from Position=" + position);
|
||||
// Get the group for this position
|
||||
ArrayList<HashMap<String, String>> group = mGroupedRemoteEventsList.get(position);
|
||||
ArrayList<String> eventIds = new ArrayList<>();
|
||||
for (HashMap<String, String> event : group) {
|
||||
Log.v(TAG,"onItemClickListener() - Adding event to edit list: " + event.get("id"));
|
||||
eventIds.add(event.get("id"));
|
||||
}
|
||||
Intent i = new Intent(getApplicationContext(), EditEventActivity.class);
|
||||
i.putStringArrayListExtra("eventIds", eventIds);
|
||||
startActivity(i);
|
||||
} else {
|
||||
Log.v(TAG,"onItemClickListener() - Editing Single event at Position=" + position);
|
||||
HashMap<String, String> eventObj = (HashMap<String, String>) adapter.getItemAtPosition(position);
|
||||
String eventId = eventObj.get("uploaded");
|
||||
Log.d(TAG, "onItemClickListener(): eventId=" + eventId + ", eventObj=" + eventObj);
|
||||
if (eventId != null) {
|
||||
String eventId = eventObj.get("id");
|
||||
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");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -662,13 +851,31 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
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
|
||||
Log.v(TAG, "onItemClickListener() - Position=" + position + ", id=" + id);// Confirmation dialog based on: https://stackoverflow.com/a/12213536/2104584
|
||||
|
||||
if (mGroupEventsCb.isChecked() && mGroupedRemoteEventsList != null) {
|
||||
Log.v(TAG,"onItemClickListener() - Creating Grouped Events List from Position=" + position);
|
||||
// Get the group for this position
|
||||
ArrayList<HashMap<String, String>> group = mGroupedRemoteEventsList.get(position);
|
||||
ArrayList<String> eventIds = new ArrayList<>();
|
||||
for (HashMap<String, String> event : group) {
|
||||
Log.v(TAG,"onItemClickListener() - Adding event to edit list: " + event.get("id"));
|
||||
eventIds.add(event.get("id"));
|
||||
}
|
||||
Intent i = new Intent(getApplicationContext(), EditEventActivity.class);
|
||||
i.putStringArrayListExtra("eventIds", eventIds);
|
||||
startActivity(i);
|
||||
} else {
|
||||
Log.v(TAG,"onItemClickListener() - Editing Single event at Position=" + position);
|
||||
HashMap<String, String> eventObj = (HashMap<String, String>) 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -776,6 +983,7 @@ public class LogManagerControlActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void showDataSharingDialog() {
|
||||
mUtil.writeToSysLogFile("MainActivity.showDataSharingDialog()");
|
||||
View aboutView = getLayoutInflater().inflate(R.layout.data_sharing_dialog_layout, null, false);
|
||||
|
||||
@@ -2,7 +2,9 @@ package uk.org.openseizuredetector;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.MenuCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
@@ -18,11 +20,13 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.rohitss.uceh.UCEHandler;
|
||||
@@ -53,6 +57,22 @@ public class MainActivity2 extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main2);
|
||||
|
||||
// Handle system window insets for all API levels
|
||||
View rootView = findViewById(R.id.activity_main2_root_layout);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
|
||||
// Get the system bar insets
|
||||
int top = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
|
||||
int bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||
|
||||
// Apply padding to your main content view
|
||||
LinearLayout content = findViewById(R.id.activity_main2_content_layout);
|
||||
content.setPadding(0, top, 0, bottom);
|
||||
|
||||
// Return the insets so they keep propagating
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
|
||||
Log.i(TAG, "onCreate()");
|
||||
|
||||
// Set our custom uncaught exception handler to report issues.
|
||||
@@ -292,6 +312,34 @@ public class MainActivity2 extends AppCompatActivity {
|
||||
Log.i(TAG, "exception starting settings activity " + ex.toString());
|
||||
}
|
||||
return true;
|
||||
case R.id.action_instructions:
|
||||
Log.i(TAG, "action_instructions");
|
||||
try {
|
||||
String url = "https://www.openseizuredetector.org.uk/?page_id=1894";
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(url));
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
} catch (Exception ex) {
|
||||
Log.v(TAG, "exception displaying instructions " + ex.toString());
|
||||
mUtil.showToast("ERROR Displaying Instructions");
|
||||
}
|
||||
return true;
|
||||
|
||||
case R.id.action_troubleshooting:
|
||||
Log.i(TAG, "action_troubleshooting");
|
||||
try {
|
||||
String url = "https://www.openseizuredetector.org.uk/?page_id=2235";
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(url));
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
} catch (Exception ex) {
|
||||
Log.v(TAG, "exception displaying troubleshooting " + ex.toString());
|
||||
mUtil.showToast("ERROR Displaying Troubleshooting Tips");
|
||||
}
|
||||
return true;
|
||||
|
||||
case R.id.action_about:
|
||||
Log.i(TAG, "action_about");
|
||||
showAbout();
|
||||
|
||||
@@ -12,11 +12,14 @@ import android.os.Handler;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
@@ -78,6 +81,21 @@ public class ReportSeizureActivity extends AppCompatActivity {
|
||||
mConnection = new SdServiceConnection(getApplicationContext());
|
||||
|
||||
setContentView(R.layout.activity_report_seizure);
|
||||
// Handle system window insets for all API levels
|
||||
View rootView = findViewById(R.id.root_layout_report_seizure);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
|
||||
// Get the system bar insets
|
||||
int top = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
|
||||
int bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||
|
||||
// Apply padding to your main content view
|
||||
LinearLayout content = findViewById(R.id.report_seizure_content_layout);
|
||||
content.setPadding(0, top, 0, bottom);
|
||||
|
||||
// Return the insets so they keep propagating
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
|
||||
mEventTypeRg = findViewById(R.id.eventTypeRg);
|
||||
mEventTypeRg.setOnCheckedChangeListener(onEventTypeChange);
|
||||
|
||||
@@ -183,17 +183,79 @@ public class StartupActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Enable the "Install Watch App" button if we have the Pebble data source selected,
|
||||
// otherwise hide it.
|
||||
b = (Button) findViewById(R.id.installOsdAppButton);
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
String dataSourceName = (prefs.getString("DataSource", "Phone"));
|
||||
if (dataSourceName.equals("Pebble")) {
|
||||
b.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.v(TAG, "install Osd Watch App button clicked");
|
||||
if (mConnection.mSdServer.mSdDataSource != null) {
|
||||
mUtil.writeToSysLogFile("Installing Watch App");
|
||||
mConnection.mSdServer.mSdDataSource.installWatchApp();
|
||||
} else {
|
||||
mUtil.showToast("Error installing watch app - Datasource has not started - please see installation instructions on web site");
|
||||
Log.v(TAG, "Displaying Installation Instructions");
|
||||
try {
|
||||
String url = "https://www.openseizuredetector.org.uk/?page_id=1894";
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(url));
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
} catch (Exception ex) {
|
||||
Log.i(TAG, "exception starting install watch app activity " + ex.toString());
|
||||
mUtil.showToast("Error Displaying Installation Instructions - try http://www.openseizuredetector.org.uk/?page_id=1894 instead");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
b.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
b = (Button) findViewById(R.id.instructionsButton);
|
||||
b.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.v(TAG, "instructions button clicked");
|
||||
try {
|
||||
String url = "https://www.openseizuredetector.org.uk/?page_id=1894";
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(url));
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
} catch (Exception ex) {
|
||||
Log.v(TAG, "exception displaying instructions " + ex.toString());
|
||||
mUtil.showToast("ERROR Displaying Instructions");
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
b = (Button) findViewById(R.id.troubleshootingButton);
|
||||
b.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.v(TAG, "troubleshooting button clicked");
|
||||
try {
|
||||
String url = "https://www.openseizuredetector.org.uk/?page_id=2235";
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(url));
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
} catch (Exception ex) {
|
||||
Log.v(TAG, "exception displaying troubleshooting " + ex.toString());
|
||||
mUtil.showToast("ERROR Displaying Troubleshooting Tips");
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Connect to the background service
|
||||
mConnection = new SdServiceConnection(getApplicationContext());
|
||||
|
||||
}
|
||||
|
||||
@@ -369,13 +369,13 @@ public class WebApiConnection_osdapi extends WebApiConnection {
|
||||
new Response.Listener<String>() {
|
||||
@Override
|
||||
public void onResponse(String response) {
|
||||
Log.v(TAG, "Response is: " + response);
|
||||
Log.v(TAG, "updateEvent.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());
|
||||
Log.e(TAG, "updateEvent.onResponse(): Error: " + e.getMessage() + "," + e.toString());
|
||||
callback.accept(null);
|
||||
}
|
||||
}
|
||||
@@ -385,9 +385,9 @@ public class WebApiConnection_osdapi extends WebApiConnection {
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
mServerConnectionOk = false;
|
||||
if (error != null) {
|
||||
Log.e(TAG, "Create Event Error: " + error.toString() + ", message:" + error.getMessage());
|
||||
Log.e(TAG, "updateEvent.onErrorResponse(): Error: " + error.toString() + ", message:" + error.getMessage());
|
||||
} else {
|
||||
Log.e(TAG, "Create Event Error - returned null response");
|
||||
Log.e(TAG, "updateEvent.onErrorResponse(): Error - returned null response");
|
||||
}
|
||||
callback.accept(null);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root_layout_export_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="uk.org.openseizuredetector.LogManager">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/export_data_content_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
@@ -89,3 +99,4 @@
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,10 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root_layout_edit_event"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="uk.org.openseizuredetector.LogManager">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/edit_event_content_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
@@ -67,6 +77,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/event_date"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -91,6 +102,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/alarm_state"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -115,6 +127,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/event_type"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -138,6 +151,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/event_sub_type"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -145,7 +159,6 @@
|
||||
android:textSize="20sp" />
|
||||
|
||||
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/eventSubTypeRg"
|
||||
android:layout_width="match_parent"
|
||||
@@ -168,3 +181,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,6 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root_layout_log_manager_control"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="uk.org.openseizuredetector.LogManager">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
@@ -8,6 +16,7 @@
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:id="@+id/log_manager_control_content_layout"
|
||||
tools:context="uk.org.openseizuredetector.LogManager">
|
||||
|
||||
<TextView
|
||||
@@ -121,48 +130,67 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/authStatusTv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/not_authenticated" />
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/not_authenticated" /> <!-- Space after TextView -->
|
||||
|
||||
<Button
|
||||
android:id="@+id/auth_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/authenticate" />
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/authenticate" /> <!-- Space after this button -->
|
||||
|
||||
<Button
|
||||
android:id="@+id/refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/refreshBtn" />
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/refreshBtn" /> <!-- Space before this button -->
|
||||
<!-- No marginEnd needed if ProgressBar is last and you want it close,
|
||||
or add marginEnd if you want space before ProgressBar too -->
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/remoteAccessPb"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp" /> <!-- Space before ProgressBar -->
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/include_warnings_cb"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/group_events_cb"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/include_warnings"></CheckBox>
|
||||
android:layout_weight="1"
|
||||
android:checked="true"
|
||||
android:text="@string/group_remote_events" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/include_warnings_cb"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/include_warnings" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/include_nda_cb"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/include_nda"></CheckBox>
|
||||
android:layout_weight="1"
|
||||
android:text="@string/include_nda" />
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
@@ -216,3 +244,5 @@
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/activity_main2_root_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="uk.org.openseizuredetector.LogManager">
|
||||
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout
|
||||
android:id="@+id/activity_main2_content_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical">
|
||||
@@ -24,3 +33,4 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,6 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root_layout_report_seizure"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="uk.org.openseizuredetector.LogManager">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/report_seizure_content_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
@@ -181,3 +190,4 @@
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/date"
|
||||
android:id="@+id/group_event_time_tv" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=" : "/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="alarm"
|
||||
android:id="@+id/group_event_alarmState_tv"
|
||||
/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=" : "/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="uploaded"
|
||||
android:id="@+id/group_event_uploaded"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
<!-- <TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="dataJSON"
|
||||
android:id="@+id/event_dataJSON" />
|
||||
-->
|
||||
</LinearLayout>
|
||||
@@ -35,12 +35,36 @@
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:text="Help"
|
||||
android:id="@+id/instructionsButton"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<Button
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:text="Trouble-\nshooting"
|
||||
android:id="@+id/troubleshootingButton"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<Button
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:text="@string/edit_settings"
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
@@ -105,6 +105,14 @@
|
||||
|
||||
<group android:id="@+id/grp6">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_instructions"
|
||||
app:showAsAction="never|withText"
|
||||
android:title="Help" />
|
||||
<item
|
||||
android:id="@+id/action_troubleshooting"
|
||||
app:showAsAction="never|withText"
|
||||
android:title="Troubleshooting" />
|
||||
<item
|
||||
android:id="@+id/action_about"
|
||||
app:showAsAction="never|withText"
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
<string name="app_name">OpenSeizureDetector</string>
|
||||
<string name="changelog">
|
||||
"\n
|
||||
\nV4.3.1 - Fixed user interface issues on Android-15 and Android-16
|
||||
\nV4.3.0 - Added support for Android 15 (API 35) and above.
|
||||
\n - Simplified data sharing editor by grouping events for editing.
|
||||
\nV4.2.12 - Added buttons and menu items for 'Help' and 'Troubleshooting' to point users to the web page instructions.
|
||||
\nV4.2 - Added support for PineTime and BangleJS Watches using Bluetooth data source.
|
||||
\n - Added support for Version 2 of the Garmin watch app, which reduces battery drain
|
||||
\n - Added new, swipeable user interface to simplify the main screen.
|
||||
@@ -587,4 +591,5 @@
|
||||
<string name="DefaultSMSFalseAlarmMsgText">False Alarm, Sorry!</string>
|
||||
<string name="sms_false_alarm_message_summary">Text of \'False Alarm\' SMS message</string>
|
||||
<string name="sms_false_alarm_message_title">SMS False Alarm Message</string>
|
||||
<string name="group_remote_events">Group Remote Events</string>
|
||||
</resources>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.4.1'
|
||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Thu Oct 10 20:04:34 BST 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user