Minor changes to seizure report
This commit is contained in:
@@ -43,7 +43,7 @@
|
|||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:icon="@drawable/star_of_life_48x48"
|
android:icon="@drawable/floga_app_icon"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
@@ -61,18 +61,18 @@
|
|||||||
<activity android:name=".BLEScanActivity" />
|
<activity android:name=".BLEScanActivity" />
|
||||||
<activity android:name=".ExportDataActivity" /> <!-- android:usesCleartextTraffic="true" -->
|
<activity android:name=".ExportDataActivity" /> <!-- android:usesCleartextTraffic="true" -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".StartupActivity"
|
android:name=".StartupActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
<intent-filter>
|
android:icon="@drawable/floga_app_icon">
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/star_of_life_48x48"
|
android:icon="@drawable/floga_app_icon"
|
||||||
android:label="@string/app_name" />
|
android:label="@string/app_name" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".PrefActivity"
|
android:name=".PrefActivity"
|
||||||
|
|||||||
@@ -10,27 +10,33 @@ import java.text.ParseException;
|
|||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
public class ReportManager {
|
public class ReportManager {
|
||||||
private static final String TAG = "ReportManager";
|
private static final String TAG = "ReportManager";
|
||||||
private static final int GROUP_THRESHOLD_SECS = 30;
|
private static final int FALLBACK_GROUP_THRESHOLD_SECS = 30;
|
||||||
private static final int MONTHS_TO_SHOW = 12;
|
private static final int SAME_SEIZURE_START_TOLERANCE_SECS = 45;
|
||||||
|
private static final int DEFAULT_MARKER_DURATION_SECS = 5 * 60;
|
||||||
|
private static final int MAX_TIMELINE_DURATION_SECS = 15 * 60;
|
||||||
|
private static final boolean ADD_DEMO_EVENTS = true;
|
||||||
|
private static final int DEMO_EVENT_COUNT = 10;
|
||||||
|
|
||||||
public static String generateHtmlReport(SQLiteDatabase db, int days) {
|
public static String generateHtmlReport(SQLiteDatabase db, int days) {
|
||||||
if (db == null) {
|
if (db == null) {
|
||||||
return "<html><body><h1>Error: Database not available</h1></body></html>";
|
return "<html><body><h1>Error: Database not available</h1></body></html>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch full calendar months for the report selector. This avoids the month dropdown
|
// Read locally stored alarm events. The report's month selector is built
|
||||||
// losing the previous month when the app moves into a new month.
|
// from the timestamps actually present in this result, so older months do not vanish
|
||||||
|
// just because the current calendar month changes.
|
||||||
String query = "SELECT dataTime, status, type, notes, dataJSON " +
|
String query = "SELECT dataTime, status, type, notes, dataJSON " +
|
||||||
"FROM events " +
|
"FROM events " +
|
||||||
"WHERE dataTime >= date('now', 'start of month', '-" + (MONTHS_TO_SHOW - 1) + " months') " +
|
"WHERE status IN (2, 5) " +
|
||||||
"AND status IN (1, 2, 3, 5) " +
|
|
||||||
"ORDER BY dataTime ASC";
|
"ORDER BY dataTime ASC";
|
||||||
|
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
@@ -47,11 +53,19 @@ public class ReportManager {
|
|||||||
event.type = cursor.getString(2);
|
event.type = cursor.getString(2);
|
||||||
event.notes = cursor.getString(3);
|
event.notes = cursor.getString(3);
|
||||||
|
|
||||||
|
event.durationSeconds = parseDurationSecondsFromNotes(event.notes);
|
||||||
|
event.hr = parseHeartRateFromNotes(event.notes);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String dataJson = cursor.getString(4);
|
String dataJson = cursor.getString(4);
|
||||||
if (dataJson != null) {
|
if (dataJson != null && dataJson.trim().length() > 0) {
|
||||||
JSONObject jo = new JSONObject(dataJson);
|
JSONObject jo = new JSONObject(dataJson);
|
||||||
event.hr = jo.optDouble("hr", 0.0);
|
double jsonHr = optPositiveDouble(jo, "hr");
|
||||||
|
if (jsonHr <= 0.0) jsonHr = optPositiveDouble(jo, "HR");
|
||||||
|
if (jsonHr <= 0.0) jsonHr = optPositiveDouble(jo, "mHR");
|
||||||
|
if (jsonHr <= 0.0) jsonHr = optPositiveDouble(jo, "heartRate");
|
||||||
|
if (jsonHr > 0.0) event.hr = jsonHr;
|
||||||
|
|
||||||
event.alarmPhrase = jo.optString("alarmPhrase", "");
|
event.alarmPhrase = jo.optString("alarmPhrase", "");
|
||||||
event.alarmCause = jo.optString("alarmCause", "").trim();
|
event.alarmCause = jo.optString("alarmCause", "").trim();
|
||||||
}
|
}
|
||||||
@@ -61,6 +75,11 @@ public class ReportManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
event.date = sdf.parse(event.dataTime);
|
event.date = sdf.parse(event.dataTime);
|
||||||
|
if (event.date != null && event.durationSeconds > 0) {
|
||||||
|
event.inferredStartDate = new Date(event.date.getTime() - event.durationSeconds * 1000L);
|
||||||
|
} else {
|
||||||
|
event.inferredStartDate = event.date;
|
||||||
|
}
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
Log.w(TAG, "Error parsing date: " + event.dataTime);
|
Log.w(TAG, "Error parsing date: " + event.dataTime);
|
||||||
}
|
}
|
||||||
@@ -74,10 +93,101 @@ public class ReportManager {
|
|||||||
if (cursor != null) cursor.close();
|
if (cursor != null) cursor.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ADD_DEMO_EVENTS) {
|
||||||
|
addDemoEvents(rawEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(rawEvents, (event1, event2) -> {
|
||||||
|
if (event1.date == null && event2.date == null) return 0;
|
||||||
|
if (event1.date == null) return 1;
|
||||||
|
if (event2.date == null) return -1;
|
||||||
|
return event1.date.compareTo(event2.date);
|
||||||
|
});
|
||||||
|
|
||||||
ArrayList<SeizureGroup> groups = groupEvents(rawEvents);
|
ArrayList<SeizureGroup> groups = groupEvents(rawEvents);
|
||||||
return buildCalendarHtml(groups, days);
|
return buildCalendarHtml(groups, days);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addDemoEvents(ArrayList<SeizureEvent> events) {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.UK);
|
||||||
|
|
||||||
|
Calendar monthStart = Calendar.getInstance();
|
||||||
|
monthStart.set(Calendar.YEAR, 2026);
|
||||||
|
monthStart.set(Calendar.MONTH, Calendar.APRIL); // April is month 3 internally
|
||||||
|
monthStart.set(Calendar.DAY_OF_MONTH, 1);
|
||||||
|
monthStart.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
monthStart.set(Calendar.MINUTE, 0);
|
||||||
|
monthStart.set(Calendar.SECOND, 0);
|
||||||
|
monthStart.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
Calendar monthEnd = (Calendar) monthStart.clone();
|
||||||
|
monthEnd.add(Calendar.MONTH, 1);
|
||||||
|
monthEnd.add(Calendar.SECOND, -1);
|
||||||
|
|
||||||
|
int daysInMonth = monthStart.getActualMaximum(Calendar.DAY_OF_MONTH);
|
||||||
|
|
||||||
|
|
||||||
|
// 7 evening events, 2 night events, 1 afternoon event.
|
||||||
|
// Night: 00-06
|
||||||
|
// Afternoon: 12-18
|
||||||
|
// Evening: 18-24
|
||||||
|
int[] demoDays = {
|
||||||
|
1, 4, 4, 7, 12,
|
||||||
|
15, 19, 22, 28, 29
|
||||||
|
};
|
||||||
|
|
||||||
|
// 7 evening events, 2 night events, 1 afternoon event.
|
||||||
|
// Day 4 has two events.
|
||||||
|
int[] demoHours = {
|
||||||
|
23, 2, 21, 19, 4,
|
||||||
|
22, 20, 15, 18, 23
|
||||||
|
};
|
||||||
|
|
||||||
|
int[] demoMinutes = {
|
||||||
|
41, 16, 53, 9, 34,
|
||||||
|
27, 48, 22, 5, 37
|
||||||
|
};
|
||||||
|
|
||||||
|
int[] demoDurationsSecs = {
|
||||||
|
95, 125, 65, 140, 85,
|
||||||
|
110, 180, 100, 55, 75
|
||||||
|
};
|
||||||
|
|
||||||
|
int[] demoHr = {
|
||||||
|
99, 107, 88, 112, 86,
|
||||||
|
104, 121, 100, 91, 95
|
||||||
|
};
|
||||||
|
|
||||||
|
int count = Math.min(DEMO_EVENT_COUNT, demoDays.length);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int day = demoDays[i];
|
||||||
|
if (day > daysInMonth) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar eventCal = (Calendar) monthStart.clone();
|
||||||
|
eventCal.set(Calendar.DAY_OF_MONTH, day);
|
||||||
|
eventCal.set(Calendar.HOUR_OF_DAY, demoHours[i]);
|
||||||
|
eventCal.set(Calendar.MINUTE, demoMinutes[i]);
|
||||||
|
eventCal.set(Calendar.SECOND, 0);
|
||||||
|
eventCal.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
SeizureEvent event = new SeizureEvent();
|
||||||
|
event.date = eventCal.getTime();
|
||||||
|
event.dataTime = sdf.format(event.date);
|
||||||
|
event.status = 2; // ALARM only
|
||||||
|
event.type = "seizure";
|
||||||
|
event.durationSeconds = demoDurationsSecs[i];
|
||||||
|
event.inferredStartDate = new Date(event.date.getTime() - event.durationSeconds * 1000L);
|
||||||
|
event.notes = "Duration: " + demoDurationsSecs[i] + "s HR: " + demoHr[i];
|
||||||
|
event.hr = demoHr[i];
|
||||||
|
event.alarmPhrase = "Demo alarm";
|
||||||
|
event.alarmCause = "Demo data";
|
||||||
|
|
||||||
|
events.add(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
private static ArrayList<SeizureGroup> groupEvents(ArrayList<SeizureEvent> events) {
|
private static ArrayList<SeizureGroup> groupEvents(ArrayList<SeizureEvent> events) {
|
||||||
ArrayList<SeizureGroup> groups = new ArrayList<>();
|
ArrayList<SeizureGroup> groups = new ArrayList<>();
|
||||||
if (events.isEmpty()) return groups;
|
if (events.isEmpty()) return groups;
|
||||||
@@ -87,25 +197,11 @@ public class ReportManager {
|
|||||||
for (SeizureEvent event : events) {
|
for (SeizureEvent event : events) {
|
||||||
if (currentGroup == null) {
|
if (currentGroup == null) {
|
||||||
currentGroup = new SeizureGroup(event);
|
currentGroup = new SeizureGroup(event);
|
||||||
|
} else if (shouldMergeIntoGroup(currentGroup, event)) {
|
||||||
|
currentGroup.addEvent(event);
|
||||||
} else {
|
} else {
|
||||||
long diffSecs = 0;
|
groups.add(currentGroup);
|
||||||
if (event.date != null && currentGroup.lastDate != null) {
|
currentGroup = new SeizureGroup(event);
|
||||||
diffSecs = (event.date.getTime() - currentGroup.lastDate.getTime()) / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffSecs <= GROUP_THRESHOLD_SECS) {
|
|
||||||
currentGroup.lastDate = event.date;
|
|
||||||
currentGroup.lastEvent = event;
|
|
||||||
if (event.notes != null && event.notes.contains("Duration:")) {
|
|
||||||
currentGroup.durationStr = extractDuration(event.notes);
|
|
||||||
}
|
|
||||||
if (event.hr > 0) {
|
|
||||||
currentGroup.hr = event.hr;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
groups.add(currentGroup);
|
|
||||||
currentGroup = new SeizureGroup(event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,10 +212,172 @@ public class ReportManager {
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean shouldMergeIntoGroup(SeizureGroup currentGroup, SeizureEvent event) {
|
||||||
|
if (currentGroup == null || event == null) return false;
|
||||||
|
|
||||||
|
// Preferred rule: rows from the same continuing seizure usually have a similar
|
||||||
|
// inferred start time because alarmDuration grows as the same alarm continues.
|
||||||
|
// This avoids chaining two distinct seizures together just because their log rows
|
||||||
|
// are near each other in time.
|
||||||
|
if (currentGroup.inferredStartDate != null && event.inferredStartDate != null
|
||||||
|
&& currentGroup.hasDurationEvidence && event.durationSeconds > 0) {
|
||||||
|
long startDiffSecs = Math.abs(event.inferredStartDate.getTime()
|
||||||
|
- currentGroup.inferredStartDate.getTime()) / 1000L;
|
||||||
|
return startDiffSecs <= SAME_SEIZURE_START_TOLERANCE_SECS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for old/manual rows with no duration in notes. Keep this deliberately
|
||||||
|
// short so nearby but separate seizures are less likely to be merged.
|
||||||
|
if (event.date != null && currentGroup.lastDate != null) {
|
||||||
|
long diffSecs = (event.date.getTime() - currentGroup.lastDate.getTime()) / 1000L;
|
||||||
|
return diffSecs >= 0 && diffSecs <= FALLBACK_GROUP_THRESHOLD_SECS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double optPositiveDouble(JSONObject jo, String key) {
|
||||||
|
try {
|
||||||
|
if (jo == null || !jo.has(key) || jo.isNull(key)) return 0.0;
|
||||||
|
double value = jo.optDouble(key, 0.0);
|
||||||
|
return value > 0.0 ? value : 0.0;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseDurationSecondsFromNotes(String notes) {
|
||||||
|
String duration = extractDuration(notes);
|
||||||
|
if (duration == null || duration.equals("unknown") || duration.equals("N/A")) return 0;
|
||||||
|
return parseDurationSeconds(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double parseHeartRateFromNotes(String notes) {
|
||||||
|
if (notes == null) return 0.0;
|
||||||
|
String lower = notes.toLowerCase(Locale.UK);
|
||||||
|
int idx = lower.indexOf("hr:");
|
||||||
|
if (idx < 0) idx = lower.indexOf("heart rate:");
|
||||||
|
if (idx < 0) return 0.0;
|
||||||
|
|
||||||
|
int start = notes.indexOf(':', idx);
|
||||||
|
if (start < 0 || start + 1 >= notes.length()) return 0.0;
|
||||||
|
|
||||||
|
StringBuilder number = new StringBuilder();
|
||||||
|
for (int i = start + 1; i < notes.length(); i++) {
|
||||||
|
char ch = notes.charAt(i);
|
||||||
|
if ((ch >= '0' && ch <= '9') || ch == '.') {
|
||||||
|
number.append(ch);
|
||||||
|
} else if (number.length() > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
double value = Double.parseDouble(number.toString());
|
||||||
|
return value > 0.0 ? value : 0.0;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseDurationSeconds(String durationStr) {
|
||||||
|
if (durationStr == null) return 0;
|
||||||
|
|
||||||
|
String s = durationStr.trim().toLowerCase(Locale.UK);
|
||||||
|
if (s.isEmpty() || s.equals("n/a") || s.equals("unknown")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Supports HH:MM:SS
|
||||||
|
if (s.matches("\\d{1,2}:\\d{1,2}:\\d{1,2}")) {
|
||||||
|
String[] parts = s.split(":");
|
||||||
|
int hours = Integer.parseInt(parts[0]);
|
||||||
|
int mins = Integer.parseInt(parts[1]);
|
||||||
|
int secs = Integer.parseInt(parts[2]);
|
||||||
|
return Math.max(1, hours * 3600 + mins * 60 + secs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supports MM:SS
|
||||||
|
if (s.matches("\\d{1,2}:\\d{1,2}")) {
|
||||||
|
String[] parts = s.split(":");
|
||||||
|
int mins = Integer.parseInt(parts[0]);
|
||||||
|
int secs = Integer.parseInt(parts[1]);
|
||||||
|
return Math.max(1, mins * 60 + secs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supports compact values like "123s", "123 sec", "2 min 30 sec".
|
||||||
|
int totalSeconds = 0;
|
||||||
|
String[] tokens = s.replace(",", " ")
|
||||||
|
.replace("seconds", " sec")
|
||||||
|
.replace("second", " sec")
|
||||||
|
.replace("secs", " sec")
|
||||||
|
.replace("minutes", " min")
|
||||||
|
.replace("minute", " min")
|
||||||
|
.replace("mins", " min")
|
||||||
|
.replace("hours", " hour")
|
||||||
|
.replace("hrs", " hour")
|
||||||
|
.replace("hr", " hour")
|
||||||
|
.split("\\s+");
|
||||||
|
|
||||||
|
for (int i = 0; i < tokens.length; i++) {
|
||||||
|
String token = tokens[i];
|
||||||
|
int value = -1;
|
||||||
|
try {
|
||||||
|
if (token.endsWith("s") && token.length() > 1) {
|
||||||
|
value = Integer.parseInt(token.substring(0, token.length() - 1));
|
||||||
|
totalSeconds += value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value = Integer.parseInt(token);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String unit = (i + 1 < tokens.length) ? tokens[i + 1] : "sec";
|
||||||
|
if (unit.startsWith("hour")) {
|
||||||
|
totalSeconds += value * 3600;
|
||||||
|
} else if (unit.startsWith("min")) {
|
||||||
|
totalSeconds += value * 60;
|
||||||
|
} else if (unit.startsWith("sec") || unit.equals("s")) {
|
||||||
|
totalSeconds += value;
|
||||||
|
} else {
|
||||||
|
// Bare number in the existing local notes is normally seconds.
|
||||||
|
totalSeconds += value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalSeconds > 0) return totalSeconds;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatDurationSeconds(int durationSeconds) {
|
||||||
|
if (durationSeconds <= 0) return "N/A";
|
||||||
|
if (durationSeconds < 60) return durationSeconds + "s";
|
||||||
|
|
||||||
|
int hours = durationSeconds / 3600;
|
||||||
|
int mins = (durationSeconds % 3600) / 60;
|
||||||
|
int secs = durationSeconds % 60;
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return String.format(Locale.UK, "%dh %02dm %02ds", hours, mins, secs);
|
||||||
|
}
|
||||||
|
if (secs > 0) {
|
||||||
|
return String.format(Locale.UK, "%dm %02ds", mins, secs);
|
||||||
|
}
|
||||||
|
return mins + "m";
|
||||||
|
}
|
||||||
|
|
||||||
private static String extractDuration(String notes) {
|
private static String extractDuration(String notes) {
|
||||||
try {
|
try {
|
||||||
int start = notes.indexOf("Duration:") + 9;
|
if (notes == null) return "unknown";
|
||||||
int end = notes.indexOf("HR:");
|
int durationIdx = notes.indexOf("Duration:");
|
||||||
|
if (durationIdx < 0) return "unknown";
|
||||||
|
int start = durationIdx + 9;
|
||||||
|
int end = notes.indexOf("HR:", start);
|
||||||
if (end == -1) end = notes.length();
|
if (end == -1) end = notes.length();
|
||||||
return notes.substring(start, end).trim();
|
return notes.substring(start, end).trim();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -157,57 +415,9 @@ public class ReportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static int parseDurationMinutes(String durationStr) {
|
private static int parseDurationMinutes(String durationStr) {
|
||||||
if (durationStr == null) return 5;
|
int seconds = parseDurationSeconds(durationStr);
|
||||||
|
if (seconds <= 0) return DEFAULT_MARKER_DURATION_SECS / 60;
|
||||||
String s = durationStr.trim().toLowerCase(Locale.UK);
|
return Math.max(1, (int) Math.ceil(seconds / 60.0));
|
||||||
if (s.isEmpty() || s.equals("n/a") || s.equals("unknown")) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Supports HH:MM:SS
|
|
||||||
if (s.matches("\\d{1,2}:\\d{1,2}:\\d{1,2}")) {
|
|
||||||
String[] parts = s.split(":");
|
|
||||||
int hours = Integer.parseInt(parts[0]);
|
|
||||||
int mins = Integer.parseInt(parts[1]);
|
|
||||||
int secs = Integer.parseInt(parts[2]);
|
|
||||||
int totalSeconds = hours * 3600 + mins * 60 + secs;
|
|
||||||
return Math.max(1, (int) Math.ceil(totalSeconds / 60.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supports MM:SS
|
|
||||||
if (s.matches("\\d{1,2}:\\d{1,2}")) {
|
|
||||||
String[] parts = s.split(":");
|
|
||||||
int mins = Integer.parseInt(parts[0]);
|
|
||||||
int secs = Integer.parseInt(parts[1]);
|
|
||||||
int totalSeconds = mins * 60 + secs;
|
|
||||||
return Math.max(1, (int) Math.ceil(totalSeconds / 60.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supports text like "2 min 30 sec" or "1 hour 5 min"
|
|
||||||
int totalMinutes = 0;
|
|
||||||
String[] tokens = s.replace(",", " ").split("\\s+");
|
|
||||||
for (int i = 0; i < tokens.length - 1; i++) {
|
|
||||||
try {
|
|
||||||
int value = Integer.parseInt(tokens[i]);
|
|
||||||
String unit = tokens[i + 1];
|
|
||||||
if (unit.startsWith("hour") || unit.startsWith("hr")) {
|
|
||||||
totalMinutes += value * 60;
|
|
||||||
} else if (unit.startsWith("min")) {
|
|
||||||
totalMinutes += value;
|
|
||||||
} else if (unit.startsWith("sec")) {
|
|
||||||
if (value > 0) totalMinutes += 1;
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException ignored) {
|
|
||||||
// Keep checking the rest of the duration text.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalMinutes > 0) return totalMinutes;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getStatusColor(int status) {
|
private static String getStatusColor(int status) {
|
||||||
@@ -271,7 +481,7 @@ public class ReportManager {
|
|||||||
SimpleDateFormat monthFormat = new SimpleDateFormat("MMMM yyyy", Locale.UK);
|
SimpleDateFormat monthFormat = new SimpleDateFormat("MMMM yyyy", Locale.UK);
|
||||||
SimpleDateFormat monthIdFormat = new SimpleDateFormat("yyyy-MM", Locale.UK);
|
SimpleDateFormat monthIdFormat = new SimpleDateFormat("yyyy-MM", Locale.UK);
|
||||||
|
|
||||||
// Group seizures by day and by month.
|
// Group alarms by day and by month.
|
||||||
Map<String, ArrayList<SeizureGroup>> byDay = new HashMap<>();
|
Map<String, ArrayList<SeizureGroup>> byDay = new HashMap<>();
|
||||||
Map<String, Integer> eventsByMonth = new HashMap<>();
|
Map<String, Integer> eventsByMonth = new HashMap<>();
|
||||||
for (SeizureGroup g : groups) {
|
for (SeizureGroup g : groups) {
|
||||||
@@ -291,38 +501,41 @@ public class ReportManager {
|
|||||||
Calendar todayCal = Calendar.getInstance();
|
Calendar todayCal = Calendar.getInstance();
|
||||||
Date today = todayCal.getTime();
|
Date today = todayCal.getTime();
|
||||||
|
|
||||||
// Build a fixed list of recent full months so the dropdown remains useful across
|
// Build the selector from months that actually have event data. Newest months are
|
||||||
// month boundaries, for example allowing April to be selected after May begins.
|
// shown first. The current month is selected only if it has events; otherwise the
|
||||||
Calendar firstMonth = Calendar.getInstance();
|
// latest available month is selected.
|
||||||
firstMonth.add(Calendar.MONTH, -(MONTHS_TO_SHOW - 1));
|
ArrayList<String> monthIds = new ArrayList<>(eventsByMonth.keySet());
|
||||||
firstMonth.set(Calendar.DAY_OF_MONTH, 1);
|
Collections.sort(monthIds);
|
||||||
firstMonth.set(Calendar.HOUR_OF_DAY, 0);
|
Collections.reverse(monthIds);
|
||||||
firstMonth.set(Calendar.MINUTE, 0);
|
|
||||||
firstMonth.set(Calendar.SECOND, 0);
|
|
||||||
firstMonth.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
Calendar lastMonth = Calendar.getInstance();
|
String currentMonthId = monthIdFormat.format(today);
|
||||||
lastMonth.set(Calendar.DAY_OF_MONTH, 1);
|
String defaultMonthId = monthIds.contains(currentMonthId)
|
||||||
lastMonth.set(Calendar.HOUR_OF_DAY, 0);
|
? currentMonthId
|
||||||
lastMonth.set(Calendar.MINUTE, 0);
|
: (monthIds.isEmpty() ? currentMonthId : monthIds.get(0));
|
||||||
lastMonth.set(Calendar.SECOND, 0);
|
|
||||||
lastMonth.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
ArrayList<Calendar> reportMonths = new ArrayList<>();
|
ArrayList<Calendar> reportMonths = new ArrayList<>();
|
||||||
Calendar monthCursor = (Calendar) firstMonth.clone();
|
for (String monthId : monthIds) {
|
||||||
while (!monthCursor.after(lastMonth)) {
|
try {
|
||||||
reportMonths.add((Calendar) monthCursor.clone());
|
Date monthDate = monthIdFormat.parse(monthId);
|
||||||
monthCursor.add(Calendar.MONTH, 1);
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.setTime(monthDate);
|
||||||
|
cal.set(Calendar.DAY_OF_MONTH, 1);
|
||||||
|
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
cal.set(Calendar.MINUTE, 0);
|
||||||
|
cal.set(Calendar.SECOND, 0);
|
||||||
|
cal.set(Calendar.MILLISECOND, 0);
|
||||||
|
reportMonths.add(cal);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.w(TAG, "Error parsing report month: " + monthId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String defaultMonthId = monthIdFormat.format(today);
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("<!DOCTYPE html><html><head>");
|
sb.append("<!DOCTYPE html><html><head>");
|
||||||
sb.append("<meta charset='UTF-8'>");
|
sb.append("<meta charset='UTF-8'>");
|
||||||
sb.append("<meta name='viewport' content='width=device-width, initial-scale=1'>");
|
sb.append("<meta name='viewport' content='width=device-width, initial-scale=1'>");
|
||||||
sb.append("<title>ClinX02 Seizure Report</title>");
|
sb.append("<title>FLOGA Seizure Report</title>");
|
||||||
sb.append("<style>");
|
sb.append("<style>");
|
||||||
sb.append("* { box-sizing: border-box; }");
|
sb.append("* { box-sizing: border-box; }");
|
||||||
sb.append("body { font-family: Arial, sans-serif; margin: 0; padding: 16px; background: #eef1f5; color: #222; }");
|
sb.append("body { font-family: Arial, sans-serif; margin: 0; padding: 16px; background: #eef1f5; color: #222; }");
|
||||||
sb.append(".container { max-width: 1100px; margin: 0 auto; }");
|
sb.append(".container { max-width: 1100px; margin: 0 auto; }");
|
||||||
@@ -367,7 +580,7 @@ public class ReportManager {
|
|||||||
sb.append(".day-header { text-align: center; font-weight: bold; padding: 6px; color: #555; font-size: 13px; }");
|
sb.append(".day-header { text-align: center; font-weight: bold; padding: 6px; color: #555; font-size: 13px; }");
|
||||||
sb.append(".day-box { background: white; border-radius: 8px; padding: 6px; min-height: 86px; border: 1px solid #ddd; box-shadow: 0 1px 4px rgba(0,0,0,0.04); }");
|
sb.append(".day-box { background: white; border-radius: 8px; padding: 6px; min-height: 86px; border: 1px solid #ddd; box-shadow: 0 1px 4px rgba(0,0,0,0.04); }");
|
||||||
sb.append(".day-box.today { border: 2px solid #2563eb; }");
|
sb.append(".day-box.today { border: 2px solid #2563eb; }");
|
||||||
sb.append(".day-box.has-seizures { background: #fff8f8; border-color: #fecaca; }");
|
sb.append(".day-box.has-alarms { background: #fff8f8; border-color: #fecaca; }");
|
||||||
sb.append(".day-box.empty { background: transparent; border: none; box-shadow: none; }");
|
sb.append(".day-box.empty { background: transparent; border: none; box-shadow: none; }");
|
||||||
sb.append(".day-num { font-size: 13px; font-weight: bold; color: #333; margin-bottom: 4px; }");
|
sb.append(".day-num { font-size: 13px; font-weight: bold; color: #333; margin-bottom: 4px; }");
|
||||||
sb.append(".timeline { position: relative; height: 28px; margin-top: 6px; border-radius: 6px; overflow: hidden; border: 1px solid #d1d5db; background: white; isolation: isolate; }");
|
sb.append(".timeline { position: relative; height: 28px; margin-top: 6px; border-radius: 6px; overflow: hidden; border: 1px solid #d1d5db; background: white; isolation: isolate; }");
|
||||||
@@ -381,9 +594,9 @@ public class ReportManager {
|
|||||||
sb.append(".time-marker { position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(0,0,0,0.16); }");
|
sb.append(".time-marker { position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(0,0,0,0.16); }");
|
||||||
sb.append(".event-bar { position: absolute; top: 5px; height: 18px; border-radius: 4px; opacity: 0.96; border: 1px solid rgba(0,0,0,0.18); z-index: 2; }");
|
sb.append(".event-bar { position: absolute; top: 5px; height: 18px; border-radius: 4px; opacity: 0.96; border: 1px solid rgba(0,0,0,0.18); z-index: 2; }");
|
||||||
sb.append(".time-scale { display: flex; justify-content: space-between; font-size: 9px; color: #6b7280; margin-top: 3px; }");
|
sb.append(".time-scale { display: flex; justify-content: space-between; font-size: 9px; color: #6b7280; margin-top: 3px; }");
|
||||||
sb.append(".seizure-count { font-size: 11px; color: #6b7280; margin-top: 4px; }");
|
sb.append(".alarm-count { font-size: 11px; color: #6b7280; margin-top: 4px; }");
|
||||||
sb.append(".table-wrap { width: 100%; overflow-x: auto; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }");
|
sb.append(".table-wrap { width: 100%; overflow-x: auto; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }");
|
||||||
sb.append(".detail-table { width: 100%; border-collapse: collapse; background: white; overflow: hidden; min-width: 680px; }");
|
sb.append(".detail-table { width: 100%; border-collapse: collapse; background: white; overflow: hidden; min-width: 600px; }");
|
||||||
sb.append(".detail-table th { background: #2563eb; color: white; padding: 12px; text-align: left; font-size: 13px; }");
|
sb.append(".detail-table th { background: #2563eb; color: white; padding: 12px; text-align: left; font-size: 13px; }");
|
||||||
sb.append(".detail-table td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 13px; vertical-align: top; }");
|
sb.append(".detail-table td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 13px; vertical-align: top; }");
|
||||||
sb.append(".detail-table tr:hover { background: #f9fafb; }");
|
sb.append(".detail-table tr:hover { background: #f9fafb; }");
|
||||||
@@ -410,31 +623,31 @@ public class ReportManager {
|
|||||||
sb.append(".timeline { height: 22px; }");
|
sb.append(".timeline { height: 22px; }");
|
||||||
sb.append(".event-bar { top: 4px; height: 14px; }");
|
sb.append(".event-bar { top: 4px; height: 14px; }");
|
||||||
sb.append(".time-scale { font-size: 8px; }");
|
sb.append(".time-scale { font-size: 8px; }");
|
||||||
sb.append(".seizure-count { font-size: 10px; }");
|
sb.append(".alarm-count { font-size: 10px; }");
|
||||||
sb.append(".detail-table th, .detail-table td { font-size: 11px; padding: 6px; }");
|
sb.append(".detail-table th, .detail-table td { font-size: 11px; padding: 6px; }");
|
||||||
sb.append("}");
|
sb.append("}");
|
||||||
sb.append("</style></head><body>");
|
sb.append("</style></head><body>");
|
||||||
sb.append("<div class='container'>");
|
sb.append("<div class='container'>");
|
||||||
|
|
||||||
sb.append("<h1>ClinX02 Seizure Report</h1>");
|
sb.append("<h1>FLOGA Seizure Report</h1>");
|
||||||
sb.append("<p class='subtitle'>Calendar timeline overview and detailed event log</p>");
|
sb.append("<p class='subtitle'>Calendar timeline overview and detailed alarm log</p>");
|
||||||
|
|
||||||
// Summary box
|
// Summary box
|
||||||
sb.append("<div class='summary'>");
|
sb.append("<div class='summary'>");
|
||||||
sb.append("<div class='stats'>");
|
sb.append("<div class='stats'>");
|
||||||
|
|
||||||
sb.append("<div class='stat-card'>");
|
sb.append("<div class='stat-card'>");
|
||||||
sb.append("<div class='stat-label'>Calendar range</div>");
|
sb.append("<div class='stat-label'>Months with events</div>");
|
||||||
sb.append("<div class='stat-value'>").append(MONTHS_TO_SHOW).append(" months</div>");
|
sb.append("<div class='stat-value'>").append(eventsByMonth.size()).append("</div>");
|
||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
|
|
||||||
sb.append("<div class='stat-card'>");
|
sb.append("<div class='stat-card'>");
|
||||||
sb.append("<div class='stat-label'>Total seizure events</div>");
|
sb.append("<div class='stat-label'>Total alarm events</div>");
|
||||||
sb.append("<div class='stat-value'>").append(groups.size()).append("</div>");
|
sb.append("<div class='stat-value'>").append(groups.size()).append("</div>");
|
||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
|
|
||||||
sb.append("<div class='stat-card'>");
|
sb.append("<div class='stat-card'>");
|
||||||
sb.append("<div class='stat-label'>Days with seizures</div>");
|
sb.append("<div class='stat-label'>Days with alarms</div>");
|
||||||
sb.append("<div class='stat-value'>").append(byDay.size()).append("</div>");
|
sb.append("<div class='stat-value'>").append(byDay.size()).append("</div>");
|
||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
|
|
||||||
@@ -443,18 +656,6 @@ public class ReportManager {
|
|||||||
.append("&token=clinx02secure' download='seizure_report.html'>Download Report</a>");
|
.append("&token=clinx02secure' download='seizure_report.html'>Download Report</a>");
|
||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
|
|
||||||
// Legend
|
|
||||||
sb.append("<div class='legend'>");
|
|
||||||
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#e5e7eb'></div>Night background (00-06)</div>");
|
|
||||||
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#fde68a'></div>Morning background (06-12)</div>");
|
|
||||||
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#fdba74'></div>Afternoon background (12-18)</div>");
|
|
||||||
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#c4b5fd'></div>Evening background (18-24)</div>");
|
|
||||||
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#f59e0b'></div>Warning bar</div>");
|
|
||||||
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#ef4444'></div>Alarm bar</div>");
|
|
||||||
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#8b5cf6'></div>Fall bar</div>");
|
|
||||||
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#06b6d4'></div>Manual bar</div>");
|
|
||||||
sb.append("</div>");
|
|
||||||
|
|
||||||
// Month selector
|
// Month selector
|
||||||
sb.append("<div class='month-controls'>");
|
sb.append("<div class='month-controls'>");
|
||||||
sb.append("<label for='monthSelect'>Month:</label>");
|
sb.append("<label for='monthSelect'>Month:</label>");
|
||||||
@@ -466,7 +667,7 @@ public class ReportManager {
|
|||||||
int count = eventCount == null ? 0 : eventCount;
|
int count = eventCount == null ? 0 : eventCount;
|
||||||
sb.append("<option value='").append(monthId).append("'").append(selected).append(">");
|
sb.append("<option value='").append(monthId).append("'").append(selected).append(">");
|
||||||
sb.append(monthFormat.format(reportMonth.getTime())).append(" (").append(count)
|
sb.append(monthFormat.format(reportMonth.getTime())).append(" (").append(count)
|
||||||
.append(count == 1 ? " event" : " events").append(")");
|
.append(count == 1 ? " alarm" : " alarms").append(")");
|
||||||
sb.append("</option>");
|
sb.append("</option>");
|
||||||
}
|
}
|
||||||
sb.append("</select>");
|
sb.append("</select>");
|
||||||
@@ -505,7 +706,7 @@ public class ReportManager {
|
|||||||
sb.append("<div class='month-summary-panel");
|
sb.append("<div class='month-summary-panel");
|
||||||
if (activeMonth) sb.append(" active");
|
if (activeMonth) sb.append(" active");
|
||||||
sb.append("' id='summary-").append(monthId).append("' data-month='").append(monthId).append("'>");
|
sb.append("' id='summary-").append(monthId).append("' data-month='").append(monthId).append("'>");
|
||||||
sb.append("<div class='summary-chart-title'>Seizures by time of day</div>");
|
sb.append("<div class='summary-chart-title'>Alarms by time of day</div>");
|
||||||
appendSummaryBar(sb, "Night", "00-06", "night", nightCount, maxTimeCount);
|
appendSummaryBar(sb, "Night", "00-06", "night", nightCount, maxTimeCount);
|
||||||
appendSummaryBar(sb, "Morning", "06-12", "morning", morningCount, maxTimeCount);
|
appendSummaryBar(sb, "Morning", "06-12", "morning", morningCount, maxTimeCount);
|
||||||
appendSummaryBar(sb, "Afternoon", "12-18", "afternoon", afternoonCount, maxTimeCount);
|
appendSummaryBar(sb, "Afternoon", "12-18", "afternoon", afternoonCount, maxTimeCount);
|
||||||
@@ -513,6 +714,16 @@ public class ReportManager {
|
|||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calendar legend
|
||||||
|
sb.append("<div class='legend'>");
|
||||||
|
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#e5e7eb'></div>Night background (00-06)</div>");
|
||||||
|
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#fde68a'></div>Morning background (06-12)</div>");
|
||||||
|
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#fdba74'></div>Afternoon background (12-18)</div>");
|
||||||
|
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#c4b5fd'></div>Evening background (18-24)</div>");
|
||||||
|
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#ef4444'></div>Alarm bar</div>");
|
||||||
|
sb.append("<div class='legend-item'><div class='legend-dot' style='background:#06b6d4'></div>Manual alarm bar</div>");
|
||||||
|
sb.append("</div>");
|
||||||
|
|
||||||
// Calendar months
|
// Calendar months
|
||||||
for (Calendar reportMonth : reportMonths) {
|
for (Calendar reportMonth : reportMonths) {
|
||||||
String monthId = monthIdFormat.format(reportMonth.getTime());
|
String monthId = monthIdFormat.format(reportMonth.getTime());
|
||||||
@@ -549,7 +760,7 @@ public class ReportManager {
|
|||||||
|
|
||||||
String boxClass = "day-box";
|
String boxClass = "day-box";
|
||||||
if (isToday) boxClass += " today";
|
if (isToday) boxClass += " today";
|
||||||
if (dayGroups != null && !dayGroups.isEmpty()) boxClass += " has-seizures";
|
if (dayGroups != null && !dayGroups.isEmpty()) boxClass += " has-alarms";
|
||||||
|
|
||||||
sb.append("<div class='").append(boxClass).append("'>");
|
sb.append("<div class='").append(boxClass).append("'>");
|
||||||
sb.append("<div class='day-num'>").append(day).append("</div>");
|
sb.append("<div class='day-num'>").append(day).append("</div>");
|
||||||
@@ -570,9 +781,12 @@ public class ReportManager {
|
|||||||
|
|
||||||
for (SeizureGroup g : dayGroups) {
|
for (SeizureGroup g : dayGroups) {
|
||||||
int startMinutes = getMinutesOfDay(g.startDate);
|
int startMinutes = getMinutesOfDay(g.startDate);
|
||||||
int durationMinutes = parseDurationMinutes(g.durationStr);
|
int timelineSeconds = g.durationSeconds > 0
|
||||||
|
? Math.min(g.durationSeconds, MAX_TIMELINE_DURATION_SECS)
|
||||||
|
: DEFAULT_MARKER_DURATION_SECS;
|
||||||
|
int durationMinutes = Math.max(1, (int) Math.ceil(timelineSeconds / 60.0));
|
||||||
|
|
||||||
double leftPct = (startMinutes / 1440.0) * 100.0;
|
double leftPct = Math.max(0.0, Math.min(100.0, (startMinutes / 1440.0) * 100.0));
|
||||||
double widthPct = Math.max((durationMinutes / 1440.0) * 100.0, 1.4);
|
double widthPct = Math.max((durationMinutes / 1440.0) * 100.0, 1.4);
|
||||||
|
|
||||||
if (leftPct + widthPct > 100.0) {
|
if (leftPct + widthPct > 100.0) {
|
||||||
@@ -597,8 +811,8 @@ public class ReportManager {
|
|||||||
|
|
||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
sb.append("<div class='time-scale'><span>00</span><span>06</span><span>12</span><span>18</span><span>24</span></div>");
|
sb.append("<div class='time-scale'><span>00</span><span>06</span><span>12</span><span>18</span><span>24</span></div>");
|
||||||
sb.append("<div class='seizure-count'>").append(dayGroups.size())
|
sb.append("<div class='alarm-count'>").append(dayGroups.size())
|
||||||
.append(dayGroups.size() == 1 ? " event" : " events").append("</div>");
|
.append(dayGroups.size() == 1 ? " alarm" : " alarms").append("</div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
@@ -609,19 +823,18 @@ public class ReportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detailed table below. JavaScript filters this table to the selected month.
|
// Detailed table below. JavaScript filters this table to the selected month.
|
||||||
sb.append("<h2>Event Details</h2>");
|
sb.append("<h2>Alarm Details</h2>");
|
||||||
if (groups.isEmpty()) {
|
if (groups.isEmpty()) {
|
||||||
sb.append("<p>No seizure events recorded in this period.</p>");
|
sb.append("<p>No alarm events recorded in this period.</p>");
|
||||||
} else {
|
} else {
|
||||||
sb.append("<p id='emptyMonthMessage' class='no-month-events'>No seizure events recorded for the selected month.</p>");
|
sb.append("<p id='emptyMonthMessage' class='no-month-events'>No alarm events recorded for the selected month.</p>");
|
||||||
sb.append("<div class='table-controls' id='eventTableControls'>");
|
sb.append("<div class='table-controls' id='eventTableControls'>");
|
||||||
sb.append("<div><label for='rowsPerPage'>Entries per page: </label>");
|
sb.append("<div><label for='rowsPerPage'>Entries per page: </label>");
|
||||||
sb.append("<select id='rowsPerPage' onchange='changePageSize()'>");
|
sb.append("<select id='rowsPerPage' onchange='changePageSize()'>");
|
||||||
sb.append("<option value='5'>5</option>");
|
|
||||||
sb.append("<option value='10' selected>10</option>");
|
sb.append("<option value='10' selected>10</option>");
|
||||||
sb.append("<option value='25'>25</option>");
|
sb.append("<option value='25'>25</option>");
|
||||||
sb.append("<option value='50'>50</option>");
|
sb.append("<option value='50'>50</option>");
|
||||||
sb.append("<option value='all'>All</option>");
|
sb.append("<option value='100'>100</option>");
|
||||||
sb.append("</select></div>");
|
sb.append("</select></div>");
|
||||||
sb.append("<div class='pager'>");
|
sb.append("<div class='pager'>");
|
||||||
sb.append("<button type='button' id='prevPageBtn' onclick='previousPage()'>Previous</button>");
|
sb.append("<button type='button' id='prevPageBtn' onclick='previousPage()'>Previous</button>");
|
||||||
@@ -632,16 +845,15 @@ public class ReportManager {
|
|||||||
sb.append("<div id='tableInfo' class='table-info'></div>");
|
sb.append("<div id='tableInfo' class='table-info'></div>");
|
||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
sb.append("<div class='table-wrap' id='eventTableWrap'><table class='detail-table'>");
|
sb.append("<div class='table-wrap' id='eventTableWrap'><table class='detail-table'>");
|
||||||
sb.append("<tr><th>#</th><th>Date & Time</th><th>Status</th><th>Duration</th><th>Heart Rate</th><th>Cause</th></tr>");
|
sb.append("<tr><th>#</th><th>Date & Time</th><th>Status</th><th>Duration</th><th>Heart Rate</th></tr>");
|
||||||
SimpleDateFormat displaySdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.UK);
|
SimpleDateFormat displaySdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.UK);
|
||||||
int i = 1;
|
int i = 1;
|
||||||
for (SeizureGroup g : groups) {
|
for (int gi = groups.size() - 1; gi >= 0; gi--) {
|
||||||
|
SeizureGroup g = groups.get(gi);
|
||||||
String timeStr = g.startDate != null ? displaySdf.format(g.startDate) : g.firstEvent.dataTime;
|
String timeStr = g.startDate != null ? displaySdf.format(g.startDate) : g.firstEvent.dataTime;
|
||||||
String statusStr = statusToString(g.firstEvent.status);
|
String statusStr = statusToString(g.firstEvent.status);
|
||||||
String cssClass = statusToCssClass(g.firstEvent.status);
|
String cssClass = statusToCssClass(g.firstEvent.status);
|
||||||
String hrStr = g.hr > 0 ? String.format(Locale.UK, "%.1f bpm", g.hr) : "N/A";
|
String hrStr = g.hr > 0 ? String.format(Locale.UK, "%d bpm", Math.round(g.hr)) : "N/A";
|
||||||
String cause = g.firstEvent.alarmCause != null && !g.firstEvent.alarmCause.isEmpty()
|
|
||||||
? g.firstEvent.alarmCause : "Unknown";
|
|
||||||
String dotColor = getTimeOfDayColor(g.startDate);
|
String dotColor = getTimeOfDayColor(g.startDate);
|
||||||
String eventMonth = g.startDate != null ? monthIdFormat.format(g.startDate) : "";
|
String eventMonth = g.startDate != null ? monthIdFormat.format(g.startDate) : "";
|
||||||
|
|
||||||
@@ -652,13 +864,12 @@ public class ReportManager {
|
|||||||
sb.append("<td><span class='status-pill ").append(cssClass).append("'>").append(statusStr).append("</span></td>");
|
sb.append("<td><span class='status-pill ").append(cssClass).append("'>").append(statusStr).append("</span></td>");
|
||||||
sb.append("<td>").append(escapeHtml(g.durationStr)).append("</td>");
|
sb.append("<td>").append(escapeHtml(g.durationStr)).append("</td>");
|
||||||
sb.append("<td>").append(hrStr).append("</td>");
|
sb.append("<td>").append(hrStr).append("</td>");
|
||||||
sb.append("<td>").append(escapeHtml(cause)).append("</td>");
|
|
||||||
sb.append("</tr>");
|
sb.append("</tr>");
|
||||||
}
|
}
|
||||||
sb.append("</table></div>");
|
sb.append("</table></div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.append("<p class='footer'><small>Generated by ClinX02 OpenSeizureDetector</small></p>");
|
sb.append("<p class='footer'><small>Generated by FLOGA</small></p>");
|
||||||
sb.append("</div>");
|
sb.append("</div>");
|
||||||
|
|
||||||
sb.append("<script>");
|
sb.append("<script>");
|
||||||
@@ -754,7 +965,9 @@ public class ReportManager {
|
|||||||
String alarmPhrase;
|
String alarmPhrase;
|
||||||
String alarmCause;
|
String alarmCause;
|
||||||
double hr = 0.0;
|
double hr = 0.0;
|
||||||
|
int durationSeconds = 0;
|
||||||
Date date;
|
Date date;
|
||||||
|
Date inferredStartDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class SeizureGroup {
|
static class SeizureGroup {
|
||||||
@@ -762,18 +975,53 @@ public class ReportManager {
|
|||||||
SeizureEvent lastEvent;
|
SeizureEvent lastEvent;
|
||||||
Date startDate;
|
Date startDate;
|
||||||
Date lastDate;
|
Date lastDate;
|
||||||
|
Date inferredStartDate;
|
||||||
String durationStr = "N/A";
|
String durationStr = "N/A";
|
||||||
|
int durationSeconds = 0;
|
||||||
double hr = 0.0;
|
double hr = 0.0;
|
||||||
|
boolean hasDurationEvidence = false;
|
||||||
|
|
||||||
SeizureGroup(SeizureEvent first) {
|
SeizureGroup(SeizureEvent first) {
|
||||||
this.firstEvent = first;
|
this.firstEvent = first;
|
||||||
this.lastEvent = first;
|
this.lastEvent = first;
|
||||||
this.startDate = first.date;
|
this.startDate = first.inferredStartDate != null ? first.inferredStartDate : first.date;
|
||||||
this.lastDate = first.date;
|
this.lastDate = first.date;
|
||||||
if (first.notes != null && first.notes.contains("Duration:")) {
|
this.inferredStartDate = first.inferredStartDate;
|
||||||
this.durationStr = extractDuration(first.notes);
|
this.durationSeconds = first.durationSeconds;
|
||||||
|
this.hasDurationEvidence = first.durationSeconds > 0;
|
||||||
|
this.durationStr = first.durationSeconds > 0
|
||||||
|
? formatDurationSeconds(first.durationSeconds)
|
||||||
|
: extractDuration(first.notes);
|
||||||
|
if (this.durationStr == null || this.durationStr.equals("unknown") || this.durationStr.isEmpty()) {
|
||||||
|
this.durationStr = "N/A";
|
||||||
}
|
}
|
||||||
this.hr = first.hr;
|
this.hr = first.hr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addEvent(SeizureEvent event) {
|
||||||
|
this.lastEvent = event;
|
||||||
|
this.lastDate = event.date;
|
||||||
|
|
||||||
|
if (event.durationSeconds > this.durationSeconds) {
|
||||||
|
this.durationSeconds = event.durationSeconds;
|
||||||
|
this.durationStr = formatDurationSeconds(event.durationSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.hr > 0.0) {
|
||||||
|
this.hr = event.hr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.firstEvent.alarmCause == null || this.firstEvent.alarmCause.trim().isEmpty())
|
||||||
|
&& event.alarmCause != null && !event.alarmCause.trim().isEmpty()) {
|
||||||
|
this.firstEvent.alarmCause = event.alarmCause;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inferredStartDate == null && event.inferredStartDate != null) {
|
||||||
|
this.inferredStartDate = event.inferredStartDate;
|
||||||
|
this.startDate = event.inferredStartDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasDurationEvidence = this.hasDurationEvidence || event.durationSeconds > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import java.text.DecimalFormat;
|
|||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on example at:
|
* Based on example at:
|
||||||
@@ -127,11 +128,14 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
private boolean mSMSAlarm = false;
|
private boolean mSMSAlarm = false;
|
||||||
// Emergency-services escalation SMS.
|
// Emergency-services escalation SMS.
|
||||||
// This is separate from the normal carer/contact SMS.
|
// This is separate from the normal carer/contact SMS.
|
||||||
|
// Emergency-services escalation SMS.
|
||||||
|
// This is separate from the normal caregiver/contact SMS.
|
||||||
private boolean mEmergencySmsEnabled = false;
|
private boolean mEmergencySmsEnabled = false;
|
||||||
private boolean mEmergencySmsNotifyContacts = true;
|
private boolean mEmergencySmsNotifyContacts = true;
|
||||||
private String mEmergencySmsNumber = "";
|
private String mEmergencySmsNumber = "";
|
||||||
private String mEmergencySmsMessage =
|
private String mEmergencySmsMessage =
|
||||||
"Possible seizure emergency. The wearer may be having a tonic-clonic seizure and the alarm has remained active."; private int mEmergencySmsDelaySecs = 300;
|
"Possible seizure emergency. The wearer may be having a tonic-clonic seizure and the alarm has remained active.";
|
||||||
|
private int mEmergencySmsDelaySecs = 300;
|
||||||
private EmergencySmsTimer mEmergencySmsTimer = null;
|
private EmergencySmsTimer mEmergencySmsTimer = null;
|
||||||
private boolean mEmergencySmsSent = false;
|
private boolean mEmergencySmsSent = false;
|
||||||
|
|
||||||
@@ -441,6 +445,8 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
// Stop the Cancel Alarm Latch timer
|
// Stop the Cancel Alarm Latch timer
|
||||||
Log.d(TAG, "onDestroy(): stopping alarm latch timer");
|
Log.d(TAG, "onDestroy(): stopping alarm latch timer");
|
||||||
stopLatchTimer();
|
stopLatchTimer();
|
||||||
|
// Stop the Emergency SMS timer
|
||||||
|
stopEmergencySmsTimer();
|
||||||
|
|
||||||
|
|
||||||
// Stop the location finder.
|
// Stop the location finder.
|
||||||
@@ -636,6 +642,9 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
sdData.alarmPhrase = "OK";
|
sdData.alarmPhrase = "OK";
|
||||||
sdData.alarmStanding = false;
|
sdData.alarmStanding = false;
|
||||||
sdData.fallAlarmStanding = false;
|
sdData.fallAlarmStanding = false;
|
||||||
|
|
||||||
|
resetEmergencySmsState();
|
||||||
|
|
||||||
showNotification(0);
|
showNotification(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -644,6 +653,9 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
sdData.alarmPhrase = "MUTE";
|
sdData.alarmPhrase = "MUTE";
|
||||||
sdData.alarmStanding = false;
|
sdData.alarmStanding = false;
|
||||||
sdData.fallAlarmStanding = false;
|
sdData.fallAlarmStanding = false;
|
||||||
|
|
||||||
|
resetEmergencySmsState();
|
||||||
|
|
||||||
showNotification(0);
|
showNotification(0);
|
||||||
}
|
}
|
||||||
// Handle warning alarm state
|
// Handle warning alarm state
|
||||||
@@ -1102,6 +1114,11 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mEmergencySmsDelaySecs <= 0) {
|
||||||
|
Log.w(TAG, "startEmergencySmsTimer() - invalid delay, using 300 seconds");
|
||||||
|
mEmergencySmsDelaySecs = 300;
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "startEmergencySmsTimer() - starting emergency SMS timer for "
|
Log.i(TAG, "startEmergencySmsTimer() - starting emergency SMS timer for "
|
||||||
+ mEmergencySmsDelaySecs + " seconds");
|
+ mEmergencySmsDelaySecs + " seconds");
|
||||||
|
|
||||||
@@ -1114,6 +1131,15 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void resetEmergencySmsState() {
|
||||||
|
Log.i(TAG, "resetEmergencySmsState()");
|
||||||
|
|
||||||
|
mAlarmStartTime = 0;
|
||||||
|
mLastAlarmDateStr = "";
|
||||||
|
mEmergencySmsSent = false;
|
||||||
|
|
||||||
|
stopEmergencySmsTimer();
|
||||||
|
}
|
||||||
private void stopEmergencySmsTimer() {
|
private void stopEmergencySmsTimer() {
|
||||||
if (mEmergencySmsTimer != null) {
|
if (mEmergencySmsTimer != null) {
|
||||||
Log.i(TAG, "stopEmergencySmsTimer() - cancelling emergency SMS timer");
|
Log.i(TAG, "stopEmergencySmsTimer() - cancelling emergency SMS timer");
|
||||||
@@ -1163,13 +1189,11 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
*/
|
*/
|
||||||
public void acceptAlarm() {
|
public void acceptAlarm() {
|
||||||
Log.i(TAG, "acceptAlarm()");
|
Log.i(TAG, "acceptAlarm()");
|
||||||
|
|
||||||
mSdData.alarmStanding = false;
|
mSdData.alarmStanding = false;
|
||||||
mSdData.fallAlarmStanding = false;
|
mSdData.fallAlarmStanding = false;
|
||||||
mAlarmStartTime = 0;
|
|
||||||
mLastAlarmDateStr = "";
|
|
||||||
mEmergencySmsSent = false;
|
|
||||||
|
|
||||||
stopEmergencySmsTimer();
|
resetEmergencySmsState();
|
||||||
|
|
||||||
mSdDataSource.acceptAlarm();
|
mSdDataSource.acceptAlarm();
|
||||||
stopLatchTimer();
|
stopLatchTimer();
|
||||||
@@ -1618,11 +1642,24 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msgStr == null) {
|
||||||
|
Log.w(TAG, "sendSMSDirect() - null message, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "sendSMSDirect() - Sending to " + number);
|
Log.i(TAG, "sendSMSDirect() - Sending to " + number);
|
||||||
|
Log.i(TAG, "sendSMSDirect() - message length = " + msgStr.length());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SmsManager sm = SmsManager.getDefault();
|
SmsManager sm = SmsManager.getDefault();
|
||||||
sm.sendTextMessage(number, null, msgStr, null, null);
|
|
||||||
|
if (msgStr.length() > 150) {
|
||||||
|
java.util.ArrayList<String> parts = sm.divideMessage(msgStr);
|
||||||
|
Log.i(TAG, "sendSMSDirect() - sending multipart SMS, parts = " + parts.size());
|
||||||
|
sm.sendMultipartTextMessage(number, null, parts, null, null);
|
||||||
|
} else {
|
||||||
|
sm.sendTextMessage(number, null, msgStr, null, null);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "sendSMSDirect - Failed to send SMS Message");
|
Log.e(TAG, "sendSMSDirect - Failed to send SMS Message");
|
||||||
mUtil.writeToSysLogFile("sendSMSDirect - Failed to send SMS Message");
|
mUtil.writeToSysLogFile("sendSMSDirect - Failed to send SMS Message");
|
||||||
@@ -1649,20 +1686,21 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
|
|
||||||
String emergencyMessage = mEmergencySmsMessage
|
String emergencyMessage = mEmergencySmsMessage
|
||||||
+ alarmTimePart
|
+ alarmTimePart
|
||||||
+ " Alarm duration: "
|
+ " Duration: "
|
||||||
+ mEmergencySmsDelaySecs
|
+ mEmergencySmsDelaySecs
|
||||||
+ " seconds."
|
+ " sec."
|
||||||
+ locationPart
|
+ locationPart
|
||||||
+ " Device ID: "
|
+ " Device: "
|
||||||
+ shortUuidStr;
|
+ shortUuidStr;
|
||||||
|
Log.i(TAG, "sendEmergencySms() - emergency message length = " + emergencyMessage.length());
|
||||||
|
|
||||||
Log.i(TAG, "sendEmergencySms() - sending emergency SMS to " + mEmergencySmsNumber);
|
Log.i(TAG, "sendEmergencySms() - sending emergency SMS to " + mEmergencySmsNumber);
|
||||||
Log.i(TAG, "sendEmergencySms() - message: " + emergencyMessage);
|
Log.i(TAG, "sendEmergencySms() - message: " + emergencyMessage);
|
||||||
|
|
||||||
sendSMSDirect(mEmergencySmsNumber, emergencyMessage);
|
sendSMSDirect(mEmergencySmsNumber.trim(), emergencyMessage);
|
||||||
|
|
||||||
if (mEmergencySmsNotifyContacts) {
|
if (mEmergencySmsNotifyContacts) {
|
||||||
String contactMessage = "Emergency services have been contacted for seizure alert"
|
String contactMessage = "Emergency services have been contacted for seizure alert."
|
||||||
+ alarmTimePart
|
+ alarmTimePart
|
||||||
+ locationPart
|
+ locationPart
|
||||||
+ " Device: "
|
+ " Device: "
|
||||||
@@ -1780,8 +1818,8 @@ public class SdServer extends Service implements SdDataReceiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendEmergencySms();
|
|
||||||
mEmergencySmsSent = true;
|
mEmergencySmsSent = true;
|
||||||
|
sendEmergencySms();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Reference in New Issue
Block a user